mirror of
https://github.com/ansible/awx.git
synced 2026-01-15 20:00:43 -03:30
Merge branch 'rbac' of github.com:ansible/ansible-tower into devel
This commit is contained in:
commit
2494cc56df
@ -26,19 +26,6 @@ class MongoFilterBackend(BaseFilterBackend):
|
||||
def filter_queryset(self, request, queryset, view):
|
||||
return queryset
|
||||
|
||||
class ActiveOnlyBackend(BaseFilterBackend):
|
||||
'''
|
||||
Filter to show only objects where is_active/active is True.
|
||||
'''
|
||||
|
||||
def filter_queryset(self, request, queryset, view):
|
||||
for field in queryset.model._meta.fields:
|
||||
if field.name == 'is_active':
|
||||
queryset = queryset.filter(is_active=True)
|
||||
elif field.name == 'active':
|
||||
queryset = queryset.filter(active=True)
|
||||
return queryset
|
||||
|
||||
class TypeFilterBackend(BaseFilterBackend):
|
||||
'''
|
||||
Filter on type field now returned with all objects.
|
||||
@ -166,12 +153,12 @@ class FieldLookupBackend(BaseFilterBackend):
|
||||
for key, values in request.query_params.lists():
|
||||
if key in self.RESERVED_NAMES:
|
||||
continue
|
||||
|
||||
|
||||
# HACK: Make job event filtering by host name mostly work even
|
||||
# when not capturing job event hosts M2M.
|
||||
if queryset.model._meta.object_name == 'JobEvent' and key.startswith('hosts__name'):
|
||||
key = key.replace('hosts__name', 'or__host__name')
|
||||
or_filters.append((False, 'host__name__isnull', True))
|
||||
or_filters.append((False, 'host__name__isnull', True))
|
||||
|
||||
# Custom __int filter suffix (internal use only).
|
||||
q_int = False
|
||||
|
||||
@ -7,7 +7,6 @@ import logging
|
||||
import time
|
||||
|
||||
# Django
|
||||
from django.http import Http404
|
||||
from django.conf import settings
|
||||
from django.db import connection
|
||||
from django.shortcuts import get_object_or_404
|
||||
@ -26,6 +25,7 @@ from rest_framework import views
|
||||
# AWX
|
||||
from awx.main.models import * # noqa
|
||||
from awx.main.utils import * # noqa
|
||||
from awx.api.serializers import ResourceAccessListElementSerializer
|
||||
|
||||
__all__ = ['APIView', 'GenericAPIView', 'ListAPIView', 'SimpleListAPIView',
|
||||
'ListCreateAPIView', 'SubListAPIView', 'SubListCreateAPIView',
|
||||
@ -33,6 +33,7 @@ __all__ = ['APIView', 'GenericAPIView', 'ListAPIView', 'SimpleListAPIView',
|
||||
'RetrieveUpdateAPIView', 'RetrieveDestroyAPIView',
|
||||
'RetrieveUpdateDestroyAPIView', 'DestroyAPIView',
|
||||
'SubDetailAPIView',
|
||||
'ResourceAccessList',
|
||||
'ParentMixin',]
|
||||
|
||||
logger = logging.getLogger('awx.api.generics')
|
||||
@ -298,7 +299,7 @@ class SubListAPIView(ListAPIView, ParentMixin):
|
||||
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()
|
||||
sublist_qs = getattrd(parent, self.relationship).distinct()
|
||||
return qs & sublist_qs
|
||||
|
||||
class SubListCreateAPIView(SubListAPIView, ListCreateAPIView):
|
||||
@ -348,7 +349,7 @@ class SubListCreateAPIView(SubListAPIView, ListCreateAPIView):
|
||||
# object deserialized
|
||||
obj = serializer.save()
|
||||
serializer = self.get_serializer(instance=obj)
|
||||
|
||||
|
||||
headers = {'Location': obj.get_absolute_url()}
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
|
||||
|
||||
@ -359,7 +360,7 @@ class SubListCreateAttachDetachAPIView(SubListCreateAPIView):
|
||||
def attach(self, request, *args, **kwargs):
|
||||
created = False
|
||||
parent = self.get_parent_object()
|
||||
relationship = getattr(parent, self.relationship)
|
||||
relationship = getattrd(parent, self.relationship)
|
||||
sub_id = request.data.get('id', None)
|
||||
data = request.data
|
||||
|
||||
@ -378,7 +379,7 @@ class SubListCreateAttachDetachAPIView(SubListCreateAPIView):
|
||||
|
||||
# Retrive the sub object (whether created or by ID).
|
||||
sub = get_object_or_400(self.model, pk=sub_id)
|
||||
|
||||
|
||||
# Verify we have permission to attach.
|
||||
if not request.user.can_access(self.parent_model, 'attach', parent, sub,
|
||||
self.relationship, data,
|
||||
@ -405,7 +406,7 @@ class SubListCreateAttachDetachAPIView(SubListCreateAPIView):
|
||||
|
||||
parent = self.get_parent_object()
|
||||
parent_key = getattr(self, 'parent_key', None)
|
||||
relationship = getattr(parent, self.relationship)
|
||||
relationship = getattrd(parent, self.relationship)
|
||||
sub = get_object_or_400(self.model, pk=sub_id)
|
||||
|
||||
if not request.user.can_access(self.parent_model, 'unattach', parent,
|
||||
@ -413,9 +414,7 @@ class SubListCreateAttachDetachAPIView(SubListCreateAPIView):
|
||||
raise PermissionDenied()
|
||||
|
||||
if parent_key:
|
||||
# sub object has a ForeignKey to the parent, so we can't remove it
|
||||
# from the set, only mark it as inactive.
|
||||
sub.mark_inactive()
|
||||
sub.delete()
|
||||
else:
|
||||
relationship.remove(sub)
|
||||
|
||||
@ -455,17 +454,9 @@ class RetrieveDestroyAPIView(RetrieveAPIView, generics.RetrieveDestroyAPIView):
|
||||
def destroy(self, request, *args, **kwargs):
|
||||
# somewhat lame that delete has to call it's own permissions check
|
||||
obj = self.get_object()
|
||||
# FIXME: Why isn't the active check being caught earlier by RBAC?
|
||||
if not getattr(obj, 'active', True):
|
||||
raise Http404()
|
||||
if not getattr(obj, 'is_active', True):
|
||||
raise Http404()
|
||||
if not request.user.can_access(self.model, 'delete', obj):
|
||||
raise PermissionDenied()
|
||||
if hasattr(obj, 'mark_inactive'):
|
||||
obj.mark_inactive()
|
||||
else:
|
||||
raise NotImplementedError('destroy() not implemented yet for %s' % obj)
|
||||
obj.delete()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
class RetrieveUpdateDestroyAPIView(RetrieveUpdateAPIView, RetrieveDestroyAPIView):
|
||||
@ -473,3 +464,20 @@ class RetrieveUpdateDestroyAPIView(RetrieveUpdateAPIView, RetrieveDestroyAPIView
|
||||
|
||||
class DestroyAPIView(GenericAPIView, generics.DestroyAPIView):
|
||||
pass
|
||||
|
||||
|
||||
class ResourceAccessList(ListAPIView):
|
||||
|
||||
serializer_class = ResourceAccessListElementSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
self.object_id = self.kwargs['pk']
|
||||
resource_model = getattr(self, 'resource_model')
|
||||
obj = resource_model.objects.get(pk=self.object_id)
|
||||
|
||||
roles = set([p.role for p in obj.role_permissions.all()])
|
||||
ancestors = set()
|
||||
for r in roles:
|
||||
ancestors.update(set(r.ancestors.all()))
|
||||
return User.objects.filter(roles__in=list(ancestors))
|
||||
|
||||
|
||||
@ -103,11 +103,7 @@ class ModelAccessPermission(permissions.BasePermission):
|
||||
if not request.user or request.user.is_anonymous():
|
||||
return False
|
||||
|
||||
# Don't allow inactive users (and respond with a 403).
|
||||
if not request.user.is_active:
|
||||
raise PermissionDenied('your account is inactive')
|
||||
|
||||
# Always allow superusers (as long as they are active).
|
||||
# Always allow superusers
|
||||
if getattr(view, 'always_allow_superuser', True) and request.user.is_superuser:
|
||||
return True
|
||||
|
||||
@ -121,6 +117,7 @@ class ModelAccessPermission(permissions.BasePermission):
|
||||
check_method = getattr(self, 'check_%s_permissions' % request.method.lower(), None)
|
||||
result = check_method and check_method(request, view, obj)
|
||||
if not result:
|
||||
print('Yarr permission denied: %s %s %s' % (request.method, repr(view), repr(obj),)) # TODO: XXX: This shouldn't have been committed but anoek is sloppy, remove me after we're done fixing bugs
|
||||
raise PermissionDenied()
|
||||
|
||||
return result
|
||||
@ -161,8 +158,6 @@ class JobTemplateCallbackPermission(ModelAccessPermission):
|
||||
raise PermissionDenied()
|
||||
elif not host_config_key:
|
||||
raise PermissionDenied()
|
||||
elif obj and not obj.active:
|
||||
raise PermissionDenied()
|
||||
elif obj and obj.host_config_key != host_config_key:
|
||||
raise PermissionDenied()
|
||||
else:
|
||||
@ -182,7 +177,7 @@ class TaskPermission(ModelAccessPermission):
|
||||
# Verify that the ID present in the auth token is for a valid, active
|
||||
# unified job.
|
||||
try:
|
||||
unified_job = UnifiedJob.objects.get(active=True, status='running',
|
||||
unified_job = UnifiedJob.objects.get(status='running',
|
||||
pk=int(request.auth.split('-')[0]))
|
||||
except (UnifiedJob.DoesNotExist, TypeError):
|
||||
return False
|
||||
|
||||
@ -16,6 +16,7 @@ import yaml
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import authenticate
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.core.exceptions import ObjectDoesNotExist, ValidationError as DjangoValidationError
|
||||
from django.db import models
|
||||
@ -36,6 +37,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
|
||||
@ -90,6 +92,46 @@ SUMMARIZABLE_FK_FIELDS = {
|
||||
}
|
||||
|
||||
|
||||
def reverseGenericForeignKey(content_object):
|
||||
'''
|
||||
Computes a reverse for a GenericForeignKey field.
|
||||
|
||||
Returns a dictionary of the form
|
||||
{ '<type>': reverse(<type detail>) }
|
||||
for example
|
||||
{ 'organization': '/api/v1/organizations/1/' }
|
||||
'''
|
||||
|
||||
ret = {}
|
||||
if type(content_object) is Organization:
|
||||
ret['organization'] = reverse('api:organization_detail', args=(content_object.pk,))
|
||||
if type(content_object) is User:
|
||||
ret['user'] = reverse('api:user_detail', args=(content_object.pk,))
|
||||
if type(content_object) is Team:
|
||||
ret['team'] = reverse('api:team_detail', args=(content_object.pk,))
|
||||
if type(content_object) is Project:
|
||||
ret['project'] = reverse('api:project_detail', args=(content_object.pk,))
|
||||
if type(content_object) is Inventory:
|
||||
ret['inventory'] = reverse('api:inventory_detail', args=(content_object.pk,))
|
||||
if type(content_object) is Host:
|
||||
ret['host'] = reverse('api:host_detail', args=(content_object.pk,))
|
||||
if type(content_object) is Group:
|
||||
ret['group'] = reverse('api:group_detail', args=(content_object.pk,))
|
||||
if type(content_object) is InventorySource:
|
||||
ret['inventory_source'] = reverse('api:inventory_source_detail', args=(content_object.pk,))
|
||||
if type(content_object) is Credential:
|
||||
ret['credential'] = reverse('api:credential_detail', args=(content_object.pk,))
|
||||
if type(content_object) is JobTemplate:
|
||||
ret['job_template'] = reverse('api:job_template_detail', args=(content_object.pk,))
|
||||
if type(content_object) is Role:
|
||||
ret['role'] = reverse('api:role_detail', args=(content_object.pk,))
|
||||
if type(content_object) is Job:
|
||||
ret['job'] = reverse('api:job_detail', args=(content_object.pk,))
|
||||
if type(content_object) is JobEvent:
|
||||
ret['job_event'] = reverse('api:job_event_detail', args=(content_object.pk,))
|
||||
return ret
|
||||
|
||||
|
||||
class BaseSerializerMetaclass(serializers.SerializerMetaclass):
|
||||
'''
|
||||
Custom metaclass to enable attribute inheritance from Meta objects on
|
||||
@ -122,7 +164,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': ''},
|
||||
@ -210,7 +252,7 @@ class BaseSerializer(serializers.ModelSerializer):
|
||||
# make certain fields read only
|
||||
created = serializers.SerializerMethodField()
|
||||
modified = serializers.SerializerMethodField()
|
||||
active = serializers.SerializerMethodField()
|
||||
|
||||
|
||||
def get_type(self, obj):
|
||||
return get_type_for_model(self.Meta.model)
|
||||
@ -245,9 +287,9 @@ class BaseSerializer(serializers.ModelSerializer):
|
||||
|
||||
def get_related(self, obj):
|
||||
res = OrderedDict()
|
||||
if getattr(obj, 'created_by', None) and obj.created_by.is_active:
|
||||
if getattr(obj, 'created_by', None):
|
||||
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):
|
||||
res['modified_by'] = reverse('api:user_detail', args=(obj.modified_by.pk,))
|
||||
return res
|
||||
|
||||
@ -272,10 +314,6 @@ class BaseSerializer(serializers.ModelSerializer):
|
||||
continue
|
||||
if fkval == obj:
|
||||
continue
|
||||
if hasattr(fkval, 'active') and not fkval.active:
|
||||
continue
|
||||
if hasattr(fkval, 'is_active') and not fkval.is_active:
|
||||
continue
|
||||
summary_fields[fk] = OrderedDict()
|
||||
for field in related_fields:
|
||||
fval = getattr(fkval, field, None)
|
||||
@ -291,14 +329,32 @@ class BaseSerializer(serializers.ModelSerializer):
|
||||
# Can be raised by the reverse accessor for a OneToOneField.
|
||||
except ObjectDoesNotExist:
|
||||
pass
|
||||
if getattr(obj, 'created_by', None) and obj.created_by.is_active:
|
||||
if getattr(obj, 'created_by', None):
|
||||
summary_fields['created_by'] = OrderedDict()
|
||||
for field in SUMMARIZABLE_FK_FIELDS['user']:
|
||||
summary_fields['created_by'][field] = getattr(obj.created_by, field)
|
||||
if getattr(obj, 'modified_by', None) and obj.modified_by.is_active:
|
||||
if getattr(obj, 'modified_by', None):
|
||||
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,
|
||||
'description': role.description,
|
||||
'url': role.get_absolute_url(),
|
||||
}
|
||||
if len(roles) > 0:
|
||||
summary_fields['roles'] = roles
|
||||
return summary_fields
|
||||
|
||||
def get_created(self, obj):
|
||||
@ -317,14 +373,6 @@ class BaseSerializer(serializers.ModelSerializer):
|
||||
else:
|
||||
return obj.modified
|
||||
|
||||
def get_active(self, obj):
|
||||
if obj is None:
|
||||
return False
|
||||
elif isinstance(obj, User):
|
||||
return obj.is_active
|
||||
else:
|
||||
return obj.active
|
||||
|
||||
def build_standard_field(self, field_name, model_field):
|
||||
|
||||
# DRF 3.3 serializers.py::build_standard_field() -> utils/field_mapping.py::get_field_kwargs() short circuits
|
||||
@ -332,7 +380,7 @@ class BaseSerializer(serializers.ModelSerializer):
|
||||
#
|
||||
# This logic is to force rendering choice's on an uneditable field.
|
||||
# Note: Consider expanding this rendering for more than just choices fields
|
||||
# Note: This logic works in conjuction with
|
||||
# Note: This logic works in conjuction with
|
||||
if hasattr(model_field, 'choices') and model_field.choices:
|
||||
was_editable = model_field.editable
|
||||
model_field.editable = True
|
||||
@ -473,6 +521,10 @@ class BaseSerializer(serializers.ModelSerializer):
|
||||
raise ValidationError(d)
|
||||
return attrs
|
||||
|
||||
def to_representation(self, obj):
|
||||
ret = super(BaseSerializer, self).to_representation(obj)
|
||||
return ret
|
||||
|
||||
|
||||
class EmptySerializer(serializers.Serializer):
|
||||
pass
|
||||
@ -499,11 +551,11 @@ class UnifiedJobTemplateSerializer(BaseSerializer):
|
||||
|
||||
def get_related(self, obj):
|
||||
res = super(UnifiedJobTemplateSerializer, self).get_related(obj)
|
||||
if obj.current_job and obj.current_job.active:
|
||||
if obj.current_job:
|
||||
res['current_job'] = obj.current_job.get_absolute_url()
|
||||
if obj.last_job and obj.last_job.active:
|
||||
if obj.last_job:
|
||||
res['last_job'] = obj.last_job.get_absolute_url()
|
||||
if obj.next_schedule and obj.next_schedule.active:
|
||||
if obj.next_schedule:
|
||||
res['next_schedule'] = obj.next_schedule.get_absolute_url()
|
||||
return res
|
||||
|
||||
@ -558,9 +610,9 @@ class UnifiedJobSerializer(BaseSerializer):
|
||||
|
||||
def get_related(self, obj):
|
||||
res = super(UnifiedJobSerializer, self).get_related(obj)
|
||||
if obj.unified_job_template and obj.unified_job_template.active:
|
||||
if obj.unified_job_template:
|
||||
res['unified_job_template'] = obj.unified_job_template.get_absolute_url()
|
||||
if obj.schedule and obj.schedule.active:
|
||||
if obj.schedule:
|
||||
res['schedule'] = obj.schedule.get_absolute_url()
|
||||
if isinstance(obj, ProjectUpdate):
|
||||
res['stdout'] = reverse('api:project_update_stdout', args=(obj.pk,))
|
||||
@ -739,8 +791,9 @@ 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,)),
|
||||
access_list = reverse('api:user_access_list', args=(obj.pk,)),
|
||||
))
|
||||
return res
|
||||
|
||||
@ -795,6 +848,7 @@ class OrganizationSerializer(BaseSerializer):
|
||||
notifiers_any = reverse('api:organization_notifiers_any_list', args=(obj.pk,)),
|
||||
notifiers_success = reverse('api:organization_notifiers_success_list', args=(obj.pk,)),
|
||||
notifiers_error = reverse('api:organization_notifiers_error_list', args=(obj.pk,)),
|
||||
access_list = reverse('api:organization_access_list', args=(obj.pk,)),
|
||||
))
|
||||
return res
|
||||
|
||||
@ -819,7 +873,7 @@ class ProjectOptionsSerializer(BaseSerializer):
|
||||
|
||||
def get_related(self, obj):
|
||||
res = super(ProjectOptionsSerializer, self).get_related(obj)
|
||||
if obj.credential and obj.credential.active:
|
||||
if obj.credential:
|
||||
res['credential'] = reverse('api:credential_detail',
|
||||
args=(obj.credential.pk,))
|
||||
return res
|
||||
@ -848,7 +902,7 @@ class ProjectOptionsSerializer(BaseSerializer):
|
||||
|
||||
def to_representation(self, obj):
|
||||
ret = super(ProjectOptionsSerializer, self).to_representation(obj)
|
||||
if obj is not None and 'credential' in ret and (not obj.credential or not obj.credential.active):
|
||||
if obj is not None and 'credential' in ret and not obj.credential:
|
||||
ret['credential'] = None
|
||||
return ret
|
||||
|
||||
@ -869,7 +923,6 @@ class ProjectSerializer(UnifiedJobTemplateSerializer, ProjectOptionsSerializer):
|
||||
def get_related(self, obj):
|
||||
res = super(ProjectSerializer, self).get_related(obj)
|
||||
res.update(dict(
|
||||
organizations = reverse('api:project_organizations_list', args=(obj.pk,)),
|
||||
teams = reverse('api:project_teams_list', args=(obj.pk,)),
|
||||
playbooks = reverse('api:project_playbooks', args=(obj.pk,)),
|
||||
update = reverse('api:project_update_view', args=(obj.pk,)),
|
||||
@ -879,7 +932,11 @@ class ProjectSerializer(UnifiedJobTemplateSerializer, ProjectOptionsSerializer):
|
||||
notifiers_any = reverse('api:project_notifiers_any_list', args=(obj.pk,)),
|
||||
notifiers_success = reverse('api:project_notifiers_success_list', args=(obj.pk,)),
|
||||
notifiers_error = reverse('api:project_notifiers_error_list', args=(obj.pk,)),
|
||||
access_list = reverse('api:project_access_list', args=(obj.pk,)),
|
||||
))
|
||||
if obj.organization:
|
||||
res['organization'] = reverse('api:organization_detail',
|
||||
args=(obj.organization.pk,))
|
||||
# Backwards compatibility.
|
||||
if obj.current_update:
|
||||
res['current_update'] = reverse('api:project_update_detail',
|
||||
@ -978,15 +1035,16 @@ class InventorySerializer(BaseSerializerWithVariables):
|
||||
job_templates = reverse('api:inventory_job_template_list', args=(obj.pk,)),
|
||||
scan_job_templates = reverse('api:inventory_scan_job_template_list', args=(obj.pk,)),
|
||||
ad_hoc_commands = reverse('api:inventory_ad_hoc_commands_list', args=(obj.pk,)),
|
||||
access_list = reverse('api:inventory_access_list', args=(obj.pk,)),
|
||||
#single_fact = reverse('api:inventory_single_fact_view', args=(obj.pk,)),
|
||||
))
|
||||
if obj.organization and obj.organization.active:
|
||||
if obj.organization:
|
||||
res['organization'] = reverse('api:organization_detail', args=(obj.organization.pk,))
|
||||
return res
|
||||
|
||||
def to_representation(self, obj):
|
||||
ret = super(InventorySerializer, self).to_representation(obj)
|
||||
if obj is not None and 'organization' in ret and (not obj.organization or not obj.organization.active):
|
||||
if obj is not None and 'organization' in ret and not obj.organization:
|
||||
ret['organization'] = None
|
||||
return ret
|
||||
|
||||
@ -1041,11 +1099,11 @@ class HostSerializer(BaseSerializerWithVariables):
|
||||
fact_versions = reverse('api:host_fact_versions_list', args=(obj.pk,)),
|
||||
#single_fact = reverse('api:host_single_fact_view', args=(obj.pk,)),
|
||||
))
|
||||
if obj.inventory and obj.inventory.active:
|
||||
if obj.inventory:
|
||||
res['inventory'] = reverse('api:inventory_detail', args=(obj.inventory.pk,))
|
||||
if obj.last_job and obj.last_job.active:
|
||||
if obj.last_job:
|
||||
res['last_job'] = reverse('api:job_detail', args=(obj.last_job.pk,))
|
||||
if obj.last_job_host_summary and obj.last_job_host_summary.job.active:
|
||||
if obj.last_job_host_summary:
|
||||
res['last_job_host_summary'] = reverse('api:job_host_summary_detail', args=(obj.last_job_host_summary.pk,))
|
||||
return res
|
||||
|
||||
@ -1061,7 +1119,7 @@ class HostSerializer(BaseSerializerWithVariables):
|
||||
'name': j.job.job_template.name if j.job.job_template is not None else "",
|
||||
'status': j.job.status,
|
||||
'finished': j.job.finished,
|
||||
} for j in obj.job_host_summaries.filter(job__active=True).select_related('job__job_template').order_by('-created')[:5]]})
|
||||
} for j in obj.job_host_summaries.select_related('job__job_template').order_by('-created')[:5]]})
|
||||
return d
|
||||
|
||||
def _get_host_port_from_name(self, name):
|
||||
@ -1110,11 +1168,11 @@ class HostSerializer(BaseSerializerWithVariables):
|
||||
ret = super(HostSerializer, self).to_representation(obj)
|
||||
if not obj:
|
||||
return ret
|
||||
if 'inventory' in ret and (not obj.inventory or not obj.inventory.active):
|
||||
if 'inventory' in ret and not obj.inventory:
|
||||
ret['inventory'] = None
|
||||
if 'last_job' in ret and (not obj.last_job or not obj.last_job.active):
|
||||
if 'last_job' in ret and not obj.last_job:
|
||||
ret['last_job'] = None
|
||||
if 'last_job_host_summary' in ret and (not obj.last_job_host_summary or not obj.last_job_host_summary.job.active):
|
||||
if 'last_job_host_summary' in ret and not obj.last_job_host_summary:
|
||||
ret['last_job_host_summary'] = None
|
||||
return ret
|
||||
|
||||
@ -1148,9 +1206,10 @@ class GroupSerializer(BaseSerializerWithVariables):
|
||||
activity_stream = reverse('api:group_activity_stream_list', args=(obj.pk,)),
|
||||
inventory_sources = reverse('api:group_inventory_sources_list', args=(obj.pk,)),
|
||||
ad_hoc_commands = reverse('api:group_ad_hoc_commands_list', args=(obj.pk,)),
|
||||
access_list = reverse('api:group_access_list', args=(obj.pk,)),
|
||||
#single_fact = reverse('api:group_single_fact_view', args=(obj.pk,)),
|
||||
))
|
||||
if obj.inventory and obj.inventory.active:
|
||||
if obj.inventory:
|
||||
res['inventory'] = reverse('api:inventory_detail', args=(obj.inventory.pk,))
|
||||
if obj.inventory_source:
|
||||
res['inventory_source'] = reverse('api:inventory_source_detail', args=(obj.inventory_source.pk,))
|
||||
@ -1163,7 +1222,7 @@ class GroupSerializer(BaseSerializerWithVariables):
|
||||
|
||||
def to_representation(self, obj):
|
||||
ret = super(GroupSerializer, self).to_representation(obj)
|
||||
if obj is not None and 'inventory' in ret and (not obj.inventory or not obj.inventory.active):
|
||||
if obj is not None and 'inventory' in ret and not obj.inventory:
|
||||
ret['inventory'] = None
|
||||
return ret
|
||||
|
||||
@ -1179,7 +1238,7 @@ class GroupTreeSerializer(GroupSerializer):
|
||||
def get_children(self, obj):
|
||||
if obj is None:
|
||||
return {}
|
||||
children_qs = obj.children.filter(active=True)
|
||||
children_qs = obj.children
|
||||
children_qs = children_qs.select_related('inventory')
|
||||
children_qs = children_qs.prefetch_related('inventory_source')
|
||||
return GroupTreeSerializer(children_qs, many=True).data
|
||||
@ -1244,7 +1303,7 @@ class CustomInventoryScriptSerializer(BaseSerializer):
|
||||
def get_related(self, obj):
|
||||
res = super(CustomInventoryScriptSerializer, self).get_related(obj)
|
||||
|
||||
if obj.organization and obj.organization.active:
|
||||
if obj.organization:
|
||||
res['organization'] = reverse('api:organization_detail', args=(obj.organization.pk,))
|
||||
return res
|
||||
|
||||
@ -1257,10 +1316,10 @@ class InventorySourceOptionsSerializer(BaseSerializer):
|
||||
|
||||
def get_related(self, obj):
|
||||
res = super(InventorySourceOptionsSerializer, self).get_related(obj)
|
||||
if obj.credential and obj.credential.active:
|
||||
if obj.credential:
|
||||
res['credential'] = reverse('api:credential_detail',
|
||||
args=(obj.credential.pk,))
|
||||
if obj.source_script and obj.source_script.active:
|
||||
if obj.source_script:
|
||||
res['source_script'] = reverse('api:inventory_script_detail', args=(obj.source_script.pk,))
|
||||
return res
|
||||
|
||||
@ -1305,7 +1364,7 @@ class InventorySourceOptionsSerializer(BaseSerializer):
|
||||
ret = super(InventorySourceOptionsSerializer, self).to_representation(obj)
|
||||
if obj is None:
|
||||
return ret
|
||||
if 'credential' in ret and (not obj.credential or not obj.credential.active):
|
||||
if 'credential' in ret and not obj.credential:
|
||||
ret['credential'] = None
|
||||
return ret
|
||||
|
||||
@ -1336,9 +1395,9 @@ class InventorySourceSerializer(UnifiedJobTemplateSerializer, InventorySourceOpt
|
||||
notifiers_success = reverse('api:inventory_source_notifiers_success_list', args=(obj.pk,)),
|
||||
notifiers_error = reverse('api:inventory_source_notifiers_error_list', args=(obj.pk,)),
|
||||
))
|
||||
if obj.inventory and obj.inventory.active:
|
||||
if obj.inventory:
|
||||
res['inventory'] = reverse('api:inventory_detail', args=(obj.inventory.pk,))
|
||||
if obj.group and obj.group.active:
|
||||
if obj.group:
|
||||
res['group'] = reverse('api:group_detail', args=(obj.group.pk,))
|
||||
# Backwards compatibility.
|
||||
if obj.current_update:
|
||||
@ -1353,9 +1412,9 @@ class InventorySourceSerializer(UnifiedJobTemplateSerializer, InventorySourceOpt
|
||||
ret = super(InventorySourceSerializer, self).to_representation(obj)
|
||||
if obj is None:
|
||||
return ret
|
||||
if 'inventory' in ret and (not obj.inventory or not obj.inventory.active):
|
||||
if 'inventory' in ret and not obj.inventory:
|
||||
ret['inventory'] = None
|
||||
if 'group' in ret and (not obj.group or not obj.group.active):
|
||||
if 'group' in ret and not obj.group:
|
||||
ret['group'] = None
|
||||
return ret
|
||||
|
||||
@ -1409,74 +1468,79 @@ 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,)),
|
||||
access_list = reverse('api:team_access_list', args=(obj.pk,)),
|
||||
))
|
||||
if obj.organization and obj.organization.active:
|
||||
if obj.organization:
|
||||
res['organization'] = reverse('api:organization_detail', args=(obj.organization.pk,))
|
||||
return res
|
||||
|
||||
def to_representation(self, obj):
|
||||
ret = super(TeamSerializer, self).to_representation(obj)
|
||||
if obj is not None and 'organization' in ret and (not obj.organization or not obj.organization.active):
|
||||
if obj is not None and 'organization' in ret and not obj.organization:
|
||||
ret['organization'] = None
|
||||
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
|
||||
|
||||
def validate(self, attrs):
|
||||
# Can only set either user or team.
|
||||
user = attrs.get('user', self.instance and self.instance.user or None)
|
||||
team = attrs.get('team', self.instance and self.instance.team or None)
|
||||
if user and team:
|
||||
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.
|
||||
permission_type = attrs.get('permission_type', self.instance and self.instance.permission_type or None)
|
||||
project = attrs.get('project', self.instance and self.instance.project or None)
|
||||
if permission_type in ('admin', 'read', 'write') and project:
|
||||
raise serializers.ValidationError('project cannot be assigned for '
|
||||
'inventory-only permissions')
|
||||
# Project is required when setting deployment permissions.
|
||||
if permission_type in ('run', 'check') and not project:
|
||||
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
|
||||
ret = super(RoleSerializer, self).get_related(obj)
|
||||
ret['users'] = reverse('api:role_users_list', args=(obj.pk,))
|
||||
ret['teams'] = reverse('api:role_teams_list', args=(obj.pk,))
|
||||
try:
|
||||
if obj.content_object:
|
||||
ret.update(reverseGenericForeignKey(obj.content_object))
|
||||
except AttributeError:
|
||||
# AttributeError's happen if our content_object is pointing at
|
||||
# a model that no longer exists. This is dirty data and ideally
|
||||
# doesn't exist, but in case it does, let's not puke.
|
||||
pass
|
||||
return ret
|
||||
|
||||
|
||||
|
||||
class ResourceAccessListElementSerializer(UserSerializer):
|
||||
|
||||
def to_representation(self, user):
|
||||
ret = super(ResourceAccessListElementSerializer, self).to_representation(user)
|
||||
object_id = self.context['view'].object_id
|
||||
obj = self.context['view'].resource_model.objects.get(pk=object_id)
|
||||
|
||||
if 'summary_fields' not in ret:
|
||||
ret['summary_fields'] = {}
|
||||
ret['summary_fields']['permissions'] = get_user_permissions_on_resource(obj, user)
|
||||
|
||||
def format_role_perm(role):
|
||||
role_dict = { 'id': role.id, 'name': role.name, 'description': role.description}
|
||||
try:
|
||||
role_dict['resource_name'] = role.content_object.name
|
||||
role_dict['resource_type'] = role.content_type.name
|
||||
role_dict['related'] = reverseGenericForeignKey(role.content_object)
|
||||
except:
|
||||
pass
|
||||
|
||||
return { 'role': role_dict, 'permissions': get_role_permissions_on_resource(obj, role)}
|
||||
|
||||
content_type = ContentType.objects.get_for_model(obj)
|
||||
direct_permissive_role_ids = RolePermission.objects.filter(content_type=content_type, object_id=obj.id).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 = RolePermission.objects.filter(content_type=content_type, object_id=obj.id).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
|
||||
@ -1498,9 +1562,9 @@ class CredentialSerializer(BaseSerializer):
|
||||
|
||||
def to_representation(self, obj):
|
||||
ret = super(CredentialSerializer, self).to_representation(obj)
|
||||
if obj is not None and 'user' in ret and (not obj.user or not obj.user.is_active):
|
||||
if obj is not None and 'user' in ret and not obj.user:
|
||||
ret['user'] = None
|
||||
if obj is not None and 'team' in ret and (not obj.team or not obj.team.active):
|
||||
if obj is not None and 'team' in ret and not obj.team:
|
||||
ret['team'] = None
|
||||
return ret
|
||||
|
||||
@ -1519,7 +1583,8 @@ class CredentialSerializer(BaseSerializer):
|
||||
def get_related(self, obj):
|
||||
res = super(CredentialSerializer, self).get_related(obj)
|
||||
res.update(dict(
|
||||
activity_stream = reverse('api:credential_activity_stream_list', args=(obj.pk,))
|
||||
activity_stream = reverse('api:credential_activity_stream_list', args=(obj.pk,)),
|
||||
access_list = reverse('api:credential_access_list', args=(obj.pk,)),
|
||||
))
|
||||
if obj.user:
|
||||
res['user'] = reverse('api:user_detail', args=(obj.user.pk,))
|
||||
@ -1538,13 +1603,13 @@ class JobOptionsSerializer(BaseSerializer):
|
||||
|
||||
def get_related(self, obj):
|
||||
res = super(JobOptionsSerializer, self).get_related(obj)
|
||||
if obj.inventory and obj.inventory.active:
|
||||
if obj.inventory:
|
||||
res['inventory'] = reverse('api:inventory_detail', args=(obj.inventory.pk,))
|
||||
if obj.project and obj.project.active:
|
||||
if obj.project:
|
||||
res['project'] = reverse('api:project_detail', args=(obj.project.pk,))
|
||||
if obj.credential and obj.credential.active:
|
||||
if obj.credential:
|
||||
res['credential'] = reverse('api:credential_detail', args=(obj.credential.pk,))
|
||||
if obj.cloud_credential and obj.cloud_credential.active:
|
||||
if obj.cloud_credential:
|
||||
res['cloud_credential'] = reverse('api:credential_detail',
|
||||
args=(obj.cloud_credential.pk,))
|
||||
return res
|
||||
@ -1553,15 +1618,15 @@ class JobOptionsSerializer(BaseSerializer):
|
||||
ret = super(JobOptionsSerializer, self).to_representation(obj)
|
||||
if obj is None:
|
||||
return ret
|
||||
if 'inventory' in ret and (not obj.inventory or not obj.inventory.active):
|
||||
if 'inventory' in ret and not obj.inventory:
|
||||
ret['inventory'] = None
|
||||
if 'project' in ret and (not obj.project or not obj.project.active):
|
||||
if 'project' in ret and not obj.project:
|
||||
ret['project'] = None
|
||||
if 'playbook' in ret:
|
||||
ret['playbook'] = ''
|
||||
if 'credential' in ret and (not obj.credential or not obj.credential.active):
|
||||
if 'credential' in ret and not obj.credential:
|
||||
ret['credential'] = None
|
||||
if 'cloud_credential' in ret and (not obj.cloud_credential or not obj.cloud_credential.active):
|
||||
if 'cloud_credential' in ret and not obj.cloud_credential:
|
||||
ret['cloud_credential'] = None
|
||||
return ret
|
||||
|
||||
@ -1598,6 +1663,7 @@ class JobTemplateSerializer(UnifiedJobTemplateSerializer, JobOptionsSerializer):
|
||||
notifiers_any = reverse('api:job_template_notifiers_any_list', args=(obj.pk,)),
|
||||
notifiers_success = reverse('api:job_template_notifiers_success_list', args=(obj.pk,)),
|
||||
notifiers_error = reverse('api:job_template_notifiers_error_list', args=(obj.pk,)),
|
||||
access_list = reverse('api:job_template_access_list', args=(obj.pk,)),
|
||||
))
|
||||
if obj.host_config_key:
|
||||
res['callback'] = reverse('api:job_template_callback', args=(obj.pk,))
|
||||
@ -1623,7 +1689,7 @@ class JobTemplateSerializer(UnifiedJobTemplateSerializer, JobOptionsSerializer):
|
||||
else:
|
||||
d['can_copy'] = False
|
||||
d['can_edit'] = False
|
||||
d['recent_jobs'] = [{'id': x.id, 'status': x.status, 'finished': x.finished} for x in obj.jobs.filter(active=True).order_by('-created')[:10]]
|
||||
d['recent_jobs'] = [{'id': x.id, 'status': x.status, 'finished': x.finished} for x in obj.jobs.order_by('-created')[:10]]
|
||||
return d
|
||||
|
||||
def validate(self, attrs):
|
||||
@ -1654,7 +1720,7 @@ class JobSerializer(UnifiedJobSerializer, JobOptionsSerializer):
|
||||
activity_stream = reverse('api:job_activity_stream_list', args=(obj.pk,)),
|
||||
notifications = reverse('api:job_notifications_list', args=(obj.pk,)),
|
||||
))
|
||||
if obj.job_template and obj.job_template.active:
|
||||
if obj.job_template:
|
||||
res['job_template'] = reverse('api:job_template_detail',
|
||||
args=(obj.job_template.pk,))
|
||||
if obj.can_start or True:
|
||||
@ -1699,7 +1765,7 @@ class JobSerializer(UnifiedJobSerializer, JobOptionsSerializer):
|
||||
ret = super(JobSerializer, self).to_representation(obj)
|
||||
if obj is None:
|
||||
return ret
|
||||
if 'job_template' in ret and (not obj.job_template or not obj.job_template.active):
|
||||
if 'job_template' in ret and not obj.job_template:
|
||||
ret['job_template'] = None
|
||||
|
||||
if obj.job_template and obj.job_template.survey_enabled:
|
||||
@ -1754,7 +1820,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()):
|
||||
@ -1763,11 +1829,11 @@ class JobRelaunchSerializer(JobSerializer):
|
||||
|
||||
def validate(self, attrs):
|
||||
obj = self.context.get('obj')
|
||||
if not obj.credential or obj.credential.active is False:
|
||||
if not obj.credential:
|
||||
raise serializers.ValidationError(dict(credential=["Credential not found or deleted."]))
|
||||
if obj.job_type != PERM_INVENTORY_SCAN and (obj.project is None or not obj.project.active):
|
||||
if obj.job_type != PERM_INVENTORY_SCAN and obj.project is None:
|
||||
raise serializers.ValidationError(dict(errors=["Job Template Project is missing or undefined"]))
|
||||
if obj.inventory is None or not obj.inventory.active:
|
||||
if obj.inventory is None:
|
||||
raise serializers.ValidationError(dict(errors=["Job Template Inventory is missing or undefined"]))
|
||||
attrs = super(JobRelaunchSerializer, self).validate(attrs)
|
||||
return attrs
|
||||
@ -1807,9 +1873,9 @@ class AdHocCommandSerializer(UnifiedJobSerializer):
|
||||
|
||||
def get_related(self, obj):
|
||||
res = super(AdHocCommandSerializer, self).get_related(obj)
|
||||
if obj.inventory and obj.inventory.active:
|
||||
if obj.inventory:
|
||||
res['inventory'] = reverse('api:inventory_detail', args=(obj.inventory.pk,))
|
||||
if obj.credential and obj.credential.active:
|
||||
if obj.credential:
|
||||
res['credential'] = reverse('api:credential_detail', args=(obj.credential.pk,))
|
||||
res.update(dict(
|
||||
events = reverse('api:ad_hoc_command_ad_hoc_command_events_list', args=(obj.pk,)),
|
||||
@ -1821,9 +1887,9 @@ class AdHocCommandSerializer(UnifiedJobSerializer):
|
||||
|
||||
def to_representation(self, obj):
|
||||
ret = super(AdHocCommandSerializer, self).to_representation(obj)
|
||||
if 'inventory' in ret and (not obj.inventory or not obj.inventory.active):
|
||||
if 'inventory' in ret and not obj.inventory:
|
||||
ret['inventory'] = None
|
||||
if 'credential' in ret and (not obj.credential or not obj.credential.active):
|
||||
if 'credential' in ret and not obj.credential:
|
||||
ret['credential'] = None
|
||||
# For the UI, only module_name is returned for name, instead of the
|
||||
# longer module name + module_args format.
|
||||
@ -1879,7 +1945,7 @@ class SystemJobSerializer(UnifiedJobSerializer):
|
||||
|
||||
def get_related(self, obj):
|
||||
res = super(SystemJobSerializer, self).get_related(obj)
|
||||
if obj.system_job_template and obj.system_job_template.active:
|
||||
if obj.system_job_template:
|
||||
res['system_job_template'] = reverse('api:system_job_template_detail',
|
||||
args=(obj.system_job_template.pk,))
|
||||
res['notifications'] = reverse('api:system_job_notifications_list', args=(obj.pk,))
|
||||
@ -2018,7 +2084,7 @@ class JobLaunchSerializer(BaseSerializer):
|
||||
}
|
||||
|
||||
def get_credential_needed_to_start(self, obj):
|
||||
return not (obj and obj.credential and obj.credential.active)
|
||||
return not (obj and obj.credential)
|
||||
|
||||
def get_survey_enabled(self, obj):
|
||||
if obj:
|
||||
@ -2031,7 +2097,7 @@ class JobLaunchSerializer(BaseSerializer):
|
||||
data = self.context.get('data')
|
||||
|
||||
credential = attrs.get('credential', obj and obj.credential or None)
|
||||
if not credential or not credential.active:
|
||||
if not credential:
|
||||
errors['credential'] = 'Credential not provided'
|
||||
|
||||
# fill passwords dict with request data passwords
|
||||
@ -2062,9 +2128,9 @@ class JobLaunchSerializer(BaseSerializer):
|
||||
if validation_errors:
|
||||
errors['variables_needed_to_start'] = validation_errors
|
||||
|
||||
if obj.job_type != PERM_INVENTORY_SCAN and (obj.project is None or not obj.project.active):
|
||||
if obj.job_type != PERM_INVENTORY_SCAN and (obj.project is None):
|
||||
errors['project'] = 'Job Template Project is missing or undefined'
|
||||
if obj.inventory is None or not obj.inventory.active:
|
||||
if obj.inventory is None:
|
||||
errors['inventory'] = 'Job Template Inventory is missing or undefined'
|
||||
|
||||
if errors:
|
||||
@ -2100,7 +2166,7 @@ class NotifierSerializer(BaseSerializer):
|
||||
test = reverse('api:notifier_test', args=(obj.pk,)),
|
||||
notifications = reverse('api:notifier_notification_list', args=(obj.pk,)),
|
||||
))
|
||||
if obj.organization and obj.organization.active:
|
||||
if obj.organization:
|
||||
res['organization'] = reverse('api:organization_detail', args=(obj.organization.pk,))
|
||||
return res
|
||||
|
||||
@ -2158,7 +2224,7 @@ class ScheduleSerializer(BaseSerializer):
|
||||
res.update(dict(
|
||||
unified_jobs = reverse('api:schedule_unified_jobs_list', args=(obj.pk,)),
|
||||
))
|
||||
if obj.unified_job_template and obj.unified_job_template.active:
|
||||
if obj.unified_job_template:
|
||||
res['unified_job_template'] = obj.unified_job_template.get_absolute_url()
|
||||
return res
|
||||
|
||||
@ -2385,8 +2451,6 @@ class AuthTokenSerializer(serializers.Serializer):
|
||||
if username and password:
|
||||
user = authenticate(username=username, password=password)
|
||||
if user:
|
||||
if not user.is_active:
|
||||
raise serializers.ValidationError('User account is disabled.')
|
||||
attrs['user'] = user
|
||||
return attrs
|
||||
else:
|
||||
@ -2396,7 +2460,7 @@ class AuthTokenSerializer(serializers.Serializer):
|
||||
|
||||
|
||||
class FactVersionSerializer(BaseFactSerializer):
|
||||
|
||||
|
||||
class Meta:
|
||||
model = Fact
|
||||
fields = ('related', 'module', 'timestamp')
|
||||
|
||||
@ -24,6 +24,7 @@ organization_urls = patterns('awx.api.views',
|
||||
url(r'^(?P<pk>[0-9]+)/notifiers_any/$', 'organization_notifiers_any_list'),
|
||||
url(r'^(?P<pk>[0-9]+)/notifiers_error/$', 'organization_notifiers_error_list'),
|
||||
url(r'^(?P<pk>[0-9]+)/notifiers_success/$', 'organization_notifiers_success_list'),
|
||||
url(r'^(?P<pk>[0-9]+)/access_list/$', 'organization_access_list'),
|
||||
)
|
||||
|
||||
user_urls = patterns('awx.api.views',
|
||||
@ -34,15 +35,15 @@ 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'),
|
||||
url(r'^(?P<pk>[0-9]+)/access_list/$', 'user_access_list'),
|
||||
)
|
||||
|
||||
project_urls = patterns('awx.api.views',
|
||||
url(r'^$', 'project_list'),
|
||||
url(r'^(?P<pk>[0-9]+)/$', 'project_detail'),
|
||||
url(r'^(?P<pk>[0-9]+)/playbooks/$', 'project_playbooks'),
|
||||
url(r'^(?P<pk>[0-9]+)/organizations/$', 'project_organizations_list'),
|
||||
url(r'^(?P<pk>[0-9]+)/teams/$', 'project_teams_list'),
|
||||
url(r'^(?P<pk>[0-9]+)/update/$', 'project_update_view'),
|
||||
url(r'^(?P<pk>[0-9]+)/project_updates/$', 'project_updates_list'),
|
||||
@ -51,6 +52,7 @@ project_urls = patterns('awx.api.views',
|
||||
url(r'^(?P<pk>[0-9]+)/notifiers_any/$', 'project_notifiers_any_list'),
|
||||
url(r'^(?P<pk>[0-9]+)/notifiers_error/$', 'project_notifiers_error_list'),
|
||||
url(r'^(?P<pk>[0-9]+)/notifiers_success/$', 'project_notifiers_success_list'),
|
||||
url(r'^(?P<pk>[0-9]+)/access_list/$', 'project_access_list'),
|
||||
)
|
||||
|
||||
project_update_urls = patterns('awx.api.views',
|
||||
@ -66,8 +68,9 @@ 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'),
|
||||
url(r'^(?P<pk>[0-9]+)/access_list/$', 'team_access_list'),
|
||||
)
|
||||
|
||||
inventory_urls = patterns('awx.api.views',
|
||||
@ -84,6 +87,7 @@ inventory_urls = patterns('awx.api.views',
|
||||
url(r'^(?P<pk>[0-9]+)/job_templates/$', 'inventory_job_template_list'),
|
||||
url(r'^(?P<pk>[0-9]+)/scan_job_templates/$', 'inventory_scan_job_template_list'),
|
||||
url(r'^(?P<pk>[0-9]+)/ad_hoc_commands/$', 'inventory_ad_hoc_commands_list'),
|
||||
url(r'^(?P<pk>[0-9]+)/access_list/$', 'inventory_access_list'),
|
||||
#url(r'^(?P<pk>[0-9]+)/single_fact/$', 'inventory_single_fact_view'),
|
||||
)
|
||||
|
||||
@ -117,6 +121,7 @@ group_urls = patterns('awx.api.views',
|
||||
url(r'^(?P<pk>[0-9]+)/activity_stream/$', 'group_activity_stream_list'),
|
||||
url(r'^(?P<pk>[0-9]+)/inventory_sources/$', 'group_inventory_sources_list'),
|
||||
url(r'^(?P<pk>[0-9]+)/ad_hoc_commands/$', 'group_ad_hoc_commands_list'),
|
||||
url(r'^(?P<pk>[0-9]+)/access_list/$', 'group_access_list'),
|
||||
#url(r'^(?P<pk>[0-9]+)/single_fact/$', 'group_single_fact_view'),
|
||||
)
|
||||
|
||||
@ -150,11 +155,17 @@ credential_urls = patterns('awx.api.views',
|
||||
url(r'^$', 'credential_list'),
|
||||
url(r'^(?P<pk>[0-9]+)/activity_stream/$', 'credential_activity_stream_list'),
|
||||
url(r'^(?P<pk>[0-9]+)/$', 'credential_detail'),
|
||||
url(r'^(?P<pk>[0-9]+)/access_list/$', 'credential_access_list'),
|
||||
# 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'),
|
||||
)
|
||||
|
||||
job_template_urls = patterns('awx.api.views',
|
||||
@ -169,6 +180,7 @@ job_template_urls = patterns('awx.api.views',
|
||||
url(r'^(?P<pk>[0-9]+)/notifiers_any/$', 'job_template_notifiers_any_list'),
|
||||
url(r'^(?P<pk>[0-9]+)/notifiers_error/$', 'job_template_notifiers_error_list'),
|
||||
url(r'^(?P<pk>[0-9]+)/notifiers_success/$', 'job_template_notifiers_success_list'),
|
||||
url(r'^(?P<pk>[0-9]+)/access_list/$', 'job_template_access_list'),
|
||||
)
|
||||
|
||||
job_urls = patterns('awx.api.views',
|
||||
@ -266,7 +278,6 @@ v1_urls = patterns('awx.api.views',
|
||||
url(r'^me/$', 'user_me_list'),
|
||||
url(r'^dashboard/$', 'dashboard_view'),
|
||||
url(r'^dashboard/graphs/jobs/$', 'dashboard_jobs_graph_view'),
|
||||
url(r'^dashboard/graphs/inventory/$', 'dashboard_inventory_graph_view'),
|
||||
url(r'^settings/', include(settings_urls)),
|
||||
url(r'^schedules/', include(schedule_urls)),
|
||||
url(r'^organizations/', include(organization_urls)),
|
||||
@ -281,7 +292,7 @@ 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'^job_templates/', include(job_template_urls)),
|
||||
url(r'^jobs/', include(job_urls)),
|
||||
url(r'^job_host_summaries/', include(job_host_summary_urls)),
|
||||
|
||||
351
awx/api/views.py
351
awx/api/views.py
@ -131,6 +131,7 @@ class ApiV1RootView(APIView):
|
||||
data['system_job_templates'] = reverse('api:system_job_template_list')
|
||||
data['system_jobs'] = reverse('api:system_job_list')
|
||||
data['schedules'] = reverse('api:schedule_list')
|
||||
data['roles'] = reverse('api:role_list')
|
||||
data['notifiers'] = reverse('api:notifier_list')
|
||||
data['notifications'] = reverse('api:notification_list')
|
||||
data['unified_job_templates'] = reverse('api:unified_job_template_list')
|
||||
@ -213,7 +214,7 @@ class ApiV1ConfigView(APIView):
|
||||
user_ldap_fields.extend(getattr(settings, 'AUTH_LDAP_USER_FLAGS_BY_GROUP', {}).keys())
|
||||
data['user_ldap_fields'] = user_ldap_fields
|
||||
|
||||
if request.user.is_superuser or request.user.admin_of_organizations.filter(active=True).count():
|
||||
if request.user.is_superuser or Organization.accessible_objects(request.user, {'write': True}).count():
|
||||
data.update(dict(
|
||||
project_base_dir = settings.PROJECTS_ROOT,
|
||||
project_local_paths = Project.get_local_path_choices(),
|
||||
@ -286,8 +287,7 @@ class DashboardView(APIView):
|
||||
def get(self, request, format=None):
|
||||
''' Show Dashboard Details '''
|
||||
data = OrderedDict()
|
||||
data['related'] = {'jobs_graph': reverse('api:dashboard_jobs_graph_view'),
|
||||
'inventory_graph': reverse('api:dashboard_inventory_graph_view')}
|
||||
data['related'] = {'jobs_graph': reverse('api:dashboard_jobs_graph_view')}
|
||||
user_inventory = get_user_queryset(request.user, Inventory)
|
||||
inventory_with_failed_hosts = user_inventory.filter(hosts_with_active_failures__gt=0)
|
||||
user_inventory_external = user_inventory.filter(has_inventory_sources=True)
|
||||
@ -433,49 +433,6 @@ class DashboardJobsGraphView(APIView):
|
||||
element[1]])
|
||||
return Response(dashboard_data)
|
||||
|
||||
class DashboardInventoryGraphView(APIView):
|
||||
|
||||
view_name = "Dashboard Inventory Graphs"
|
||||
new_in_200 = True
|
||||
|
||||
def get(self, request, format=None):
|
||||
period = request.query_params.get('period', 'month')
|
||||
|
||||
end_date = now()
|
||||
if period == 'month':
|
||||
start_date = end_date - dateutil.relativedelta.relativedelta(months=1)
|
||||
start_date = start_date.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
delta = dateutil.relativedelta.relativedelta(days=1)
|
||||
elif period == 'week':
|
||||
start_date = end_date - dateutil.relativedelta.relativedelta(weeks=1)
|
||||
start_date = start_date.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
delta = dateutil.relativedelta.relativedelta(days=1)
|
||||
elif period == 'day':
|
||||
start_date = end_date - dateutil.relativedelta.relativedelta(days=1)
|
||||
start_date = start_date.replace(minute=0, second=0, microsecond=0)
|
||||
delta = dateutil.relativedelta.relativedelta(hours=1)
|
||||
else:
|
||||
raise ParseError(u'Unknown period "%s"' % force_text(period))
|
||||
|
||||
host_stats = []
|
||||
date = start_date
|
||||
while date < end_date:
|
||||
next_date = date + delta
|
||||
# Find all hosts that existed at end of intevral that are still
|
||||
# active or were deleted after the end of interval. Slow but
|
||||
# accurate; haven't yet found a better way to do it.
|
||||
hosts_qs = Host.objects.filter(created__lt=next_date)
|
||||
hosts_qs = hosts_qs.filter(Q(active=True) | Q(active=False, modified__gte=next_date))
|
||||
hostnames = set()
|
||||
for name, active in hosts_qs.values_list('name', 'active').iterator():
|
||||
if not active:
|
||||
name = re.sub(r'^_deleted_.*?_', '', name)
|
||||
hostnames.add(name)
|
||||
host_stats.append((time.mktime(date.timetuple()), len(hostnames)))
|
||||
date = next_date
|
||||
|
||||
return Response({'hosts': host_stats})
|
||||
|
||||
|
||||
class ScheduleList(ListAPIView):
|
||||
|
||||
@ -571,7 +528,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'])
|
||||
@ -607,7 +564,7 @@ class OrganizationList(ListCreateAPIView):
|
||||
# by the license, then we are only willing to create this organization
|
||||
# if no organizations exist in the system.
|
||||
if (not feature_enabled('multiple_organizations') and
|
||||
self.model.objects.filter(active=True).count() > 0):
|
||||
self.model.objects.count() > 0):
|
||||
raise LicenseForbids('Your Tower license only permits a single '
|
||||
'organization to exist.')
|
||||
|
||||
@ -703,16 +660,16 @@ class OrganizationUsersList(SubListCreateAttachDetachAPIView):
|
||||
model = User
|
||||
serializer_class = UserSerializer
|
||||
parent_model = Organization
|
||||
relationship = 'users'
|
||||
relationship = 'member_role.members'
|
||||
|
||||
class OrganizationAdminsList(SubListCreateAttachDetachAPIView):
|
||||
|
||||
model = User
|
||||
serializer_class = UserSerializer
|
||||
parent_model = Organization
|
||||
relationship = 'admins'
|
||||
relationship = 'admin_role.members'
|
||||
|
||||
class OrganizationProjectsList(SubListCreateAttachDetachAPIView):
|
||||
class OrganizationProjectsList(SubListCreateAPIView):
|
||||
|
||||
model = Project
|
||||
serializer_class = ProjectSerializer
|
||||
@ -774,6 +731,12 @@ class OrganizationNotifiersSuccessList(SubListCreateAttachDetachAPIView):
|
||||
parent_model = Organization
|
||||
relationship = 'notifiers_success'
|
||||
|
||||
class OrganizationAccessList(ResourceAccessList):
|
||||
|
||||
model = User # needs to be User for AccessLists's
|
||||
resource_model = Organization
|
||||
new_in_300 = True
|
||||
|
||||
class TeamList(ListCreateAPIView):
|
||||
|
||||
model = Team
|
||||
@ -789,26 +752,28 @@ class TeamUsersList(SubListCreateAttachDetachAPIView):
|
||||
model = User
|
||||
serializer_class = UserSerializer
|
||||
parent_model = Team
|
||||
relationship = 'users'
|
||||
relationship = 'member_role.members'
|
||||
|
||||
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.
|
||||
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.filter(id__in=Role.visible_roles(self.request.user))
|
||||
|
||||
# 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):
|
||||
|
||||
@ -852,6 +817,11 @@ class TeamActivityStreamList(SubListAPIView):
|
||||
Q(credential__in=parent.credentials.all()) |
|
||||
Q(permission__in=parent.permissions.all()))
|
||||
|
||||
class TeamAccessList(ResourceAccessList):
|
||||
|
||||
model = User # needs to be User for AccessLists's
|
||||
resource_model = Team
|
||||
new_in_300 = True
|
||||
|
||||
class ProjectList(ListCreateAPIView):
|
||||
|
||||
@ -861,7 +831,7 @@ class ProjectList(ListCreateAPIView):
|
||||
def get(self, request, *args, **kwargs):
|
||||
# Not optimal, but make sure the project status and last_updated fields
|
||||
# are up to date here...
|
||||
projects_qs = Project.objects.filter(active=True)
|
||||
projects_qs = Project.objects
|
||||
projects_qs = projects_qs.select_related('current_job', 'last_job')
|
||||
for project in projects_qs:
|
||||
project._set_status_and_last_job_run()
|
||||
@ -886,13 +856,6 @@ class ProjectPlaybooks(RetrieveAPIView):
|
||||
model = Project
|
||||
serializer_class = ProjectPlaybooksSerializer
|
||||
|
||||
class ProjectOrganizationsList(SubListCreateAttachDetachAPIView):
|
||||
|
||||
model = Organization
|
||||
serializer_class = OrganizationSerializer
|
||||
parent_model = Project
|
||||
relationship = 'organizations'
|
||||
|
||||
class ProjectTeamsList(SubListCreateAttachDetachAPIView):
|
||||
|
||||
model = Team
|
||||
@ -1016,6 +979,12 @@ class ProjectUpdateNotificationsList(SubListAPIView):
|
||||
parent_model = Project
|
||||
relationship = 'notifications'
|
||||
|
||||
class ProjectAccessList(ResourceAccessList):
|
||||
|
||||
model = User # needs to be User for AccessLists's
|
||||
resource_model = Project
|
||||
new_in_300 = True
|
||||
|
||||
class UserList(ListCreateAPIView):
|
||||
|
||||
model = User
|
||||
@ -1030,20 +999,42 @@ class UserMeList(ListAPIView):
|
||||
def get_queryset(self):
|
||||
return self.model.objects.filter(pk=self.request.user.pk)
|
||||
|
||||
class UserTeamsList(SubListAPIView):
|
||||
class UserTeamsList(ListAPIView):
|
||||
|
||||
model = Team
|
||||
model = User
|
||||
serializer_class = TeamSerializer
|
||||
parent_model = User
|
||||
relationship = 'teams'
|
||||
|
||||
class UserPermissionsList(SubListCreateAttachDetachAPIView):
|
||||
def get_queryset(self):
|
||||
u = User.objects.get(pk=self.kwargs['pk'])
|
||||
if not u.accessible_by(self.request.user, {'read': True}):
|
||||
raise PermissionDenied()
|
||||
return Team.accessible_objects(self.request.user, {'read': True}).filter(member_role__members=u)
|
||||
|
||||
model = Permission
|
||||
serializer_class = PermissionSerializer
|
||||
class UserRolesList(SubListCreateAttachDetachAPIView):
|
||||
|
||||
model = Role
|
||||
serializer_class = RoleSerializer
|
||||
parent_model = User
|
||||
relationship = 'permissions'
|
||||
parent_key = 'user'
|
||||
relationship='roles'
|
||||
permission_classes = (IsAuthenticated,)
|
||||
|
||||
def get_queryset(self):
|
||||
#u = User.objects.get(pk=self.kwargs['pk'])
|
||||
return Role.visible_roles(self.request.user).filter(members__in=[int(self.kwargs['pk']), ])
|
||||
|
||||
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)
|
||||
|
||||
def check_parent_access(self, parent=None):
|
||||
# We hide roles that shouldn't be seen in our queryset
|
||||
return True
|
||||
|
||||
|
||||
|
||||
class UserProjectsList(SubListAPIView):
|
||||
|
||||
@ -1055,8 +1046,9 @@ class UserProjectsList(SubListAPIView):
|
||||
def get_queryset(self):
|
||||
parent = self.get_parent_object()
|
||||
self.check_parent_access(parent)
|
||||
qs = self.request.user.get_queryset(self.model)
|
||||
return qs.filter(teams__in=parent.teams.distinct())
|
||||
my_qs = Project.accessible_objects(self.request.user, {'read': True})
|
||||
user_qs = Project.accessible_objects(parent, {'read': True})
|
||||
return my_qs & user_qs
|
||||
|
||||
class UserCredentialsList(SubListCreateAttachDetachAPIView):
|
||||
|
||||
@ -1132,10 +1124,14 @@ class UserDetail(RetrieveUpdateDestroyAPIView):
|
||||
can_delete = request.user.can_access(User, 'delete', obj)
|
||||
if not can_delete:
|
||||
raise PermissionDenied('Cannot delete user')
|
||||
for own_credential in Credential.objects.filter(user=obj):
|
||||
own_credential.mark_inactive()
|
||||
return super(UserDetail, self).destroy(request, *args, **kwargs)
|
||||
|
||||
class UserAccessList(ResourceAccessList):
|
||||
|
||||
model = User # needs to be User for AccessLists's
|
||||
resource_model = User
|
||||
new_in_300 = True
|
||||
|
||||
class CredentialList(ListCreateAPIView):
|
||||
|
||||
model = Credential
|
||||
@ -1164,10 +1160,11 @@ class CredentialActivityStreamList(SubListAPIView):
|
||||
# Okay, let it through.
|
||||
return super(type(self), self).get(request, *args, **kwargs)
|
||||
|
||||
class PermissionDetail(RetrieveUpdateDestroyAPIView):
|
||||
class CredentialAccessList(ResourceAccessList):
|
||||
|
||||
model = Permission
|
||||
serializer_class = PermissionSerializer
|
||||
model = User # needs to be User for AccessLists's
|
||||
resource_model = Credential
|
||||
new_in_300 = True
|
||||
|
||||
class InventoryScriptList(ListCreateAPIView):
|
||||
|
||||
@ -1228,6 +1225,12 @@ class InventoryActivityStreamList(SubListAPIView):
|
||||
qs = self.request.user.get_queryset(self.model)
|
||||
return qs.filter(Q(inventory=parent) | Q(host__in=parent.hosts.all()) | Q(group__in=parent.groups.all()))
|
||||
|
||||
class InventoryAccessList(ResourceAccessList):
|
||||
|
||||
model = User # needs to be User for AccessLists's
|
||||
resource_model = Inventory
|
||||
new_in_300 = True
|
||||
|
||||
class InventoryJobTemplateList(SubListAPIView):
|
||||
|
||||
model = JobTemplate
|
||||
@ -1370,7 +1373,7 @@ class HostFactVersionsList(ListAPIView, ParentMixin, SystemTrackingEnforcementMi
|
||||
to_spec = dateutil.parser.parse(to_spec)
|
||||
|
||||
host_obj = self.get_parent_object()
|
||||
|
||||
|
||||
return Fact.get_timeline(host_obj.id, module=module_spec, ts_from=from_spec, ts_to=to_spec)
|
||||
|
||||
def list(self, *args, **kwargs):
|
||||
@ -1426,7 +1429,7 @@ class GroupChildrenList(SubListCreateAttachDetachAPIView):
|
||||
if sub_id is not None:
|
||||
return super(GroupChildrenList, self).unattach(request, *args, **kwargs)
|
||||
parent = self.get_parent_object()
|
||||
parent.mark_inactive()
|
||||
parent.delete()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
def _unattach(self, request, *args, **kwargs): # FIXME: Disabled for now for UI support.
|
||||
@ -1449,8 +1452,8 @@ class GroupChildrenList(SubListCreateAttachDetachAPIView):
|
||||
sub, self.relationship):
|
||||
raise PermissionDenied()
|
||||
|
||||
if sub.parents.filter(active=True).exclude(pk=parent.pk).count() == 0:
|
||||
sub.mark_inactive()
|
||||
if sub.parents.exclude(pk=parent.pk).count() == 0:
|
||||
sub.delete()
|
||||
else:
|
||||
relationship.remove(sub)
|
||||
|
||||
@ -1509,7 +1512,7 @@ class GroupAllHostsList(SubListAPIView):
|
||||
def get_queryset(self):
|
||||
parent = self.get_parent_object()
|
||||
self.check_parent_access(parent)
|
||||
qs = self.request.user.get_queryset(self.model)
|
||||
qs = self.request.user.get_queryset(self.model).distinct() # need distinct for '&' operator
|
||||
sublist_qs = parent.all_hosts.distinct()
|
||||
return qs & sublist_qs
|
||||
|
||||
@ -1552,17 +1555,18 @@ class GroupDetail(RetrieveUpdateDestroyAPIView):
|
||||
|
||||
def destroy(self, request, *args, **kwargs):
|
||||
obj = self.get_object()
|
||||
# FIXME: Why isn't the active check being caught earlier by RBAC?
|
||||
if not getattr(obj, 'active', True):
|
||||
raise Http404()
|
||||
if not getattr(obj, 'is_active', True):
|
||||
raise Http404()
|
||||
if not request.user.can_access(self.model, 'delete', obj):
|
||||
raise PermissionDenied()
|
||||
if hasattr(obj, 'mark_inactive'):
|
||||
obj.mark_inactive_recursive()
|
||||
obj.delete_recursive()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
class GroupAccessList(ResourceAccessList):
|
||||
|
||||
model = User # needs to be User for AccessLists's
|
||||
resource_model = Group
|
||||
new_in_300 = True
|
||||
|
||||
|
||||
class InventoryGroupsList(SubListCreateAttachDetachAPIView):
|
||||
|
||||
model = Group
|
||||
@ -1582,7 +1586,7 @@ class InventoryRootGroupsList(SubListCreateAttachDetachAPIView):
|
||||
def get_queryset(self):
|
||||
parent = self.get_parent_object()
|
||||
self.check_parent_access(parent)
|
||||
qs = self.request.user.get_queryset(self.model)
|
||||
qs = self.request.user.get_queryset(self.model).distinct() # need distinct for '&' operator
|
||||
return qs & parent.root_groups
|
||||
|
||||
class BaseVariableData(RetrieveUpdateAPIView):
|
||||
@ -1620,9 +1624,9 @@ class InventoryScriptView(RetrieveAPIView):
|
||||
hostvars = bool(request.query_params.get('hostvars', ''))
|
||||
show_all = bool(request.query_params.get('all', ''))
|
||||
if show_all:
|
||||
hosts_q = dict(active=True)
|
||||
hosts_q = dict()
|
||||
else:
|
||||
hosts_q = dict(active=True, enabled=True)
|
||||
hosts_q = dict(enabled=True)
|
||||
if hostname:
|
||||
host = get_object_or_404(obj.hosts, name=hostname, **hosts_q)
|
||||
data = host.variables_dict
|
||||
@ -1640,8 +1644,7 @@ class InventoryScriptView(RetrieveAPIView):
|
||||
all_group['hosts'] = groupless_hosts
|
||||
|
||||
# Build in-memory mapping of groups and their hosts.
|
||||
group_hosts_kw = dict(group__inventory_id=obj.id, group__active=True,
|
||||
host__inventory_id=obj.id, host__active=True)
|
||||
group_hosts_kw = dict(group__inventory_id=obj.id, host__inventory_id=obj.id)
|
||||
if 'enabled' in hosts_q:
|
||||
group_hosts_kw['host__enabled'] = hosts_q['enabled']
|
||||
group_hosts_qs = Group.hosts.through.objects.filter(**group_hosts_kw)
|
||||
@ -1654,8 +1657,8 @@ class InventoryScriptView(RetrieveAPIView):
|
||||
|
||||
# Build in-memory mapping of groups and their children.
|
||||
group_parents_qs = Group.parents.through.objects.filter(
|
||||
from_group__inventory_id=obj.id, from_group__active=True,
|
||||
to_group__inventory_id=obj.id, to_group__active=True,
|
||||
from_group__inventory_id=obj.id,
|
||||
to_group__inventory_id=obj.id,
|
||||
)
|
||||
group_parents_qs = group_parents_qs.order_by('from_group__name')
|
||||
group_parents_qs = group_parents_qs.values_list('from_group_id', 'from_group__name', 'to_group_id')
|
||||
@ -1665,7 +1668,7 @@ class InventoryScriptView(RetrieveAPIView):
|
||||
group_children.append(from_group_name)
|
||||
|
||||
# Now use in-memory maps to build up group info.
|
||||
for group in obj.groups.filter(active=True):
|
||||
for group in obj.groups.all():
|
||||
group_info = OrderedDict()
|
||||
group_info['hosts'] = group_hosts_map.get(group.id, [])
|
||||
group_info['children'] = group_children_map.get(group.id, [])
|
||||
@ -1711,9 +1714,9 @@ class InventoryTreeView(RetrieveAPIView):
|
||||
|
||||
def retrieve(self, request, *args, **kwargs):
|
||||
inventory = self.get_object()
|
||||
group_children_map = inventory.get_group_children_map(active=True)
|
||||
root_group_pks = inventory.root_groups.filter(active=True).order_by('name').values_list('pk', flat=True)
|
||||
groups_qs = inventory.groups.filter(active=True)
|
||||
group_children_map = inventory.get_group_children_map()
|
||||
root_group_pks = inventory.root_groups.order_by('name').values_list('pk', flat=True)
|
||||
groups_qs = inventory.groups
|
||||
groups_qs = groups_qs.select_related('inventory')
|
||||
groups_qs = groups_qs.prefetch_related('inventory_source')
|
||||
all_group_data = GroupSerializer(groups_qs, many=True).data
|
||||
@ -1917,7 +1920,7 @@ class JobTemplateLaunch(RetrieveAPIView, GenericAPIView):
|
||||
if obj:
|
||||
for p in obj.passwords_needed_to_start:
|
||||
data[p] = u''
|
||||
if obj.credential and obj.credential.active:
|
||||
if obj.credential:
|
||||
data.pop('credential', None)
|
||||
else:
|
||||
data['credential'] = None
|
||||
@ -2117,7 +2120,7 @@ class JobTemplateCallback(GenericAPIView):
|
||||
return set()
|
||||
# Find the host objects to search for a match.
|
||||
obj = self.get_object()
|
||||
qs = obj.inventory.hosts.filter(active=True)
|
||||
qs = obj.inventory.hosts
|
||||
# First try for an exact match on the name.
|
||||
try:
|
||||
return set([qs.get(name__in=remote_hosts)])
|
||||
@ -2177,7 +2180,7 @@ class JobTemplateCallback(GenericAPIView):
|
||||
# match again.
|
||||
inventory_sources_already_updated = []
|
||||
if len(matching_hosts) != 1:
|
||||
inventory_sources = job_template.inventory.inventory_sources.filter(active=True, update_on_launch=True)
|
||||
inventory_sources = job_template.inventory.inventory_sources.filter( update_on_launch=True)
|
||||
inventory_update_pks = set()
|
||||
for inventory_source in inventory_sources:
|
||||
if inventory_source.needs_update_on_launch:
|
||||
@ -2244,6 +2247,12 @@ class JobTemplateJobsList(SubListCreateAPIView):
|
||||
relationship = 'jobs'
|
||||
parent_key = 'job_template'
|
||||
|
||||
class JobTemplateAccessList(ResourceAccessList):
|
||||
|
||||
model = User # needs to be User for AccessLists's
|
||||
resource_model = JobTemplate
|
||||
new_in_300 = True
|
||||
|
||||
class SystemJobTemplateList(ListAPIView):
|
||||
|
||||
model = SystemJobTemplate
|
||||
@ -3035,7 +3044,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)
|
||||
@ -3246,6 +3255,116 @@ class SettingsReset(APIView):
|
||||
TowerSettings.objects.filter(key=settings_key).delete()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
|
||||
class RoleList(ListAPIView):
|
||||
|
||||
model = Role
|
||||
serializer_class = RoleSerializer
|
||||
permission_classes = (IsAuthenticated,)
|
||||
new_in_300 = True
|
||||
|
||||
def get_queryset(self):
|
||||
if self.request.user.is_superuser:
|
||||
return Role.objects
|
||||
return Role.visible_roles(self.request.user)
|
||||
|
||||
|
||||
class RoleDetail(RetrieveAPIView):
|
||||
|
||||
model = Role
|
||||
serializer_class = RoleSerializer
|
||||
permission_classes = (IsAuthenticated,)
|
||||
new_in_300 = True
|
||||
|
||||
|
||||
class RoleUsersList(SubListCreateAttachDetachAPIView):
|
||||
|
||||
model = User
|
||||
serializer_class = UserSerializer
|
||||
parent_model = Role
|
||||
relationship = 'members'
|
||||
permission_classes = (IsAuthenticated,)
|
||||
new_in_300 = True
|
||||
|
||||
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'
|
||||
permission_classes = (IsAuthenticated,)
|
||||
new_in_300 = True
|
||||
|
||||
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'
|
||||
permission_classes = (IsAuthenticated,)
|
||||
new_in_300 = True
|
||||
|
||||
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'
|
||||
permission_classes = (IsAuthenticated,)
|
||||
new_in_300 = True
|
||||
|
||||
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
|
||||
|
||||
|
||||
|
||||
|
||||
# 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).
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -2,10 +2,28 @@
|
||||
# All Rights Reserved.
|
||||
|
||||
# Django
|
||||
from django.db.models.signals import (
|
||||
post_init,
|
||||
pre_save,
|
||||
post_save,
|
||||
post_delete,
|
||||
)
|
||||
from django.db.models.signals import m2m_changed
|
||||
from django.db import models
|
||||
from django.db.models.fields.related import SingleRelatedObjectDescriptor
|
||||
from django.db.models.fields.related import (
|
||||
add_lazy_relation,
|
||||
SingleRelatedObjectDescriptor,
|
||||
ReverseSingleRelatedObjectDescriptor,
|
||||
ManyRelatedObjectsDescriptor,
|
||||
ReverseManyRelatedObjectsDescriptor,
|
||||
)
|
||||
|
||||
# AWX
|
||||
from awx.main.models.rbac import RolePermission, Role, batch_role_ancestor_rebuilding
|
||||
|
||||
|
||||
__all__ = ['AutoOneToOneField', 'ImplicitRoleField']
|
||||
|
||||
__all__ = ['AutoOneToOneField']
|
||||
|
||||
# Based on AutoOneToOneField from django-annoying:
|
||||
# https://bitbucket.org/offline/django-annoying/src/a0de8b294db3/annoying/fields.py
|
||||
@ -32,3 +50,213 @@ class AutoOneToOneField(models.OneToOneField):
|
||||
def contribute_to_related_class(self, cls, related):
|
||||
setattr(cls, related.get_accessor_name(),
|
||||
AutoSingleRelatedObjectDescriptor(related))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
def resolve_role_field(obj, field):
|
||||
ret = []
|
||||
|
||||
field_components = field.split('.', 1)
|
||||
if hasattr(obj, field_components[0]):
|
||||
obj = getattr(obj, field_components[0])
|
||||
else:
|
||||
return []
|
||||
|
||||
if len(field_components) == 1:
|
||||
if type(obj) is not ImplicitRoleDescriptor and type(obj) is not Role:
|
||||
raise Exception('%s refers to a %s, not an ImplicitRoleField or Role' % (field, str(type(obj))))
|
||||
ret.append(obj)
|
||||
else:
|
||||
if type(obj) is ManyRelatedObjectsDescriptor:
|
||||
for o in obj.all():
|
||||
ret += resolve_role_field(o, field_components[1])
|
||||
else:
|
||||
ret += resolve_role_field(obj, field_components[1])
|
||||
|
||||
return ret
|
||||
|
||||
|
||||
class ImplicitRoleDescriptor(ReverseSingleRelatedObjectDescriptor):
|
||||
pass
|
||||
|
||||
|
||||
class ImplicitRoleField(models.ForeignKey):
|
||||
"""Implicitly creates a role entry for a resource"""
|
||||
|
||||
def __init__(self, role_name=None, role_description=None, permissions=None, parent_role=None, *args, **kwargs):
|
||||
self.role_name = role_name
|
||||
self.role_description = role_description if role_description else ""
|
||||
self.permissions = permissions
|
||||
self.parent_role = parent_role
|
||||
|
||||
kwargs.setdefault('to', 'Role')
|
||||
kwargs.setdefault('related_name', '+')
|
||||
kwargs.setdefault('null', 'True')
|
||||
super(ImplicitRoleField, self).__init__(*args, **kwargs)
|
||||
|
||||
def contribute_to_class(self, cls, name):
|
||||
super(ImplicitRoleField, self).contribute_to_class(cls, name)
|
||||
setattr(cls, self.name, ImplicitRoleDescriptor(self))
|
||||
|
||||
if not hasattr(cls, '__implicit_role_fields'):
|
||||
setattr(cls, '__implicit_role_fields', [])
|
||||
getattr(cls, '__implicit_role_fields').append(self)
|
||||
|
||||
post_init.connect(self._post_init, cls, True, dispatch_uid='implicit-role-post-init')
|
||||
pre_save.connect(self._pre_save, cls, True, dispatch_uid='implicit-role-pre-save')
|
||||
post_save.connect(self._post_save, cls, True, dispatch_uid='implicit-role-post-save')
|
||||
post_delete.connect(self._post_delete, cls, True)
|
||||
add_lazy_relation(cls, self, "self", self.bind_m2m_changed)
|
||||
|
||||
def bind_m2m_changed(self, _self, _role_class, cls):
|
||||
if not self.parent_role:
|
||||
return
|
||||
|
||||
field_names = self.parent_role
|
||||
if type(field_names) is not list:
|
||||
field_names = [field_names]
|
||||
|
||||
for field_name in field_names:
|
||||
if field_name.startswith('singleton:'):
|
||||
continue
|
||||
|
||||
field_name, sep, field_attr = field_name.partition('.')
|
||||
field = getattr(cls, field_name)
|
||||
|
||||
if type(field) is ReverseManyRelatedObjectsDescriptor or \
|
||||
type(field) is ManyRelatedObjectsDescriptor:
|
||||
|
||||
if '.' in field_attr:
|
||||
raise Exception('Referencing deep roles through ManyToMany fields is unsupported.')
|
||||
|
||||
if type(field) is ReverseManyRelatedObjectsDescriptor:
|
||||
sender = field.through
|
||||
else:
|
||||
sender = field.related.through
|
||||
|
||||
reverse = type(field) is ManyRelatedObjectsDescriptor
|
||||
m2m_changed.connect(self.m2m_update(field_attr, reverse), sender, weak=False)
|
||||
|
||||
def m2m_update(self, field_attr, _reverse):
|
||||
def _m2m_update(instance, action, model, pk_set, reverse, **kwargs):
|
||||
if action == 'post_add' or action == 'pre_remove':
|
||||
if _reverse:
|
||||
reverse = not reverse
|
||||
|
||||
if reverse:
|
||||
for pk in pk_set:
|
||||
obj = model.objects.get(pk=pk)
|
||||
if action == 'post_add':
|
||||
getattr(instance, field_attr).children.add(getattr(obj, self.name))
|
||||
if action == 'pre_remove':
|
||||
getattr(instance, field_attr).children.remove(getattr(obj, self.name))
|
||||
|
||||
else:
|
||||
for pk in pk_set:
|
||||
obj = model.objects.get(pk=pk)
|
||||
if action == 'post_add':
|
||||
getattr(instance, self.name).parents.add(getattr(obj, field_attr))
|
||||
if action == 'pre_remove':
|
||||
getattr(instance, self.name).parents.remove(getattr(obj, field_attr))
|
||||
return _m2m_update
|
||||
|
||||
|
||||
def _post_init(self, instance, *args, **kwargs):
|
||||
original_parent_roles = dict()
|
||||
if instance.pk:
|
||||
for implicit_role_field in getattr(instance.__class__, '__implicit_role_fields'):
|
||||
original_parent_roles[implicit_role_field.name] = implicit_role_field._resolve_parent_roles(instance)
|
||||
|
||||
setattr(instance, '__original_parent_roles', original_parent_roles)
|
||||
|
||||
def _create_role_instance_if_not_exists(self, instance):
|
||||
role = getattr(instance, self.name, None)
|
||||
if role:
|
||||
return role
|
||||
role = Role.objects.create(
|
||||
name=self.role_name,
|
||||
description=self.role_description
|
||||
)
|
||||
setattr(instance, self.name, role)
|
||||
|
||||
def _patch_role_content_object_and_grant_permissions(self, instance):
|
||||
role = getattr(instance, self.name)
|
||||
role.content_object = instance
|
||||
role.save()
|
||||
|
||||
if self.permissions is not None:
|
||||
permissions = RolePermission(
|
||||
role=role,
|
||||
resource=instance,
|
||||
auto_generated=True
|
||||
)
|
||||
|
||||
if 'all' in self.permissions and self.permissions['all']:
|
||||
del self.permissions['all']
|
||||
self.permissions['create'] = True
|
||||
self.permissions['read'] = True
|
||||
self.permissions['write'] = True
|
||||
self.permissions['update'] = True
|
||||
self.permissions['delete'] = True
|
||||
self.permissions['scm_update'] = True
|
||||
self.permissions['use'] = True
|
||||
self.permissions['execute'] = True
|
||||
|
||||
for k,v in self.permissions.items():
|
||||
setattr(permissions, k, v)
|
||||
permissions.save()
|
||||
|
||||
def _pre_save(self, instance, *args, **kwargs):
|
||||
for implicit_role_field in getattr(instance.__class__, '__implicit_role_fields'):
|
||||
implicit_role_field._create_role_instance_if_not_exists(instance)
|
||||
|
||||
def _post_save(self, instance, created, *args, **kwargs):
|
||||
if created:
|
||||
for implicit_role_field in getattr(instance.__class__, '__implicit_role_fields'):
|
||||
implicit_role_field._patch_role_content_object_and_grant_permissions(instance)
|
||||
|
||||
original_parent_roles = getattr(instance, '__original_parent_roles')
|
||||
|
||||
if created:
|
||||
for implicit_role_field in getattr(instance.__class__, '__implicit_role_fields'):
|
||||
original_parent_roles[implicit_role_field.name] = set()
|
||||
|
||||
new_parent_roles = dict()
|
||||
for implicit_role_field in getattr(instance.__class__, '__implicit_role_fields'):
|
||||
new_parent_roles[implicit_role_field.name] = implicit_role_field._resolve_parent_roles(instance)
|
||||
setattr(instance, '__original_parent_roles', new_parent_roles)
|
||||
|
||||
with batch_role_ancestor_rebuilding():
|
||||
for implicit_role_field in getattr(instance.__class__, '__implicit_role_fields'):
|
||||
cur_role = getattr(instance, implicit_role_field.name)
|
||||
original_parents = original_parent_roles[implicit_role_field.name]
|
||||
new_parents = new_parent_roles[implicit_role_field.name]
|
||||
cur_role.parents.remove(*list(original_parents - new_parents))
|
||||
cur_role.parents.add(*list(new_parents - original_parents))
|
||||
|
||||
|
||||
def _resolve_parent_roles(self, instance):
|
||||
if not self.parent_role:
|
||||
return set()
|
||||
|
||||
paths = self.parent_role if type(self.parent_role) is list else [self.parent_role]
|
||||
parent_roles = set()
|
||||
for path in paths:
|
||||
if path.startswith("singleton:"):
|
||||
parents = [Role.singleton(path[10:])]
|
||||
else:
|
||||
parents = resolve_role_field(instance, path)
|
||||
for parent in parents:
|
||||
parent_roles.add(parent)
|
||||
return parent_roles
|
||||
|
||||
def _post_delete(self, instance, *args, **kwargs):
|
||||
this_role = getattr(instance, self.name)
|
||||
children = [c for c in this_role.children.all()]
|
||||
this_role.delete()
|
||||
with batch_role_ancestor_rebuilding():
|
||||
for child in children:
|
||||
child.rebuild_role_ancestor_list()
|
||||
|
||||
@ -111,8 +111,6 @@ class Command(BaseCommand):
|
||||
|
||||
n_deleted_items = 0
|
||||
n_deleted_items += self.cleanup_model(User)
|
||||
for model in self.get_models(PrimordialModel):
|
||||
n_deleted_items += self.cleanup_model(model)
|
||||
|
||||
if not self.dry_run:
|
||||
self.logger.log(99, "Removed %d items", n_deleted_items)
|
||||
|
||||
@ -19,7 +19,7 @@ class Command(BaseCommand):
|
||||
|
||||
# Create a default organization as the first superuser found.
|
||||
try:
|
||||
superuser = User.objects.filter(is_superuser=True, is_active=True).order_by('pk')[0]
|
||||
superuser = User.objects.filter(is_superuser=True).order_by('pk')[0]
|
||||
except IndexError:
|
||||
superuser = None
|
||||
with impersonate(superuser):
|
||||
|
||||
352
awx/main/management/commands/generate_dummy_data.py
Normal file
352
awx/main/management/commands/generate_dummy_data.py
Normal file
@ -0,0 +1,352 @@
|
||||
# Copyright (c) 2016 Ansible, Inc.
|
||||
# All Rights Reserved
|
||||
|
||||
# Python
|
||||
import sys
|
||||
from collections import defaultdict
|
||||
from optparse import make_option
|
||||
|
||||
|
||||
# Django
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.utils.timezone import now
|
||||
from django.contrib.auth.models import User
|
||||
from django.db import transaction
|
||||
|
||||
# awx
|
||||
from awx.main.models import * # noqa
|
||||
|
||||
|
||||
|
||||
class Rollback(Exception):
|
||||
pass
|
||||
|
||||
class Command(BaseCommand):
|
||||
option_list = BaseCommand.option_list + (
|
||||
make_option('--organizations', action='store', type='int', default=3,
|
||||
help='Number of organizations to create'),
|
||||
make_option('--users', action='store', type='int', default=10,
|
||||
help='Number of users to create'),
|
||||
make_option('--teams', action='store', type='int', default=5,
|
||||
help='Number of teams to create'),
|
||||
make_option('--projects', action='store', type='int', default=10,
|
||||
help='Number of projects to create'),
|
||||
make_option('--job-templates', action='store', type='int', default=20,
|
||||
help='Number of job templates to create'),
|
||||
make_option('--credentials', action='store', type='int', default=5,
|
||||
help='Number of credentials to create'),
|
||||
make_option('--inventories', action='store', type='int', default=5,
|
||||
help='Number of credentials to create'),
|
||||
make_option('--inventory-groups', action='store', type='int', default=10,
|
||||
help='Number of credentials to create'),
|
||||
make_option('--inventory-hosts', action='store', type='int', default=40,
|
||||
help='number of credentials to create'),
|
||||
make_option('--jobs', action='store', type='int', default=200,
|
||||
help='number of job entries to create'),
|
||||
make_option('--job-events', action='store', type='int', default=500,
|
||||
help='number of job event entries to create'),
|
||||
make_option('--pretend', action='store_true',
|
||||
help="Don't commit the data to the database"),
|
||||
make_option('--prefix', action='store', type='string', default='',
|
||||
help="Prefix generated names with this string"),
|
||||
#make_option('--spread-bias', action='store', type='string', default='exponential',
|
||||
# help='"exponential" to bias associations exponentially front loaded for - for ex'),
|
||||
)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
n_organizations = int(options['organizations'])
|
||||
n_users = int(options['users'])
|
||||
n_teams = int(options['teams'])
|
||||
n_projects = int(options['projects'])
|
||||
n_job_templates = int(options['job_templates'])
|
||||
n_credentials = int(options['credentials'])
|
||||
n_inventories = int(options['inventories'])
|
||||
n_inventory_groups = int(options['inventory_groups'])
|
||||
n_inventory_hosts = int(options['inventory_hosts'])
|
||||
n_jobs = int(options['jobs'])
|
||||
n_job_events = int(options['job_events'])
|
||||
prefix = options['prefix']
|
||||
|
||||
organizations = []
|
||||
users = []
|
||||
teams = []
|
||||
projects = []
|
||||
job_templates = []
|
||||
credentials = []
|
||||
inventories = []
|
||||
inventory_groups = []
|
||||
inventory_hosts = []
|
||||
jobs = []
|
||||
#job_events = []
|
||||
|
||||
def spread(n, m):
|
||||
ret = []
|
||||
# At least one in each slot, split up the rest exponentially so the first
|
||||
# buckets contain a lot of entries
|
||||
for i in xrange(m):
|
||||
if n > 0:
|
||||
ret.append(1)
|
||||
n -= 1
|
||||
else:
|
||||
ret.append(0)
|
||||
|
||||
for i in xrange(m):
|
||||
n_in_this_slot = n // 2
|
||||
n-= n_in_this_slot
|
||||
ret[i] += n_in_this_slot
|
||||
if n > 0 and len(ret):
|
||||
ret[0] += n
|
||||
return ret
|
||||
|
||||
ids = defaultdict(lambda: 0)
|
||||
|
||||
|
||||
try:
|
||||
|
||||
with transaction.atomic():
|
||||
with batch_role_ancestor_rebuilding():
|
||||
|
||||
print('# Creating %d organizations' % n_organizations)
|
||||
for i in xrange(n_organizations):
|
||||
sys.stdout.write('\r%d ' % (i + 1))
|
||||
sys.stdout.flush()
|
||||
organizations.append(Organization.objects.create(name='%s Organization %d' % (prefix, i)))
|
||||
print('')
|
||||
|
||||
print('# Creating %d users' % n_users)
|
||||
org_idx = 0
|
||||
for n in spread(n_users, n_organizations):
|
||||
for i in range(n):
|
||||
ids['user'] += 1
|
||||
user_id = ids['user']
|
||||
sys.stdout.write('\r Assigning %d to %s: %d ' % (n, organizations[org_idx].name, i+ 1))
|
||||
sys.stdout.flush()
|
||||
user = User.objects.create(username='%suser-%d' % (prefix, user_id))
|
||||
organizations[org_idx].member_role.members.add(user)
|
||||
users.append(user)
|
||||
org_idx += 1
|
||||
print('')
|
||||
|
||||
print('# Creating %d teams' % n_teams)
|
||||
org_idx = 0
|
||||
for n in spread(n_teams, n_organizations):
|
||||
org = organizations[org_idx]
|
||||
for i in range(n):
|
||||
ids['team'] += 1
|
||||
team_id = ids['team']
|
||||
sys.stdout.write('\r Assigning %d to %s: %d ' % (n, org.name, i+ 1))
|
||||
sys.stdout.flush()
|
||||
team = Team.objects.create(name='%s Team %d Org %d' % (prefix, team_id, org_idx), organization=org)
|
||||
teams.append(team)
|
||||
org_idx += 1
|
||||
print('')
|
||||
|
||||
print('# Adding users to teams')
|
||||
for org in organizations:
|
||||
org_teams = [t for t in org.teams.all()]
|
||||
org_users = [u for u in org.member_role.members.all()]
|
||||
print(' Spreading %d users accross %d teams for %s' % (len(org_users), len(org_teams), org.name))
|
||||
# Our normal spread for most users
|
||||
cur_user_idx = 0
|
||||
cur_team_idx = 0
|
||||
for n in spread(len(org_users), len(org_teams)):
|
||||
team = org_teams[cur_team_idx]
|
||||
for i in range(n):
|
||||
if cur_user_idx < len(org_users):
|
||||
user = org_users[cur_user_idx]
|
||||
team.member_role.members.add(user)
|
||||
cur_user_idx += 1
|
||||
cur_team_idx += 1
|
||||
|
||||
# First user gets added to all teams
|
||||
for team in org_teams:
|
||||
team.member_role.members.add(org_users[0])
|
||||
|
||||
|
||||
print('# Creating %d credentials for users' % (n_credentials - n_credentials // 2))
|
||||
user_idx = 0
|
||||
for n in spread(n_credentials - n_credentials // 2, n_users):
|
||||
user = users[user_idx]
|
||||
for i in range(n):
|
||||
ids['credential'] += 1
|
||||
sys.stdout.write('\r %d ' % (ids['credential']))
|
||||
sys.stdout.flush()
|
||||
credential_id = ids['credential']
|
||||
credential = Credential.objects.create(name='%s Credential %d User %d' % (prefix, credential_id, user_idx), user=user)
|
||||
credentials.append(credential)
|
||||
user_idx += 1
|
||||
print('')
|
||||
|
||||
print('# Creating %d credentials for teams' % (n_credentials // 2))
|
||||
team_idx = 0
|
||||
starting_credential_id = ids['credential']
|
||||
for n in spread(n_credentials - n_credentials // 2, n_teams):
|
||||
team = teams[team_idx]
|
||||
for i in range(n):
|
||||
ids['credential'] += 1
|
||||
sys.stdout.write('\r %d ' % (ids['credential'] - starting_credential_id))
|
||||
sys.stdout.flush()
|
||||
credential_id = ids['credential']
|
||||
credential = Credential.objects.create(name='%s Credential %d team %d' % (prefix, credential_id, team_idx), team=team)
|
||||
credentials.append(credential)
|
||||
team_idx += 1
|
||||
print('')
|
||||
|
||||
print('# Creating %d projects' % n_projects)
|
||||
org_idx = 0
|
||||
for n in spread(n_projects, n_organizations):
|
||||
org = organizations[org_idx]
|
||||
for i in range(n):
|
||||
ids['project'] += 1
|
||||
project_id = ids['project']
|
||||
sys.stdout.write('\r Assigning %d to %s: %d ' % (n, org.name, i+ 1))
|
||||
sys.stdout.flush()
|
||||
project = Project.objects.create(name='%s Project %d Org %d' % (prefix, project_id, org_idx), organization=org)
|
||||
projects.append(project)
|
||||
|
||||
org_idx += 1
|
||||
print('')
|
||||
|
||||
|
||||
print('# Creating %d inventories' % n_inventories)
|
||||
org_idx = 0
|
||||
for n in spread(n_inventories, min(n_inventories // 4 + 1, n_organizations)):
|
||||
org = organizations[org_idx]
|
||||
for i in range(n):
|
||||
ids['inventory'] += 1
|
||||
inventory_id = ids['inventory']
|
||||
sys.stdout.write('\r Assigning %d to %s: %d ' % (n, org.name, i+ 1))
|
||||
sys.stdout.flush()
|
||||
inventory = Inventory.objects.create(name='%s Inventory %d Org %d' % (prefix, inventory_id, org_idx), organization=org)
|
||||
inventories.append(inventory)
|
||||
|
||||
org_idx += 1
|
||||
print('')
|
||||
|
||||
|
||||
print('# Creating %d inventory_groups' % n_inventory_groups)
|
||||
inv_idx = 0
|
||||
for n in spread(n_inventory_groups, n_inventories):
|
||||
inventory = inventories[inv_idx]
|
||||
parent_list = [None] * 3
|
||||
for i in range(n):
|
||||
ids['group'] += 1
|
||||
group_id = ids['group']
|
||||
sys.stdout.write('\r Assigning %d to %s: %d ' % (n, inventory.name, i+ 1))
|
||||
sys.stdout.flush()
|
||||
group = Group.objects.create(
|
||||
name='%s Group %d Inventory %d' % (prefix, group_id, inv_idx),
|
||||
inventory=inventory,
|
||||
)
|
||||
# Have each group have up to 3 parent groups
|
||||
for parent_n in range(3):
|
||||
if i // 4 + parent_n < len(parent_list) and parent_list[i // 4 + parent_n]:
|
||||
group.parents.add(parent_list[i // 4 + parent_n])
|
||||
if parent_list[i // 4] is None:
|
||||
parent_list[i // 4] = group
|
||||
else:
|
||||
parent_list.append(group)
|
||||
inventory_groups.append(group)
|
||||
|
||||
inv_idx += 1
|
||||
print('')
|
||||
|
||||
|
||||
print('# Creating %d inventory_hosts' % n_inventory_hosts)
|
||||
group_idx = 0
|
||||
for n in spread(n_inventory_hosts, n_inventory_groups):
|
||||
group = inventory_groups[group_idx]
|
||||
for i in range(n):
|
||||
ids['host'] += 1
|
||||
host_id = ids['host']
|
||||
sys.stdout.write('\r Assigning %d to %s: %d ' % (n, group.name, i+ 1))
|
||||
sys.stdout.flush()
|
||||
host = Host.objects.create(name='%s Host %d Group %d' % (prefix, host_id, group_idx), inventory=group.inventory)
|
||||
# Add the host to up to 3 groups
|
||||
host.groups.add(group)
|
||||
for m in range(2):
|
||||
if group_idx + m < len(inventory_groups) and group.inventory.id == inventory_groups[group_idx + m].inventory.id:
|
||||
host.groups.add(inventory_groups[group_idx + m])
|
||||
|
||||
inventory_hosts.append(host)
|
||||
|
||||
group_idx += 1
|
||||
print('')
|
||||
|
||||
print('# Creating %d job_templates' % n_job_templates)
|
||||
project_idx = 0
|
||||
inv_idx = 0
|
||||
for n in spread(n_job_templates, n_projects):
|
||||
project = projects[project_idx]
|
||||
for i in range(n):
|
||||
ids['job_template'] += 1
|
||||
job_template_id = ids['job_template']
|
||||
sys.stdout.write('\r Assigning %d to %s: %d ' % (n, project.name, i+ 1))
|
||||
sys.stdout.flush()
|
||||
|
||||
inventory = None
|
||||
org_inv_count = project.organization.inventories.count()
|
||||
if org_inv_count > 0:
|
||||
inventory = project.organization.inventories.all()[inv_idx % org_inv_count]
|
||||
|
||||
job_template = JobTemplate.objects.create(
|
||||
name='%s Job Template %d Project %d' % (prefix, job_template_id, project_idx),
|
||||
inventory=inventory,
|
||||
project=project,
|
||||
)
|
||||
job_templates.append(job_template)
|
||||
inv_idx += 1
|
||||
project_idx += 1
|
||||
print('')
|
||||
|
||||
print('# Creating %d jobs' % n_jobs)
|
||||
group_idx = 0
|
||||
job_template_idx = 0
|
||||
for n in spread(n_jobs, n_job_templates):
|
||||
job_template = job_templates[job_template_idx]
|
||||
for i in range(n):
|
||||
sys.stdout.write('\r Assigning %d to %s: %d ' % (n, job_template.name, i+ 1))
|
||||
sys.stdout.flush()
|
||||
job = Job.objects.create(job_template=job_template)
|
||||
jobs.append(job)
|
||||
|
||||
if job_template.inventory:
|
||||
inv_groups = [g for g in job_template.inventory.groups.all()]
|
||||
if len(inv_groups):
|
||||
JobHostSummary.objects.bulk_create([
|
||||
JobHostSummary(
|
||||
job=job, host=h, host_name=h.name, processed=1,
|
||||
created=now(), modified=now()
|
||||
)
|
||||
for h in inv_groups[group_idx % len(inv_groups)].hosts.all()[:100]
|
||||
])
|
||||
group_idx += 1
|
||||
job_template_idx += 1
|
||||
if n:
|
||||
print('')
|
||||
|
||||
print('# Creating %d job events' % n_job_events)
|
||||
job_idx = 0
|
||||
for n in spread(n_job_events, n_jobs):
|
||||
job = jobs[job_idx]
|
||||
sys.stdout.write('\r Creating %d job events for job %d' % (n, job.id))
|
||||
sys.stdout.flush()
|
||||
JobEvent.objects.bulk_create([
|
||||
JobEvent(
|
||||
created=now(),
|
||||
modified=now(),
|
||||
job=job,
|
||||
event='runner_on_ok'
|
||||
)
|
||||
for i in range(n)
|
||||
])
|
||||
job_idx += 1
|
||||
if n:
|
||||
print('')
|
||||
|
||||
if options['pretend']:
|
||||
raise Rollback()
|
||||
except Rollback:
|
||||
print('Rolled back changes')
|
||||
pass
|
||||
return
|
||||
@ -53,13 +53,13 @@ class MemObject(object):
|
||||
'''
|
||||
Common code shared between in-memory groups and hosts.
|
||||
'''
|
||||
|
||||
|
||||
def __init__(self, name, source_dir):
|
||||
assert name, 'no name'
|
||||
assert source_dir, 'no source dir'
|
||||
self.name = name
|
||||
self.source_dir = source_dir
|
||||
|
||||
|
||||
def load_vars(self, base_path):
|
||||
all_vars = {}
|
||||
files_found = 0
|
||||
@ -107,7 +107,7 @@ class MemGroup(MemObject):
|
||||
group_vars = os.path.join(source_dir, 'group_vars', self.name)
|
||||
self.variables = self.load_vars(group_vars)
|
||||
logger.debug('Loaded group: %s', self.name)
|
||||
|
||||
|
||||
def child_group_by_name(self, name, loader):
|
||||
if name == 'all':
|
||||
return
|
||||
@ -266,7 +266,7 @@ class BaseLoader(object):
|
||||
logger.debug('Filtering group %s', name)
|
||||
return None
|
||||
if name not in self.all_group.all_groups:
|
||||
group = MemGroup(name, self.source_dir)
|
||||
group = MemGroup(name, self.source_dir)
|
||||
if not child:
|
||||
all_group.add_child_group(group)
|
||||
self.all_group.all_groups[name] = group
|
||||
@ -315,7 +315,7 @@ class IniLoader(BaseLoader):
|
||||
for t in tokens[1:]:
|
||||
k,v = t.split('=', 1)
|
||||
host.variables[k] = v
|
||||
group.add_host(host)
|
||||
group.add_host(host)
|
||||
elif input_mode == 'children':
|
||||
group.child_group_by_name(line, self)
|
||||
elif input_mode == 'vars':
|
||||
@ -328,7 +328,7 @@ class IniLoader(BaseLoader):
|
||||
# from API documentation:
|
||||
#
|
||||
# if called with --list, inventory outputs like so:
|
||||
#
|
||||
#
|
||||
# {
|
||||
# "databases" : {
|
||||
# "hosts" : [ "host1.example.com", "host2.example.com" ],
|
||||
@ -581,7 +581,7 @@ class Command(NoArgsCommand):
|
||||
def _get_instance_id(self, from_dict, default=''):
|
||||
'''
|
||||
Retrieve the instance ID from the given dict of host variables.
|
||||
|
||||
|
||||
The instance ID variable may be specified as 'foo.bar', in which case
|
||||
the lookup will traverse into nested dicts, equivalent to:
|
||||
|
||||
@ -633,7 +633,7 @@ class Command(NoArgsCommand):
|
||||
else:
|
||||
q = dict(name=self.inventory_name)
|
||||
try:
|
||||
self.inventory = Inventory.objects.filter(active=True).get(**q)
|
||||
self.inventory = Inventory.objects.get(**q)
|
||||
except Inventory.DoesNotExist:
|
||||
raise CommandError('Inventory with %s = %s cannot be found' % q.items()[0])
|
||||
except Inventory.MultipleObjectsReturned:
|
||||
@ -648,8 +648,7 @@ class Command(NoArgsCommand):
|
||||
if inventory_source_id:
|
||||
try:
|
||||
self.inventory_source = InventorySource.objects.get(pk=inventory_source_id,
|
||||
inventory=self.inventory,
|
||||
active=True)
|
||||
inventory=self.inventory)
|
||||
except InventorySource.DoesNotExist:
|
||||
raise CommandError('Inventory source with id=%s not found' %
|
||||
inventory_source_id)
|
||||
@ -669,7 +668,6 @@ class Command(NoArgsCommand):
|
||||
source_path=os.path.abspath(self.source),
|
||||
overwrite=self.overwrite,
|
||||
overwrite_vars=self.overwrite_vars,
|
||||
active=True,
|
||||
)
|
||||
self.inventory_update = self.inventory_source.create_inventory_update(
|
||||
job_args=json.dumps(sys.argv),
|
||||
@ -703,7 +701,7 @@ class Command(NoArgsCommand):
|
||||
host_qs = self.inventory_source.group.all_hosts
|
||||
else:
|
||||
host_qs = self.inventory.hosts.all()
|
||||
host_qs = host_qs.filter(active=True, instance_id='',
|
||||
host_qs = host_qs.filter(instance_id='',
|
||||
variables__contains=self.instance_id_var.split('.')[0])
|
||||
for host in host_qs:
|
||||
instance_id = self._get_instance_id(host.variables_dict)
|
||||
@ -740,7 +738,7 @@ class Command(NoArgsCommand):
|
||||
hosts_qs = self.inventory_source.group.all_hosts
|
||||
# FIXME: Also include hosts from inventory_source.managed_hosts?
|
||||
else:
|
||||
hosts_qs = self.inventory.hosts.filter(active=True)
|
||||
hosts_qs = self.inventory.hosts
|
||||
# Build list of all host pks, remove all that should not be deleted.
|
||||
del_host_pks = set(hosts_qs.values_list('pk', flat=True))
|
||||
if self.instance_id_var:
|
||||
@ -765,7 +763,7 @@ class Command(NoArgsCommand):
|
||||
del_pks = all_del_pks[offset:(offset + self._batch_size)]
|
||||
for host in hosts_qs.filter(pk__in=del_pks):
|
||||
host_name = host.name
|
||||
host.mark_inactive()
|
||||
host.delete()
|
||||
self.logger.info('Deleted host "%s"', host_name)
|
||||
if settings.SQL_DEBUG:
|
||||
self.logger.warning('host deletions took %d queries for %d hosts',
|
||||
@ -785,7 +783,7 @@ class Command(NoArgsCommand):
|
||||
groups_qs = self.inventory_source.group.all_children
|
||||
# FIXME: Also include groups from inventory_source.managed_groups?
|
||||
else:
|
||||
groups_qs = self.inventory.groups.filter(active=True)
|
||||
groups_qs = self.inventory.groups
|
||||
# Build list of all group pks, remove those that should not be deleted.
|
||||
del_group_pks = set(groups_qs.values_list('pk', flat=True))
|
||||
all_group_names = self.all_group.all_groups.keys()
|
||||
@ -799,7 +797,8 @@ class Command(NoArgsCommand):
|
||||
del_pks = all_del_pks[offset:(offset + self._batch_size)]
|
||||
for group in groups_qs.filter(pk__in=del_pks):
|
||||
group_name = group.name
|
||||
group.mark_inactive(recompute=False)
|
||||
with ignore_inventory_computed_fields():
|
||||
group.delete()
|
||||
self.logger.info('Group "%s" deleted', group_name)
|
||||
if settings.SQL_DEBUG:
|
||||
self.logger.warning('group deletions took %d queries for %d groups',
|
||||
@ -821,10 +820,10 @@ class Command(NoArgsCommand):
|
||||
if self.inventory_source.group:
|
||||
db_groups = self.inventory_source.group.all_children
|
||||
else:
|
||||
db_groups = self.inventory.groups.filter(active=True)
|
||||
db_groups = self.inventory.groups
|
||||
for db_group in db_groups:
|
||||
# Delete child group relationships not present in imported data.
|
||||
db_children = db_group.children.filter(active=True)
|
||||
db_children = db_group.children
|
||||
db_children_name_pk_map = dict(db_children.values_list('name', 'pk'))
|
||||
mem_children = self.all_group.all_groups[db_group.name].children
|
||||
for mem_group in mem_children:
|
||||
@ -839,7 +838,7 @@ class Command(NoArgsCommand):
|
||||
db_child.name, db_group.name)
|
||||
# FIXME: Inventory source group relationships
|
||||
# Delete group/host relationships not present in imported data.
|
||||
db_hosts = db_group.hosts.filter(active=True)
|
||||
db_hosts = db_group.hosts
|
||||
del_host_pks = set(db_hosts.values_list('pk', flat=True))
|
||||
mem_hosts = self.all_group.all_groups[db_group.name].hosts
|
||||
all_mem_host_names = [h.name for h in mem_hosts if not h.instance_id]
|
||||
@ -860,7 +859,7 @@ class Command(NoArgsCommand):
|
||||
del_pks = del_host_pks[offset:(offset + self._batch_size)]
|
||||
for db_host in db_hosts.filter(pk__in=del_pks):
|
||||
group_host_count += 1
|
||||
if db_host not in db_group.hosts.filter(active=True):
|
||||
if db_host not in db_group.hosts:
|
||||
continue
|
||||
db_group.hosts.remove(db_host)
|
||||
self.logger.info('Host "%s" removed from group "%s"',
|
||||
@ -1036,7 +1035,7 @@ class Command(NoArgsCommand):
|
||||
all_host_pks = sorted(mem_host_pk_map.keys())
|
||||
for offset in xrange(0, len(all_host_pks), self._batch_size):
|
||||
host_pks = all_host_pks[offset:(offset + self._batch_size)]
|
||||
for db_host in self.inventory.hosts.filter(active=True, pk__in=host_pks):
|
||||
for db_host in self.inventory.hosts.filter( pk__in=host_pks):
|
||||
if db_host.pk in host_pks_updated:
|
||||
continue
|
||||
mem_host = mem_host_pk_map[db_host.pk]
|
||||
@ -1048,7 +1047,7 @@ class Command(NoArgsCommand):
|
||||
all_instance_ids = sorted(mem_host_instance_id_map.keys())
|
||||
for offset in xrange(0, len(all_instance_ids), self._batch_size):
|
||||
instance_ids = all_instance_ids[offset:(offset + self._batch_size)]
|
||||
for db_host in self.inventory.hosts.filter(active=True, instance_id__in=instance_ids):
|
||||
for db_host in self.inventory.hosts.filter( instance_id__in=instance_ids):
|
||||
if db_host.pk in host_pks_updated:
|
||||
continue
|
||||
mem_host = mem_host_instance_id_map[db_host.instance_id]
|
||||
@ -1060,7 +1059,7 @@ class Command(NoArgsCommand):
|
||||
all_host_names = sorted(mem_host_name_map.keys())
|
||||
for offset in xrange(0, len(all_host_names), self._batch_size):
|
||||
host_names = all_host_names[offset:(offset + self._batch_size)]
|
||||
for db_host in self.inventory.hosts.filter(active=True, name__in=host_names):
|
||||
for db_host in self.inventory.hosts.filter( name__in=host_names):
|
||||
if db_host.pk in host_pks_updated:
|
||||
continue
|
||||
mem_host = mem_host_name_map[db_host.name]
|
||||
@ -1297,7 +1296,7 @@ class Command(NoArgsCommand):
|
||||
except CommandError as e:
|
||||
self.mark_license_failure(save=True)
|
||||
raise e
|
||||
|
||||
|
||||
if self.inventory_source.group:
|
||||
inv_name = 'group "%s"' % (self.inventory_source.group.name)
|
||||
else:
|
||||
@ -1336,7 +1335,7 @@ class Command(NoArgsCommand):
|
||||
self.inventory_update.result_traceback = tb
|
||||
self.inventory_update.status = status
|
||||
self.inventory_update.save(update_fields=['status', 'result_traceback'])
|
||||
|
||||
|
||||
if exc and isinstance(exc, CommandError):
|
||||
sys.exit(1)
|
||||
elif exc:
|
||||
|
||||
@ -13,9 +13,9 @@ class HostManager(models.Manager):
|
||||
def active_count(self):
|
||||
"""Return count of active, unique hosts for licensing."""
|
||||
try:
|
||||
return self.filter(active=True, inventory__active=True).order_by('name').distinct('name').count()
|
||||
return self.order_by('name').distinct('name').count()
|
||||
except NotImplementedError: # For unit tests only, SQLite doesn't support distinct('name')
|
||||
return len(set(self.filter(active=True, inventory__active=True).values_list('name', flat=True)))
|
||||
return len(set(self.values_list('name', flat=True)))
|
||||
|
||||
class InstanceManager(models.Manager):
|
||||
"""A custom manager class for the Instance model.
|
||||
|
||||
65
awx/main/migrations/0006_v300_active_flag_removal.py
Normal file
65
awx/main/migrations/0006_v300_active_flag_removal.py
Normal file
@ -0,0 +1,65 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from awx.main.migrations import _cleanup_deleted as cleanup_deleted
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('main', '0005_v300_migrate_facts'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(cleanup_deleted.cleanup_deleted),
|
||||
|
||||
migrations.RemoveField(
|
||||
model_name='credential',
|
||||
name='active',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='custominventoryscript',
|
||||
name='active',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='group',
|
||||
name='active',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='host',
|
||||
name='active',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='inventory',
|
||||
name='active',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='notifier',
|
||||
name='active',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='organization',
|
||||
name='active',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='permission',
|
||||
name='active',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='schedule',
|
||||
name='active',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='team',
|
||||
name='active',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='unifiedjob',
|
||||
name='active',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='unifiedjobtemplate',
|
||||
name='active',
|
||||
),
|
||||
]
|
||||
223
awx/main/migrations/0007_v300_rbac_changes.py
Normal file
223
awx/main/migrations/0007_v300_rbac_changes.py
Normal file
@ -0,0 +1,223 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
import taggit.managers
|
||||
import awx.main.fields
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('taggit', '0002_auto_20150616_2121'),
|
||||
('contenttypes', '0002_remove_content_type_name'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('main', '0006_v300_active_flag_removal'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
'Organization',
|
||||
'admins',
|
||||
'deprecated_admins',
|
||||
),
|
||||
migrations.RenameField(
|
||||
'Organization',
|
||||
'users',
|
||||
'deprecated_users',
|
||||
),
|
||||
migrations.RenameField(
|
||||
'Team',
|
||||
'users',
|
||||
'deprecated_users',
|
||||
),
|
||||
|
||||
migrations.CreateModel(
|
||||
name='Role',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('created', models.DateTimeField(default=None, editable=False)),
|
||||
('modified', models.DateTimeField(default=None, editable=False)),
|
||||
('description', models.TextField(default=b'', blank=True)),
|
||||
('name', models.CharField(max_length=512)),
|
||||
('singleton_name', models.TextField(default=None, unique=True, null=True, db_index=True)),
|
||||
('object_id', models.PositiveIntegerField(default=None, null=True)),
|
||||
('ancestors', models.ManyToManyField(related_name='descendents', to='main.Role')),
|
||||
('content_type', models.ForeignKey(default=None, to='contenttypes.ContentType', null=True)),
|
||||
('created_by', models.ForeignKey(related_name="{u'class': 'role', u'app_label': 'main'}(class)s_created+", on_delete=django.db.models.deletion.SET_NULL, default=None, editable=False, to=settings.AUTH_USER_MODEL, null=True)),
|
||||
('members', models.ManyToManyField(related_name='roles', to=settings.AUTH_USER_MODEL)),
|
||||
('modified_by', models.ForeignKey(related_name="{u'class': 'role', u'app_label': 'main'}(class)s_modified+", on_delete=django.db.models.deletion.SET_NULL, default=None, editable=False, to=settings.AUTH_USER_MODEL, null=True)),
|
||||
('parents', models.ManyToManyField(related_name='children', to='main.Role')),
|
||||
('tags', taggit.managers.TaggableManager(to='taggit.Tag', through='taggit.TaggedItem', blank=True, help_text='A comma-separated list of tags.', verbose_name='Tags')),
|
||||
],
|
||||
options={
|
||||
'db_table': 'main_rbac_roles',
|
||||
'verbose_name_plural': 'roles',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='RolePermission',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('created', models.DateTimeField(default=None, editable=False)),
|
||||
('modified', models.DateTimeField(default=None, editable=False)),
|
||||
('auto_generated', models.BooleanField(default=False)),
|
||||
('object_id', models.PositiveIntegerField(default=None)),
|
||||
('create', models.IntegerField(default=0)),
|
||||
('read', models.IntegerField(default=0)),
|
||||
('write', models.IntegerField(default=0)),
|
||||
('update', models.IntegerField(default=0)),
|
||||
('delete', models.IntegerField(default=0)),
|
||||
('execute', models.IntegerField(default=0)),
|
||||
('scm_update', models.IntegerField(default=0)),
|
||||
('use', models.IntegerField(default=0)),
|
||||
('content_type', models.ForeignKey(default=None, to='contenttypes.ContentType')),
|
||||
('role', models.ForeignKey(related_name='permissions', to='main.Role')),
|
||||
],
|
||||
options={
|
||||
'db_table': 'main_rbac_permissions',
|
||||
'verbose_name_plural': 'permissions',
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='credential',
|
||||
name='owner_role',
|
||||
field=awx.main.fields.ImplicitRoleField(related_name='+', to='main.Role', null=b'True'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='credential',
|
||||
name='auditor_role',
|
||||
field=awx.main.fields.ImplicitRoleField(related_name='+', to='main.Role', null=b'True'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='credential',
|
||||
name='usage_role',
|
||||
field=awx.main.fields.ImplicitRoleField(related_name='+', to='main.Role', null=b'True'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='group',
|
||||
name='admin_role',
|
||||
field=awx.main.fields.ImplicitRoleField(related_name='+', to='main.Role', null=b'True'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='group',
|
||||
name='auditor_role',
|
||||
field=awx.main.fields.ImplicitRoleField(related_name='+', to='main.Role', null=b'True'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='group',
|
||||
name='executor_role',
|
||||
field=awx.main.fields.ImplicitRoleField(related_name='+', to='main.Role', null=b'True'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='group',
|
||||
name='updater_role',
|
||||
field=awx.main.fields.ImplicitRoleField(related_name='+', to='main.Role', null=b'True'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='inventory',
|
||||
name='admin_role',
|
||||
field=awx.main.fields.ImplicitRoleField(related_name='+', to='main.Role', null=b'True'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='inventory',
|
||||
name='auditor_role',
|
||||
field=awx.main.fields.ImplicitRoleField(related_name='+', to='main.Role', null=b'True'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='inventory',
|
||||
name='executor_role',
|
||||
field=awx.main.fields.ImplicitRoleField(related_name='+', to='main.Role', null=b'True'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='inventory',
|
||||
name='updater_role',
|
||||
field=awx.main.fields.ImplicitRoleField(related_name='+', to='main.Role', null=b'True'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='jobtemplate',
|
||||
name='admin_role',
|
||||
field=awx.main.fields.ImplicitRoleField(related_name='+', to='main.Role', null=b'True'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='jobtemplate',
|
||||
name='auditor_role',
|
||||
field=awx.main.fields.ImplicitRoleField(related_name='+', to='main.Role', null=b'True'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='jobtemplate',
|
||||
name='executor_role',
|
||||
field=awx.main.fields.ImplicitRoleField(related_name='+', to='main.Role', null=b'True'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='organization',
|
||||
name='admin_role',
|
||||
field=awx.main.fields.ImplicitRoleField(related_name='+', to='main.Role', null=b'True'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='organization',
|
||||
name='auditor_role',
|
||||
field=awx.main.fields.ImplicitRoleField(related_name='+', to='main.Role', null=b'True'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='organization',
|
||||
name='member_role',
|
||||
field=awx.main.fields.ImplicitRoleField(related_name='+', to='main.Role', null=b'True'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='project',
|
||||
name='admin_role',
|
||||
field=awx.main.fields.ImplicitRoleField(related_name='+', to='main.Role', null=b'True'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='project',
|
||||
name='auditor_role',
|
||||
field=awx.main.fields.ImplicitRoleField(related_name='+', to='main.Role', null=b'True'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='project',
|
||||
name='member_role',
|
||||
field=awx.main.fields.ImplicitRoleField(related_name='+', to='main.Role', null=b'True'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='project',
|
||||
name='scm_update_role',
|
||||
field=awx.main.fields.ImplicitRoleField(related_name='+', to='main.Role', null=b'True'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='team',
|
||||
name='admin_role',
|
||||
field=awx.main.fields.ImplicitRoleField(related_name='+', to='main.Role', null=b'True'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='team',
|
||||
name='auditor_role',
|
||||
field=awx.main.fields.ImplicitRoleField(related_name='+', to='main.Role', null=b'True'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='team',
|
||||
name='member_role',
|
||||
field=awx.main.fields.ImplicitRoleField(related_name='+', to='main.Role', null=b'True'),
|
||||
),
|
||||
migrations.AlterIndexTogether(
|
||||
name='rolepermission',
|
||||
index_together=set([('content_type', 'object_id')]),
|
||||
),
|
||||
migrations.RenameField(
|
||||
model_name='organization',
|
||||
old_name='projects',
|
||||
new_name='deprecated_projects',
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='organization',
|
||||
name='deprecated_projects',
|
||||
field=models.ManyToManyField(related_name='deprecated_organizations', to='main.Project', blank=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='project',
|
||||
name='organization',
|
||||
field=models.ForeignKey(related_name='projects', to='main.Organization', blank=True, null=True),
|
||||
),
|
||||
]
|
||||
21
awx/main/migrations/0008_v300_rbac_migrations.py
Normal file
21
awx/main/migrations/0008_v300_rbac_migrations.py
Normal file
@ -0,0 +1,21 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from awx.main.migrations import _rbac as rbac
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('main', '0007_v300_rbac_changes'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(rbac.migrate_users),
|
||||
migrations.RunPython(rbac.migrate_organization),
|
||||
migrations.RunPython(rbac.migrate_team),
|
||||
migrations.RunPython(rbac.migrate_inventory),
|
||||
migrations.RunPython(rbac.migrate_projects),
|
||||
migrations.RunPython(rbac.migrate_credential),
|
||||
]
|
||||
@ -107,7 +107,7 @@ def create_system_job_templates(apps, schema_editor):
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('main', '0005_v300_migrate_facts'),
|
||||
('main', '0008_v300_rbac_migrations'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
85
awx/main/migrations/_cleanup_deleted.py
Normal file
85
awx/main/migrations/_cleanup_deleted.py
Normal file
@ -0,0 +1,85 @@
|
||||
# Copyright (c) 2016 Ansible, Inc.
|
||||
# All Rights Reserved.
|
||||
|
||||
# Python
|
||||
import logging
|
||||
|
||||
# Django
|
||||
from django.db import transaction
|
||||
from django.utils.dateparse import parse_datetime
|
||||
|
||||
def cleanup_deleted(apps, schema_editor):
|
||||
logger = logging.getLogger('awx.main.migrations.cleanup_deleted')
|
||||
|
||||
def cleanup_model(model):
|
||||
|
||||
'''
|
||||
Presume the '_deleted_' string to be in the 'name' field unless considering the User model.
|
||||
When considering the User model, presume the '_d_' string to be in the 'username' field.
|
||||
'''
|
||||
logger.debug('cleaning up model %s', model)
|
||||
|
||||
name_field = 'name'
|
||||
name_prefix = '_deleted_'
|
||||
active_field = None
|
||||
n_deleted_items = 0
|
||||
for field in model._meta.fields:
|
||||
if field.name in ('is_active', 'active'):
|
||||
active_field = field.name
|
||||
if field.name == 'is_active': # is User model
|
||||
name_field = 'username'
|
||||
name_prefix = '_d_'
|
||||
if not active_field:
|
||||
logger.warning('skipping model %s, no active field', model)
|
||||
return n_deleted_items
|
||||
qs = model.objects.filter(**{
|
||||
active_field: False,
|
||||
'%s__startswith' % name_field: name_prefix,
|
||||
})
|
||||
pks_to_delete = set()
|
||||
for instance in qs.iterator():
|
||||
dt = parse_datetime(getattr(instance, name_field).split('_')[2])
|
||||
if not dt:
|
||||
logger.warning('unable to find deleted timestamp in %s field', name_field)
|
||||
else:
|
||||
action_text = 'deleting'
|
||||
logger.info('%s %s', action_text, instance)
|
||||
n_deleted_items += 1
|
||||
instance.delete()
|
||||
|
||||
# Cleanup objects in batches instead of deleting each one individually.
|
||||
if len(pks_to_delete) >= 50:
|
||||
model.objects.filter(pk__in=pks_to_delete).delete()
|
||||
pks_to_delete.clear()
|
||||
if len(pks_to_delete):
|
||||
model.objects.filter(pk__in=pks_to_delete).delete()
|
||||
return n_deleted_items
|
||||
|
||||
logger = logging.getLogger('awx.main.commands.cleanup_deleted')
|
||||
handler = logging.StreamHandler()
|
||||
handler.setFormatter(logging.Formatter('%(message)s'))
|
||||
logger.addHandler(handler)
|
||||
logger.propagate = False
|
||||
|
||||
with transaction.atomic():
|
||||
n_deleted_items = 0
|
||||
|
||||
models = [
|
||||
apps.get_model('auth', "User"),
|
||||
apps.get_model('main', 'Credential'),
|
||||
apps.get_model('main', 'CustomInventoryScript'),
|
||||
apps.get_model('main', 'Group'),
|
||||
apps.get_model('main', 'Host'),
|
||||
apps.get_model('main', 'Inventory'),
|
||||
apps.get_model('main', 'Notifier'),
|
||||
apps.get_model('main', 'Organization'),
|
||||
apps.get_model('main', 'Permission'),
|
||||
apps.get_model('main', 'Schedule'),
|
||||
apps.get_model('main', 'Team'),
|
||||
apps.get_model('main', 'UnifiedJob'),
|
||||
apps.get_model('main', 'UnifiedJobTemplate'),
|
||||
]
|
||||
|
||||
for model in models:
|
||||
n_deleted_items += cleanup_model(model)
|
||||
logger.log(99, "Removed %d items", n_deleted_items)
|
||||
1675
awx/main/migrations/_old_access.py
Normal file
1675
awx/main/migrations/_old_access.py
Normal file
File diff suppressed because it is too large
Load Diff
369
awx/main/migrations/_rbac.py
Normal file
369
awx/main/migrations/_rbac.py
Normal file
@ -0,0 +1,369 @@
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.db.models import Q
|
||||
|
||||
from collections import defaultdict
|
||||
from awx.main.utils import getattrd
|
||||
import _old_access as old_access
|
||||
|
||||
def migrate_users(apps, schema_editor):
|
||||
migrations = list()
|
||||
|
||||
User = apps.get_model('auth', "User")
|
||||
Role = apps.get_model('main', "Role")
|
||||
RolePermission = apps.get_model('main', "RolePermission")
|
||||
|
||||
for user in User.objects.all():
|
||||
try:
|
||||
Role.objects.get(content_type=ContentType.objects.get_for_model(User), object_id=user.id)
|
||||
except Role.DoesNotExist:
|
||||
role = Role.objects.create(
|
||||
singleton_name = '%s-admin_role' % user.username,
|
||||
content_object = user,
|
||||
)
|
||||
role.members.add(user)
|
||||
RolePermission.objects.create(
|
||||
role = role,
|
||||
resource = user,
|
||||
create=1, read=1, write=1, delete=1, update=1,
|
||||
execute=1, scm_update=1, use=1,
|
||||
)
|
||||
|
||||
if user.is_superuser:
|
||||
Role.singleton('System Administrator').members.add(user)
|
||||
migrations.append(user)
|
||||
return migrations
|
||||
|
||||
def migrate_organization(apps, schema_editor):
|
||||
migrations = defaultdict(list)
|
||||
organization = apps.get_model('main', "Organization")
|
||||
for org in organization.objects.all():
|
||||
for admin in org.deprecated_admins.all():
|
||||
org.admin_role.members.add(admin)
|
||||
migrations[org.name].append(admin)
|
||||
for user in org.deprecated_users.all():
|
||||
org.auditor_role.members.add(user)
|
||||
migrations[org.name].append(user)
|
||||
return migrations
|
||||
|
||||
def migrate_team(apps, schema_editor):
|
||||
migrations = defaultdict(list)
|
||||
team = apps.get_model('main', 'Team')
|
||||
for t in team.objects.all():
|
||||
for user in t.deprecated_users.all():
|
||||
t.member_role.members.add(user)
|
||||
migrations[t.name].append(user)
|
||||
return migrations
|
||||
|
||||
def attrfunc(attr_path):
|
||||
'''attrfunc returns a function that will
|
||||
attempt to use the attr_path to access the attribute
|
||||
of an instance that is passed in to the returned function.
|
||||
|
||||
Example:
|
||||
get_org = attrfunc('inventory.organization')
|
||||
org = get_org(JobTemplateInstance)
|
||||
'''
|
||||
def attr(inst):
|
||||
return getattrd(inst, attr_path)
|
||||
return attr
|
||||
|
||||
def _update_credential_parents(org, cred):
|
||||
org.admin_role.children.add(cred.owner_role)
|
||||
org.member_role.children.add(cred.usage_role)
|
||||
cred.user, cred.team = None, None
|
||||
cred.save()
|
||||
|
||||
def _discover_credentials(instances, cred, orgfunc):
|
||||
'''_discover_credentials will find shared credentials across
|
||||
organizations. If a shared credential is found, it will duplicate
|
||||
the credential, ensure the proper role permissions are added to the new
|
||||
credential, and update any references from the old to the newly created
|
||||
credential.
|
||||
|
||||
instances is a list of all objects that were matched when filtered
|
||||
with cred.
|
||||
|
||||
orgfunc is a function that when called with an instance from instances
|
||||
will produce an Organization object.
|
||||
'''
|
||||
orgs = defaultdict(list)
|
||||
for inst in instances:
|
||||
orgs[orgfunc(inst)].append(inst)
|
||||
|
||||
if len(orgs) == 1:
|
||||
_update_credential_parents(instances[0].inventory.organization, cred)
|
||||
else:
|
||||
for pos, org in enumerate(orgs):
|
||||
if pos == 0:
|
||||
_update_credential_parents(org, cred)
|
||||
else:
|
||||
# Create a new credential
|
||||
cred.pk = None
|
||||
cred.save()
|
||||
|
||||
# Unlink the old information from the new credential
|
||||
cred.user, cred.team = None, None
|
||||
cred.owner_role, cred.usage_role = None, None
|
||||
cred.save()
|
||||
|
||||
for i in orgs[org]:
|
||||
i.credential = cred
|
||||
i.save()
|
||||
_update_credential_parents(org, cred)
|
||||
|
||||
def migrate_credential(apps, schema_editor):
|
||||
Credential = apps.get_model('main', "Credential")
|
||||
JobTemplate = apps.get_model('main', 'JobTemplate')
|
||||
Project = apps.get_model('main', 'Project')
|
||||
InventorySource = apps.get_model('main', 'InventorySource')
|
||||
|
||||
migrated = []
|
||||
for cred in Credential.objects.all():
|
||||
migrated.append(cred)
|
||||
|
||||
results = (JobTemplate.objects.filter(Q(credential=cred) | Q(cloud_credential=cred)).all() or
|
||||
InventorySource.objects.filter(credential=cred).all())
|
||||
if results:
|
||||
if len(results) == 1:
|
||||
_update_credential_parents(results[0].inventory.organization, cred)
|
||||
else:
|
||||
_discover_credentials(results, cred, attrfunc('inventory.organization'))
|
||||
continue
|
||||
|
||||
projs = Project.objects.filter(credential=cred).all()
|
||||
if projs:
|
||||
if len(projs) == 1:
|
||||
_update_credential_parents(projs[0].organization, cred)
|
||||
else:
|
||||
_discover_credentials(projs, cred, attrfunc('organization'))
|
||||
continue
|
||||
|
||||
if cred.team is not None:
|
||||
cred.team.admin_role.children.add(cred.owner_role)
|
||||
cred.team.member_role.children.add(cred.usage_role)
|
||||
cred.user, cred.team = None, None
|
||||
cred.save()
|
||||
|
||||
elif cred.user is not None:
|
||||
cred.user.admin_role.children.add(cred.owner_role)
|
||||
cred.user, cred.team = None, None
|
||||
cred.save()
|
||||
|
||||
# no match found, log
|
||||
return migrated
|
||||
|
||||
|
||||
def migrate_inventory(apps, schema_editor):
|
||||
migrations = defaultdict(dict)
|
||||
|
||||
Inventory = apps.get_model('main', 'Inventory')
|
||||
Permission = apps.get_model('main', 'Permission')
|
||||
|
||||
for inventory in Inventory.objects.all():
|
||||
teams, users = [], []
|
||||
for perm in Permission.objects.filter(inventory=inventory):
|
||||
role = None
|
||||
execrole = None
|
||||
if perm.permission_type == 'admin':
|
||||
role = inventory.admin_role
|
||||
pass
|
||||
elif perm.permission_type == 'read':
|
||||
role = inventory.auditor_role
|
||||
pass
|
||||
elif perm.permission_type == 'write':
|
||||
role = inventory.updater_role
|
||||
pass
|
||||
elif perm.permission_type == 'check':
|
||||
pass
|
||||
elif perm.permission_type == 'run':
|
||||
pass
|
||||
else:
|
||||
raise Exception('Unhandled permission type for inventory: %s' % perm.permission_type)
|
||||
if perm.run_ad_hoc_commands:
|
||||
execrole = inventory.executor_role
|
||||
|
||||
if perm.team:
|
||||
if role:
|
||||
perm.team.member_role.children.add(role)
|
||||
if execrole:
|
||||
perm.team.member_role.children.add(execrole)
|
||||
|
||||
teams.append(perm.team)
|
||||
|
||||
if perm.user:
|
||||
if role:
|
||||
role.members.add(perm.user)
|
||||
if execrole:
|
||||
execrole.members.add(perm.user)
|
||||
users.append(perm.user)
|
||||
migrations[inventory.name]['teams'] = teams
|
||||
migrations[inventory.name]['users'] = users
|
||||
return migrations
|
||||
|
||||
def migrate_projects(apps, schema_editor):
|
||||
'''
|
||||
I can see projects when:
|
||||
X I am a superuser.
|
||||
X I am an admin in an organization associated with the project.
|
||||
X I am a user in an organization associated with the project.
|
||||
X I am on a team associated with the project.
|
||||
X I have been explicitly granted permission to run/check jobs using the
|
||||
project.
|
||||
X I created the project but it isn't associated with an organization
|
||||
I can change/delete when:
|
||||
X I am a superuser.
|
||||
X I am an admin in an organization associated with the project.
|
||||
X I created the project but it isn't associated with an organization
|
||||
'''
|
||||
migrations = defaultdict(lambda: defaultdict(set))
|
||||
|
||||
Project = apps.get_model('main', 'Project')
|
||||
Permission = apps.get_model('main', 'Permission')
|
||||
JobTemplate = apps.get_model('main', 'JobTemplate')
|
||||
|
||||
# Migrate projects to single organizations, duplicating as necessary
|
||||
for project in [p for p in Project.objects.all()]:
|
||||
original_project_name = project.name
|
||||
project_orgs = project.deprecated_organizations.distinct().all()
|
||||
|
||||
if project_orgs.count() > 1:
|
||||
first_org = None
|
||||
for org in project_orgs:
|
||||
if first_org is None:
|
||||
# For the first org, re-use our existing Project object, so don't do the below duplication effort
|
||||
first_org = org
|
||||
project.name = first_org.name + ' - ' + original_project_name
|
||||
project.organization = first_org
|
||||
project.save()
|
||||
else:
|
||||
new_prj = Project.objects.create(
|
||||
created = project.created,
|
||||
description = project.description,
|
||||
name = org.name + ' - ' + original_project_name,
|
||||
old_pk = project.old_pk,
|
||||
created_by_id = project.created_by_id,
|
||||
scm_type = project.scm_type,
|
||||
scm_url = project.scm_url,
|
||||
scm_branch = project.scm_branch,
|
||||
scm_clean = project.scm_clean,
|
||||
scm_delete_on_update = project.scm_delete_on_update,
|
||||
scm_delete_on_next_update = project.scm_delete_on_next_update,
|
||||
scm_update_on_launch = project.scm_update_on_launch,
|
||||
scm_update_cache_timeout = project.scm_update_cache_timeout,
|
||||
credential = project.credential,
|
||||
organization = org
|
||||
)
|
||||
migrations[original_project_name]['projects'].add(new_prj)
|
||||
job_templates = JobTemplate.objects.filter(inventory__organization=org).all()
|
||||
for jt in job_templates:
|
||||
jt.project = new_prj
|
||||
jt.save()
|
||||
|
||||
# Migrate permissions
|
||||
for project in [p for p in Project.objects.all()]:
|
||||
if project.organization is None and project.created_by is not None:
|
||||
project.admin_role.members.add(project.created_by)
|
||||
migrations[project.name]['users'].add(project.created_by)
|
||||
|
||||
for team in project.teams.all():
|
||||
team.member_role.children.add(project.member_role)
|
||||
migrations[project.name]['teams'].add(team)
|
||||
|
||||
if project.organization is not None:
|
||||
for user in project.organization.deprecated_users.all():
|
||||
project.member_role.members.add(user)
|
||||
migrations[project.name]['users'].add(user)
|
||||
|
||||
for perm in Permission.objects.filter(project=project):
|
||||
# All perms at this level just imply a user or team can read
|
||||
if perm.team:
|
||||
perm.team.member_role.children.add(project.member_role)
|
||||
migrations[project.name]['teams'].add(perm.team)
|
||||
|
||||
if perm.user:
|
||||
project.member_role.members.add(perm.user)
|
||||
migrations[project.name]['users'].add(perm.user)
|
||||
|
||||
return migrations
|
||||
|
||||
|
||||
|
||||
def migrate_job_templates(apps, schema_editor):
|
||||
'''
|
||||
NOTE: This must be run after orgs, inventory, projects, credential, and
|
||||
users have been migrated
|
||||
'''
|
||||
|
||||
|
||||
'''
|
||||
I can see job templates when:
|
||||
X I am a superuser.
|
||||
- I can read the inventory, project and credential (which means I am an
|
||||
org admin or member of a team with access to all of the above).
|
||||
- I have permission explicitly granted to check/deploy with the inventory
|
||||
and project.
|
||||
|
||||
|
||||
#This does not mean I would be able to launch a job from the template or
|
||||
#edit the template.
|
||||
- access.py can_read for JobTemplate enforces that you can only
|
||||
see it if you can launch it, so the above imply launch too
|
||||
'''
|
||||
|
||||
|
||||
'''
|
||||
Tower administrators, organization administrators, and project
|
||||
administrators, within a project under their purview, may create and modify
|
||||
new job templates for that project.
|
||||
|
||||
When editing a job template, they may select among the inventory groups and
|
||||
credentials in the organization for which they have usage permissions, or
|
||||
they may leave either blank to be selected at runtime.
|
||||
|
||||
Additionally, they may specify one or more users/teams that have execution
|
||||
permission for that job template, among the users/teams that are a member
|
||||
of that project.
|
||||
|
||||
That execution permission is valid irrespective of any explicit permissions
|
||||
the user has or has not been granted to the inventory group or credential
|
||||
specified in the job template.
|
||||
|
||||
'''
|
||||
|
||||
migrations = defaultdict(lambda: defaultdict(set))
|
||||
|
||||
User = apps.get_model('auth', 'User')
|
||||
JobTemplate = apps.get_model('main', 'JobTemplate')
|
||||
Team = apps.get_model('main', 'Team')
|
||||
Permission = apps.get_model('main', 'Permission')
|
||||
|
||||
for jt in JobTemplate.objects.all():
|
||||
permission = Permission.objects.filter(
|
||||
inventory=jt.inventory,
|
||||
project=jt.project,
|
||||
permission_type__in=['create', 'check', 'run'] if jt.job_type == 'check' else ['create', 'run'],
|
||||
)
|
||||
|
||||
for team in Team.objects.all():
|
||||
if permission.filter(team=team).exists():
|
||||
team.member_role.children.add(jt.executor_role)
|
||||
migrations[jt.name]['teams'].add(team)
|
||||
|
||||
|
||||
for user in User.objects.all():
|
||||
if permission.filter(user=user).exists():
|
||||
jt.executor_role.members.add(user)
|
||||
migrations[jt.name]['users'].add(user)
|
||||
|
||||
if jt.accessible_by(user, {'execute': True}):
|
||||
# If the job template is already accessible by the user, because they
|
||||
# are a sytem, organization, or project admin, then don't add an explicit
|
||||
# role entry for them
|
||||
continue
|
||||
|
||||
if old_access.check_user_access(user, jt.__class__, 'start', jt, False):
|
||||
jt.executor_role.members.add(user)
|
||||
migrations[jt.name]['users'].add(user)
|
||||
|
||||
|
||||
return migrations
|
||||
@ -3,6 +3,7 @@
|
||||
|
||||
# Django
|
||||
from django.conf import settings # noqa
|
||||
from django.contrib.contenttypes.fields import GenericRelation
|
||||
|
||||
# AWX
|
||||
from awx.main.models.base import * # noqa
|
||||
@ -17,6 +18,8 @@ from awx.main.models.schedules import * # noqa
|
||||
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
|
||||
from awx.main.models.notifications import * # noqa
|
||||
from awx.main.models.fact import * # noqa
|
||||
|
||||
@ -35,8 +38,14 @@ _PythonSerializer.handle_m2m_field = _new_handle_m2m_field
|
||||
# Add custom methods to User model for permissions checks.
|
||||
from django.contrib.auth.models import User # noqa
|
||||
from awx.main.access import * # noqa
|
||||
|
||||
|
||||
User.add_to_class('get_queryset', get_user_queryset)
|
||||
User.add_to_class('can_access', check_user_access)
|
||||
User.add_to_class('accessible_by', user_accessible_by)
|
||||
User.add_to_class('accessible_objects', user_accessible_objects)
|
||||
User.add_to_class('admin_role', user_admin_role)
|
||||
User.add_to_class('role_permissions', GenericRelation('main.RolePermission'))
|
||||
|
||||
# Import signal handlers only after models have been defined.
|
||||
import awx.main.signals # noqa
|
||||
|
||||
@ -87,7 +87,7 @@ class AdHocCommand(UnifiedJob):
|
||||
|
||||
def clean_inventory(self):
|
||||
inv = self.inventory
|
||||
if not inv or not inv.active:
|
||||
if not inv:
|
||||
raise ValidationError('Inventory is no longer available.')
|
||||
return inv
|
||||
|
||||
@ -123,7 +123,7 @@ class AdHocCommand(UnifiedJob):
|
||||
@property
|
||||
def passwords_needed_to_start(self):
|
||||
'''Return list of password field names needed to start the job.'''
|
||||
if self.credential and self.credential.active:
|
||||
if self.credential:
|
||||
return self.credential.passwords_needed
|
||||
else:
|
||||
return []
|
||||
@ -164,14 +164,14 @@ class AdHocCommand(UnifiedJob):
|
||||
def task_impact(self):
|
||||
# NOTE: We sorta have to assume the host count matches and that forks default to 5
|
||||
from awx.main.models.inventory import Host
|
||||
count_hosts = Host.objects.filter(active=True, enabled=True, inventory__ad_hoc_commands__pk=self.pk).count()
|
||||
count_hosts = Host.objects.filter( enabled=True, inventory__ad_hoc_commands__pk=self.pk).count()
|
||||
return min(count_hosts, 5 if self.forks == 0 else self.forks) * 10
|
||||
|
||||
def generate_dependencies(self, active_tasks):
|
||||
from awx.main.models import InventoryUpdate
|
||||
if not self.inventory:
|
||||
return []
|
||||
inventory_sources = self.inventory.inventory_sources.filter(active=True, update_on_launch=True)
|
||||
inventory_sources = self.inventory.inventory_sources.filter( update_on_launch=True)
|
||||
inventory_sources_found = []
|
||||
dependencies = []
|
||||
for obj in active_tasks:
|
||||
|
||||
@ -203,15 +203,6 @@ class PasswordFieldsModel(BaseModel):
|
||||
def _password_field_allows_ask(self, field):
|
||||
return False # Override in subclasses if needed.
|
||||
|
||||
def mark_inactive(self, save=True):
|
||||
'''
|
||||
When marking a password model inactive we'll clear sensitive fields
|
||||
'''
|
||||
for sensitive_field in self.PASSWORD_FIELDS:
|
||||
setattr(self, sensitive_field, "")
|
||||
self.save()
|
||||
super(PasswordFieldsModel, self).mark_inactive(save=save)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
new_instance = not bool(self.pk)
|
||||
# If update_fields has been specified, add our field names to it,
|
||||
@ -273,29 +264,9 @@ class PrimordialModel(CreatedModifiedModel):
|
||||
editable=False,
|
||||
on_delete=models.SET_NULL,
|
||||
)
|
||||
active = models.BooleanField(
|
||||
default=True,
|
||||
editable=False,
|
||||
)
|
||||
|
||||
tags = TaggableManager(blank=True)
|
||||
|
||||
def mark_inactive(self, save=True, update_fields=None, skip_active_check=False):
|
||||
'''Use instead of delete to rename and mark inactive.'''
|
||||
update_fields = update_fields or []
|
||||
if skip_active_check or self.active:
|
||||
dtnow = now()
|
||||
if 'name' in self._meta.get_all_field_names():
|
||||
self.name = "_deleted_%s_%s" % (dtnow.isoformat(), self.name)
|
||||
if 'name' not in update_fields:
|
||||
update_fields.append('name')
|
||||
self.active = False
|
||||
if 'active' not in update_fields:
|
||||
update_fields.append('active')
|
||||
if save:
|
||||
self.save(update_fields=update_fields)
|
||||
return update_fields
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
update_fields = kwargs.get('update_fields', [])
|
||||
user = get_current_user()
|
||||
|
||||
@ -11,14 +11,20 @@ from django.core.exceptions import ValidationError, NON_FIELD_ERRORS
|
||||
from django.core.urlresolvers import reverse
|
||||
|
||||
# AWX
|
||||
from awx.main.fields import ImplicitRoleField
|
||||
from awx.main.constants import CLOUD_PROVIDERS
|
||||
from awx.main.utils import decrypt_field
|
||||
from awx.main.models.base import * # noqa
|
||||
from awx.main.models.mixins import ResourceMixin
|
||||
from awx.main.models.rbac import (
|
||||
ROLE_SINGLETON_SYSTEM_ADMINISTRATOR,
|
||||
ROLE_SINGLETON_SYSTEM_AUDITOR,
|
||||
)
|
||||
|
||||
__all__ = ['Credential']
|
||||
|
||||
|
||||
class Credential(PasswordFieldsModel, CommonModelNameNotUnique):
|
||||
class Credential(PasswordFieldsModel, CommonModelNameNotUnique, ResourceMixin):
|
||||
'''
|
||||
A credential contains information about how to talk to a remote resource
|
||||
Usually this is a SSH key location, and possibly an unlock password.
|
||||
@ -153,6 +159,27 @@ class Credential(PasswordFieldsModel, CommonModelNameNotUnique):
|
||||
default='',
|
||||
help_text=_('Vault password (or "ASK" to prompt the user).'),
|
||||
)
|
||||
owner_role = ImplicitRoleField(
|
||||
role_name='Credential Owner',
|
||||
role_description='Owner of the credential',
|
||||
parent_role=[
|
||||
'singleton:' + ROLE_SINGLETON_SYSTEM_ADMINISTRATOR,
|
||||
],
|
||||
permissions = {'all': True}
|
||||
)
|
||||
auditor_role = ImplicitRoleField(
|
||||
role_name='Credential Auditor',
|
||||
role_description='Auditor of the credential',
|
||||
parent_role=[
|
||||
'singleton:' + ROLE_SINGLETON_SYSTEM_AUDITOR,
|
||||
],
|
||||
permissions = {'read': True}
|
||||
)
|
||||
usage_role = ImplicitRoleField(
|
||||
role_name='Credential User',
|
||||
role_description='May use this credential, but not read sensitive portions or modify it',
|
||||
permissions = {'use': True}
|
||||
)
|
||||
|
||||
@property
|
||||
def needs_ssh_password(self):
|
||||
@ -349,6 +376,7 @@ class Credential(PasswordFieldsModel, CommonModelNameNotUnique):
|
||||
update_fields.append('cloud')
|
||||
super(Credential, self).save(*args, **kwargs)
|
||||
|
||||
|
||||
def validate_ssh_private_key(data):
|
||||
"""Validate that the given SSH private key or certificate is,
|
||||
in fact, valid.
|
||||
|
||||
@ -19,13 +19,14 @@ from django.utils.timezone import now
|
||||
|
||||
# AWX
|
||||
from awx.main.constants import CLOUD_PROVIDERS
|
||||
from awx.main.fields import AutoOneToOneField
|
||||
from awx.main.fields import AutoOneToOneField, ImplicitRoleField
|
||||
from awx.main.managers import HostManager
|
||||
from awx.main.models.base import * # noqa
|
||||
from awx.main.models.jobs import Job
|
||||
from awx.main.models.unified_jobs import * # noqa
|
||||
from awx.main.models.mixins import ResourceMixin
|
||||
from awx.main.models.notifications import Notifier
|
||||
from awx.main.utils import ignore_inventory_computed_fields, _inventory_updates
|
||||
from awx.main.utils import _inventory_updates
|
||||
from awx.main.conf import tower_settings
|
||||
|
||||
__all__ = ['Inventory', 'Host', 'Group', 'InventorySource', 'InventoryUpdate', 'CustomInventoryScript']
|
||||
@ -33,7 +34,7 @@ __all__ = ['Inventory', 'Host', 'Group', 'InventorySource', 'InventoryUpdate', '
|
||||
logger = logging.getLogger('awx.main.models.inventory')
|
||||
|
||||
|
||||
class Inventory(CommonModel):
|
||||
class Inventory(CommonModel, ResourceMixin):
|
||||
'''
|
||||
an inventory source contains lists and hosts.
|
||||
'''
|
||||
@ -95,34 +96,41 @@ class Inventory(CommonModel):
|
||||
editable=False,
|
||||
help_text=_('Number of external inventory sources in this inventory with failures.'),
|
||||
)
|
||||
admin_role = ImplicitRoleField(
|
||||
role_name='Inventory Administrator',
|
||||
role_description='May manage this inventory',
|
||||
parent_role='organization.admin_role',
|
||||
permissions = {'all': True}
|
||||
)
|
||||
auditor_role = ImplicitRoleField(
|
||||
role_name='Inventory Auditor',
|
||||
role_description='May view but not modify this inventory',
|
||||
parent_role='organization.auditor_role',
|
||||
permissions = {'read': True}
|
||||
)
|
||||
updater_role = ImplicitRoleField(
|
||||
role_name='Inventory Updater',
|
||||
role_description='May update the inventory',
|
||||
permissions = {'read': True, 'update': True}
|
||||
)
|
||||
executor_role = ImplicitRoleField(
|
||||
role_name='Inventory Executor',
|
||||
role_description='May execute jobs against this inventory',
|
||||
permissions = {'read': True, 'execute': True}
|
||||
)
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('api:inventory_detail', args=(self.pk,))
|
||||
|
||||
def mark_inactive(self, save=True):
|
||||
'''
|
||||
When marking inventory inactive, also mark hosts and groups inactive.
|
||||
'''
|
||||
with ignore_inventory_computed_fields():
|
||||
for host in self.hosts.filter(active=True):
|
||||
host.mark_inactive()
|
||||
for group in self.groups.filter(active=True):
|
||||
group.mark_inactive(recompute=False)
|
||||
for inventory_source in self.inventory_sources.filter(active=True):
|
||||
inventory_source.mark_inactive()
|
||||
super(Inventory, self).mark_inactive(save=save)
|
||||
|
||||
variables_dict = VarsDictProperty('variables')
|
||||
|
||||
def get_group_hosts_map(self, active=None):
|
||||
def get_group_hosts_map(self):
|
||||
'''
|
||||
Return dictionary mapping group_id to set of child host_id's.
|
||||
'''
|
||||
# FIXME: Cache this mapping?
|
||||
group_hosts_kw = dict(group__inventory_id=self.pk, host__inventory_id=self.pk)
|
||||
if active is not None:
|
||||
group_hosts_kw['group__active'] = active
|
||||
group_hosts_kw['host__active'] = active
|
||||
group_hosts_qs = Group.hosts.through.objects.filter(**group_hosts_kw)
|
||||
group_hosts_qs = group_hosts_qs.values_list('group_id', 'host_id')
|
||||
group_hosts_map = {}
|
||||
@ -131,15 +139,12 @@ class Inventory(CommonModel):
|
||||
group_host_ids.add(host_id)
|
||||
return group_hosts_map
|
||||
|
||||
def get_group_parents_map(self, active=None):
|
||||
def get_group_parents_map(self):
|
||||
'''
|
||||
Return dictionary mapping group_id to set of parent group_id's.
|
||||
'''
|
||||
# FIXME: Cache this mapping?
|
||||
group_parents_kw = dict(from_group__inventory_id=self.pk, to_group__inventory_id=self.pk)
|
||||
if active is not None:
|
||||
group_parents_kw['from_group__active'] = active
|
||||
group_parents_kw['to_group__active'] = active
|
||||
group_parents_qs = Group.parents.through.objects.filter(**group_parents_kw)
|
||||
group_parents_qs = group_parents_qs.values_list('from_group_id', 'to_group_id')
|
||||
group_parents_map = {}
|
||||
@ -148,15 +153,12 @@ class Inventory(CommonModel):
|
||||
group_parents.add(to_group_id)
|
||||
return group_parents_map
|
||||
|
||||
def get_group_children_map(self, active=None):
|
||||
def get_group_children_map(self):
|
||||
'''
|
||||
Return dictionary mapping group_id to set of child group_id's.
|
||||
'''
|
||||
# FIXME: Cache this mapping?
|
||||
group_parents_kw = dict(from_group__inventory_id=self.pk, to_group__inventory_id=self.pk)
|
||||
if active is not None:
|
||||
group_parents_kw['from_group__active'] = active
|
||||
group_parents_kw['to_group__active'] = active
|
||||
group_parents_qs = Group.parents.through.objects.filter(**group_parents_kw)
|
||||
group_parents_qs = group_parents_qs.values_list('from_group_id', 'to_group_id')
|
||||
group_children_map = {}
|
||||
@ -167,12 +169,12 @@ class Inventory(CommonModel):
|
||||
|
||||
def update_host_computed_fields(self):
|
||||
'''
|
||||
Update computed fields for all active hosts in this inventory.
|
||||
Update computed fields for all hosts in this inventory.
|
||||
'''
|
||||
hosts_to_update = {}
|
||||
hosts_qs = self.hosts.filter(active=True)
|
||||
hosts_qs = self.hosts
|
||||
# Define queryset of all hosts with active failures.
|
||||
hosts_with_active_failures = hosts_qs.filter(last_job_host_summary__isnull=False, last_job_host_summary__job__active=True, last_job_host_summary__failed=True).values_list('pk', flat=True)
|
||||
hosts_with_active_failures = hosts_qs.filter(last_job_host_summary__isnull=False, last_job_host_summary__failed=True).values_list('pk', flat=True)
|
||||
# Find all hosts that need the has_active_failures flag set.
|
||||
hosts_to_set = hosts_qs.filter(has_active_failures=False, pk__in=hosts_with_active_failures)
|
||||
for host_pk in hosts_to_set.values_list('pk', flat=True):
|
||||
@ -184,7 +186,7 @@ class Inventory(CommonModel):
|
||||
host_updates = hosts_to_update.setdefault(host_pk, {})
|
||||
host_updates['has_active_failures'] = False
|
||||
# Define queryset of all hosts with cloud inventory sources.
|
||||
hosts_with_cloud_inventory = hosts_qs.filter(inventory_sources__active=True, inventory_sources__source__in=CLOUD_INVENTORY_SOURCES).values_list('pk', flat=True)
|
||||
hosts_with_cloud_inventory = hosts_qs.filter(inventory_sources__source__in=CLOUD_INVENTORY_SOURCES).values_list('pk', flat=True)
|
||||
# Find all hosts that need the has_inventory_sources flag set.
|
||||
hosts_to_set = hosts_qs.filter(has_inventory_sources=False, pk__in=hosts_with_cloud_inventory)
|
||||
for host_pk in hosts_to_set.values_list('pk', flat=True):
|
||||
@ -209,13 +211,13 @@ class Inventory(CommonModel):
|
||||
'''
|
||||
Update computed fields for all active groups in this inventory.
|
||||
'''
|
||||
group_children_map = self.get_group_children_map(active=True)
|
||||
group_hosts_map = self.get_group_hosts_map(active=True)
|
||||
active_host_pks = set(self.hosts.filter(active=True).values_list('pk', flat=True))
|
||||
failed_host_pks = set(self.hosts.filter(active=True, last_job_host_summary__job__active=True, last_job_host_summary__failed=True).values_list('pk', flat=True))
|
||||
# active_group_pks = set(self.groups.filter(active=True).values_list('pk', flat=True))
|
||||
group_children_map = self.get_group_children_map()
|
||||
group_hosts_map = self.get_group_hosts_map()
|
||||
active_host_pks = set(self.hosts.values_list('pk', flat=True))
|
||||
failed_host_pks = set(self.hosts.filter(last_job_host_summary__failed=True).values_list('pk', flat=True))
|
||||
# active_group_pks = set(self.groups.values_list('pk', flat=True))
|
||||
failed_group_pks = set() # Update below as we check each group.
|
||||
groups_with_cloud_pks = set(self.groups.filter(active=True, inventory_sources__active=True, inventory_sources__source__in=CLOUD_INVENTORY_SOURCES).values_list('pk', flat=True))
|
||||
groups_with_cloud_pks = set(self.groups.filter(inventory_sources__source__in=CLOUD_INVENTORY_SOURCES).values_list('pk', flat=True))
|
||||
groups_to_update = {}
|
||||
|
||||
# Build list of group pks to check, starting with the groups at the
|
||||
@ -287,11 +289,11 @@ class Inventory(CommonModel):
|
||||
self.update_host_computed_fields()
|
||||
if update_groups:
|
||||
self.update_group_computed_fields()
|
||||
active_hosts = self.hosts.filter(active=True)
|
||||
active_hosts = self.hosts
|
||||
failed_hosts = active_hosts.filter(has_active_failures=True)
|
||||
active_groups = self.groups.filter(active=True)
|
||||
active_groups = self.groups
|
||||
failed_groups = active_groups.filter(has_active_failures=True)
|
||||
active_inventory_sources = self.inventory_sources.filter(active=True, source__in=CLOUD_INVENTORY_SOURCES)
|
||||
active_inventory_sources = self.inventory_sources.filter( source__in=CLOUD_INVENTORY_SOURCES)
|
||||
failed_inventory_sources = active_inventory_sources.filter(last_job_failed=True)
|
||||
computed_fields = {
|
||||
'has_active_failures': bool(failed_hosts.count()),
|
||||
@ -320,7 +322,7 @@ class Inventory(CommonModel):
|
||||
return self.groups.exclude(parents__pk__in=group_pks).distinct()
|
||||
|
||||
|
||||
class Host(CommonModelNameNotUnique):
|
||||
class Host(CommonModelNameNotUnique, ResourceMixin):
|
||||
'''
|
||||
A managed node
|
||||
'''
|
||||
@ -391,24 +393,13 @@ class Host(CommonModelNameNotUnique):
|
||||
def get_absolute_url(self):
|
||||
return reverse('api:host_detail', args=(self.pk,))
|
||||
|
||||
def mark_inactive(self, save=True, from_inventory_import=False, skip_active_check=False):
|
||||
'''
|
||||
When marking hosts inactive, remove all associations to related
|
||||
inventory sources.
|
||||
'''
|
||||
super(Host, self).mark_inactive(save=save, skip_active_check=skip_active_check)
|
||||
if not from_inventory_import:
|
||||
self.inventory_sources.clear()
|
||||
|
||||
def update_computed_fields(self, update_inventory=True, update_groups=True):
|
||||
'''
|
||||
Update model fields that are computed from database relationships.
|
||||
'''
|
||||
has_active_failures = bool(self.last_job_host_summary and
|
||||
self.last_job_host_summary.job.active and
|
||||
self.last_job_host_summary.failed)
|
||||
active_inventory_sources = self.inventory_sources.filter(active=True,
|
||||
source__in=CLOUD_INVENTORY_SOURCES)
|
||||
active_inventory_sources = self.inventory_sources.filter(source__in=CLOUD_INVENTORY_SOURCES)
|
||||
computed_fields = {
|
||||
'has_active_failures': has_active_failures,
|
||||
'has_inventory_sources': bool(active_inventory_sources.count()),
|
||||
@ -424,7 +415,7 @@ class Host(CommonModelNameNotUnique):
|
||||
# change.
|
||||
# NOTE: I think this is no longer needed
|
||||
# if update_groups:
|
||||
# for group in self.all_groups.filter(active=True):
|
||||
# for group in self.all_groups:
|
||||
# group.update_computed_fields()
|
||||
# if update_inventory:
|
||||
# self.inventory.update_computed_fields(update_groups=False,
|
||||
@ -456,7 +447,7 @@ class Host(CommonModelNameNotUnique):
|
||||
# Use .job_events.all() to get events affecting this host.
|
||||
|
||||
|
||||
class Group(CommonModelNameNotUnique):
|
||||
class Group(CommonModelNameNotUnique, ResourceMixin):
|
||||
'''
|
||||
A group containing managed hosts. A group or host may belong to multiple
|
||||
groups.
|
||||
@ -526,6 +517,26 @@ class Group(CommonModelNameNotUnique):
|
||||
editable=False,
|
||||
help_text=_('Inventory source(s) that created or modified this group.'),
|
||||
)
|
||||
admin_role = ImplicitRoleField(
|
||||
role_name='Inventory Group Administrator',
|
||||
parent_role=['inventory.admin_role', 'parents.admin_role'],
|
||||
permissions = {'all': True}
|
||||
)
|
||||
auditor_role = ImplicitRoleField(
|
||||
role_name='Inventory Group Auditor',
|
||||
parent_role=['inventory.auditor_role', 'parents.auditor_role'],
|
||||
permissions = {'read': True}
|
||||
)
|
||||
updater_role = ImplicitRoleField(
|
||||
role_name='Inventory Group Updater',
|
||||
parent_role=['inventory.updater_role', 'parents.updater_role'],
|
||||
permissions = {'read': True, 'write': True, 'create': True, 'use': True},
|
||||
)
|
||||
executor_role = ImplicitRoleField(
|
||||
role_name='Inventory Group Executor',
|
||||
parent_role=['inventory.executor_role', 'parents.executor_role'],
|
||||
permissions = {'read':True, 'execute':True},
|
||||
)
|
||||
|
||||
def __unicode__(self):
|
||||
return self.name
|
||||
@ -534,11 +545,12 @@ class Group(CommonModelNameNotUnique):
|
||||
return reverse('api:group_detail', args=(self.pk,))
|
||||
|
||||
@transaction.atomic
|
||||
def mark_inactive_recursive(self):
|
||||
from awx.main.tasks import bulk_inventory_element_delete
|
||||
def delete_recursive(self):
|
||||
from awx.main.utils import ignore_inventory_computed_fields
|
||||
from awx.main.tasks import update_inventory_computed_fields
|
||||
from awx.main.signals import disable_activity_stream
|
||||
|
||||
|
||||
def mark_actual():
|
||||
all_group_hosts = Group.hosts.through.objects.select_related("host", "group").filter(group__inventory=self.inventory)
|
||||
group_hosts = {'groups': {}, 'hosts': {}}
|
||||
@ -588,51 +600,24 @@ class Group(CommonModelNameNotUnique):
|
||||
for direct_child in group_children[group]:
|
||||
linked_children.append((group, direct_child))
|
||||
marked_groups.append(group)
|
||||
Group.objects.filter(id__in=marked_groups).update(active=False)
|
||||
Host.objects.filter(id__in=marked_hosts).update(active=False)
|
||||
Group.parents.through.objects.filter(to_group__id__in=marked_groups)
|
||||
Group.hosts.through.objects.filter(group__id__in=marked_groups)
|
||||
Group.inventory_sources.through.objects.filter(group__id__in=marked_groups).delete()
|
||||
bulk_inventory_element_delete.delay(self.inventory.id, groups=marked_groups, hosts=marked_hosts)
|
||||
Group.objects.filter(id__in=marked_groups).delete()
|
||||
Host.objects.filter(id__in=marked_hosts).delete()
|
||||
update_inventory_computed_fields.delay(self.inventory.id)
|
||||
with ignore_inventory_computed_fields():
|
||||
with disable_activity_stream():
|
||||
mark_actual()
|
||||
|
||||
def mark_inactive(self, save=True, recompute=True, from_inventory_import=False, skip_active_check=False):
|
||||
'''
|
||||
When marking groups inactive, remove all associations to related
|
||||
groups/hosts/inventory_sources.
|
||||
'''
|
||||
def mark_actual():
|
||||
super(Group, self).mark_inactive(save=save, skip_active_check=skip_active_check)
|
||||
self.inventory_source.mark_inactive(save=save)
|
||||
self.inventory_sources.clear()
|
||||
self.parents.clear()
|
||||
self.children.clear()
|
||||
self.hosts.clear()
|
||||
i = self.inventory
|
||||
|
||||
if from_inventory_import:
|
||||
super(Group, self).mark_inactive(save=save, skip_active_check=skip_active_check)
|
||||
elif recompute:
|
||||
with ignore_inventory_computed_fields():
|
||||
mark_actual()
|
||||
i.update_computed_fields()
|
||||
else:
|
||||
mark_actual()
|
||||
|
||||
def update_computed_fields(self):
|
||||
'''
|
||||
Update model fields that are computed from database relationships.
|
||||
'''
|
||||
active_hosts = self.all_hosts.filter(active=True)
|
||||
failed_hosts = active_hosts.filter(last_job_host_summary__job__active=True,
|
||||
last_job_host_summary__failed=True)
|
||||
active_groups = self.all_children.filter(active=True)
|
||||
active_hosts = self.all_hosts
|
||||
failed_hosts = active_hosts.filter(last_job_host_summary__failed=True)
|
||||
active_groups = self.all_children
|
||||
# FIXME: May not be accurate unless we always update groups depth-first.
|
||||
failed_groups = active_groups.filter(has_active_failures=True)
|
||||
active_inventory_sources = self.inventory_sources.filter(active=True,
|
||||
source__in=CLOUD_INVENTORY_SOURCES)
|
||||
active_inventory_sources = self.inventory_sources.filter(source__in=CLOUD_INVENTORY_SOURCES)
|
||||
computed_fields = {
|
||||
'total_hosts': active_hosts.count(),
|
||||
'has_active_failures': bool(failed_hosts.count()),
|
||||
@ -1150,7 +1135,7 @@ class InventorySource(UnifiedJobTemplate, InventorySourceOptions):
|
||||
return 'never updated'
|
||||
# inherit the child job status
|
||||
else:
|
||||
return self.last_job.status
|
||||
return self.last_job.status
|
||||
else:
|
||||
return 'none'
|
||||
|
||||
@ -1159,7 +1144,7 @@ class InventorySource(UnifiedJobTemplate, InventorySourceOptions):
|
||||
|
||||
def _can_update(self):
|
||||
if self.source == 'custom':
|
||||
return bool(self.source_script and self.source_script.active)
|
||||
return bool(self.source_script)
|
||||
else:
|
||||
return bool(self.source in CLOUD_INVENTORY_SOURCES)
|
||||
|
||||
@ -1176,7 +1161,7 @@ class InventorySource(UnifiedJobTemplate, InventorySourceOptions):
|
||||
|
||||
@property
|
||||
def needs_update_on_launch(self):
|
||||
if self.active and self.source and self.update_on_launch:
|
||||
if self.source and self.update_on_launch:
|
||||
if not self.last_job_run:
|
||||
return True
|
||||
if (self.last_job_run + datetime.timedelta(seconds=self.update_cache_timeout)) <= now():
|
||||
@ -1185,7 +1170,7 @@ class InventorySource(UnifiedJobTemplate, InventorySourceOptions):
|
||||
|
||||
@property
|
||||
def notifiers(self):
|
||||
base_notifiers = Notifier.objects.filter(active=True)
|
||||
base_notifiers = Notifier.objects
|
||||
error_notifiers = list(base_notifiers.filter(organization_notifiers_for_errors=self.inventory.organization))
|
||||
success_notifiers = list(base_notifiers.filter(organization_notifiers_for_success=self.inventory.organization))
|
||||
any_notifiers = list(base_notifiers.filter(organization_notifiers_for_any=self.inventory.organization))
|
||||
@ -1194,7 +1179,7 @@ class InventorySource(UnifiedJobTemplate, InventorySourceOptions):
|
||||
def clean_source(self):
|
||||
source = self.source
|
||||
if source and self.group:
|
||||
qs = self.group.inventory_sources.filter(source__in=CLOUD_INVENTORY_SOURCES, active=True, group__active=True)
|
||||
qs = self.group.inventory_sources.filter(source__in=CLOUD_INVENTORY_SOURCES)
|
||||
existing_sources = qs.exclude(pk=self.pk)
|
||||
if existing_sources.count():
|
||||
s = u', '.join([x.group.name for x in existing_sources])
|
||||
@ -1238,7 +1223,7 @@ class InventoryUpdate(UnifiedJob, InventorySourceOptions):
|
||||
def save(self, *args, **kwargs):
|
||||
update_fields = kwargs.get('update_fields', [])
|
||||
inventory_source = self.inventory_source
|
||||
if self.active and inventory_source.inventory and self.name == inventory_source.name:
|
||||
if inventory_source.inventory and self.name == inventory_source.name:
|
||||
if inventory_source.group:
|
||||
self.name = '%s (%s)' % (inventory_source.group.name, inventory_source.inventory.name)
|
||||
else:
|
||||
@ -1274,7 +1259,7 @@ class InventoryUpdate(UnifiedJob, InventorySourceOptions):
|
||||
return False
|
||||
|
||||
if (self.source not in ('custom', 'ec2') and
|
||||
not (self.credential and self.credential.active)):
|
||||
not (self.credential)):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
@ -28,6 +28,9 @@ from awx.main.utils import decrypt_field, ignore_inventory_computed_fields
|
||||
from awx.main.utils import emit_websocket_notification
|
||||
from awx.main.redact import PlainTextCleaner
|
||||
from awx.main.conf import tower_settings
|
||||
from awx.main.fields import ImplicitRoleField
|
||||
from awx.main.models.mixins import ResourceMixin
|
||||
|
||||
|
||||
logger = logging.getLogger('awx.main.models.jobs')
|
||||
|
||||
@ -146,12 +149,12 @@ class JobOptions(BaseModel):
|
||||
@property
|
||||
def passwords_needed_to_start(self):
|
||||
'''Return list of password field names needed to start the job.'''
|
||||
if self.credential and self.credential.active:
|
||||
if self.credential:
|
||||
return self.credential.passwords_needed
|
||||
else:
|
||||
return []
|
||||
|
||||
class JobTemplate(UnifiedJobTemplate, JobOptions):
|
||||
class JobTemplate(UnifiedJobTemplate, JobOptions, ResourceMixin):
|
||||
'''
|
||||
A job template is a reusable job definition for applying a project (with
|
||||
playbook) to an inventory source with a given credential.
|
||||
@ -180,6 +183,23 @@ class JobTemplate(UnifiedJobTemplate, JobOptions):
|
||||
blank=True,
|
||||
default={},
|
||||
)
|
||||
admin_role = ImplicitRoleField(
|
||||
role_name='Job Template Administrator',
|
||||
role_description='Full access to all settings',
|
||||
parent_role='project.admin_role',
|
||||
permissions = {'all': True}
|
||||
)
|
||||
auditor_role = ImplicitRoleField(
|
||||
role_name='Job Template Auditor',
|
||||
role_description='Read-only access to all settings',
|
||||
parent_role='project.auditor_role',
|
||||
permissions = {'read': True}
|
||||
)
|
||||
executor_role = ImplicitRoleField(
|
||||
role_name='Job Template Runner',
|
||||
role_description='May run the job template',
|
||||
permissions = {'read': True, 'execute': True}
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _get_unified_job_class(cls):
|
||||
@ -337,14 +357,14 @@ class JobTemplate(UnifiedJobTemplate, JobOptions):
|
||||
# Return all notifiers defined on the Job Template, on the Project, and on the Organization for each trigger type
|
||||
# TODO: Currently there is no org fk on project so this will need to be added once that is
|
||||
# available after the rbac pr
|
||||
base_notifiers = Notifier.objects.filter(active=True)
|
||||
base_notifiers = Notifier.objects
|
||||
error_notifiers = list(base_notifiers.filter(unifiedjobtemplate_notifiers_for_errors__in=[self, self.project]))
|
||||
success_notifiers = list(base_notifiers.filter(unifiedjobtemplate_notifiers_for_success__in=[self, self.project]))
|
||||
any_notifiers = list(base_notifiers.filter(unifiedjobtemplate_notifiers_for_any__in=[self, self.project]))
|
||||
# Get Organization Notifiers
|
||||
error_notifiers = set(error_notifiers + list(base_notifiers.filter(organization_notifiers_for_errors__in=self.project.organizations.all())))
|
||||
success_notifiers = set(success_notifiers + list(base_notifiers.filter(organization_notifiers_for_success__in=self.project.organizations.all())))
|
||||
any_notifiers = set(any_notifiers + list(base_notifiers.filter(organization_notifiers_for_any__in=self.project.organizations.all())))
|
||||
error_notifiers = set(error_notifiers + list(base_notifiers.filter(organization_notifiers_for_errors=self.project.organization)))
|
||||
success_notifiers = set(success_notifiers + list(base_notifiers.filter(organization_notifiers_for_success=self.project.organization)))
|
||||
any_notifiers = set(any_notifiers + list(base_notifiers.filter(organization_notifiers_for_any=self.project.organization)))
|
||||
return dict(error=list(error_notifiers), success=list(success_notifiers), any=list(any_notifiers))
|
||||
|
||||
class Job(UnifiedJob, JobOptions):
|
||||
@ -473,7 +493,7 @@ class Job(UnifiedJob, JobOptions):
|
||||
from awx.main.models import InventoryUpdate, ProjectUpdate
|
||||
if self.inventory is None or self.project is None:
|
||||
return []
|
||||
inventory_sources = self.inventory.inventory_sources.filter(active=True, update_on_launch=True)
|
||||
inventory_sources = self.inventory.inventory_sources.filter( update_on_launch=True)
|
||||
project_found = False
|
||||
inventory_sources_found = []
|
||||
dependencies = []
|
||||
@ -572,7 +592,7 @@ class Job(UnifiedJob, JobOptions):
|
||||
if not super(Job, self).can_start:
|
||||
return False
|
||||
|
||||
if not (self.credential and self.credential.active):
|
||||
if not (self.credential):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
91
awx/main/models/mixins.py
Normal file
91
awx/main/models/mixins.py
Normal file
@ -0,0 +1,91 @@
|
||||
# Django
|
||||
from django.db import models
|
||||
from django.db.models.aggregates import Max
|
||||
from django.contrib.contenttypes.fields import GenericRelation
|
||||
|
||||
# AWX
|
||||
from awx.main.models.rbac import (
|
||||
get_user_permissions_on_resource,
|
||||
get_role_permissions_on_resource,
|
||||
)
|
||||
|
||||
|
||||
__all__ = ['ResourceMixin']
|
||||
|
||||
class ResourceMixin(models.Model):
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
role_permissions = GenericRelation('main.RolePermission')
|
||||
|
||||
@classmethod
|
||||
def accessible_objects(cls, user, permissions):
|
||||
'''
|
||||
Use instead of `MyModel.objects` when you want to only consider
|
||||
resources that a user has specific permissions for. For example:
|
||||
|
||||
MyModel.accessible_objects(user, {'read': True}).filter(name__istartswith='bar');
|
||||
|
||||
NOTE: This should only be used for list type things. If you have a
|
||||
specific resource you want to check permissions on, it is more
|
||||
performant to resolve the resource in question then call
|
||||
`myresource.get_permissions(user)`.
|
||||
'''
|
||||
return ResourceMixin._accessible_objects(cls, user, permissions)
|
||||
|
||||
@staticmethod
|
||||
def _accessible_objects(cls, user, permissions):
|
||||
qs = cls.objects.filter(
|
||||
role_permissions__role__ancestors__members=user
|
||||
)
|
||||
for perm in permissions:
|
||||
qs = qs.annotate(**{'max_' + perm: Max('role_permissions__' + perm)})
|
||||
qs = qs.filter(**{'max_' + perm: int(permissions[perm])})
|
||||
|
||||
#return cls.objects.filter(resource__in=qs)
|
||||
return qs
|
||||
|
||||
|
||||
def get_permissions(self, user):
|
||||
'''
|
||||
Returns a dict (or None) of the permissions a user has for a given
|
||||
resource.
|
||||
|
||||
Note: Each field in the dict is the `or` of all respective permissions
|
||||
that have been granted to the roles that are applicable for the given
|
||||
user.
|
||||
|
||||
In example, if a user has been granted read access through a permission
|
||||
on one role and write access through a permission on a separate role,
|
||||
the returned dict will denote that the user has both read and write
|
||||
access.
|
||||
'''
|
||||
|
||||
return get_user_permissions_on_resource(self, user)
|
||||
|
||||
|
||||
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.
|
||||
'''
|
||||
|
||||
return get_role_permissions_on_resource(self, role)
|
||||
|
||||
|
||||
def accessible_by(self, user, permissions):
|
||||
'''
|
||||
Returns true if the user has all of the specified permissions
|
||||
'''
|
||||
|
||||
perms = self.get_permissions(user)
|
||||
if perms is None:
|
||||
return False
|
||||
for k in permissions:
|
||||
if k not in perms or perms[k] < permissions[k]:
|
||||
return False
|
||||
return True
|
||||
@ -16,14 +16,20 @@ from django.utils.timezone import now as tz_now
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
# AWX
|
||||
from awx.main.fields import AutoOneToOneField
|
||||
from awx.main.fields import AutoOneToOneField, ImplicitRoleField
|
||||
from awx.main.models.base import * # noqa
|
||||
from awx.main.models.rbac import (
|
||||
ALL_PERMISSIONS,
|
||||
ROLE_SINGLETON_SYSTEM_ADMINISTRATOR,
|
||||
ROLE_SINGLETON_SYSTEM_AUDITOR,
|
||||
)
|
||||
from awx.main.models.mixins import ResourceMixin
|
||||
from awx.main.conf import tower_settings
|
||||
|
||||
__all__ = ['Organization', 'Team', 'Permission', 'Profile', 'AuthToken']
|
||||
|
||||
|
||||
class Organization(CommonModel, NotificationFieldsModel):
|
||||
class Organization(CommonModel, NotificationFieldsModel, ResourceMixin):
|
||||
'''
|
||||
An organization is the basic unit of multi-tenancy divisions
|
||||
'''
|
||||
@ -32,21 +38,40 @@ class Organization(CommonModel, NotificationFieldsModel):
|
||||
app_label = 'main'
|
||||
ordering = ('name',)
|
||||
|
||||
users = models.ManyToManyField(
|
||||
deprecated_users = models.ManyToManyField(
|
||||
'auth.User',
|
||||
blank=True,
|
||||
related_name='organizations',
|
||||
related_name='deprecated_organizations',
|
||||
)
|
||||
admins = models.ManyToManyField(
|
||||
deprecated_admins = models.ManyToManyField(
|
||||
'auth.User',
|
||||
blank=True,
|
||||
related_name='admin_of_organizations',
|
||||
related_name='deprecated_admin_of_organizations',
|
||||
)
|
||||
projects = models.ManyToManyField(
|
||||
deprecated_projects = models.ManyToManyField(
|
||||
'Project',
|
||||
blank=True,
|
||||
related_name='organizations',
|
||||
related_name='deprecated_organizations',
|
||||
)
|
||||
admin_role = ImplicitRoleField(
|
||||
role_name='Organization Administrator',
|
||||
role_description='May manage all aspects of this organization',
|
||||
parent_role='singleton:' + ROLE_SINGLETON_SYSTEM_ADMINISTRATOR,
|
||||
permissions = ALL_PERMISSIONS,
|
||||
)
|
||||
auditor_role = ImplicitRoleField(
|
||||
role_name='Organization Auditor',
|
||||
role_description='May read all settings associated with this organization',
|
||||
parent_role='singleton:' + ROLE_SINGLETON_SYSTEM_AUDITOR,
|
||||
permissions = {'read': True}
|
||||
)
|
||||
member_role = ImplicitRoleField(
|
||||
role_name='Organization Member',
|
||||
role_description='A member of this organization',
|
||||
parent_role='admin_role',
|
||||
permissions = {'read': True}
|
||||
)
|
||||
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('api:organization_detail', args=(self.pk,))
|
||||
@ -54,14 +79,9 @@ class Organization(CommonModel, NotificationFieldsModel):
|
||||
def __unicode__(self):
|
||||
return self.name
|
||||
|
||||
def mark_inactive(self, save=True):
|
||||
for script in self.custom_inventory_scripts.all():
|
||||
script.organization = None
|
||||
script.save()
|
||||
super(Organization, self).mark_inactive(save=save)
|
||||
|
||||
|
||||
class Team(CommonModelNameNotUnique):
|
||||
class Team(CommonModelNameNotUnique, ResourceMixin):
|
||||
'''
|
||||
A team is a group of users that work on common projects.
|
||||
'''
|
||||
@ -71,10 +91,10 @@ class Team(CommonModelNameNotUnique):
|
||||
unique_together = [('organization', 'name')]
|
||||
ordering = ('organization__name', 'name')
|
||||
|
||||
users = models.ManyToManyField(
|
||||
deprecated_users = models.ManyToManyField(
|
||||
'auth.User',
|
||||
blank=True,
|
||||
related_name='teams',
|
||||
related_name='deprecated_teams',
|
||||
)
|
||||
organization = models.ForeignKey(
|
||||
'Organization',
|
||||
@ -88,21 +108,36 @@ class Team(CommonModelNameNotUnique):
|
||||
blank=True,
|
||||
related_name='teams',
|
||||
)
|
||||
admin_role = ImplicitRoleField(
|
||||
role_name='Team Administrator',
|
||||
role_description='May manage this team',
|
||||
parent_role='organization.admin_role',
|
||||
permissions = ALL_PERMISSIONS,
|
||||
)
|
||||
auditor_role = ImplicitRoleField(
|
||||
role_name='Team Auditor',
|
||||
role_description='May read all settings associated with this team',
|
||||
parent_role='organization.auditor_role',
|
||||
permissions = {'read': True}
|
||||
)
|
||||
member_role = ImplicitRoleField(
|
||||
role_name='Team Member',
|
||||
role_description='A member of this team',
|
||||
parent_role='admin_role',
|
||||
permissions = {'read':True},
|
||||
)
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('api:team_detail', args=(self.pk,))
|
||||
|
||||
def mark_inactive(self, save=True):
|
||||
'''
|
||||
When marking a team inactive we'll wipe out its credentials also
|
||||
'''
|
||||
for cred in self.credentials.all():
|
||||
cred.mark_inactive()
|
||||
super(Team, self).mark_inactive(save=save)
|
||||
|
||||
class Permission(CommonModelNameNotUnique):
|
||||
'''
|
||||
A permission allows a user, project, or team to be able to use an inventory source.
|
||||
|
||||
NOTE: This class is deprecated, permissions and access is to be handled by
|
||||
our new RBAC system. This class should be able to be safely removed after a 3.0.0
|
||||
migration. - anoek 2016-01-28
|
||||
'''
|
||||
|
||||
class Meta:
|
||||
@ -174,7 +209,7 @@ class Profile(CreatedModifiedModel):
|
||||
)
|
||||
|
||||
"""
|
||||
Since expiration and session expiration is event driven a token could be
|
||||
Since expiration and session expiration is event driven a token could be
|
||||
invalidated for both reasons. Further, we only support a single reason for a
|
||||
session token being invalid. For this case, mark the token as expired.
|
||||
|
||||
@ -198,7 +233,7 @@ class AuthToken(BaseModel):
|
||||
|
||||
class Meta:
|
||||
app_label = 'main'
|
||||
|
||||
|
||||
key = models.CharField(max_length=40, primary_key=True)
|
||||
user = models.ForeignKey('auth.User', related_name='auth_tokens',
|
||||
on_delete=models.CASCADE)
|
||||
@ -303,22 +338,6 @@ class AuthToken(BaseModel):
|
||||
return self.key
|
||||
|
||||
|
||||
# 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:
|
||||
# Set timestamp to datetime.isoformat() but without the time zone
|
||||
# offset to stay withint the 30 character username limit.
|
||||
dtnow = tz_now()
|
||||
deleted_ts = dtnow.strftime('%Y-%m-%dT%H:%M:%S.%f')
|
||||
user.username = '_d_%s' % deleted_ts
|
||||
user.is_active = False
|
||||
if save:
|
||||
user.save()
|
||||
|
||||
User.add_to_class('mark_inactive', user_mark_inactive)
|
||||
|
||||
|
||||
# Add get_absolute_url method to User model if not present.
|
||||
if not hasattr(User, 'get_absolute_url'):
|
||||
def user_get_absolute_url(user):
|
||||
|
||||
@ -22,8 +22,14 @@ from awx.main.models.base import * # noqa
|
||||
from awx.main.models.jobs import Job
|
||||
from awx.main.models.notifications import Notifier
|
||||
from awx.main.models.unified_jobs import * # noqa
|
||||
from awx.main.models.mixins import ResourceMixin
|
||||
from awx.main.utils import update_scm_url
|
||||
from awx.main.fields import ImplicitRoleField
|
||||
from awx.main.conf import tower_settings
|
||||
from awx.main.models.rbac import (
|
||||
ROLE_SINGLETON_SYSTEM_ADMINISTRATOR,
|
||||
ROLE_SINGLETON_SYSTEM_AUDITOR,
|
||||
)
|
||||
|
||||
__all__ = ['Project', 'ProjectUpdate']
|
||||
|
||||
@ -51,7 +57,7 @@ class ProjectOptions(models.Model):
|
||||
paths = [x.decode('utf-8') for x in os.listdir(settings.PROJECTS_ROOT)
|
||||
if (os.path.isdir(os.path.join(settings.PROJECTS_ROOT, x)) and
|
||||
not x.startswith('.') and not x.startswith('_'))]
|
||||
qs = Project.objects.filter(active=True)
|
||||
qs = Project.objects
|
||||
used_paths = qs.values_list('local_path', flat=True)
|
||||
return [x for x in paths if x not in used_paths]
|
||||
else:
|
||||
@ -187,7 +193,7 @@ class ProjectOptions(models.Model):
|
||||
return sorted(results, key=lambda x: smart_str(x).lower())
|
||||
|
||||
|
||||
class Project(UnifiedJobTemplate, ProjectOptions):
|
||||
class Project(UnifiedJobTemplate, ProjectOptions, ResourceMixin):
|
||||
'''
|
||||
A project represents a playbook git repo that can access a set of inventories
|
||||
'''
|
||||
@ -196,6 +202,13 @@ class Project(UnifiedJobTemplate, ProjectOptions):
|
||||
app_label = 'main'
|
||||
ordering = ('id',)
|
||||
|
||||
organization = models.ForeignKey(
|
||||
'Organization',
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=models.CASCADE,
|
||||
related_name='projects',
|
||||
)
|
||||
scm_delete_on_next_update = models.BooleanField(
|
||||
default=False,
|
||||
editable=False,
|
||||
@ -207,6 +220,36 @@ class Project(UnifiedJobTemplate, ProjectOptions):
|
||||
default=0,
|
||||
blank=True,
|
||||
)
|
||||
admin_role = ImplicitRoleField(
|
||||
role_name='Project Administrator',
|
||||
role_description='May manage this project',
|
||||
parent_role=[
|
||||
'organization.admin_role',
|
||||
'teams.member_role',
|
||||
'singleton:' + ROLE_SINGLETON_SYSTEM_ADMINISTRATOR,
|
||||
],
|
||||
permissions = {'all': True}
|
||||
)
|
||||
auditor_role = ImplicitRoleField(
|
||||
role_name='Project Auditor',
|
||||
role_description='May read all settings associated with this project',
|
||||
parent_role=[
|
||||
'organization.auditor_role',
|
||||
'singleton:' + ROLE_SINGLETON_SYSTEM_AUDITOR,
|
||||
],
|
||||
permissions = {'read': True}
|
||||
)
|
||||
member_role = ImplicitRoleField(
|
||||
role_name='Project Member',
|
||||
role_description='Implies membership within this project',
|
||||
permissions = {'read': True}
|
||||
)
|
||||
scm_update_role = ImplicitRoleField(
|
||||
role_name='Project Updater',
|
||||
role_description='May update this project from the source control management system',
|
||||
parent_role='admin_role',
|
||||
permissions = {'scm_update': True}
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _get_unified_job_class(cls):
|
||||
@ -301,10 +344,10 @@ class Project(UnifiedJobTemplate, ProjectOptions):
|
||||
if (self.last_job_run + datetime.timedelta(seconds=self.scm_update_cache_timeout)) > now():
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
@property
|
||||
def needs_update_on_launch(self):
|
||||
if self.active and self.scm_type and self.scm_update_on_launch:
|
||||
if self.scm_type and self.scm_update_on_launch:
|
||||
if not self.last_job_run:
|
||||
return True
|
||||
if (self.last_job_run + datetime.timedelta(seconds=self.scm_update_cache_timeout)) <= now():
|
||||
@ -313,14 +356,14 @@ class Project(UnifiedJobTemplate, ProjectOptions):
|
||||
|
||||
@property
|
||||
def notifiers(self):
|
||||
base_notifiers = Notifier.objects.filter(active=True)
|
||||
base_notifiers = Notifier.objects
|
||||
error_notifiers = list(base_notifiers.filter(unifiedjobtemplate_notifiers_for_errors=self))
|
||||
success_notifiers = list(base_notifiers.filter(unifiedjobtemplate_notifiers_for_success=self))
|
||||
any_notifiers = list(base_notifiers.filter(unifiedjobtemplate_notifiers_for_any=self))
|
||||
# Get Organization Notifiers
|
||||
error_notifiers = set(error_notifiers + list(base_notifiers.filter(organization_notifiers_for_errors__in=self.organizations.all())))
|
||||
success_notifiers = set(success_notifiers + list(base_notifiers.filter(organization_notifiers_for_success__in=self.organizations.all())))
|
||||
any_notifiers = set(any_notifiers + list(base_notifiers.filter(organization_notifiers_for_any__in=self.organizations.all())))
|
||||
error_notifiers = set(error_notifiers + list(base_notifiers.filter(organization_notifiers_for_errors=self.organization)))
|
||||
success_notifiers = set(success_notifiers + list(base_notifiers.filter(organization_notifiers_for_success=self.organization)))
|
||||
any_notifiers = set(any_notifiers + list(base_notifiers.filter(organization_notifiers_for_any=self.organization)))
|
||||
return dict(error=list(error_notifiers), success=list(success_notifiers), any=list(any_notifiers))
|
||||
|
||||
def get_absolute_url(self):
|
||||
|
||||
245
awx/main/models/rbac.py
Normal file
245
awx/main/models/rbac.py
Normal file
@ -0,0 +1,245 @@
|
||||
# Copyright (c) 2016 Ansible, Inc.
|
||||
# All Rights Reserved.
|
||||
|
||||
# Python
|
||||
import logging
|
||||
import threading
|
||||
import contextlib
|
||||
|
||||
# Django
|
||||
from django.db import models, transaction
|
||||
from django.db.models import Q
|
||||
from django.db.models.aggregates import Max
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.contrib.contenttypes.fields import GenericForeignKey
|
||||
|
||||
# AWX
|
||||
from awx.main.models.base import * # noqa
|
||||
|
||||
__all__ = [
|
||||
'Role',
|
||||
'RolePermission',
|
||||
'batch_role_ancestor_rebuilding',
|
||||
'get_user_permissions_on_resource',
|
||||
'get_role_permissions_on_resource',
|
||||
'ROLE_SINGLETON_SYSTEM_ADMINISTRATOR',
|
||||
'ROLE_SINGLETON_SYSTEM_AUDITOR',
|
||||
]
|
||||
|
||||
logger = logging.getLogger('awx.main.models.rbac')
|
||||
|
||||
ROLE_SINGLETON_SYSTEM_ADMINISTRATOR='System Administrator'
|
||||
ROLE_SINGLETON_SYSTEM_AUDITOR='System Auditor'
|
||||
|
||||
ALL_PERMISSIONS = {'create': True, 'read': True, 'update': True, 'delete': True,
|
||||
'write': True, 'scm_update': True, 'use': True, 'execute': True}
|
||||
|
||||
|
||||
tls = threading.local() # thread local storage
|
||||
|
||||
@contextlib.contextmanager
|
||||
def batch_role_ancestor_rebuilding(allow_nesting=False):
|
||||
'''
|
||||
Batches the role ancestor rebuild work necessary whenever role-role
|
||||
relations change. This can result in a big speedup when performing
|
||||
any bulk manipulation.
|
||||
|
||||
WARNING: Calls to anything related to checking access/permissions
|
||||
while within the context of the batch_role_ancestor_rebuilding will
|
||||
likely not work.
|
||||
'''
|
||||
|
||||
batch_role_rebuilding = getattr(tls, 'batch_role_rebuilding', False)
|
||||
|
||||
try:
|
||||
setattr(tls, 'batch_role_rebuilding', True)
|
||||
if not batch_role_rebuilding:
|
||||
setattr(tls, 'roles_needing_rebuilding', set())
|
||||
yield
|
||||
|
||||
finally:
|
||||
setattr(tls, 'batch_role_rebuilding', batch_role_rebuilding)
|
||||
if not batch_role_rebuilding:
|
||||
rebuild_set = getattr(tls, 'roles_needing_rebuilding')
|
||||
with transaction.atomic():
|
||||
for role in Role.objects.filter(id__in=list(rebuild_set)).all():
|
||||
# TODO: We can reduce this to one rebuild call with our new upcoming rebuild method.. do this
|
||||
role.rebuild_role_ancestor_list()
|
||||
delattr(tls, 'roles_needing_rebuilding')
|
||||
|
||||
|
||||
class Role(CommonModelNameNotUnique):
|
||||
'''
|
||||
Role model
|
||||
'''
|
||||
|
||||
class Meta:
|
||||
app_label = 'main'
|
||||
verbose_name_plural = _('roles')
|
||||
db_table = 'main_rbac_roles'
|
||||
|
||||
singleton_name = models.TextField(null=True, default=None, db_index=True, unique=True)
|
||||
parents = models.ManyToManyField('Role', related_name='children')
|
||||
ancestors = models.ManyToManyField('Role', related_name='descendents') # auto-generated by `rebuild_role_ancestor_list`
|
||||
members = models.ManyToManyField('auth.User', related_name='roles')
|
||||
content_type = models.ForeignKey(ContentType, null=True, default=None)
|
||||
object_id = models.PositiveIntegerField(null=True, default=None)
|
||||
content_object = GenericForeignKey('content_type', 'object_id')
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
super(Role, self).save(*args, **kwargs)
|
||||
self.rebuild_role_ancestor_list()
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('api:role_detail', args=(self.pk,))
|
||||
|
||||
|
||||
def rebuild_role_ancestor_list(self):
|
||||
'''
|
||||
Updates our `ancestors` map to accurately reflect all of the ancestors for a role
|
||||
|
||||
You should never need to call this. Signal handlers should be calling
|
||||
this method when the role hierachy changes automatically.
|
||||
|
||||
Note that this method relies on any parents' ancestor list being correct.
|
||||
'''
|
||||
global tls
|
||||
batch_role_rebuilding = getattr(tls, 'batch_role_rebuilding', False)
|
||||
|
||||
if batch_role_rebuilding:
|
||||
roles_needing_rebuilding = getattr(tls, 'roles_needing_rebuilding')
|
||||
roles_needing_rebuilding.add(self.id)
|
||||
return
|
||||
|
||||
actual_ancestors = set(Role.objects.filter(id=self.id).values_list('parents__ancestors__id', flat=True))
|
||||
actual_ancestors.add(self.id)
|
||||
if None in actual_ancestors:
|
||||
actual_ancestors.remove(None)
|
||||
stored_ancestors = set(self.ancestors.all().values_list('id', flat=True))
|
||||
|
||||
# If it differs, update, and then update all of our children
|
||||
if actual_ancestors != stored_ancestors:
|
||||
for id in actual_ancestors - stored_ancestors:
|
||||
self.ancestors.add(id)
|
||||
for id in stored_ancestors - actual_ancestors:
|
||||
self.ancestors.remove(id)
|
||||
|
||||
for child in self.children.all():
|
||||
child.rebuild_role_ancestor_list()
|
||||
|
||||
@staticmethod
|
||||
def visible_roles(user):
|
||||
return Role.objects.filter(Q(descendents__in=user.roles.filter()) | Q(ancestors__in=user.roles.filter()))
|
||||
|
||||
@staticmethod
|
||||
def singleton(name):
|
||||
try:
|
||||
return Role.objects.get(singleton_name=name)
|
||||
except Role.DoesNotExist:
|
||||
ret = Role.objects.create(singleton_name=name, name=name)
|
||||
return ret
|
||||
|
||||
def is_ancestor_of(self, role):
|
||||
return role.ancestors.filter(id=self.id).exists()
|
||||
|
||||
|
||||
class RolePermission(CreatedModifiedModel):
|
||||
'''
|
||||
Defines the permissions a role has
|
||||
'''
|
||||
|
||||
class Meta:
|
||||
app_label = 'main'
|
||||
verbose_name_plural = _('permissions')
|
||||
db_table = 'main_rbac_permissions'
|
||||
index_together = [
|
||||
('content_type', 'object_id')
|
||||
]
|
||||
|
||||
role = models.ForeignKey(
|
||||
Role,
|
||||
null=False,
|
||||
on_delete=models.CASCADE,
|
||||
related_name='permissions',
|
||||
)
|
||||
content_type = models.ForeignKey(ContentType, null=False, default=None)
|
||||
object_id = models.PositiveIntegerField(null=False, default=None)
|
||||
resource = GenericForeignKey('content_type', 'object_id')
|
||||
auto_generated = models.BooleanField(default=False)
|
||||
|
||||
create = models.IntegerField(default = 0)
|
||||
read = models.IntegerField(default = 0)
|
||||
write = models.IntegerField(default = 0)
|
||||
delete = models.IntegerField(default = 0)
|
||||
update = models.IntegerField(default = 0)
|
||||
execute = models.IntegerField(default = 0)
|
||||
scm_update = models.IntegerField(default = 0)
|
||||
use = models.IntegerField(default = 0)
|
||||
|
||||
|
||||
|
||||
def get_user_permissions_on_resource(resource, user):
|
||||
'''
|
||||
Returns a dict (or None) of the permissions a user has for a given
|
||||
resource.
|
||||
|
||||
Note: Each field in the dict is the `or` of all respective permissions
|
||||
that have been granted to the roles that are applicable for the given
|
||||
user.
|
||||
|
||||
In example, if a user has been granted read access through a permission
|
||||
on one role and write access through a permission on a separate role,
|
||||
the returned dict will denote that the user has both read and write
|
||||
access.
|
||||
'''
|
||||
|
||||
qs = RolePermission.objects.filter(
|
||||
content_type=ContentType.objects.get_for_model(resource),
|
||||
object_id=resource.id,
|
||||
role__ancestors__in=user.roles.all()
|
||||
)
|
||||
|
||||
res = qs = qs.aggregate(
|
||||
create = Max('create'),
|
||||
read = Max('read'),
|
||||
write = Max('write'),
|
||||
update = Max('update'),
|
||||
delete = Max('delete'),
|
||||
scm_update = Max('scm_update'),
|
||||
execute = Max('execute'),
|
||||
use = Max('use')
|
||||
)
|
||||
if res['read'] is None:
|
||||
return None
|
||||
return res
|
||||
|
||||
def get_role_permissions_on_resource(resource, 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 = RolePermission.objects.filter(
|
||||
content_type=ContentType.objects.get_for_model(resource),
|
||||
object_id=resource.id,
|
||||
role__ancestors=role
|
||||
)
|
||||
|
||||
res = qs = qs.aggregate(
|
||||
create = Max('create'),
|
||||
read = Max('read'),
|
||||
write = Max('write'),
|
||||
update = Max('update'),
|
||||
delete = Max('delete'),
|
||||
scm_update = Max('scm_update'),
|
||||
execute = Max('execute'),
|
||||
use = Max('use')
|
||||
)
|
||||
if res['read'] is None:
|
||||
return None
|
||||
return res
|
||||
@ -27,7 +27,7 @@ __all__ = ['Schedule']
|
||||
class ScheduleFilterMethods(object):
|
||||
|
||||
def enabled(self, enabled=True):
|
||||
return self.filter(enabled=enabled, active=enabled)
|
||||
return self.filter(enabled=enabled)
|
||||
|
||||
def before(self, dt):
|
||||
return self.filter(next_run__lt=dt)
|
||||
|
||||
@ -210,17 +210,6 @@ class UnifiedJobTemplate(PolymorphicModel, CommonModelNameNotUnique, Notificatio
|
||||
self.next_job_run = related_schedules[0].next_run
|
||||
self.save(update_fields=['next_schedule', 'next_job_run'])
|
||||
|
||||
def mark_inactive(self, save=True):
|
||||
'''
|
||||
When marking a unified job template inactive, also mark its schedules
|
||||
inactive.
|
||||
'''
|
||||
for schedule in self.schedules.filter(active=True):
|
||||
schedule.mark_inactive()
|
||||
schedule.enabled = False
|
||||
schedule.save()
|
||||
super(UnifiedJobTemplate, self).mark_inactive(save=save)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
# If update_fields has been specified, add our field names to it,
|
||||
# if it hasn't been specified, then we're just doing a normal save.
|
||||
|
||||
@ -8,7 +8,7 @@ import threading
|
||||
import json
|
||||
|
||||
# Django
|
||||
from django.db.models.signals import pre_save, post_save, pre_delete, post_delete, m2m_changed
|
||||
from django.db.models.signals import post_save, pre_delete, post_delete, m2m_changed
|
||||
from django.dispatch import receiver
|
||||
|
||||
# Django-CRUM
|
||||
@ -27,9 +27,8 @@ __all__ = []
|
||||
|
||||
logger = logging.getLogger('awx.main.signals')
|
||||
|
||||
# Update has_active_failures for inventory/groups when a Host/Group is deleted
|
||||
# or marked inactive, when a Host-Group or Group-Group relationship is updated,
|
||||
# or when a Job is deleted or marked inactive.
|
||||
# Update has_active_failures for inventory/groups when a Host/Group is deleted,
|
||||
# when a Host-Group or Group-Group relationship is updated, or when a Job is deleted
|
||||
|
||||
def emit_job_event_detail(sender, **kwargs):
|
||||
instance = kwargs['instance']
|
||||
@ -69,7 +68,7 @@ def emit_update_inventory_computed_fields(sender, **kwargs):
|
||||
else:
|
||||
sender_name = unicode(sender._meta.verbose_name)
|
||||
if kwargs['signal'] == post_save:
|
||||
if sender == Job and instance.active:
|
||||
if sender == Job:
|
||||
return
|
||||
sender_action = 'saved'
|
||||
elif kwargs['signal'] == post_delete:
|
||||
@ -92,7 +91,6 @@ def emit_update_inventory_on_created_or_deleted(sender, **kwargs):
|
||||
return
|
||||
instance = kwargs['instance']
|
||||
if ('created' in kwargs and kwargs['created']) or \
|
||||
(hasattr(instance, '_saved_active_state') and instance._saved_active_state != instance.active) or \
|
||||
kwargs['signal'] == post_delete:
|
||||
pass
|
||||
else:
|
||||
@ -108,34 +106,172 @@ def emit_update_inventory_on_created_or_deleted(sender, **kwargs):
|
||||
if inventory is not None:
|
||||
update_inventory_computed_fields.delay(inventory.id, True)
|
||||
|
||||
def store_initial_active_state(sender, **kwargs):
|
||||
instance = kwargs['instance']
|
||||
if instance.id is not None:
|
||||
instance._saved_active_state = sender.objects.get(id=instance.id).active
|
||||
def rebuild_role_ancestor_list(reverse, model, instance, pk_set, **kwargs):
|
||||
'When a role parent is added or removed, update our role hierarchy list'
|
||||
if reverse:
|
||||
for id in pk_set:
|
||||
model.objects.get(id=id).rebuild_role_ancestor_list()
|
||||
else:
|
||||
instance._saved_active_state = True
|
||||
instance.rebuild_role_ancestor_list()
|
||||
|
||||
def sync_superuser_status_to_rbac(instance, **kwargs):
|
||||
'When the is_superuser flag is changed on a user, reflect that in the membership of the System Admnistrator role'
|
||||
if instance.is_superuser:
|
||||
Role.singleton(ROLE_SINGLETON_SYSTEM_ADMINISTRATOR).members.add(instance)
|
||||
else:
|
||||
Role.singleton(ROLE_SINGLETON_SYSTEM_ADMINISTRATOR).members.remove(instance)
|
||||
|
||||
def create_user_role(instance, **kwargs):
|
||||
try:
|
||||
instance.admin_role
|
||||
except Role.DoesNotExist:
|
||||
role = Role.objects.create(
|
||||
singleton_name = '%s-admin_role' % instance.username,
|
||||
content_object = instance,
|
||||
)
|
||||
role.members.add(instance)
|
||||
RolePermission.objects.create(
|
||||
role = role,
|
||||
resource = instance,
|
||||
auto_generated = True,
|
||||
create=1, read=1, write=1, delete=1, update=1,
|
||||
execute=1, scm_update=1, use=1,
|
||||
)
|
||||
|
||||
def org_admin_edit_members(instance, action, model, reverse, pk_set, **kwargs):
|
||||
content_type = ContentType.objects.get_for_model(Organization)
|
||||
|
||||
if reverse:
|
||||
return
|
||||
else:
|
||||
if instance.content_type == content_type and \
|
||||
instance.content_object.member_role.id == instance.id:
|
||||
items = model.objects.filter(pk__in=pk_set).all()
|
||||
for user in items:
|
||||
if action == 'post_add':
|
||||
instance.content_object.admin_role.children.add(user.admin_role)
|
||||
if action == 'pre_remove':
|
||||
instance.content_object.admin_role.children.remove(user.admin_role)
|
||||
|
||||
def grant_host_access_to_group_roles(instance, action, model, reverse, pk_set, **kwargs):
|
||||
'Add/remove RolePermission entries for Group roles that contain this host'
|
||||
|
||||
if action == 'post_add':
|
||||
def grant(host, group):
|
||||
RolePermission.objects.create(
|
||||
resource=host,
|
||||
role=group.admin_role,
|
||||
auto_generated=True,
|
||||
create=1,
|
||||
read=1, write=1,
|
||||
delete=1,
|
||||
update=1,
|
||||
execute=1,
|
||||
scm_update=1,
|
||||
use=1,
|
||||
)
|
||||
RolePermission.objects.create(
|
||||
resource=host,
|
||||
role=group.auditor_role,
|
||||
auto_generated=True,
|
||||
read=1,
|
||||
)
|
||||
RolePermission.objects.create(
|
||||
resource=host,
|
||||
role=group.updater_role,
|
||||
auto_generated=True,
|
||||
read=1,
|
||||
write=1,
|
||||
create=1,
|
||||
use=1
|
||||
)
|
||||
RolePermission.objects.create(
|
||||
resource=host,
|
||||
role=group.executor_role,
|
||||
auto_generated=True,
|
||||
read=1,
|
||||
execute=1
|
||||
)
|
||||
|
||||
if reverse:
|
||||
host = instance
|
||||
for group_id in pk_set:
|
||||
grant(host, Group.objects.get(id=group_id))
|
||||
else:
|
||||
group = instance
|
||||
for host_id in pk_set:
|
||||
grant(Host.objects.get(id=host_id), group)
|
||||
|
||||
if action == 'pre_remove':
|
||||
host_content_type = ContentType.objects.get_for_model(Host)
|
||||
|
||||
def remove_grant(host, group):
|
||||
RolePermission.objects.filter(
|
||||
content_type = host_content_type,
|
||||
object_id = host.id,
|
||||
auto_generated = True,
|
||||
role__in = [group.admin_role, group.updater_role, group.auditor_role, group.executor_role]
|
||||
).delete()
|
||||
|
||||
if reverse:
|
||||
host = instance
|
||||
for group_id in pk_set:
|
||||
remove_grant(host, Group.objects.get(id=group_id))
|
||||
else:
|
||||
group = instance
|
||||
for host_id in pk_set:
|
||||
remove_grant(Host.objects.get(id=host_id), group)
|
||||
|
||||
|
||||
def grant_host_access_to_inventory(instance, **kwargs):
|
||||
'Add/remove RolePermission entries for the Inventory that contains this host'
|
||||
host_content_type = ContentType.objects.get_for_model(Host)
|
||||
inventory_content_type = ContentType.objects.get_for_model(Inventory)
|
||||
|
||||
# Clear out any existing perms.. in case we switched inventory or something
|
||||
qs = RolePermission.objects.filter(
|
||||
content_type=host_content_type,
|
||||
object_id=instance.id,
|
||||
auto_generated=True,
|
||||
role__content_type=inventory_content_type
|
||||
)
|
||||
if qs.count() == 1 and qs[0].role.object_id == instance.inventory.id:
|
||||
# No change
|
||||
return
|
||||
qs.delete()
|
||||
|
||||
RolePermission.objects.create(
|
||||
resource=instance,
|
||||
role=instance.inventory.admin_role,
|
||||
auto_generated=True,
|
||||
create=1, read=1, write=1, delete=1, update=1,
|
||||
execute=1, scm_update=1, use=1,
|
||||
)
|
||||
|
||||
|
||||
pre_save.connect(store_initial_active_state, sender=Host)
|
||||
post_save.connect(emit_update_inventory_on_created_or_deleted, sender=Host)
|
||||
post_delete.connect(emit_update_inventory_on_created_or_deleted, sender=Host)
|
||||
pre_save.connect(store_initial_active_state, sender=Group)
|
||||
post_save.connect(emit_update_inventory_on_created_or_deleted, sender=Group)
|
||||
post_delete.connect(emit_update_inventory_on_created_or_deleted, sender=Group)
|
||||
m2m_changed.connect(emit_update_inventory_computed_fields, sender=Group.hosts.through)
|
||||
m2m_changed.connect(emit_update_inventory_computed_fields, sender=Group.parents.through)
|
||||
m2m_changed.connect(emit_update_inventory_computed_fields, sender=Host.inventory_sources.through)
|
||||
m2m_changed.connect(emit_update_inventory_computed_fields, sender=Group.inventory_sources.through)
|
||||
pre_save.connect(store_initial_active_state, sender=InventorySource)
|
||||
post_save.connect(emit_update_inventory_on_created_or_deleted, sender=InventorySource)
|
||||
post_delete.connect(emit_update_inventory_on_created_or_deleted, sender=InventorySource)
|
||||
pre_save.connect(store_initial_active_state, sender=Job)
|
||||
post_save.connect(emit_update_inventory_on_created_or_deleted, sender=Job)
|
||||
post_delete.connect(emit_update_inventory_on_created_or_deleted, sender=Job)
|
||||
post_save.connect(emit_job_event_detail, sender=JobEvent)
|
||||
post_save.connect(emit_ad_hoc_command_event_detail, sender=AdHocCommandEvent)
|
||||
m2m_changed.connect(rebuild_role_ancestor_list, Role.parents.through)
|
||||
m2m_changed.connect(org_admin_edit_members, Role.members.through)
|
||||
m2m_changed.connect(grant_host_access_to_group_roles, Group.hosts.through)
|
||||
post_save.connect(grant_host_access_to_inventory, Host)
|
||||
post_save.connect(sync_superuser_status_to_rbac, sender=User)
|
||||
post_save.connect(create_user_role, sender=User)
|
||||
|
||||
# Migrate hosts, groups to parent group(s) whenever a group is deleted or
|
||||
# marked as inactive.
|
||||
|
||||
# Migrate hosts, groups to parent group(s) whenever a group is deleted
|
||||
|
||||
@receiver(pre_delete, sender=Group)
|
||||
def save_related_pks_before_group_delete(sender, **kwargs):
|
||||
@ -158,80 +294,28 @@ def migrate_children_from_deleted_group_to_parent_groups(sender, **kwargs):
|
||||
with ignore_inventory_group_removal():
|
||||
with ignore_inventory_computed_fields():
|
||||
if parents_pks:
|
||||
for parent_group in Group.objects.filter(pk__in=parents_pks, active=True):
|
||||
for child_host in Host.objects.filter(pk__in=hosts_pks, active=True):
|
||||
for parent_group in Group.objects.filter(pk__in=parents_pks):
|
||||
for child_host in Host.objects.filter(pk__in=hosts_pks):
|
||||
logger.debug('adding host %s to parent %s after group deletion',
|
||||
child_host, parent_group)
|
||||
parent_group.hosts.add(child_host)
|
||||
for child_group in Group.objects.filter(pk__in=children_pks, active=True):
|
||||
for child_group in Group.objects.filter(pk__in=children_pks):
|
||||
logger.debug('adding group %s to parent %s after group deletion',
|
||||
child_group, parent_group)
|
||||
parent_group.children.add(child_group)
|
||||
inventory_pk = getattr(instance, '_saved_inventory_pk', None)
|
||||
if inventory_pk:
|
||||
try:
|
||||
inventory = Inventory.objects.get(pk=inventory_pk, active=True)
|
||||
inventory = Inventory.objects.get(pk=inventory_pk)
|
||||
inventory.update_computed_fields()
|
||||
except Inventory.DoesNotExist:
|
||||
pass
|
||||
|
||||
@receiver(pre_save, sender=Group)
|
||||
def save_related_pks_before_group_marked_inactive(sender, **kwargs):
|
||||
if getattr(_inventory_updates, 'is_removing', False):
|
||||
return
|
||||
instance = kwargs['instance']
|
||||
if not instance.pk or instance.active:
|
||||
return
|
||||
instance._saved_inventory_pk = instance.inventory.pk
|
||||
instance._saved_parents_pks = set(instance.parents.values_list('pk', flat=True))
|
||||
instance._saved_hosts_pks = set(instance.hosts.values_list('pk', flat=True))
|
||||
instance._saved_children_pks = set(instance.children.values_list('pk', flat=True))
|
||||
instance._saved_inventory_source_pk = instance.inventory_source.pk
|
||||
|
||||
@receiver(post_save, sender=Group)
|
||||
def migrate_children_from_inactive_group_to_parent_groups(sender, **kwargs):
|
||||
if getattr(_inventory_updates, 'is_removing', False):
|
||||
return
|
||||
instance = kwargs['instance']
|
||||
if instance.active:
|
||||
return
|
||||
parents_pks = getattr(instance, '_saved_parents_pks', [])
|
||||
hosts_pks = getattr(instance, '_saved_hosts_pks', [])
|
||||
children_pks = getattr(instance, '_saved_children_pks', [])
|
||||
with ignore_inventory_group_removal():
|
||||
with ignore_inventory_computed_fields():
|
||||
if parents_pks:
|
||||
for parent_group in Group.objects.filter(pk__in=parents_pks, active=True):
|
||||
for child_host in Host.objects.filter(pk__in=hosts_pks, active=True):
|
||||
logger.debug('moving host %s to parent %s after marking group %s inactive',
|
||||
child_host, parent_group, instance)
|
||||
parent_group.hosts.add(child_host)
|
||||
for child_group in Group.objects.filter(pk__in=children_pks, active=True):
|
||||
logger.debug('moving group %s to parent %s after marking group %s inactive',
|
||||
child_group, parent_group, instance)
|
||||
parent_group.children.add(child_group)
|
||||
parent_group.children.remove(instance)
|
||||
inventory_source_pk = getattr(instance, '_saved_inventory_source_pk', None)
|
||||
if inventory_source_pk:
|
||||
try:
|
||||
inventory_source = InventorySource.objects.get(pk=inventory_source_pk, active=True)
|
||||
inventory_source.mark_inactive()
|
||||
except InventorySource.DoesNotExist:
|
||||
pass
|
||||
inventory_pk = getattr(instance, '_saved_inventory_pk', None)
|
||||
if not getattr(_inventory_updates, 'is_updating', False):
|
||||
if inventory_pk:
|
||||
try:
|
||||
inventory = Inventory.objects.get(pk=inventory_pk, active=True)
|
||||
inventory.update_computed_fields()
|
||||
except Inventory.DoesNotExist:
|
||||
pass
|
||||
|
||||
# Update host pointers to last_job and last_job_host_summary when a job is
|
||||
# marked inactive or deleted.
|
||||
# Update host pointers to last_job and last_job_host_summary when a job is deleted
|
||||
|
||||
def _update_host_last_jhs(host):
|
||||
jhs_qs = JobHostSummary.objects.filter(job__active=True, host__pk=host.pk)
|
||||
jhs_qs = JobHostSummary.objects.filter(host__pk=host.pk)
|
||||
try:
|
||||
jhs = jhs_qs.order_by('-job__pk')[0]
|
||||
except IndexError:
|
||||
@ -247,19 +331,10 @@ def _update_host_last_jhs(host):
|
||||
if update_fields:
|
||||
host.save(update_fields=update_fields)
|
||||
|
||||
@receiver(post_save, sender=Job)
|
||||
def update_host_last_job_when_job_marked_inactive(sender, **kwargs):
|
||||
instance = kwargs['instance']
|
||||
if instance.active:
|
||||
return
|
||||
hosts_qs = Host.objects.filter(active=True, last_job__pk=instance.pk)
|
||||
for host in hosts_qs:
|
||||
_update_host_last_jhs(host)
|
||||
|
||||
@receiver(pre_delete, sender=Job)
|
||||
def save_host_pks_before_job_delete(sender, **kwargs):
|
||||
instance = kwargs['instance']
|
||||
hosts_qs = Host.objects.filter(active=True, last_job__pk=instance.pk)
|
||||
hosts_qs = Host.objects.filter( last_job__pk=instance.pk)
|
||||
instance._saved_hosts_pks = set(hosts_qs.values_list('pk', flat=True))
|
||||
|
||||
@receiver(post_delete, sender=Job)
|
||||
@ -302,7 +377,6 @@ model_serializer_mapping = {
|
||||
Credential: CredentialSerializer,
|
||||
Team: TeamSerializer,
|
||||
Project: ProjectSerializer,
|
||||
Permission: PermissionSerializer,
|
||||
JobTemplate: JobTemplateSerializer,
|
||||
Job: JobSerializer,
|
||||
AdHocCommand: AdHocCommandSerializer,
|
||||
@ -339,11 +413,6 @@ def activity_stream_update(sender, instance, **kwargs):
|
||||
except sender.DoesNotExist:
|
||||
return
|
||||
|
||||
# Handle the AWX mark-inactive for delete event
|
||||
if hasattr(instance, 'active') and not instance.active:
|
||||
activity_stream_delete(sender, instance, **kwargs)
|
||||
return
|
||||
|
||||
new = instance
|
||||
changes = model_instance_diff(old, new, model_serializer_mapping)
|
||||
if changes is None:
|
||||
|
||||
@ -13,7 +13,7 @@ class Migration(DataMigration):
|
||||
# and orm['appname.ModelName'] for models in other applications.
|
||||
|
||||
# Refresh has_active_failures for all hosts.
|
||||
for host in orm.Host.objects.filter(active=True):
|
||||
for host in orm.Host.objects:
|
||||
has_active_failures = bool(host.last_job_host_summary and
|
||||
host.last_job_host_summary.job.active and
|
||||
host.last_job_host_summary.failed)
|
||||
@ -30,9 +30,9 @@ class Migration(DataMigration):
|
||||
for subgroup in group.children.exclude(pk__in=except_group_pks):
|
||||
qs = qs | get_all_hosts_for_group(subgroup, except_group_pks)
|
||||
return qs
|
||||
for group in orm.Group.objects.filter(active=True):
|
||||
for group in orm.Group.objects:
|
||||
all_hosts = get_all_hosts_for_group(group)
|
||||
failed_hosts = all_hosts.filter(active=True,
|
||||
failed_hosts = all_hosts.filter(
|
||||
last_job_host_summary__job__active=True,
|
||||
last_job_host_summary__failed=True)
|
||||
hosts_with_active_failures = failed_hosts.count()
|
||||
@ -49,8 +49,8 @@ class Migration(DataMigration):
|
||||
|
||||
# Now update has_active_failures and hosts_with_active_failures for all
|
||||
# inventories.
|
||||
for inventory in orm.Inventory.objects.filter(active=True):
|
||||
failed_hosts = inventory.hosts.filter(active=True, has_active_failures=True)
|
||||
for inventory in orm.Inventory.objects:
|
||||
failed_hosts = inventory.hosts.filter( has_active_failures=True)
|
||||
hosts_with_active_failures = failed_hosts.count()
|
||||
has_active_failures = bool(hosts_with_active_failures)
|
||||
changed = False
|
||||
|
||||
@ -8,7 +8,7 @@ from django.db import models
|
||||
class Migration(DataMigration):
|
||||
|
||||
def forwards(self, orm):
|
||||
for iu in orm.InventoryUpdate.objects.filter(active=True):
|
||||
for iu in orm.InventoryUpdate.objects:
|
||||
if iu.inventory_source is None or iu.inventory_source.group is None or iu.inventory_source.inventory is None:
|
||||
continue
|
||||
iu.name = "%s (%s)" % (iu.inventory_source.group.name, iu.inventory_source.inventory.name)
|
||||
|
||||
@ -12,7 +12,7 @@ from django.conf import settings
|
||||
class Migration(DataMigration):
|
||||
|
||||
def forwards(self, orm):
|
||||
for j in orm.UnifiedJob.objects.filter(active=True):
|
||||
for j in orm.UnifiedJob.objects:
|
||||
cur = connection.cursor()
|
||||
stdout_filename = os.path.join(settings.JOBOUTPUT_ROOT, "%d-%s.out" % (j.pk, str(uuid.uuid1())))
|
||||
fd = open(stdout_filename, 'w')
|
||||
|
||||
@ -50,7 +50,7 @@ from awx.main.queue import FifoQueue
|
||||
from awx.main.conf import tower_settings
|
||||
from awx.main.task_engine import TaskSerializer, TASK_TIMEOUT_INTERVAL
|
||||
from awx.main.utils import (get_ansible_version, get_ssh_version, decrypt_field, update_scm_url,
|
||||
ignore_inventory_computed_fields, emit_websocket_notification,
|
||||
emit_websocket_notification,
|
||||
check_proot_installed, build_proot_temp_dir, wrap_args_with_proot)
|
||||
|
||||
__all__ = ['RunJob', 'RunSystemJob', 'RunProjectUpdate', 'RunInventoryUpdate',
|
||||
@ -109,17 +109,6 @@ def run_administrative_checks(self):
|
||||
tower_admin_emails,
|
||||
fail_silently=True)
|
||||
|
||||
@task()
|
||||
def bulk_inventory_element_delete(inventory, hosts=[], groups=[]):
|
||||
from awx.main.signals import disable_activity_stream
|
||||
with ignore_inventory_computed_fields():
|
||||
with disable_activity_stream():
|
||||
for group in groups:
|
||||
Group.objects.get(id=group).mark_inactive(skip_active_check=True)
|
||||
for host in hosts:
|
||||
Host.objects.get(id=host).mark_inactive(skip_active_check=True)
|
||||
update_inventory_computed_fields(inventory)
|
||||
|
||||
@task(bind=True)
|
||||
def tower_periodic_scheduler(self):
|
||||
def get_last_run():
|
||||
@ -879,12 +868,12 @@ class RunJob(BaseTask):
|
||||
'tower_job_id': job.pk,
|
||||
'tower_job_launch_type': job.launch_type,
|
||||
}
|
||||
if job.job_template and job.job_template.active:
|
||||
if job.job_template:
|
||||
extra_vars.update({
|
||||
'tower_job_template_id': job.job_template.pk,
|
||||
'tower_job_template_name': job.job_template.name,
|
||||
})
|
||||
if job.created_by and job.created_by.is_active:
|
||||
if job.created_by:
|
||||
extra_vars.update({
|
||||
'tower_user_id': job.created_by.pk,
|
||||
'tower_user_name': job.created_by.username,
|
||||
@ -1377,7 +1366,7 @@ class RunInventoryUpdate(BaseTask):
|
||||
runpath = tempfile.mkdtemp(prefix='ansible_tower_launch_')
|
||||
handle, path = tempfile.mkstemp(dir=runpath)
|
||||
f = os.fdopen(handle, 'w')
|
||||
if inventory_update.source_script is None or not inventory_update.source_script.active:
|
||||
if inventory_update.source_script is None:
|
||||
raise RuntimeError('Inventory Script does not exist')
|
||||
f.write(inventory_update.source_script.script.encode('utf-8'))
|
||||
f.close()
|
||||
|
||||
@ -92,7 +92,7 @@ def test_basic_fields(hosts, fact_scans, get, user):
|
||||
}
|
||||
|
||||
(host, response) = setup_common(hosts, fact_scans, get, user, epoch=epoch, get_params=search)
|
||||
|
||||
|
||||
results = response.data['results']
|
||||
assert 'related' in results[0]
|
||||
assert 'timestamp' in results[0]
|
||||
@ -118,12 +118,12 @@ def test_basic_options_fields(hosts, fact_scans, options, user):
|
||||
@pytest.mark.django_db
|
||||
def test_related_fact_view(hosts, fact_scans, get, user):
|
||||
epoch = timezone.now()
|
||||
|
||||
|
||||
(host, response) = setup_common(hosts, fact_scans, get, user, epoch=epoch)
|
||||
facts_known = Fact.get_timeline(host.id)
|
||||
assert 9 == len(facts_known)
|
||||
assert 9 == len(response.data['results'])
|
||||
|
||||
|
||||
for i, fact_known in enumerate(facts_known):
|
||||
check_url(response.data['results'][i]['related']['fact_view'], fact_known, fact_known.module)
|
||||
|
||||
@ -131,12 +131,12 @@ def test_related_fact_view(hosts, fact_scans, get, user):
|
||||
@pytest.mark.django_db
|
||||
def test_multiple_hosts(hosts, fact_scans, get, user):
|
||||
epoch = timezone.now()
|
||||
|
||||
|
||||
(host, response) = setup_common(hosts, fact_scans, get, user, epoch=epoch, host_count=3)
|
||||
facts_known = Fact.get_timeline(host.id)
|
||||
assert 9 == len(facts_known)
|
||||
assert 9 == len(response.data['results'])
|
||||
|
||||
|
||||
for i, fact_known in enumerate(facts_known):
|
||||
check_url(response.data['results'][i]['related']['fact_view'], fact_known, fact_known.module)
|
||||
|
||||
@ -153,7 +153,7 @@ def test_param_to_from(hosts, fact_scans, get, user):
|
||||
facts_known = Fact.get_timeline(host.id, ts_from=search['from'], ts_to=search['to'])
|
||||
assert 9 == len(facts_known)
|
||||
assert 9 == len(response.data['results'])
|
||||
|
||||
|
||||
check_response_facts(facts_known, response)
|
||||
|
||||
@mock.patch('awx.api.views.feature_enabled', new=mock_feature_enabled)
|
||||
@ -168,7 +168,7 @@ def test_param_module(hosts, fact_scans, get, user):
|
||||
facts_known = Fact.get_timeline(host.id, module=search['module'])
|
||||
assert 3 == len(facts_known)
|
||||
assert 3 == len(response.data['results'])
|
||||
|
||||
|
||||
check_response_facts(facts_known, response)
|
||||
|
||||
@mock.patch('awx.api.views.feature_enabled', new=mock_feature_enabled)
|
||||
@ -183,7 +183,7 @@ def test_param_from(hosts, fact_scans, get, user):
|
||||
facts_known = Fact.get_timeline(host.id, ts_from=search['from'])
|
||||
assert 3 == len(facts_known)
|
||||
assert 3 == len(response.data['results'])
|
||||
|
||||
|
||||
check_response_facts(facts_known, response)
|
||||
|
||||
@mock.patch('awx.api.views.feature_enabled', new=mock_feature_enabled)
|
||||
@ -198,14 +198,14 @@ def test_param_to(hosts, fact_scans, get, user):
|
||||
facts_known = Fact.get_timeline(host.id, ts_to=search['to'])
|
||||
assert 6 == len(facts_known)
|
||||
assert 6 == len(response.data['results'])
|
||||
|
||||
|
||||
check_response_facts(facts_known, response)
|
||||
|
||||
def _test_user_access_control(hosts, fact_scans, get, user_obj, team_obj):
|
||||
hosts = hosts(host_count=1)
|
||||
fact_scans(fact_scans=1)
|
||||
|
||||
team_obj.users.add(user_obj)
|
||||
team_obj.member_role.members.add(user_obj)
|
||||
|
||||
url = reverse('api:host_fact_versions_list', args=(hosts[0].pk,))
|
||||
response = get(url, user_obj)
|
||||
@ -235,7 +235,7 @@ def test_super_user_ok(hosts, fact_scans, get, user, team):
|
||||
@pytest.mark.django_db
|
||||
def test_user_admin_ok(organization, hosts, fact_scans, get, user, team):
|
||||
user_admin = user('johnson', False)
|
||||
organization.admins.add(user_admin)
|
||||
organization.admin_role.members.add(user_admin)
|
||||
|
||||
response = _test_user_access_control(hosts, fact_scans, get, user_admin, team)
|
||||
|
||||
@ -247,7 +247,7 @@ def test_user_admin_ok(organization, hosts, fact_scans, get, user, team):
|
||||
def test_user_admin_403(organization, organizations, hosts, fact_scans, get, user, team):
|
||||
user_admin = user('johnson', False)
|
||||
org2 = organizations(1)
|
||||
org2[0].admins.add(user_admin)
|
||||
org2[0].admin_role.members.add(user_admin)
|
||||
|
||||
response = _test_user_access_control(hosts, fact_scans, get, user_admin, team)
|
||||
|
||||
|
||||
@ -87,7 +87,7 @@ def test_basic_fields(hosts, fact_scans, get, user):
|
||||
assert 'description' in response.data['summary_fields']['host']
|
||||
assert 'host' in response.data['related']
|
||||
assert reverse('api:host_detail', args=(hosts[0].pk,)) == response.data['related']['host']
|
||||
|
||||
|
||||
@mock.patch('awx.api.views.feature_enabled', new=mock_feature_enabled)
|
||||
@pytest.mark.django_db
|
||||
def test_content(hosts, fact_scans, get, user, fact_ansible_json):
|
||||
@ -103,7 +103,7 @@ def _test_search_by_module(hosts, fact_scans, get, user, fact_json, module_name)
|
||||
'module': module_name
|
||||
}
|
||||
(fact_known, response) = setup_common(hosts, fact_scans, get, user, module_name=module_name, get_params=params)
|
||||
|
||||
|
||||
assert fact_json == json.loads(response.data['facts'])
|
||||
assert timestamp_apiformat(fact_known.timestamp) == response.data['timestamp']
|
||||
assert module_name == response.data['module']
|
||||
@ -132,7 +132,7 @@ def _test_user_access_control(hosts, fact_scans, get, user_obj, team_obj):
|
||||
hosts = hosts(host_count=1)
|
||||
fact_scans(fact_scans=1)
|
||||
|
||||
team_obj.users.add(user_obj)
|
||||
team_obj.member_role.members.add(user_obj)
|
||||
|
||||
url = reverse('api:host_fact_compare_view', args=(hosts[0].pk,))
|
||||
response = get(url, user_obj)
|
||||
@ -162,7 +162,7 @@ def test_super_user_ok(hosts, fact_scans, get, user, team):
|
||||
@pytest.mark.django_db
|
||||
def test_user_admin_ok(organization, hosts, fact_scans, get, user, team):
|
||||
user_admin = user('johnson', False)
|
||||
organization.admins.add(user_admin)
|
||||
organization.admin_role.members.add(user_admin)
|
||||
|
||||
response = _test_user_access_control(hosts, fact_scans, get, user_admin, team)
|
||||
|
||||
@ -174,7 +174,7 @@ def test_user_admin_ok(organization, hosts, fact_scans, get, user, team):
|
||||
def test_user_admin_403(organization, organizations, hosts, fact_scans, get, user, team):
|
||||
user_admin = user('johnson', False)
|
||||
org2 = organizations(1)
|
||||
org2[0].admins.add(user_admin)
|
||||
org2[0].admin_role.members.add(user_admin)
|
||||
|
||||
response = _test_user_access_control(hosts, fact_scans, get, user_admin, team)
|
||||
|
||||
|
||||
@ -15,7 +15,6 @@ from django.conf import settings
|
||||
|
||||
# AWX
|
||||
from awx.main.models.projects import Project
|
||||
from awx.main.models.organization import Organization, Permission
|
||||
from awx.main.models.base import PERM_INVENTORY_READ
|
||||
from awx.main.models.ha import Instance
|
||||
from awx.main.models.fact import Fact
|
||||
@ -25,6 +24,18 @@ from rest_framework.test import (
|
||||
force_authenticate,
|
||||
)
|
||||
|
||||
from awx.main.models.credential import Credential
|
||||
from awx.main.models.jobs import JobTemplate
|
||||
from awx.main.models.inventory import (
|
||||
Group,
|
||||
)
|
||||
from awx.main.models.organization import (
|
||||
Organization,
|
||||
Permission,
|
||||
)
|
||||
|
||||
from awx.main.models.rbac import Role
|
||||
|
||||
'''
|
||||
Disable all django model signals.
|
||||
'''
|
||||
@ -54,6 +65,131 @@ def user():
|
||||
return user
|
||||
return u
|
||||
|
||||
@pytest.fixture
|
||||
def check_jobtemplate(project, inventory, credential):
|
||||
return \
|
||||
JobTemplate.objects.create(
|
||||
job_type='check',
|
||||
project=project,
|
||||
inventory=inventory,
|
||||
credential=credential,
|
||||
name='check-job-template'
|
||||
)
|
||||
|
||||
@pytest.fixture
|
||||
def deploy_jobtemplate(project, inventory, credential):
|
||||
return \
|
||||
JobTemplate.objects.create(
|
||||
job_type='run',
|
||||
project=project,
|
||||
inventory=inventory,
|
||||
credential=credential,
|
||||
name='deploy-job-template'
|
||||
)
|
||||
|
||||
@pytest.fixture
|
||||
def team(organization):
|
||||
return organization.teams.create(name='test-team')
|
||||
|
||||
@pytest.fixture
|
||||
@mock.patch.object(Project, "update", lambda self, **kwargs: None)
|
||||
def project(instance, organization):
|
||||
prj = Project.objects.create(name="test-proj",
|
||||
description="test-proj-desc",
|
||||
scm_type="git",
|
||||
scm_url="https://github.com/jlaska/ansible-playbooks",
|
||||
organization=organization
|
||||
)
|
||||
return prj
|
||||
|
||||
@pytest.fixture
|
||||
def user_project(user):
|
||||
owner = user('owner')
|
||||
return Project.objects.create(name="test-user-project", created_by=owner, description="test-user-project-desc")
|
||||
|
||||
@pytest.fixture
|
||||
def instance(settings):
|
||||
return Instance.objects.create(uuid=settings.SYSTEM_UUID, primary=True, hostname="instance.example.org")
|
||||
|
||||
@pytest.fixture
|
||||
def organization(instance):
|
||||
return Organization.objects.create(name="test-org", description="test-org-desc")
|
||||
|
||||
@pytest.fixture
|
||||
def credential():
|
||||
return Credential.objects.create(kind='aws', name='test-cred')
|
||||
|
||||
@pytest.fixture
|
||||
def inventory(organization):
|
||||
return organization.inventories.create(name="test-inv")
|
||||
|
||||
@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 organizations(instance):
|
||||
def rf(organization_count=1):
|
||||
orgs = []
|
||||
for i in xrange(0, organization_count):
|
||||
o = Organization.objects.create(name="test-org-%d" % i, description="test-org-desc")
|
||||
orgs.append(o)
|
||||
return orgs
|
||||
return rf
|
||||
|
||||
@pytest.fixture
|
||||
def group(inventory):
|
||||
def g(name):
|
||||
try:
|
||||
return Group.objects.get(name=name, inventory=inventory)
|
||||
except:
|
||||
return Group.objects.create(inventory=inventory, name=name)
|
||||
return g
|
||||
|
||||
@pytest.fixture
|
||||
def hosts(group):
|
||||
group1 = group('group-1')
|
||||
|
||||
def rf(host_count=1):
|
||||
hosts = []
|
||||
for i in xrange(0, host_count):
|
||||
name = '%s-host-%s' % (group1.name, i)
|
||||
(host, created) = group1.inventory.hosts.get_or_create(name=name)
|
||||
if created:
|
||||
group1.hosts.add(host)
|
||||
hosts.append(host)
|
||||
return hosts
|
||||
return rf
|
||||
|
||||
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def permissions():
|
||||
return {
|
||||
'admin':{'create':True, 'read':True, 'write':True,
|
||||
'update':True, 'delete':True, 'scm_update':True, 'execute':True, 'use':True,},
|
||||
|
||||
'auditor':{'read':True, 'create':False, 'write':False,
|
||||
'update':False, 'delete':False, 'scm_update':False, 'execute':False, 'use':False,},
|
||||
|
||||
'usage':{'read':False, 'create':False, 'write':False,
|
||||
'update':False, 'delete':False, 'scm_update':False, 'execute':False, 'use':True,},
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def post():
|
||||
def rf(url, data, user=None, middleware=None, **kwargs):
|
||||
@ -173,54 +309,12 @@ def options():
|
||||
return response
|
||||
return rf
|
||||
|
||||
@pytest.fixture
|
||||
def instance(settings):
|
||||
return Instance.objects.create(uuid=settings.SYSTEM_UUID, primary=True, hostname="instance.example.org")
|
||||
|
||||
@pytest.fixture
|
||||
def organization(instance):
|
||||
return Organization.objects.create(name="test-org", description="test-org-desc")
|
||||
|
||||
@pytest.fixture
|
||||
@mock.patch.object(Project, "update", lambda self, **kwargs: None)
|
||||
def project(instance):
|
||||
return Project.objects.create(name="test-proj",
|
||||
description="test-proj-desc",
|
||||
scm_type="git",
|
||||
scm_url="https://github.com/jlaska/ansible-playbooks")
|
||||
@pytest.fixture
|
||||
def organizations(instance):
|
||||
def rf(organization_count=1):
|
||||
orgs = []
|
||||
for i in xrange(0, organization_count):
|
||||
o = Organization.objects.create(name="test-org-%d" % i, description="test-org-desc")
|
||||
orgs.append(o)
|
||||
return orgs
|
||||
return rf
|
||||
|
||||
@pytest.fixture
|
||||
def inventory(organization):
|
||||
return organization.inventories.create(name="test-inv")
|
||||
|
||||
@pytest.fixture
|
||||
def group(inventory):
|
||||
return inventory.groups.create(name='group-1')
|
||||
|
||||
@pytest.fixture
|
||||
def hosts(group):
|
||||
def rf(host_count=1):
|
||||
hosts = []
|
||||
for i in xrange(0, host_count):
|
||||
name = '%s-host-%s' % (group.name, i)
|
||||
(host, created) = group.inventory.hosts.get_or_create(name=name)
|
||||
if created:
|
||||
group.hosts.add(host)
|
||||
hosts.append(host)
|
||||
return hosts
|
||||
return rf
|
||||
|
||||
@pytest.fixture
|
||||
def fact_scans(group, fact_ansible_json, fact_packages_json, fact_services_json):
|
||||
group1 = group('group-1')
|
||||
|
||||
def rf(fact_scans=1, timestamp_epoch=timezone.now()):
|
||||
facts_json = {}
|
||||
facts = []
|
||||
@ -232,7 +326,7 @@ def fact_scans(group, fact_ansible_json, fact_packages_json, fact_services_json)
|
||||
facts_json['services'] = fact_services_json
|
||||
|
||||
for i in xrange(0, fact_scans):
|
||||
for host in group.hosts.all():
|
||||
for host in group1.hosts.all():
|
||||
for module_name in module_names:
|
||||
facts.append(Fact.objects.create(host=host, timestamp=timestamp_current, module=module_name, facts=facts_json[module_name]))
|
||||
timestamp_current += timedelta(days=1)
|
||||
@ -256,10 +350,6 @@ def fact_packages_json():
|
||||
def fact_services_json():
|
||||
return _fact_json('services')
|
||||
|
||||
@pytest.fixture
|
||||
def team(organization):
|
||||
return organization.teams.create(name='test-team')
|
||||
|
||||
@pytest.fixture
|
||||
def permission_inv_read(organization, inventory, team):
|
||||
return Permission.objects.create(inventory=inventory, team=team, permission_type=PERM_INVENTORY_READ)
|
||||
|
||||
@ -69,7 +69,7 @@ def test_encrypted_subfields(get, post, user, organization):
|
||||
assert response.data['notification_configuration']['account_token'] == "$encrypted$"
|
||||
with mock.patch.object(notifier_actual.notification_class, "send_messages", assert_send):
|
||||
notifier_actual.send("Test", {'body': "Test"})
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_inherited_notifiers(get, post, user, organization, project):
|
||||
u = user('admin-poster', True)
|
||||
@ -86,7 +86,6 @@ def test_inherited_notifiers(get, post, user, organization, project):
|
||||
u)
|
||||
assert response.status_code == 201
|
||||
notifiers.append(response.data['id'])
|
||||
organization.projects.add(project)
|
||||
i = Inventory.objects.create(name='test', organization=organization)
|
||||
i.save()
|
||||
g = Group.objects.create(name='test', inventory=i)
|
||||
@ -109,7 +108,6 @@ def test_inherited_notifiers(get, post, user, organization, project):
|
||||
@pytest.mark.django_db
|
||||
def test_notifier_merging(get, post, user, organization, project, notifier):
|
||||
user('admin-poster', True)
|
||||
organization.projects.add(project)
|
||||
organization.notifiers_any.add(notifier)
|
||||
project.notifiers_any.add(notifier)
|
||||
assert len(project.notifiers['any']) == 1
|
||||
|
||||
447
awx/main/tests/functional/test_rbac_api.py
Normal file
447
awx/main/tests/functional/test_rbac_api.py
Normal file
@ -0,0 +1,447 @@
|
||||
import mock # noqa
|
||||
import pytest
|
||||
|
||||
from django.core.urlresolvers import reverse
|
||||
from awx.main.models.rbac import Role, ROLE_SINGLETON_SYSTEM_ADMINISTRATOR
|
||||
|
||||
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
|
||||
def test_get_roles_list_user(organization, inventory, team, get, user):
|
||||
'Users can see all roles they have access to, but not all roles'
|
||||
this_user = user('user-test_get_roles_list_user')
|
||||
organization.member_role.members.add(this_user)
|
||||
custom_role = Role.objects.create(name='custom_role-test_get_roles_list_user')
|
||||
organization.member_role.children.add(custom_role)
|
||||
|
||||
url = reverse('api:role_list')
|
||||
response = get(url, this_user)
|
||||
assert response.status_code == 200
|
||||
roles = response.data
|
||||
assert roles['count'] > 0
|
||||
assert roles['count'] == len(roles['results']) # just to make sure the tests below are valid
|
||||
|
||||
role_hash = {}
|
||||
|
||||
for r in roles['results']:
|
||||
role_hash[r['id']] = r
|
||||
|
||||
assert Role.singleton(ROLE_SINGLETON_SYSTEM_ADMINISTRATOR).id in role_hash
|
||||
assert organization.admin_role.id in role_hash
|
||||
assert organization.member_role.id in role_hash
|
||||
assert this_user.admin_role.id in role_hash
|
||||
assert custom_role.id in role_hash
|
||||
|
||||
assert inventory.admin_role.id not in role_hash
|
||||
assert team.member_role.id not in role_hash
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_cant_create_role(post, admin):
|
||||
"Ensure we can't create new roles through the api"
|
||||
# Some day we might want to do this, but until that is speced out, lets
|
||||
# ensure we don't slip up and allow this implicitly through some helper or
|
||||
# another
|
||||
response = post(reverse('api:role_list'), {'name': 'New Role'}, admin)
|
||||
assert response.status_code == 405
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_cant_delete_role(delete, admin):
|
||||
"Ensure we can't delete roles through the api"
|
||||
# Some day we might want to do this, but until that is speced out, lets
|
||||
# ensure we don't slip up and allow this implicitly through some helper or
|
||||
# another
|
||||
response = delete(reverse('api:role_detail', args=(admin.admin_role.id,)), admin)
|
||||
assert response.status_code == 405
|
||||
|
||||
|
||||
|
||||
#
|
||||
# /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_user_view_other_user_roles(organization, inventory, team, get, alice, bob):
|
||||
'Users can see roles for other users, but only the roles that that user has access to see as well'
|
||||
organization.member_role.members.add(alice)
|
||||
organization.admin_role.members.add(bob)
|
||||
custom_role = Role.objects.create(name='custom_role-test_user_view_admin_roles_list')
|
||||
organization.member_role.children.add(custom_role)
|
||||
team.member_role.members.add(bob)
|
||||
|
||||
# alice and bob are in the same org and can see some child role of that org.
|
||||
# Bob is an org admin, alice can see this.
|
||||
# Bob is in a team that alice is not, alice cannot see that bob is a member of that team.
|
||||
|
||||
url = reverse('api:user_roles_list', args=(bob.id,))
|
||||
response = get(url, alice)
|
||||
assert response.status_code == 200
|
||||
roles = response.data
|
||||
assert roles['count'] > 0
|
||||
assert roles['count'] == len(roles['results']) # just to make sure the tests below are valid
|
||||
|
||||
role_hash = {}
|
||||
for r in roles['results']:
|
||||
role_hash[r['id']] = r['name']
|
||||
|
||||
assert organization.admin_role.id in role_hash
|
||||
assert custom_role.id not in role_hash # doesn't show up in the user roles list, not an explicit grant
|
||||
assert Role.singleton(ROLE_SINGLETON_SYSTEM_ADMINISTRATOR).id not in role_hash
|
||||
assert inventory.admin_role.id not in role_hash
|
||||
assert team.member_role.id not in role_hash # alice can't see this
|
||||
|
||||
# again but this time alice is part of the team, and should be able to see the team role
|
||||
team.member_role.members.add(alice)
|
||||
response = get(url, alice)
|
||||
assert response.status_code == 200
|
||||
roles = response.data
|
||||
assert roles['count'] > 0
|
||||
assert roles['count'] == len(roles['results']) # just to make sure the tests below are valid
|
||||
|
||||
role_hash = {}
|
||||
for r in roles['results']:
|
||||
role_hash[r['id']] = r['name']
|
||||
|
||||
assert team.member_role.id in role_hash # Alice can now see this
|
||||
|
||||
|
||||
|
||||
|
||||
@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_405(put, admin, role):
|
||||
url = reverse('api:role_detail', args=(role.id,))
|
||||
response = put(url, {'name': 'Some new name'}, admin)
|
||||
assert response.status_code == 405
|
||||
#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, role):
|
||||
url = reverse('api:role_detail', args=(role.id,))
|
||||
response = put(url, {'name': 'Some new name'}, alice)
|
||||
assert response.status_code == 403 or response.status_code == 405
|
||||
|
||||
|
||||
#
|
||||
# /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
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_org_admin_add_user_to_job_template(post, organization, check_jobtemplate, user):
|
||||
'Tests that a user with permissions to assign/revoke membership to a particular role can do so'
|
||||
org_admin = user('org-admin')
|
||||
joe = user('joe')
|
||||
organization.admin_role.members.add(org_admin)
|
||||
|
||||
assert check_jobtemplate.accessible_by(org_admin, {'write': True}) is True
|
||||
assert check_jobtemplate.accessible_by(joe, {'execute': True}) is False
|
||||
|
||||
post(reverse('api:role_users_list', args=(check_jobtemplate.executor_role.id,)), {'id': joe.id}, org_admin)
|
||||
|
||||
assert check_jobtemplate.accessible_by(joe, {'execute': True}) is True
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_org_admin_remove_user_to_job_template(post, organization, check_jobtemplate, user):
|
||||
'Tests that a user with permissions to assign/revoke membership to a particular role can do so'
|
||||
org_admin = user('org-admin')
|
||||
joe = user('joe')
|
||||
organization.admin_role.members.add(org_admin)
|
||||
check_jobtemplate.executor_role.members.add(joe)
|
||||
|
||||
assert check_jobtemplate.accessible_by(org_admin, {'write': True}) is True
|
||||
assert check_jobtemplate.accessible_by(joe, {'execute': True}) is True
|
||||
|
||||
post(reverse('api:role_users_list', args=(check_jobtemplate.executor_role.id,)), {'disassociate': True, 'id': joe.id}, org_admin)
|
||||
|
||||
assert check_jobtemplate.accessible_by(joe, {'execute': True}) is False
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_user_fail_to_add_user_to_job_template(post, organization, check_jobtemplate, user):
|
||||
'Tests that a user without permissions to assign/revoke membership to a particular role cannot do so'
|
||||
rando = user('rando')
|
||||
joe = user('joe')
|
||||
|
||||
assert check_jobtemplate.accessible_by(rando, {'write': True}) is False
|
||||
assert check_jobtemplate.accessible_by(joe, {'execute': True}) is False
|
||||
|
||||
res = post(reverse('api:role_users_list', args=(check_jobtemplate.executor_role.id,)), {'id': joe.id}, rando)
|
||||
assert res.status_code == 403
|
||||
|
||||
assert check_jobtemplate.accessible_by(joe, {'execute': True}) is False
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_user_fail_to_remove_user_to_job_template(post, organization, check_jobtemplate, user):
|
||||
'Tests that a user without permissions to assign/revoke membership to a particular role cannot do so'
|
||||
rando = user('rando')
|
||||
joe = user('joe')
|
||||
check_jobtemplate.executor_role.members.add(joe)
|
||||
|
||||
assert check_jobtemplate.accessible_by(rando, {'write': True}) is False
|
||||
assert check_jobtemplate.accessible_by(joe, {'execute': True}) is True
|
||||
|
||||
res = post(reverse('api:role_users_list', args=(check_jobtemplate.executor_role.id,)), {'disassociate': True, 'id': joe.id}, rando)
|
||||
assert res.status_code == 403
|
||||
|
||||
assert check_jobtemplate.accessible_by(joe, {'execute': True}) is True
|
||||
|
||||
|
||||
#
|
||||
# /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)
|
||||
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)
|
||||
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)
|
||||
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
|
||||
|
||||
|
||||
#
|
||||
# /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
|
||||
|
||||
|
||||
|
||||
|
||||
#
|
||||
# /resource/<id>/access_list
|
||||
#
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_resource_access_list(get, team, admin, role):
|
||||
team.member_role.members.add(admin)
|
||||
url = reverse('api:team_access_list', args=(team.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 '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):
|
||||
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):
|
||||
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
|
||||
260
awx/main/tests/functional/test_rbac_core.py
Normal file
260
awx/main/tests/functional/test_rbac_core.py
Normal file
@ -0,0 +1,260 @@
|
||||
import pytest
|
||||
|
||||
from awx.main.models import (
|
||||
Role,
|
||||
RolePermission,
|
||||
Organization,
|
||||
Project,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_auto_inheritance_by_children(organization, alice):
|
||||
A = Role.objects.create(name='A')
|
||||
B = Role.objects.create(name='B')
|
||||
A.members.add(alice)
|
||||
|
||||
|
||||
|
||||
assert organization.accessible_by(alice, {'read': True}) is False
|
||||
assert Organization.accessible_objects(alice, {'read': True}).count() == 0
|
||||
A.children.add(B)
|
||||
assert organization.accessible_by(alice, {'read': True}) is False
|
||||
assert Organization.accessible_objects(alice, {'read': True}).count() == 0
|
||||
A.children.add(organization.admin_role)
|
||||
assert organization.accessible_by(alice, {'read': True}) is True
|
||||
assert Organization.accessible_objects(alice, {'read': True}).count() == 1
|
||||
A.children.remove(organization.admin_role)
|
||||
assert organization.accessible_by(alice, {'read': True}) is False
|
||||
B.children.add(organization.admin_role)
|
||||
assert organization.accessible_by(alice, {'read': True}) is True
|
||||
B.children.remove(organization.admin_role)
|
||||
assert organization.accessible_by(alice, {'read': True}) is False
|
||||
assert Organization.accessible_objects(alice, {'read': True}).count() == 0
|
||||
|
||||
# We've had the case where our pre/post save init handlers in our field descriptors
|
||||
# end up creating a ton of role objects because of various not-so-obvious issues
|
||||
assert Role.objects.count() < 50
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_auto_inheritance_by_parents(organization, alice):
|
||||
A = Role.objects.create(name='A')
|
||||
B = Role.objects.create(name='B')
|
||||
A.members.add(alice)
|
||||
|
||||
assert organization.accessible_by(alice, {'read': True}) is False
|
||||
B.parents.add(A)
|
||||
assert organization.accessible_by(alice, {'read': True}) is False
|
||||
organization.admin_role.parents.add(A)
|
||||
assert organization.accessible_by(alice, {'read': True}) is True
|
||||
organization.admin_role.parents.remove(A)
|
||||
assert organization.accessible_by(alice, {'read': True}) is False
|
||||
organization.admin_role.parents.add(B)
|
||||
assert organization.accessible_by(alice, {'read': True}) is True
|
||||
organization.admin_role.parents.remove(B)
|
||||
assert organization.accessible_by(alice, {'read': True}) is False
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_permission_union(organization, alice):
|
||||
A = Role.objects.create(name='A')
|
||||
A.members.add(alice)
|
||||
B = Role.objects.create(name='B')
|
||||
B.members.add(alice)
|
||||
|
||||
assert organization.accessible_by(alice, {'read': True, 'write': True}) is False
|
||||
RolePermission.objects.create(role=A, resource=organization, read=True)
|
||||
assert organization.accessible_by(alice, {'read': True, 'write': True}) is False
|
||||
RolePermission.objects.create(role=A, resource=organization, write=True)
|
||||
assert organization.accessible_by(alice, {'read': True, 'write': True}) is True
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_accessible_objects(organization, alice, bob):
|
||||
A = Role.objects.create(name='A')
|
||||
A.members.add(alice)
|
||||
B = Role.objects.create(name='B')
|
||||
B.members.add(alice)
|
||||
B.members.add(bob)
|
||||
|
||||
assert Organization.accessible_objects(alice, {'read': True, 'write': True}).count() == 0
|
||||
RolePermission.objects.create(role=A, resource=organization, read=True)
|
||||
assert Organization.accessible_objects(alice, {'read': True, 'write': True}).count() == 0
|
||||
assert Organization.accessible_objects(bob, {'read': True, 'write': True}).count() == 0
|
||||
RolePermission.objects.create(role=B, resource=organization, write=True)
|
||||
assert Organization.accessible_objects(alice, {'read': True, 'write': True}).count() == 1
|
||||
assert Organization.accessible_objects(bob, {'read': True, 'write': True}).count() == 0
|
||||
assert Organization.accessible_objects(bob, {'read': True, 'write': True}).count() == 0
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_team_symantics(organization, team, alice):
|
||||
assert organization.accessible_by(alice, {'read': True}) is False
|
||||
team.member_role.children.add(organization.auditor_role)
|
||||
assert organization.accessible_by(alice, {'read': True}) is False
|
||||
team.member_role.members.add(alice)
|
||||
assert organization.accessible_by(alice, {'read': True}) is True
|
||||
team.member_role.members.remove(alice)
|
||||
assert organization.accessible_by(alice, {'read': True}) is False
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_auto_m2m_adjuments(organization, inventory, group, alice):
|
||||
'Ensures the auto role reparenting is working correctly through m2m maps'
|
||||
g1 = group(name='g1')
|
||||
g1.admin_role.members.add(alice)
|
||||
assert g1.accessible_by(alice, {'read': True}) is True
|
||||
g2 = group(name='g2')
|
||||
assert g2.accessible_by(alice, {'read': True}) is False
|
||||
|
||||
g2.parents.add(g1)
|
||||
assert g2.accessible_by(alice, {'read': True}) is True
|
||||
g2.parents.remove(g1)
|
||||
assert g2.accessible_by(alice, {'read': True}) is False
|
||||
|
||||
g1.children.add(g2)
|
||||
assert g2.accessible_by(alice, {'read': True}) is True
|
||||
g1.children.remove(g2)
|
||||
assert g2.accessible_by(alice, {'read': True}) is False
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_auto_field_adjuments(organization, inventory, team, alice):
|
||||
'Ensures the auto role reparenting is working correctly through non m2m fields'
|
||||
org2 = Organization.objects.create(name='Org 2', description='org 2')
|
||||
org2.admin_role.members.add(alice)
|
||||
assert inventory.accessible_by(alice, {'read': True}) is False
|
||||
inventory.organization = org2
|
||||
inventory.save()
|
||||
assert inventory.accessible_by(alice, {'read': True}) is True
|
||||
inventory.organization = organization
|
||||
inventory.save()
|
||||
assert inventory.accessible_by(alice, {'read': True}) is False
|
||||
#assert False
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_implicit_deletes(alice):
|
||||
'Ensures implicit resources and roles delete themselves'
|
||||
delorg = Organization.objects.create(name='test-org')
|
||||
child = Role.objects.create(name='child-role')
|
||||
child.parents.add(delorg.admin_role)
|
||||
delorg.admin_role.members.add(alice)
|
||||
|
||||
admin_role_id = delorg.admin_role.id
|
||||
auditor_role_id = delorg.auditor_role.id
|
||||
|
||||
assert child.ancestors.count() > 1
|
||||
assert Role.objects.filter(id=admin_role_id).count() == 1
|
||||
assert Role.objects.filter(id=auditor_role_id).count() == 1
|
||||
n_alice_roles = alice.roles.count()
|
||||
n_system_admin_children = Role.singleton('System Administrator').children.count()
|
||||
rp = RolePermission.objects.create(role=delorg.admin_role, resource=delorg, read=True)
|
||||
|
||||
delorg.delete()
|
||||
|
||||
assert Role.objects.filter(id=admin_role_id).count() == 0
|
||||
assert Role.objects.filter(id=auditor_role_id).count() == 0
|
||||
assert alice.roles.count() == (n_alice_roles - 1)
|
||||
assert RolePermission.objects.filter(id=rp.id).count() == 0
|
||||
assert Role.singleton('System Administrator').children.count() == (n_system_admin_children - 1)
|
||||
assert child.ancestors.count() == 1
|
||||
assert child.ancestors.all()[0] == child
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_content_object(user):
|
||||
'Ensure our content_object stuf seems to be working'
|
||||
|
||||
org = Organization.objects.create(name='test-org')
|
||||
assert org.admin_role.content_object.id == org.id
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_hierarchy_rebuilding():
|
||||
'Tests some subdtle cases around role hierarchy rebuilding'
|
||||
|
||||
X = Role.objects.create(name='X')
|
||||
A = Role.objects.create(name='A')
|
||||
B = Role.objects.create(name='B')
|
||||
C = Role.objects.create(name='C')
|
||||
D = Role.objects.create(name='D')
|
||||
|
||||
A.children.add(B)
|
||||
A.children.add(D)
|
||||
B.children.add(C)
|
||||
C.children.add(D)
|
||||
|
||||
assert A.is_ancestor_of(D)
|
||||
assert X.is_ancestor_of(D) is False
|
||||
|
||||
X.children.add(A)
|
||||
|
||||
assert X.is_ancestor_of(D) is True
|
||||
|
||||
X.children.remove(A)
|
||||
|
||||
# This can be the stickler, the rebuilder needs to ensure that D's role
|
||||
# hierarchy is built after both A and C are updated.
|
||||
assert X.is_ancestor_of(D) is False
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_auto_parenting():
|
||||
org1 = Organization.objects.create(name='org1')
|
||||
org2 = Organization.objects.create(name='org2')
|
||||
|
||||
prj1 = Project.objects.create(name='prj1')
|
||||
prj2 = Project.objects.create(name='prj2')
|
||||
|
||||
assert org1.admin_role.is_ancestor_of(prj1.admin_role) is False
|
||||
assert org1.admin_role.is_ancestor_of(prj2.admin_role) is False
|
||||
assert org2.admin_role.is_ancestor_of(prj1.admin_role) is False
|
||||
assert org2.admin_role.is_ancestor_of(prj2.admin_role) is False
|
||||
|
||||
prj1.organization = org1
|
||||
prj1.save()
|
||||
|
||||
assert org1.admin_role.is_ancestor_of(prj1.admin_role)
|
||||
assert org1.admin_role.is_ancestor_of(prj2.admin_role) is False
|
||||
assert org2.admin_role.is_ancestor_of(prj1.admin_role) is False
|
||||
assert org2.admin_role.is_ancestor_of(prj2.admin_role) is False
|
||||
|
||||
prj2.organization = org1
|
||||
prj2.save()
|
||||
|
||||
assert org1.admin_role.is_ancestor_of(prj1.admin_role)
|
||||
assert org1.admin_role.is_ancestor_of(prj2.admin_role)
|
||||
assert org2.admin_role.is_ancestor_of(prj1.admin_role) is False
|
||||
assert org2.admin_role.is_ancestor_of(prj2.admin_role) is False
|
||||
|
||||
prj1.organization = org2
|
||||
prj1.save()
|
||||
|
||||
assert org1.admin_role.is_ancestor_of(prj1.admin_role) is False
|
||||
assert org1.admin_role.is_ancestor_of(prj2.admin_role)
|
||||
assert org2.admin_role.is_ancestor_of(prj1.admin_role)
|
||||
assert org2.admin_role.is_ancestor_of(prj2.admin_role) is False
|
||||
|
||||
prj2.organization = org2
|
||||
prj2.save()
|
||||
|
||||
assert org1.admin_role.is_ancestor_of(prj1.admin_role) is False
|
||||
assert org1.admin_role.is_ancestor_of(prj2.admin_role) is False
|
||||
assert org2.admin_role.is_ancestor_of(prj1.admin_role)
|
||||
assert org2.admin_role.is_ancestor_of(prj2.admin_role)
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_auto_m2m_parenting(team, project, user):
|
||||
u = user('some-user')
|
||||
team.member_role.members.add(u)
|
||||
|
||||
assert project.accessible_by(u, {'read': True}) is False
|
||||
|
||||
project.teams.add(team)
|
||||
assert project.accessible_by(u, {'read': True})
|
||||
project.teams.remove(team)
|
||||
assert project.accessible_by(u, {'read': True}) is False
|
||||
|
||||
team.projects.add(project)
|
||||
assert project.accessible_by(u, {'read': True})
|
||||
team.projects.remove(project)
|
||||
assert project.accessible_by(u, {'read': True}) is False
|
||||
|
||||
206
awx/main/tests/functional/test_rbac_credential.py
Normal file
206
awx/main/tests/functional/test_rbac_credential.py
Normal file
@ -0,0 +1,206 @@
|
||||
import pytest
|
||||
|
||||
from awx.main.access import CredentialAccess
|
||||
from awx.main.models.credential import Credential
|
||||
from awx.main.models.jobs import JobTemplate
|
||||
from awx.main.models.inventory import InventorySource
|
||||
from awx.main.migrations import _rbac as rbac
|
||||
from django.apps import apps
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_credential_migration_user(credential, user, permissions):
|
||||
u = user('user', False)
|
||||
credential.user = u
|
||||
credential.save()
|
||||
|
||||
migrated = rbac.migrate_credential(apps, None)
|
||||
|
||||
assert len(migrated) == 1
|
||||
assert credential.accessible_by(u, permissions['admin'])
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_credential_usage_role(credential, user, permissions):
|
||||
u = user('user', False)
|
||||
credential.usage_role.members.add(u)
|
||||
assert credential.accessible_by(u, permissions['usage'])
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_credential_migration_team_member(credential, team, user, permissions):
|
||||
u = user('user', False)
|
||||
team.admin_role.members.add(u)
|
||||
credential.team = team
|
||||
credential.save()
|
||||
|
||||
|
||||
# No permissions pre-migration (this happens automatically so we patch this)
|
||||
team.admin_role.children.remove(credential.owner_role)
|
||||
team.member_role.children.remove(credential.usage_role)
|
||||
assert not credential.accessible_by(u, permissions['admin'])
|
||||
|
||||
migrated = rbac.migrate_credential(apps, None)
|
||||
|
||||
# Admin permissions post migration
|
||||
assert len(migrated) == 1
|
||||
assert credential.accessible_by(u, permissions['admin'])
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_credential_migration_team_admin(credential, team, user, permissions):
|
||||
u = user('user', False)
|
||||
team.member_role.members.add(u)
|
||||
credential.team = team
|
||||
credential.save()
|
||||
|
||||
assert not credential.accessible_by(u, permissions['usage'])
|
||||
|
||||
# Usage permissions post migration
|
||||
migrated = rbac.migrate_credential(apps, None)
|
||||
assert len(migrated) == 1
|
||||
assert credential.accessible_by(u, permissions['usage'])
|
||||
|
||||
def test_credential_access_superuser():
|
||||
u = User(username='admin', is_superuser=True)
|
||||
access = CredentialAccess(u)
|
||||
credential = Credential()
|
||||
|
||||
assert access.can_add(None)
|
||||
assert access.can_change(credential, None)
|
||||
assert access.can_delete(credential)
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_credential_access_admin(user, team, credential):
|
||||
u = user('org-admin', False)
|
||||
team.organization.admin_role.members.add(u)
|
||||
|
||||
access = CredentialAccess(u)
|
||||
|
||||
assert access.can_add({'user': u.pk})
|
||||
assert not access.can_change(credential, {'user': u.pk})
|
||||
|
||||
# unowned credential is superuser only
|
||||
assert not access.can_delete(credential)
|
||||
|
||||
# credential is now part of a team
|
||||
# that is part of an organization
|
||||
# that I am an admin for
|
||||
credential.owner_role.parents.add(team.admin_role)
|
||||
credential.save()
|
||||
credential.owner_role.rebuild_role_ancestor_list()
|
||||
|
||||
cred = Credential.objects.create(kind='aws', name='test-cred')
|
||||
cred.team = team
|
||||
cred.save()
|
||||
|
||||
# should have can_change access as org-admin
|
||||
assert access.can_change(credential, {'user': u.pk})
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_cred_job_template(user, deploy_jobtemplate):
|
||||
a = user('admin', False)
|
||||
org = deploy_jobtemplate.project.organization
|
||||
org.admin_role.members.add(a)
|
||||
|
||||
cred = deploy_jobtemplate.credential
|
||||
cred.user = user('john', False)
|
||||
cred.save()
|
||||
|
||||
access = CredentialAccess(a)
|
||||
rbac.migrate_credential(apps, None)
|
||||
assert access.can_change(cred, {'organization': org.pk})
|
||||
|
||||
org.admin_role.members.remove(a)
|
||||
assert not access.can_change(cred, {'organization': org.pk})
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_cred_multi_job_template_single_org(user, deploy_jobtemplate):
|
||||
a = user('admin', False)
|
||||
org = deploy_jobtemplate.project.organization
|
||||
org.admin_role.members.add(a)
|
||||
|
||||
cred = deploy_jobtemplate.credential
|
||||
cred.user = user('john', False)
|
||||
cred.save()
|
||||
|
||||
access = CredentialAccess(a)
|
||||
rbac.migrate_credential(apps, None)
|
||||
assert access.can_change(cred, {'organization': org.pk})
|
||||
|
||||
org.admin_role.members.remove(a)
|
||||
assert not access.can_change(cred, {'organization': org.pk})
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_single_cred_multi_job_template_multi_org(user, organizations, credential):
|
||||
orgs = organizations(2)
|
||||
jts = []
|
||||
for org in orgs:
|
||||
inv = org.inventories.create(name="inv-%d" % org.pk)
|
||||
jt = JobTemplate.objects.create(
|
||||
inventory=inv,
|
||||
credential=credential,
|
||||
name="test-jt-org-%d" % org.pk,
|
||||
job_type='check',
|
||||
)
|
||||
jts.append(jt)
|
||||
|
||||
a = user('admin', False)
|
||||
orgs[0].admin_role.members.add(a)
|
||||
orgs[1].admin_role.members.add(a)
|
||||
|
||||
access = CredentialAccess(a)
|
||||
rbac.migrate_credential(apps, None)
|
||||
|
||||
for jt in jts:
|
||||
jt.refresh_from_db()
|
||||
|
||||
assert jts[0].credential != jts[1].credential
|
||||
assert access.can_change(jts[0].credential, {'organization': org.pk})
|
||||
assert access.can_change(jts[1].credential, {'organization': org.pk})
|
||||
|
||||
orgs[0].admin_role.members.remove(a)
|
||||
assert not access.can_change(jts[0].credential, {'organization': org.pk})
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_cred_inventory_source(user, inventory, credential):
|
||||
u = user('member', False)
|
||||
inventory.organization.member_role.members.add(u)
|
||||
|
||||
InventorySource.objects.create(
|
||||
name="test-inv-src",
|
||||
credential=credential,
|
||||
inventory=inventory,
|
||||
)
|
||||
|
||||
assert not credential.accessible_by(u, {'use':True})
|
||||
|
||||
rbac.migrate_credential(apps, None)
|
||||
assert credential.accessible_by(u, {'use':True})
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_cred_project(user, credential, project):
|
||||
u = user('member', False)
|
||||
project.organization.member_role.members.add(u)
|
||||
project.credential = credential
|
||||
project.save()
|
||||
|
||||
assert not credential.accessible_by(u, {'use':True})
|
||||
|
||||
rbac.migrate_credential(apps, None)
|
||||
assert credential.accessible_by(u, {'use':True})
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_cred_no_org(user, credential):
|
||||
su = user('su', True)
|
||||
access = CredentialAccess(su)
|
||||
assert access.can_change(credential, {'user': su.pk})
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_cred_team(user, team, credential):
|
||||
u = user('a', False)
|
||||
team.member_role.members.add(u)
|
||||
credential.team = team
|
||||
credential.save()
|
||||
|
||||
assert not credential.accessible_by(u, {'use':True})
|
||||
|
||||
rbac.migrate_credential(apps, None)
|
||||
assert credential.accessible_by(u, {'use':True})
|
||||
273
awx/main/tests/functional/test_rbac_inventory.py
Normal file
273
awx/main/tests/functional/test_rbac_inventory.py
Normal file
@ -0,0 +1,273 @@
|
||||
import pytest
|
||||
|
||||
from awx.main.migrations import _rbac as rbac
|
||||
from awx.main.models import Permission, Host
|
||||
from awx.main.access import InventoryAccess
|
||||
from django.apps import apps
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_inventory_admin_user(inventory, permissions, user):
|
||||
u = user('admin', False)
|
||||
perm = Permission(user=u, inventory=inventory, permission_type='admin')
|
||||
perm.save()
|
||||
|
||||
assert inventory.accessible_by(u, permissions['admin']) is False
|
||||
|
||||
migrations = rbac.migrate_inventory(apps, None)
|
||||
|
||||
assert len(migrations[inventory.name]['users']) == 1
|
||||
assert len(migrations[inventory.name]['teams']) == 0
|
||||
assert inventory.accessible_by(u, permissions['admin'])
|
||||
assert inventory.executor_role.members.filter(id=u.id).exists() is False
|
||||
assert inventory.updater_role.members.filter(id=u.id).exists() is False
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_inventory_auditor_user(inventory, permissions, user):
|
||||
u = user('auditor', False)
|
||||
perm = Permission(user=u, inventory=inventory, permission_type='read')
|
||||
perm.save()
|
||||
|
||||
assert inventory.accessible_by(u, permissions['admin']) is False
|
||||
assert inventory.accessible_by(u, permissions['auditor']) is False
|
||||
|
||||
migrations = rbac.migrate_inventory(apps, None)
|
||||
|
||||
assert len(migrations[inventory.name]['users']) == 1
|
||||
assert len(migrations[inventory.name]['teams']) == 0
|
||||
assert inventory.accessible_by(u, permissions['admin']) is False
|
||||
assert inventory.accessible_by(u, permissions['auditor']) is True
|
||||
assert inventory.executor_role.members.filter(id=u.id).exists() is False
|
||||
assert inventory.updater_role.members.filter(id=u.id).exists() is False
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_inventory_updater_user(inventory, permissions, user):
|
||||
u = user('updater', False)
|
||||
perm = Permission(user=u, inventory=inventory, permission_type='write')
|
||||
perm.save()
|
||||
|
||||
assert inventory.accessible_by(u, permissions['admin']) is False
|
||||
assert inventory.accessible_by(u, permissions['auditor']) is False
|
||||
|
||||
migrations = rbac.migrate_inventory(apps, None)
|
||||
|
||||
assert len(migrations[inventory.name]['users']) == 1
|
||||
assert len(migrations[inventory.name]['teams']) == 0
|
||||
assert inventory.accessible_by(u, permissions['admin']) is False
|
||||
assert inventory.executor_role.members.filter(id=u.id).exists() is False
|
||||
assert inventory.updater_role.members.filter(id=u.id).exists()
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_inventory_executor_user(inventory, permissions, user):
|
||||
u = user('executor', False)
|
||||
perm = Permission(user=u, inventory=inventory, permission_type='read', run_ad_hoc_commands=True)
|
||||
perm.save()
|
||||
|
||||
assert inventory.accessible_by(u, permissions['admin']) is False
|
||||
assert inventory.accessible_by(u, permissions['auditor']) is False
|
||||
|
||||
migrations = rbac.migrate_inventory(apps, None)
|
||||
|
||||
assert len(migrations[inventory.name]['users']) == 1
|
||||
assert len(migrations[inventory.name]['teams']) == 0
|
||||
assert inventory.accessible_by(u, permissions['admin']) is False
|
||||
assert inventory.accessible_by(u, permissions['auditor']) is True
|
||||
assert inventory.executor_role.members.filter(id=u.id).exists()
|
||||
assert inventory.updater_role.members.filter(id=u.id).exists() is False
|
||||
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_inventory_admin_team(inventory, permissions, user, team):
|
||||
u = user('admin', False)
|
||||
perm = Permission(team=team, inventory=inventory, permission_type='admin')
|
||||
perm.save()
|
||||
team.deprecated_users.add(u)
|
||||
|
||||
assert inventory.accessible_by(u, permissions['admin']) is False
|
||||
|
||||
team_migrations = rbac.migrate_team(apps, None)
|
||||
migrations = rbac.migrate_inventory(apps, None)
|
||||
|
||||
assert len(team_migrations) == 1
|
||||
assert team.member_role.members.count() == 1
|
||||
assert len(migrations[inventory.name]['users']) == 0
|
||||
assert len(migrations[inventory.name]['teams']) == 1
|
||||
assert inventory.admin_role.members.filter(id=u.id).exists() is False
|
||||
assert inventory.auditor_role.members.filter(id=u.id).exists() is False
|
||||
assert inventory.executor_role.members.filter(id=u.id).exists() is False
|
||||
assert inventory.updater_role.members.filter(id=u.id).exists() is False
|
||||
assert inventory.accessible_by(u, permissions['auditor'])
|
||||
assert inventory.accessible_by(u, permissions['admin'])
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_inventory_auditor(inventory, permissions, user, team):
|
||||
u = user('auditor', False)
|
||||
perm = Permission(team=team, inventory=inventory, permission_type='read')
|
||||
perm.save()
|
||||
team.deprecated_users.add(u)
|
||||
|
||||
assert inventory.accessible_by(u, permissions['admin']) is False
|
||||
assert inventory.accessible_by(u, permissions['auditor']) is False
|
||||
|
||||
team_migrations = rbac.migrate_team(apps,None)
|
||||
migrations = rbac.migrate_inventory(apps, None)
|
||||
|
||||
assert len(team_migrations) == 1
|
||||
assert team.member_role.members.count() == 1
|
||||
assert len(migrations[inventory.name]['users']) == 0
|
||||
assert len(migrations[inventory.name]['teams']) == 1
|
||||
assert inventory.admin_role.members.filter(id=u.id).exists() is False
|
||||
assert inventory.auditor_role.members.filter(id=u.id).exists() is False
|
||||
assert inventory.executor_role.members.filter(id=u.id).exists() is False
|
||||
assert inventory.updater_role.members.filter(id=u.id).exists() is False
|
||||
assert inventory.accessible_by(u, permissions['auditor'])
|
||||
assert inventory.accessible_by(u, permissions['admin']) is False
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_inventory_updater(inventory, permissions, user, team):
|
||||
u = user('updater', False)
|
||||
perm = Permission(team=team, inventory=inventory, permission_type='write')
|
||||
perm.save()
|
||||
team.deprecated_users.add(u)
|
||||
|
||||
assert inventory.accessible_by(u, permissions['admin']) is False
|
||||
assert inventory.accessible_by(u, permissions['auditor']) is False
|
||||
|
||||
team_migrations = rbac.migrate_team(apps,None)
|
||||
migrations = rbac.migrate_inventory(apps, None)
|
||||
|
||||
assert len(team_migrations) == 1
|
||||
assert team.member_role.members.count() == 1
|
||||
assert len(migrations[inventory.name]['users']) == 0
|
||||
assert len(migrations[inventory.name]['teams']) == 1
|
||||
assert inventory.admin_role.members.filter(id=u.id).exists() is False
|
||||
assert inventory.auditor_role.members.filter(id=u.id).exists() is False
|
||||
assert inventory.executor_role.members.filter(id=u.id).exists() is False
|
||||
assert inventory.updater_role.members.filter(id=u.id).exists() is False
|
||||
assert team.member_role.is_ancestor_of(inventory.updater_role)
|
||||
assert team.member_role.is_ancestor_of(inventory.executor_role) is False
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_inventory_executor(inventory, permissions, user, team):
|
||||
u = user('executor', False)
|
||||
perm = Permission(team=team, inventory=inventory, permission_type='read', run_ad_hoc_commands=True)
|
||||
perm.save()
|
||||
team.deprecated_users.add(u)
|
||||
|
||||
assert inventory.accessible_by(u, permissions['admin']) is False
|
||||
assert inventory.accessible_by(u, permissions['auditor']) is False
|
||||
|
||||
team_migrations = rbac.migrate_team(apps, None)
|
||||
migrations = rbac.migrate_inventory(apps, None)
|
||||
|
||||
assert len(team_migrations) == 1
|
||||
assert team.member_role.members.count() == 1
|
||||
assert len(migrations[inventory.name]['users']) == 0
|
||||
assert len(migrations[inventory.name]['teams']) == 1
|
||||
assert inventory.admin_role.members.filter(id=u.id).exists() is False
|
||||
assert inventory.auditor_role.members.filter(id=u.id).exists() is False
|
||||
assert inventory.executor_role.members.filter(id=u.id).exists() is False
|
||||
assert inventory.updater_role.members.filter(id=u.id).exists() is False
|
||||
assert team.member_role.is_ancestor_of(inventory.updater_role) is False
|
||||
assert team.member_role.is_ancestor_of(inventory.executor_role)
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_group_parent_admin(group, permissions, user):
|
||||
u = user('admin', False)
|
||||
parent1 = group('parent-1')
|
||||
parent2 = group('parent-2')
|
||||
childA = group('child-1')
|
||||
|
||||
parent1.admin_role.members.add(u)
|
||||
assert parent1.accessible_by(u, permissions['admin'])
|
||||
assert not parent2.accessible_by(u, permissions['admin'])
|
||||
assert not childA.accessible_by(u, permissions['admin'])
|
||||
|
||||
childA.parents.add(parent1)
|
||||
assert childA.accessible_by(u, permissions['admin'])
|
||||
|
||||
childA.parents.remove(parent1)
|
||||
assert not childA.accessible_by(u, permissions['admin'])
|
||||
|
||||
parent2.children.add(childA)
|
||||
assert not childA.accessible_by(u, permissions['admin'])
|
||||
|
||||
parent2.admin_role.members.add(u)
|
||||
assert childA.accessible_by(u, permissions['admin'])
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_access_admin(organization, inventory, user):
|
||||
a = user('admin', False)
|
||||
inventory.organization = organization
|
||||
organization.admin_role.members.add(a)
|
||||
|
||||
access = InventoryAccess(a)
|
||||
assert access.can_read(inventory)
|
||||
assert access.can_add(None)
|
||||
assert access.can_add({'organization': organization.id})
|
||||
assert access.can_change(inventory, None)
|
||||
assert access.can_change(inventory, {'organization': organization.id})
|
||||
assert access.can_admin(inventory, None)
|
||||
assert access.can_admin(inventory, {'organization': organization.id})
|
||||
assert access.can_delete(inventory)
|
||||
assert access.can_run_ad_hoc_commands(inventory)
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_access_auditor(organization, inventory, user):
|
||||
u = user('admin', False)
|
||||
inventory.organization = organization
|
||||
organization.auditor_role.members.add(u)
|
||||
|
||||
access = InventoryAccess(u)
|
||||
assert access.can_read(inventory)
|
||||
assert not access.can_add(None)
|
||||
assert not access.can_add({'organization': organization.id})
|
||||
assert not access.can_change(inventory, None)
|
||||
assert not access.can_change(inventory, {'organization': organization.id})
|
||||
assert not access.can_admin(inventory, None)
|
||||
assert not access.can_admin(inventory, {'organization': organization.id})
|
||||
assert not access.can_delete(inventory)
|
||||
assert not access.can_run_ad_hoc_commands(inventory)
|
||||
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_host_access(organization, inventory, user, group):
|
||||
other_inventory = organization.inventories.create(name='other-inventory')
|
||||
inventory_admin = user('inventory_admin', False)
|
||||
my_group = group('my-group')
|
||||
not_my_group = group('not-my-group')
|
||||
group_admin = user('group_admin', False)
|
||||
|
||||
|
||||
h1 = Host.objects.create(inventory=inventory, name='host1')
|
||||
h2 = Host.objects.create(inventory=inventory, name='host2')
|
||||
h1.groups.add(my_group)
|
||||
h2.groups.add(not_my_group)
|
||||
|
||||
assert h1.accessible_by(inventory_admin, {'read': True}) is False
|
||||
assert h1.accessible_by(group_admin, {'read': True}) is False
|
||||
|
||||
inventory.admin_role.members.add(inventory_admin)
|
||||
my_group.admin_role.members.add(group_admin)
|
||||
|
||||
assert h1.accessible_by(inventory_admin, {'read': True})
|
||||
assert h2.accessible_by(inventory_admin, {'read': True})
|
||||
assert h1.accessible_by(group_admin, {'read': True})
|
||||
assert h2.accessible_by(group_admin, {'read': True}) is False
|
||||
|
||||
my_group.hosts.remove(h1)
|
||||
|
||||
assert h1.accessible_by(inventory_admin, {'read': True})
|
||||
assert h1.accessible_by(group_admin, {'read': True}) is False
|
||||
|
||||
h1.inventory = other_inventory
|
||||
h1.save()
|
||||
|
||||
assert h1.accessible_by(inventory_admin, {'read': True}) is False
|
||||
assert h1.accessible_by(group_admin, {'read': True}) is False
|
||||
|
||||
|
||||
|
||||
151
awx/main/tests/functional/test_rbac_job_templates.py
Normal file
151
awx/main/tests/functional/test_rbac_job_templates.py
Normal file
@ -0,0 +1,151 @@
|
||||
import mock
|
||||
import pytest
|
||||
|
||||
from awx.main.access import (
|
||||
BaseAccess,
|
||||
JobTemplateAccess,
|
||||
)
|
||||
from awx.main.migrations import _rbac as rbac
|
||||
from awx.main.models import Permission
|
||||
from django.apps import apps
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_job_template_migration_check(deploy_jobtemplate, check_jobtemplate, user):
|
||||
admin = user('admin', is_superuser=True)
|
||||
joe = user('joe')
|
||||
|
||||
|
||||
check_jobtemplate.project.organization.deprecated_users.add(joe)
|
||||
|
||||
Permission(user=joe, inventory=check_jobtemplate.inventory, permission_type='read').save()
|
||||
Permission(user=joe, inventory=check_jobtemplate.inventory,
|
||||
project=check_jobtemplate.project, permission_type='check').save()
|
||||
|
||||
rbac.migrate_users(apps, None)
|
||||
rbac.migrate_organization(apps, None)
|
||||
rbac.migrate_projects(apps, None)
|
||||
rbac.migrate_inventory(apps, None)
|
||||
|
||||
assert check_jobtemplate.project.accessible_by(joe, {'read': True})
|
||||
assert check_jobtemplate.accessible_by(admin, {'execute': True}) is True
|
||||
assert check_jobtemplate.accessible_by(joe, {'execute': True}) is False
|
||||
|
||||
migrations = rbac.migrate_job_templates(apps, None)
|
||||
|
||||
assert len(migrations[check_jobtemplate.name]['users']) == 1
|
||||
assert check_jobtemplate.accessible_by(admin, {'execute': True}) is True
|
||||
assert check_jobtemplate.accessible_by(joe, {'execute': True}) is True
|
||||
assert deploy_jobtemplate.accessible_by(admin, {'execute': True}) is True
|
||||
assert deploy_jobtemplate.accessible_by(joe, {'execute': True}) is False
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_job_template_migration_deploy(deploy_jobtemplate, check_jobtemplate, user):
|
||||
admin = user('admin', is_superuser=True)
|
||||
joe = user('joe')
|
||||
|
||||
|
||||
deploy_jobtemplate.project.organization.deprecated_users.add(joe)
|
||||
|
||||
Permission(user=joe, inventory=deploy_jobtemplate.inventory, permission_type='read').save()
|
||||
Permission(user=joe, inventory=deploy_jobtemplate.inventory,
|
||||
project=deploy_jobtemplate.project, permission_type='run').save()
|
||||
|
||||
rbac.migrate_users(apps, None)
|
||||
rbac.migrate_organization(apps, None)
|
||||
rbac.migrate_projects(apps, None)
|
||||
rbac.migrate_inventory(apps, None)
|
||||
|
||||
assert deploy_jobtemplate.project.accessible_by(joe, {'read': True})
|
||||
assert deploy_jobtemplate.accessible_by(admin, {'execute': True}) is True
|
||||
assert deploy_jobtemplate.accessible_by(joe, {'execute': True}) is False
|
||||
|
||||
migrations = rbac.migrate_job_templates(apps, None)
|
||||
|
||||
assert len(migrations[deploy_jobtemplate.name]['users']) == 1
|
||||
assert deploy_jobtemplate.accessible_by(admin, {'execute': True}) is True
|
||||
assert deploy_jobtemplate.accessible_by(joe, {'execute': True}) is True
|
||||
assert check_jobtemplate.accessible_by(admin, {'execute': True}) is True
|
||||
assert check_jobtemplate.accessible_by(joe, {'execute': True}) is True
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_job_template_team_migration_check(deploy_jobtemplate, check_jobtemplate, organization, team, user):
|
||||
admin = user('admin', is_superuser=True)
|
||||
joe = user('joe')
|
||||
team.deprecated_users.add(joe)
|
||||
team.organization = organization
|
||||
team.save()
|
||||
|
||||
check_jobtemplate.project.organization.deprecated_users.add(joe)
|
||||
|
||||
Permission(team=team, inventory=check_jobtemplate.inventory, permission_type='read').save()
|
||||
Permission(team=team, inventory=check_jobtemplate.inventory,
|
||||
project=check_jobtemplate.project, permission_type='check').save()
|
||||
|
||||
rbac.migrate_users(apps, None)
|
||||
rbac.migrate_team(apps, None)
|
||||
rbac.migrate_organization(apps, None)
|
||||
rbac.migrate_projects(apps, None)
|
||||
rbac.migrate_inventory(apps, None)
|
||||
|
||||
assert check_jobtemplate.project.accessible_by(joe, {'read': True})
|
||||
assert check_jobtemplate.accessible_by(admin, {'execute': True}) is True
|
||||
assert check_jobtemplate.accessible_by(joe, {'execute': True}) is False
|
||||
|
||||
migrations = rbac.migrate_job_templates(apps, None)
|
||||
|
||||
assert len(migrations[check_jobtemplate.name]['users']) == 0
|
||||
assert len(migrations[check_jobtemplate.name]['teams']) == 1
|
||||
assert check_jobtemplate.accessible_by(admin, {'execute': True}) is True
|
||||
assert check_jobtemplate.accessible_by(joe, {'execute': True}) is True
|
||||
|
||||
assert deploy_jobtemplate.accessible_by(admin, {'execute': True}) is True
|
||||
assert deploy_jobtemplate.accessible_by(joe, {'execute': True}) is False
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_job_template_team_deploy_migration(deploy_jobtemplate, check_jobtemplate, organization, team, user):
|
||||
admin = user('admin', is_superuser=True)
|
||||
joe = user('joe')
|
||||
team.deprecated_users.add(joe)
|
||||
team.organization = organization
|
||||
team.save()
|
||||
|
||||
deploy_jobtemplate.project.organization.deprecated_users.add(joe)
|
||||
|
||||
Permission(team=team, inventory=deploy_jobtemplate.inventory, permission_type='read').save()
|
||||
Permission(team=team, inventory=deploy_jobtemplate.inventory,
|
||||
project=deploy_jobtemplate.project, permission_type='run').save()
|
||||
|
||||
rbac.migrate_users(apps, None)
|
||||
rbac.migrate_team(apps, None)
|
||||
rbac.migrate_organization(apps, None)
|
||||
rbac.migrate_projects(apps, None)
|
||||
rbac.migrate_inventory(apps, None)
|
||||
|
||||
assert deploy_jobtemplate.project.accessible_by(joe, {'read': True})
|
||||
assert deploy_jobtemplate.accessible_by(admin, {'execute': True}) is True
|
||||
assert deploy_jobtemplate.accessible_by(joe, {'execute': True}) is False
|
||||
|
||||
migrations = rbac.migrate_job_templates(apps, None)
|
||||
|
||||
assert len(migrations[deploy_jobtemplate.name]['users']) == 0
|
||||
assert len(migrations[deploy_jobtemplate.name]['teams']) == 1
|
||||
assert deploy_jobtemplate.accessible_by(admin, {'execute': True}) is True
|
||||
assert deploy_jobtemplate.accessible_by(joe, {'execute': True}) is True
|
||||
|
||||
assert check_jobtemplate.accessible_by(admin, {'execute': True}) is True
|
||||
assert check_jobtemplate.accessible_by(joe, {'execute': True}) is True
|
||||
|
||||
|
||||
@mock.patch.object(BaseAccess, 'check_license', return_value=None)
|
||||
@pytest.mark.django_db
|
||||
def test_job_template_access_superuser(check_license, user, deploy_jobtemplate):
|
||||
# GIVEN a superuser
|
||||
u = user('admin', True)
|
||||
# WHEN access to a job template is checked
|
||||
access = JobTemplateAccess(u)
|
||||
# THEN all access checks should pass
|
||||
assert access.can_read(deploy_jobtemplate)
|
||||
assert access.can_add({})
|
||||
83
awx/main/tests/functional/test_rbac_organization.py
Normal file
83
awx/main/tests/functional/test_rbac_organization.py
Normal file
@ -0,0 +1,83 @@
|
||||
import mock
|
||||
import pytest
|
||||
|
||||
from awx.main.migrations import _rbac as rbac
|
||||
from awx.main.access import (
|
||||
BaseAccess,
|
||||
OrganizationAccess,
|
||||
)
|
||||
from django.apps import apps
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_organization_migration_admin(organization, permissions, user):
|
||||
u = user('admin', False)
|
||||
organization.deprecated_admins.add(u)
|
||||
|
||||
# Undo some automatic work that we're supposed to be testing with our migration
|
||||
organization.admin_role.members.remove(u)
|
||||
assert not organization.accessible_by(u, permissions['admin'])
|
||||
|
||||
migrations = rbac.migrate_organization(apps, None)
|
||||
|
||||
assert len(migrations) == 1
|
||||
assert organization.accessible_by(u, permissions['admin'])
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_organization_migration_user(organization, permissions, user):
|
||||
u = user('user', False)
|
||||
organization.deprecated_users.add(u)
|
||||
|
||||
# Undo some automatic work that we're supposed to be testing with our migration
|
||||
organization.member_role.members.remove(u)
|
||||
assert not organization.accessible_by(u, permissions['auditor'])
|
||||
|
||||
migrations = rbac.migrate_organization(apps, None)
|
||||
|
||||
assert len(migrations) == 1
|
||||
assert organization.accessible_by(u, permissions['auditor'])
|
||||
|
||||
|
||||
@mock.patch.object(BaseAccess, 'check_license', return_value=None)
|
||||
@pytest.mark.django_db
|
||||
def test_organization_access_superuser(cl, organization, user):
|
||||
access = OrganizationAccess(user('admin', True))
|
||||
organization.deprecated_users.add(user('user', False))
|
||||
|
||||
assert access.can_change(organization, None)
|
||||
assert access.can_delete(organization)
|
||||
|
||||
org = access.get_queryset()[0]
|
||||
assert len(org.deprecated_admins.all()) == 0
|
||||
assert len(org.deprecated_users.all()) == 1
|
||||
|
||||
|
||||
@mock.patch.object(BaseAccess, 'check_license', return_value=None)
|
||||
@pytest.mark.django_db
|
||||
def test_organization_access_admin(cl, organization, user):
|
||||
'''can_change because I am an admin of that org'''
|
||||
a = user('admin', False)
|
||||
organization.admin_role.members.add(a)
|
||||
organization.member_role.members.add(user('user', False))
|
||||
|
||||
access = OrganizationAccess(a)
|
||||
assert access.can_change(organization, None)
|
||||
assert access.can_delete(organization)
|
||||
|
||||
org = access.get_queryset()[0]
|
||||
assert len(org.admin_role.members.all()) == 1
|
||||
assert len(org.member_role.members.all()) == 1
|
||||
|
||||
|
||||
@mock.patch.object(BaseAccess, 'check_license', return_value=None)
|
||||
@pytest.mark.django_db
|
||||
def test_organization_access_user(cl, organization, user):
|
||||
access = OrganizationAccess(user('user', False))
|
||||
organization.member_role.members.add(user('user', False))
|
||||
|
||||
assert not access.can_change(organization, None)
|
||||
assert not access.can_delete(organization)
|
||||
|
||||
org = access.get_queryset()[0]
|
||||
assert len(org.admin_role.members.all()) == 0
|
||||
assert len(org.member_role.members.all()) == 1
|
||||
180
awx/main/tests/functional/test_rbac_project.py
Normal file
180
awx/main/tests/functional/test_rbac_project.py
Normal file
@ -0,0 +1,180 @@
|
||||
import pytest
|
||||
|
||||
from awx.main.migrations import _rbac as rbac
|
||||
from awx.main.models import Role, Permission, Project, Organization, Credential, JobTemplate, Inventory
|
||||
from django.apps import apps
|
||||
from awx.main.migrations import _old_access as old_access
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_project_migration():
|
||||
'''
|
||||
|
||||
o1 o2 o3 with o1 -- i1 o2 -- i2
|
||||
\ | /
|
||||
\ | /
|
||||
c1 ---- p1
|
||||
/ | \
|
||||
/ | \
|
||||
jt1 jt2 jt3
|
||||
| | |
|
||||
i1 i2 i1
|
||||
|
||||
|
||||
goes to
|
||||
|
||||
|
||||
o1
|
||||
|
|
||||
|
|
||||
c1 ---- p1
|
||||
/ |
|
||||
/ |
|
||||
jt1 jt3
|
||||
| |
|
||||
i1 i1
|
||||
|
||||
|
||||
o2
|
||||
|
|
||||
|
|
||||
c1 ---- p2
|
||||
|
|
||||
|
|
||||
jt2
|
||||
|
|
||||
i2
|
||||
|
||||
o3
|
||||
|
|
||||
|
|
||||
c1 ---- p3
|
||||
|
||||
|
||||
'''
|
||||
|
||||
|
||||
o1 = Organization.objects.create(name='o1')
|
||||
o2 = Organization.objects.create(name='o2')
|
||||
o3 = Organization.objects.create(name='o3')
|
||||
|
||||
c1 = Credential.objects.create(name='c1')
|
||||
|
||||
p1 = Project.objects.create(name='p1', credential=c1)
|
||||
p1.deprecated_organizations.add(o1, o2, o3)
|
||||
|
||||
i1 = Inventory.objects.create(name='i1', organization=o1)
|
||||
i2 = Inventory.objects.create(name='i2', organization=o2)
|
||||
|
||||
jt1 = JobTemplate.objects.create(name='jt1', project=p1, inventory=i1)
|
||||
jt2 = JobTemplate.objects.create(name='jt2', project=p1, inventory=i2)
|
||||
jt3 = JobTemplate.objects.create(name='jt3', project=p1, inventory=i1)
|
||||
|
||||
assert o1.projects.count() == 0
|
||||
assert o2.projects.count() == 0
|
||||
assert o3.projects.count() == 0
|
||||
|
||||
rbac.migrate_projects(apps, None)
|
||||
|
||||
jt1 = JobTemplate.objects.get(pk=jt1.pk)
|
||||
jt2 = JobTemplate.objects.get(pk=jt2.pk)
|
||||
jt3 = JobTemplate.objects.get(pk=jt3.pk)
|
||||
|
||||
assert jt1.project == jt3.project
|
||||
assert jt1.project != jt2.project
|
||||
|
||||
assert o1.projects.count() == 1
|
||||
assert o2.projects.count() == 1
|
||||
assert o3.projects.count() == 1
|
||||
assert o1.projects.all()[0].jobtemplates.count() == 2
|
||||
assert o2.projects.all()[0].jobtemplates.count() == 1
|
||||
assert o3.projects.all()[0].jobtemplates.count() == 0
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_project_user_project(user_project, project, user):
|
||||
u = user('owner')
|
||||
|
||||
assert old_access.check_user_access(u, user_project.__class__, 'read', user_project)
|
||||
assert old_access.check_user_access(u, project.__class__, 'read', project) is False
|
||||
|
||||
assert user_project.accessible_by(u, {'read': True}) is False
|
||||
assert project.accessible_by(u, {'read': True}) is False
|
||||
migrations = rbac.migrate_projects(apps, None)
|
||||
assert len(migrations[user_project.name]['users']) == 1
|
||||
assert len(migrations[user_project.name]['teams']) == 0
|
||||
assert user_project.accessible_by(u, {'read': True}) is True
|
||||
assert project.accessible_by(u, {'read': True}) is False
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_project_accessible_by_sa(user, project):
|
||||
u = user('systemadmin', is_superuser=True)
|
||||
# This gets setup by a signal, but we want to test the migration which will set this up too, so remove it
|
||||
Role.singleton('System Administrator').members.remove(u)
|
||||
|
||||
assert project.accessible_by(u, {'read': True}) is False
|
||||
rbac.migrate_organization(apps, None)
|
||||
su_migrations = rbac.migrate_users(apps, None)
|
||||
migrations = rbac.migrate_projects(apps, None)
|
||||
assert len(su_migrations) == 1
|
||||
assert len(migrations[project.name]['users']) == 0
|
||||
assert len(migrations[project.name]['teams']) == 0
|
||||
print(project.admin_role.ancestors.all())
|
||||
print(project.admin_role.ancestors.all())
|
||||
assert project.accessible_by(u, {'read': True, 'write': True}) is True
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_project_org_members(user, organization, project):
|
||||
admin = user('orgadmin')
|
||||
member = user('orgmember')
|
||||
|
||||
assert project.accessible_by(admin, {'read': True}) is False
|
||||
assert project.accessible_by(member, {'read': True}) is False
|
||||
|
||||
organization.deprecated_admins.add(admin)
|
||||
organization.deprecated_users.add(member)
|
||||
|
||||
rbac.migrate_organization(apps, None)
|
||||
migrations = rbac.migrate_projects(apps, None)
|
||||
|
||||
assert len(migrations[project.name]['users']) == 1
|
||||
assert len(migrations[project.name]['teams']) == 0
|
||||
assert project.accessible_by(admin, {'read': True, 'write': True}) is True
|
||||
assert project.accessible_by(member, {'read': True})
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_project_team(user, team, project):
|
||||
nonmember = user('nonmember')
|
||||
member = user('member')
|
||||
|
||||
team.deprecated_users.add(member)
|
||||
project.teams.add(team)
|
||||
|
||||
assert project.accessible_by(nonmember, {'read': True}) is False
|
||||
assert project.accessible_by(member, {'read': True}) is False
|
||||
|
||||
rbac.migrate_team(apps, None)
|
||||
rbac.migrate_organization(apps, None)
|
||||
migrations = rbac.migrate_projects(apps, None)
|
||||
|
||||
assert len(migrations[project.name]['users']) == 0
|
||||
assert len(migrations[project.name]['teams']) == 1
|
||||
assert project.accessible_by(member, {'read': True}) is True
|
||||
assert project.accessible_by(nonmember, {'read': True}) is False
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_project_explicit_permission(user, team, project, organization):
|
||||
u = user('prjuser')
|
||||
|
||||
assert old_access.check_user_access(u, project.__class__, 'read', project) is False
|
||||
|
||||
organization.deprecated_users.add(u)
|
||||
p = Permission(user=u, project=project, permission_type='create', name='Perm name')
|
||||
p.save()
|
||||
|
||||
assert project.accessible_by(u, {'read': True}) is False
|
||||
|
||||
rbac.migrate_organization(apps, None)
|
||||
migrations = rbac.migrate_projects(apps, None)
|
||||
|
||||
assert len(migrations[project.name]['users']) == 1
|
||||
assert project.accessible_by(u, {'read': True}) is True
|
||||
50
awx/main/tests/functional/test_rbac_team.py
Normal file
50
awx/main/tests/functional/test_rbac_team.py
Normal file
@ -0,0 +1,50 @@
|
||||
import pytest
|
||||
|
||||
from awx.main.access import TeamAccess
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_team_access_superuser(team, user):
|
||||
team.member_role.members.add(user('member', False))
|
||||
|
||||
access = TeamAccess(user('admin', True))
|
||||
|
||||
assert access.can_add(None)
|
||||
assert access.can_change(team, None)
|
||||
assert access.can_delete(team)
|
||||
|
||||
t = access.get_queryset()[0]
|
||||
assert len(t.member_role.members.all()) == 1
|
||||
assert len(t.organization.admin_role.members.all()) == 0
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_team_access_org_admin(organization, team, user):
|
||||
a = user('admin', False)
|
||||
organization.admin_role.members.add(a)
|
||||
team.organization = organization
|
||||
team.save()
|
||||
|
||||
access = TeamAccess(a)
|
||||
assert access.can_add({'organization': organization.pk})
|
||||
assert access.can_change(team, None)
|
||||
assert access.can_delete(team)
|
||||
|
||||
t = access.get_queryset()[0]
|
||||
assert len(t.member_role.members.all()) == 0
|
||||
assert len(t.organization.admin_role.members.all()) == 1
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_team_access_member(organization, team, user):
|
||||
u = user('member', False)
|
||||
team.member_role.members.add(u)
|
||||
team.organization = organization
|
||||
team.save()
|
||||
|
||||
access = TeamAccess(u)
|
||||
assert not access.can_add({'organization': organization.pk})
|
||||
assert not access.can_change(team, None)
|
||||
assert not access.can_delete(team)
|
||||
|
||||
t = access.get_queryset()[0]
|
||||
assert len(t.member_role.members.all()) == 1
|
||||
assert len(t.organization.admin_role.members.all()) == 0
|
||||
|
||||
76
awx/main/tests/functional/test_rbac_user.py
Normal file
76
awx/main/tests/functional/test_rbac_user.py
Normal file
@ -0,0 +1,76 @@
|
||||
import pytest
|
||||
|
||||
from django.apps import apps
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
from awx.main.migrations import _rbac as rbac
|
||||
from awx.main.access import UserAccess
|
||||
from awx.main.models import Role
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_user_admin(user_project, project, user):
|
||||
joe = user('joe', is_superuser = False)
|
||||
admin = user('admin', is_superuser = True)
|
||||
sa = Role.singleton('System Administrator')
|
||||
|
||||
# this should happen automatically with our signal
|
||||
assert sa.members.filter(id=admin.id).exists() is True
|
||||
sa.members.remove(admin)
|
||||
|
||||
assert sa.members.filter(id=joe.id).exists() is False
|
||||
assert sa.members.filter(id=admin.id).exists() is False
|
||||
|
||||
migrations = rbac.migrate_users(apps, None)
|
||||
|
||||
# The migration should add the admin back in
|
||||
assert sa.members.filter(id=joe.id).exists() is False
|
||||
assert sa.members.filter(id=admin.id).exists() is True
|
||||
assert len(migrations) == 1
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_user_queryset(user):
|
||||
u = user('pete', False)
|
||||
|
||||
access = UserAccess(u)
|
||||
qs = access.get_queryset()
|
||||
assert qs.count() == 1
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_user_accessible_objects(user, organization):
|
||||
admin = user('admin', False)
|
||||
u = user('john', False)
|
||||
assert User.accessible_objects(admin, {'read':True}).count() == 1
|
||||
|
||||
organization.member_role.members.add(u)
|
||||
organization.admin_role.members.add(admin)
|
||||
assert User.accessible_objects(admin, {'read':True}).count() == 2
|
||||
|
||||
organization.member_role.members.remove(u)
|
||||
assert User.accessible_objects(admin, {'read':True}).count() == 1
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_org_user_admin(user, organization):
|
||||
admin = user('orgadmin')
|
||||
member = user('orgmember')
|
||||
|
||||
organization.member_role.members.add(member)
|
||||
assert not member.accessible_by(admin, {'write':True})
|
||||
|
||||
organization.admin_role.members.add(admin)
|
||||
assert member.accessible_by(admin, {'write':True})
|
||||
|
||||
organization.admin_role.members.remove(admin)
|
||||
assert not member.accessible_by(admin, {'write':True})
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_org_user_removed(user, organization):
|
||||
admin = user('orgadmin')
|
||||
member = user('orgmember')
|
||||
|
||||
organization.admin_role.members.add(admin)
|
||||
organization.member_role.members.add(member)
|
||||
|
||||
assert member.accessible_by(admin, {'write':True})
|
||||
|
||||
organization.member_role.members.remove(member)
|
||||
assert not member.accessible_by(admin, {'write':True})
|
||||
@ -66,68 +66,68 @@ class BaseJobTestMixin(BaseTestMixin):
|
||||
# Alex is Sue's IT assistant who can also administer all of the
|
||||
# organizations.
|
||||
self.user_alex = self.make_user('alex')
|
||||
self.org_eng.admins.add(self.user_alex)
|
||||
self.org_sup.admins.add(self.user_alex)
|
||||
self.org_ops.admins.add(self.user_alex)
|
||||
self.org_eng.admin_role.members.add(self.user_alex)
|
||||
self.org_sup.admin_role.members.add(self.user_alex)
|
||||
self.org_ops.admin_role.members.add(self.user_alex)
|
||||
|
||||
# Bob is the head of engineering. He's an admin for engineering, but
|
||||
# also a user within the operations organization (so he can see the
|
||||
# results if things go wrong in production).
|
||||
self.user_bob = self.make_user('bob')
|
||||
self.org_eng.admins.add(self.user_bob)
|
||||
self.org_ops.users.add(self.user_bob)
|
||||
self.org_eng.admin_role.members.add(self.user_bob)
|
||||
self.org_ops.member_role.members.add(self.user_bob)
|
||||
|
||||
# Chuck is the lead engineer. He has full reign over engineering, but
|
||||
# no other organizations.
|
||||
self.user_chuck = self.make_user('chuck')
|
||||
self.org_eng.admins.add(self.user_chuck)
|
||||
self.org_eng.admin_role.members.add(self.user_chuck)
|
||||
|
||||
# Doug is the other engineer working under Chuck. He can write
|
||||
# playbooks and check them, but Chuck doesn't quite think he's ready to
|
||||
# run them yet. Poor Doug.
|
||||
self.user_doug = self.make_user('doug')
|
||||
self.org_eng.users.add(self.user_doug)
|
||||
self.org_eng.member_role.members.add(self.user_doug)
|
||||
|
||||
# Juan is another engineer working under Chuck. He has a little more freedom
|
||||
# to run playbooks but can't create job templates
|
||||
self.user_juan = self.make_user('juan')
|
||||
self.org_eng.users.add(self.user_juan)
|
||||
self.org_eng.member_role.members.add(self.user_juan)
|
||||
|
||||
# Hannibal is Chuck's right-hand man. Chuck usually has him create the job
|
||||
# templates that the rest of the team will use
|
||||
self.user_hannibal = self.make_user('hannibal')
|
||||
self.org_eng.users.add(self.user_hannibal)
|
||||
self.org_eng.member_role.members.add(self.user_hannibal)
|
||||
|
||||
# Eve is the head of support. She can also see what goes on in
|
||||
# operations to help them troubleshoot problems.
|
||||
self.user_eve = self.make_user('eve')
|
||||
self.org_sup.admins.add(self.user_eve)
|
||||
self.org_ops.users.add(self.user_eve)
|
||||
self.org_sup.admin_role.members.add(self.user_eve)
|
||||
self.org_ops.member_role.members.add(self.user_eve)
|
||||
|
||||
# Frank is the other support guy.
|
||||
self.user_frank = self.make_user('frank')
|
||||
self.org_sup.users.add(self.user_frank)
|
||||
self.org_sup.member_role.members.add(self.user_frank)
|
||||
|
||||
# Greg is the head of operations.
|
||||
self.user_greg = self.make_user('greg')
|
||||
self.org_ops.admins.add(self.user_greg)
|
||||
self.org_ops.admin_role.members.add(self.user_greg)
|
||||
|
||||
# Holly is an operations engineer.
|
||||
self.user_holly = self.make_user('holly')
|
||||
self.org_ops.users.add(self.user_holly)
|
||||
self.org_ops.member_role.members.add(self.user_holly)
|
||||
|
||||
# Iris is another operations engineer.
|
||||
self.user_iris = self.make_user('iris')
|
||||
self.org_ops.users.add(self.user_iris)
|
||||
self.org_ops.member_role.members.add(self.user_iris)
|
||||
|
||||
# Randall and Billybob are new ops interns that ops uses to test
|
||||
# their playbooks and inventory
|
||||
self.user_randall = self.make_user('randall')
|
||||
self.org_ops.users.add(self.user_randall)
|
||||
self.org_ops.member_role.members.add(self.user_randall)
|
||||
|
||||
# He works with Randall
|
||||
self.user_billybob = self.make_user('billybob')
|
||||
self.org_ops.users.add(self.user_billybob)
|
||||
self.org_ops.member_role.members.add(self.user_billybob)
|
||||
|
||||
# Jim is the newest intern. He can login, but can't do anything quite yet
|
||||
# except make everyone else fresh coffee.
|
||||
@ -218,24 +218,29 @@ class BaseJobTestMixin(BaseTestMixin):
|
||||
created_by=self.user_sue)
|
||||
self.team_ops_east.projects.add(self.proj_prod)
|
||||
self.team_ops_east.projects.add(self.proj_prod_east)
|
||||
self.team_ops_east.users.add(self.user_greg)
|
||||
self.team_ops_east.users.add(self.user_holly)
|
||||
self.team_ops_east.member_role.members.add(self.user_greg)
|
||||
self.team_ops_east.member_role.members.add(self.user_holly)
|
||||
self.team_ops_west = self.org_ops.teams.create(
|
||||
name='westerners',
|
||||
created_by=self.user_sue)
|
||||
self.team_ops_west.projects.add(self.proj_prod)
|
||||
self.team_ops_west.projects.add(self.proj_prod_west)
|
||||
self.team_ops_west.users.add(self.user_greg)
|
||||
self.team_ops_west.users.add(self.user_iris)
|
||||
self.team_ops_west.member_role.members.add(self.user_greg)
|
||||
self.team_ops_west.member_role.members.add(self.user_iris)
|
||||
|
||||
# The south team is no longer active having been folded into the east team
|
||||
self.team_ops_south = self.org_ops.teams.create(
|
||||
name='southerners',
|
||||
created_by=self.user_sue,
|
||||
active=False,
|
||||
)
|
||||
self.team_ops_south.projects.add(self.proj_prod)
|
||||
self.team_ops_south.users.add(self.user_greg)
|
||||
# FIXME: This code can be removed (probably)
|
||||
# - this case has been removed as we've gotten rid of the active flag, keeping
|
||||
# code around in case this has ramifications on some test failures.. if
|
||||
# you find this message and all tests are passing, then feel free to remove this
|
||||
# - anoek 2016-03-10
|
||||
#self.team_ops_south = self.org_ops.teams.create(
|
||||
# name='southerners',
|
||||
# created_by=self.user_sue,
|
||||
# active=False,
|
||||
#)
|
||||
#self.team_ops_south.projects.add(self.proj_prod)
|
||||
#self.team_ops_south.member_role.members.add(self.user_greg)
|
||||
|
||||
# The north team is going to be deleted
|
||||
self.team_ops_north = self.org_ops.teams.create(
|
||||
@ -243,7 +248,7 @@ class BaseJobTestMixin(BaseTestMixin):
|
||||
created_by=self.user_sue,
|
||||
)
|
||||
self.team_ops_north.projects.add(self.proj_prod)
|
||||
self.team_ops_north.users.add(self.user_greg)
|
||||
self.team_ops_north.member_role.members.add(self.user_greg)
|
||||
|
||||
# The testers team are interns that can only check playbooks but can't
|
||||
# run them
|
||||
@ -252,8 +257,8 @@ class BaseJobTestMixin(BaseTestMixin):
|
||||
created_by=self.user_sue,
|
||||
)
|
||||
self.team_ops_testers.projects.add(self.proj_prod)
|
||||
self.team_ops_testers.users.add(self.user_randall)
|
||||
self.team_ops_testers.users.add(self.user_billybob)
|
||||
self.team_ops_testers.member_role.members.add(self.user_randall)
|
||||
self.team_ops_testers.member_role.members.add(self.user_billybob)
|
||||
|
||||
# Each user has his/her own set of credentials.
|
||||
from awx.main.tests.data.ssh import (TEST_SSH_KEY_DATA,
|
||||
@ -337,11 +342,18 @@ class BaseJobTestMixin(BaseTestMixin):
|
||||
password='Heading270',
|
||||
created_by = self.user_sue,
|
||||
)
|
||||
self.cred_ops_south = self.team_ops_south.credentials.create(
|
||||
username='south',
|
||||
password='Heading180',
|
||||
created_by = self.user_sue,
|
||||
)
|
||||
|
||||
|
||||
# FIXME: This code can be removed (probably)
|
||||
# - this case has been removed as we've gotten rid of the active flag, keeping
|
||||
# code around in case this has ramifications on some test failures.. if
|
||||
# you find this message and all tests are passing, then feel free to remove this
|
||||
# - anoek 2016-03-10
|
||||
#self.cred_ops_south = self.team_ops_south.credentials.create(
|
||||
# username='south',
|
||||
# password='Heading180',
|
||||
# created_by = self.user_sue,
|
||||
#)
|
||||
|
||||
self.cred_ops_north = self.team_ops_north.credentials.create(
|
||||
username='north',
|
||||
|
||||
@ -39,7 +39,7 @@ class BaseAdHocCommandTest(BaseJobExecutionTest):
|
||||
self.setup_instances()
|
||||
self.setup_users()
|
||||
self.organization = self.make_organizations(self.super_django_user, 1)[0]
|
||||
self.organization.admins.add(self.normal_django_user)
|
||||
self.organization.admin_role.members.add(self.normal_django_user)
|
||||
self.inventory = self.organization.inventories.create(name='test-inventory', description='description for test-inventory')
|
||||
self.host = self.inventory.hosts.create(name='host.example.com')
|
||||
self.host2 = self.inventory.hosts.create(name='host2.example.com')
|
||||
@ -459,30 +459,19 @@ class AdHocCommandApiTest(BaseAdHocCommandTest):
|
||||
self.check_get_list(url, 'nobody', qs)
|
||||
self.check_get_list(url, None, qs, expect=401)
|
||||
|
||||
# Explicitly give other user admin permission on the inventory (still
|
||||
# Explicitly give other user updater permission on the inventory (still
|
||||
# not allowed to run ad hoc commands).
|
||||
user_perm_url = reverse('api:user_permissions_list', args=(self.other_django_user.pk,))
|
||||
user_perm_data = {
|
||||
'name': 'Allow Other to Admin Inventory',
|
||||
'inventory': self.inventory.pk,
|
||||
'permission_type': 'admin',
|
||||
}
|
||||
user_roles_list_url = reverse('api:user_roles_list', args=(self.other_django_user.pk,))
|
||||
with self.current_user('admin'):
|
||||
response = self.post(user_perm_url, user_perm_data, expect=201)
|
||||
user_perm_id = response['id']
|
||||
response = self.post(user_roles_list_url, {"id": self.inventory.updater_role.id}, expect=204)
|
||||
with self.current_user('other'):
|
||||
self.run_test_ad_hoc_command(expect=403)
|
||||
self.check_get_list(url, 'other', qs)
|
||||
|
||||
# Update permission to allow other user to run ad hoc commands. Fails
|
||||
# Add executor role permissions to other. Fails
|
||||
# when other user can't read credential.
|
||||
user_perm_url = reverse('api:permission_detail', args=(user_perm_id,))
|
||||
user_perm_data.update({
|
||||
'name': 'Allow Other to Admin Inventory and Run Ad Hoc Commands',
|
||||
'run_ad_hoc_commands': True,
|
||||
})
|
||||
with self.current_user('admin'):
|
||||
response = self.patch(user_perm_url, user_perm_data, expect=200)
|
||||
response = self.post(user_roles_list_url, {"id": self.inventory.executor_role.id}, expect=204)
|
||||
with self.current_user('other'):
|
||||
self.run_test_ad_hoc_command(expect=403)
|
||||
|
||||
@ -496,15 +485,9 @@ class AdHocCommandApiTest(BaseAdHocCommandTest):
|
||||
self.check_get_list(url, 'other', qs)
|
||||
|
||||
# Explicitly give nobody user read permission on the inventory.
|
||||
user_perm_url = reverse('api:user_permissions_list', args=(self.nobody_django_user.pk,))
|
||||
user_perm_data = {
|
||||
'name': 'Allow Nobody to Read Inventory',
|
||||
'inventory': self.inventory.pk,
|
||||
'permission_type': 'read',
|
||||
}
|
||||
nobody_roles_list_url = reverse('api:user_roles_list', args=(self.nobody_django_user.pk,))
|
||||
with self.current_user('admin'):
|
||||
response = self.post(user_perm_url, user_perm_data, expect=201)
|
||||
user_perm_id = response['id']
|
||||
response = self.post(nobody_roles_list_url, {"id": self.inventory.auditor_role.id}, expect=204)
|
||||
with self.current_user('nobody'):
|
||||
self.run_test_ad_hoc_command(credential=other_cred.pk, expect=403)
|
||||
self.check_get_list(url, 'other', qs)
|
||||
@ -520,13 +503,8 @@ class AdHocCommandApiTest(BaseAdHocCommandTest):
|
||||
|
||||
# Give the nobody user the run_ad_hoc_commands flag, and can now see
|
||||
# the one ad hoc command previously run.
|
||||
user_perm_url = reverse('api:permission_detail', args=(user_perm_id,))
|
||||
user_perm_data.update({
|
||||
'name': 'Allow Nobody to Read Inventory and Run Ad Hoc Commands',
|
||||
'run_ad_hoc_commands': True,
|
||||
})
|
||||
with self.current_user('admin'):
|
||||
response = self.patch(user_perm_url, user_perm_data, expect=200)
|
||||
response = self.post(nobody_roles_list_url, {"id": self.inventory.executor_role.id}, expect=204)
|
||||
qs = AdHocCommand.objects.filter(credential_id=nobody_cred.pk)
|
||||
self.assertEqual(qs.count(), 1)
|
||||
self.check_get_list(url, 'nobody', qs)
|
||||
@ -637,8 +615,8 @@ class AdHocCommandApiTest(BaseAdHocCommandTest):
|
||||
# Verify that the credential and inventory are null when they have
|
||||
# been deleted, can delete an ad hoc command without inventory or
|
||||
# credential.
|
||||
self.credential.mark_inactive()
|
||||
self.inventory.mark_inactive()
|
||||
self.credential.delete()
|
||||
self.inventory.delete()
|
||||
with self.current_user('admin'):
|
||||
response = self.get(url, expect=200)
|
||||
self.assertEqual(response['credential'], None)
|
||||
@ -758,7 +736,7 @@ class AdHocCommandApiTest(BaseAdHocCommandTest):
|
||||
tower_settings.AD_HOC_COMMANDS = ad_hoc_commands
|
||||
|
||||
# Try to relaunch after the inventory has been marked inactive.
|
||||
self.inventory.mark_inactive()
|
||||
self.inventory.delete()
|
||||
with self.current_user('admin'):
|
||||
response = self.get(url, expect=200)
|
||||
self.assertEqual(response['passwords_needed_to_start'], [])
|
||||
@ -947,7 +925,7 @@ class AdHocCommandApiTest(BaseAdHocCommandTest):
|
||||
self.delete(url, expect=405)
|
||||
with self.current_user('normal'):
|
||||
response = self.get(url, expect=200)
|
||||
#self.assertEqual(response['count'], 1) # FIXME: Enable once activity stream RBAC is fixed.
|
||||
self.assertEqual(response['count'], 1)
|
||||
self.post(url, {}, expect=405)
|
||||
self.put(url, {}, expect=405)
|
||||
self.patch(url, {}, expect=405)
|
||||
@ -1026,29 +1004,17 @@ class AdHocCommandApiTest(BaseAdHocCommandTest):
|
||||
# Create another unrelated inventory permission with run_ad_hoc_commands
|
||||
# set; this tests an edge case in the RBAC query where we'll return
|
||||
# can_run_ad_hoc_commands = True when we shouldn't.
|
||||
nobody_perm_url = reverse('api:user_permissions_list', args=(self.nobody_django_user.pk,))
|
||||
nobody_perm_data = {
|
||||
'name': 'Allow Nobody to Read Inventory',
|
||||
'inventory': self.inventory.pk,
|
||||
'permission_type': 'read',
|
||||
'run_ad_hoc_commands': True,
|
||||
}
|
||||
nobody_roles_list_url = reverse('api:user_roles_list', args=(self.nobody_django_user.pk,))
|
||||
with self.current_user('admin'):
|
||||
response = self.post(nobody_perm_url, nobody_perm_data, expect=201)
|
||||
response = self.post(nobody_roles_list_url, {"id": self.inventory.executor_role.id}, expect=204)
|
||||
|
||||
# Create a credential for the other user and explicitly give other
|
||||
# user admin permission on the inventory (still not allowed to run ad
|
||||
# hoc commands; can get the list but can't see any items).
|
||||
other_cred = self.create_test_credential(user=self.other_django_user)
|
||||
user_perm_url = reverse('api:user_permissions_list', args=(self.other_django_user.pk,))
|
||||
user_perm_data = {
|
||||
'name': 'Allow Other to Admin Inventory',
|
||||
'inventory': self.inventory.pk,
|
||||
'permission_type': 'admin',
|
||||
}
|
||||
user_roles_list_url = reverse('api:user_roles_list', args=(self.other_django_user.pk,))
|
||||
with self.current_user('admin'):
|
||||
response = self.post(user_perm_url, user_perm_data, expect=201)
|
||||
user_perm_id = response['id']
|
||||
response = self.post(user_roles_list_url, {"id": self.inventory.updater_role.id}, expect=204)
|
||||
with self.current_user('other'):
|
||||
response = self.get(url, expect=200)
|
||||
self.assertEqual(response['count'], 0)
|
||||
@ -1058,13 +1024,8 @@ class AdHocCommandApiTest(BaseAdHocCommandTest):
|
||||
|
||||
# Update permission to allow other user to run ad hoc commands. Can
|
||||
# only see his own ad hoc commands (because of credential permission).
|
||||
user_perm_url = reverse('api:permission_detail', args=(user_perm_id,))
|
||||
user_perm_data.update({
|
||||
'name': 'Allow Other to Admin Inventory and Run Ad Hoc Commands',
|
||||
'run_ad_hoc_commands': True,
|
||||
})
|
||||
with self.current_user('admin'):
|
||||
response = self.patch(user_perm_url, user_perm_data, expect=200)
|
||||
response = self.post(user_roles_list_url, {"id": self.inventory.executor_role.id}, expect=204)
|
||||
with self.current_user('other'):
|
||||
response = self.get(url, expect=200)
|
||||
self.assertEqual(response['count'], 0)
|
||||
|
||||
@ -1,34 +0,0 @@
|
||||
# Copyright (c) 2015 Ansible, Inc.
|
||||
# All Rights Reserved
|
||||
|
||||
# AWX
|
||||
from awx.main.tests.base import BaseTest
|
||||
from command_base import BaseCommandMixin
|
||||
|
||||
__all__ = ['AgeDeletedCommandFunctionalTest']
|
||||
|
||||
class AgeDeletedCommandFunctionalTest(BaseCommandMixin, BaseTest):
|
||||
def setUp(self):
|
||||
super(AgeDeletedCommandFunctionalTest, self).setUp()
|
||||
self.create_test_license_file()
|
||||
self.setup_instances()
|
||||
self.setup_users()
|
||||
self.organization = self.make_organization(self.super_django_user)
|
||||
self.credential = self.make_credential()
|
||||
self.credential2 = self.make_credential()
|
||||
self.credential.mark_inactive(True)
|
||||
self.credential2.mark_inactive(True)
|
||||
self.credential_active = self.make_credential()
|
||||
self.super_django_user.mark_inactive(True)
|
||||
|
||||
def test_default(self):
|
||||
result, stdout, stderr = self.run_command('age_deleted')
|
||||
self.assertEqual(stdout, 'Aged %d items\n' % 3)
|
||||
|
||||
def test_type(self):
|
||||
result, stdout, stderr = self.run_command('age_deleted', type='Credential')
|
||||
self.assertEqual(stdout, 'Aged %d items\n' % 2)
|
||||
|
||||
def test_id_type(self):
|
||||
result, stdout, stderr = self.run_command('age_deleted', type='Credential', id=self.credential.pk)
|
||||
self.assertEqual(stdout, 'Aged %d items\n' % 1)
|
||||
@ -15,7 +15,6 @@ import unittest2 as unittest
|
||||
|
||||
# Django
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import User
|
||||
from django.core.management import call_command
|
||||
from django.core.management.base import CommandError
|
||||
from django.utils.timezone import now
|
||||
@ -232,126 +231,6 @@ class DumpDataTest(BaseCommandMixin, BaseTest):
|
||||
self.assertEqual(result, None)
|
||||
json.loads(stdout)
|
||||
|
||||
class CleanupDeletedTest(BaseCommandMixin, BaseTest):
|
||||
'''
|
||||
Test cases for cleanup_deleted management command.
|
||||
'''
|
||||
|
||||
def setUp(self):
|
||||
self.start_redis()
|
||||
super(CleanupDeletedTest, self).setUp()
|
||||
self.create_test_inventories()
|
||||
|
||||
def tearDown(self):
|
||||
super(CleanupDeletedTest, self).tearDown()
|
||||
self.stop_redis()
|
||||
|
||||
def get_model_counts(self):
|
||||
def get_models(m):
|
||||
if not m._meta.abstract:
|
||||
yield m
|
||||
for sub in m.__subclasses__():
|
||||
for subm in get_models(sub):
|
||||
yield subm
|
||||
counts = {}
|
||||
for model in get_models(PrimordialModel):
|
||||
active = model.objects.filter(active=True).count()
|
||||
inactive = model.objects.filter(active=False).count()
|
||||
counts[model] = (active, inactive)
|
||||
return counts
|
||||
|
||||
def test_cleanup_our_models(self):
|
||||
# Test with nothing to be deleted.
|
||||
counts_before = self.get_model_counts()
|
||||
self.assertFalse(sum(x[1] for x in counts_before.values()))
|
||||
result, stdout, stderr = self.run_command('cleanup_deleted')
|
||||
self.assertEqual(result, None)
|
||||
counts_after = self.get_model_counts()
|
||||
self.assertEqual(counts_before, counts_after)
|
||||
# "Delete" some hosts.
|
||||
for host in Host.objects.all():
|
||||
host.mark_inactive()
|
||||
# With no parameters, "days" defaults to 90, which won't cleanup any of
|
||||
# the hosts we just removed.
|
||||
counts_before = self.get_model_counts()
|
||||
self.assertTrue(sum(x[1] for x in counts_before.values()))
|
||||
result, stdout, stderr = self.run_command('cleanup_deleted')
|
||||
self.assertEqual(result, None)
|
||||
counts_after = self.get_model_counts()
|
||||
self.assertEqual(counts_before, counts_after)
|
||||
# Even with days=1, the hosts will remain.
|
||||
counts_before = self.get_model_counts()
|
||||
self.assertTrue(sum(x[1] for x in counts_before.values()))
|
||||
result, stdout, stderr = self.run_command('cleanup_deleted', days=1)
|
||||
self.assertEqual(result, None)
|
||||
counts_after = self.get_model_counts()
|
||||
self.assertEqual(counts_before, counts_after)
|
||||
# With days=0, the hosts will be deleted.
|
||||
counts_before = self.get_model_counts()
|
||||
self.assertTrue(sum(x[1] for x in counts_before.values()))
|
||||
result, stdout, stderr = self.run_command('cleanup_deleted', days=0)
|
||||
self.assertEqual(result, None)
|
||||
counts_after = self.get_model_counts()
|
||||
self.assertNotEqual(counts_before, counts_after)
|
||||
self.assertFalse(sum(x[1] for x in counts_after.values()))
|
||||
return # Don't test how long it takes (for now).
|
||||
|
||||
# Create lots of hosts already marked as deleted.
|
||||
t = time.time()
|
||||
dtnow = now()
|
||||
for x in xrange(1000):
|
||||
hostname = "_deleted_%s_host-%d" % (dtnow.isoformat(), x)
|
||||
host = self.inventories[0].hosts.create(name=hostname, active=False)
|
||||
create_elapsed = time.time() - t
|
||||
|
||||
# Time how long it takes to cleanup deleted items, should be no more
|
||||
# then the time taken to create them.
|
||||
counts_before = self.get_model_counts()
|
||||
self.assertTrue(sum(x[1] for x in counts_before.values()))
|
||||
t = time.time()
|
||||
result, stdout, stderr = self.run_command('cleanup_deleted', days=0)
|
||||
cleanup_elapsed = time.time() - t
|
||||
self.assertEqual(result, None)
|
||||
counts_after = self.get_model_counts()
|
||||
self.assertNotEqual(counts_before, counts_after)
|
||||
self.assertFalse(sum(x[1] for x in counts_after.values()))
|
||||
self.assertTrue(cleanup_elapsed < create_elapsed,
|
||||
'create took %0.3fs, cleanup took %0.3fs, expected < %0.3fs' % (create_elapsed, cleanup_elapsed, create_elapsed))
|
||||
|
||||
def get_user_counts(self):
|
||||
active = User.objects.filter(is_active=True).count()
|
||||
inactive = User.objects.filter(is_active=False).count()
|
||||
return active, inactive
|
||||
|
||||
def test_cleanup_user_model(self):
|
||||
# Test with nothing to be deleted.
|
||||
counts_before = self.get_user_counts()
|
||||
self.assertFalse(counts_before[1])
|
||||
result, stdout, stderr = self.run_command('cleanup_deleted')
|
||||
self.assertEqual(result, None)
|
||||
counts_after = self.get_user_counts()
|
||||
self.assertEqual(counts_before, counts_after)
|
||||
# "Delete some users".
|
||||
for user in User.objects.all():
|
||||
user.mark_inactive()
|
||||
self.assertTrue(len(user.username) <= 30,
|
||||
'len(%r) == %d' % (user.username, len(user.username)))
|
||||
# With days=1, no users will be deleted.
|
||||
counts_before = self.get_user_counts()
|
||||
self.assertTrue(counts_before[1])
|
||||
result, stdout, stderr = self.run_command('cleanup_deleted', days=1)
|
||||
self.assertEqual(result, None)
|
||||
counts_after = self.get_user_counts()
|
||||
self.assertEqual(counts_before, counts_after)
|
||||
# With days=0, inactive users will be deleted.
|
||||
counts_before = self.get_user_counts()
|
||||
self.assertTrue(counts_before[1])
|
||||
result, stdout, stderr = self.run_command('cleanup_deleted', days=0)
|
||||
self.assertEqual(result, None)
|
||||
counts_after = self.get_user_counts()
|
||||
self.assertNotEqual(counts_before, counts_after)
|
||||
self.assertFalse(counts_after[1])
|
||||
|
||||
@override_settings(CELERY_ALWAYS_EAGER=True,
|
||||
CELERY_EAGER_PROPAGATES_EXCEPTIONS=True,
|
||||
ANSIBLE_TRANSPORT='local')
|
||||
@ -641,12 +520,12 @@ class InventoryImportTest(BaseCommandMixin, BaseLiveServerTest):
|
||||
self.assertEqual(inventory_source.inventory_updates.count(), 1)
|
||||
inventory_update = inventory_source.inventory_updates.all()[0]
|
||||
self.assertEqual(inventory_update.status, 'successful')
|
||||
for host in inventory.hosts.filter(active=True):
|
||||
for host in inventory.hosts:
|
||||
if host.pk in (except_host_pks or []):
|
||||
continue
|
||||
source_pks = host.inventory_sources.values_list('pk', flat=True)
|
||||
self.assertTrue(inventory_source.pk in source_pks)
|
||||
for group in inventory.groups.filter(active=True):
|
||||
for group in inventory.groups:
|
||||
if group.pk in (except_group_pks or []):
|
||||
continue
|
||||
source_pks = group.inventory_sources.values_list('pk', flat=True)
|
||||
@ -814,7 +693,7 @@ class InventoryImportTest(BaseCommandMixin, BaseLiveServerTest):
|
||||
'lbservers', 'others'])
|
||||
if overwrite:
|
||||
expected_group_names.remove('lbservers')
|
||||
group_names = set(new_inv.groups.filter(active=True).values_list('name', flat=True))
|
||||
group_names = set(new_inv.groups.values_list('name', flat=True))
|
||||
self.assertEqual(expected_group_names, group_names)
|
||||
expected_host_names = set(['web1.example.com', 'web2.example.com',
|
||||
'web3.example.com', 'db1.example.com',
|
||||
@ -824,13 +703,13 @@ class InventoryImportTest(BaseCommandMixin, BaseLiveServerTest):
|
||||
'fe80::1610:9fff:fedd:b654', '::1'])
|
||||
if overwrite:
|
||||
expected_host_names.remove('lb.example.com')
|
||||
host_names = set(new_inv.hosts.filter(active=True).values_list('name', flat=True))
|
||||
host_names = set(new_inv.hosts.values_list('name', flat=True))
|
||||
self.assertEqual(expected_host_names, host_names)
|
||||
expected_inv_vars = {'vara': 'A', 'varc': 'C'}
|
||||
if overwrite_vars:
|
||||
expected_inv_vars.pop('varc')
|
||||
self.assertEqual(new_inv.variables_dict, expected_inv_vars)
|
||||
for host in new_inv.hosts.filter(active=True):
|
||||
for host in new_inv.hosts:
|
||||
if host.name == 'web1.example.com':
|
||||
self.assertEqual(host.variables_dict,
|
||||
{'ansible_ssh_host': 'w1.example.net'})
|
||||
@ -842,35 +721,35 @@ class InventoryImportTest(BaseCommandMixin, BaseLiveServerTest):
|
||||
self.assertEqual(host.variables_dict, {'lbvar': 'ni!'})
|
||||
else:
|
||||
self.assertEqual(host.variables_dict, {})
|
||||
for group in new_inv.groups.filter(active=True):
|
||||
for group in new_inv.groups:
|
||||
if group.name == 'servers':
|
||||
expected_vars = {'varb': 'B', 'vard': 'D'}
|
||||
if overwrite_vars:
|
||||
expected_vars.pop('vard')
|
||||
self.assertEqual(group.variables_dict, expected_vars)
|
||||
children = set(group.children.filter(active=True).values_list('name', flat=True))
|
||||
children = set(group.children.values_list('name', flat=True))
|
||||
expected_children = set(['dbservers', 'webservers', 'lbservers'])
|
||||
if overwrite:
|
||||
expected_children.remove('lbservers')
|
||||
self.assertEqual(children, expected_children)
|
||||
self.assertEqual(group.hosts.filter(active=True).count(), 0)
|
||||
self.assertEqual(group.hosts.count(), 0)
|
||||
elif group.name == 'dbservers':
|
||||
self.assertEqual(group.variables_dict, {'dbvar': 'ugh'})
|
||||
self.assertEqual(group.children.filter(active=True).count(), 0)
|
||||
hosts = set(group.hosts.filter(active=True).values_list('name', flat=True))
|
||||
self.assertEqual(group.children.count(), 0)
|
||||
hosts = set(group.hosts.values_list('name', flat=True))
|
||||
host_names = set(['db1.example.com','db2.example.com'])
|
||||
self.assertEqual(hosts, host_names)
|
||||
elif group.name == 'webservers':
|
||||
self.assertEqual(group.variables_dict, {'webvar': 'blah'})
|
||||
self.assertEqual(group.children.filter(active=True).count(), 0)
|
||||
hosts = set(group.hosts.filter(active=True).values_list('name', flat=True))
|
||||
self.assertEqual(group.children.count(), 0)
|
||||
hosts = set(group.hosts.values_list('name', flat=True))
|
||||
host_names = set(['web1.example.com','web2.example.com',
|
||||
'web3.example.com'])
|
||||
self.assertEqual(hosts, host_names)
|
||||
elif group.name == 'lbservers':
|
||||
self.assertEqual(group.variables_dict, {})
|
||||
self.assertEqual(group.children.filter(active=True).count(), 0)
|
||||
hosts = set(group.hosts.filter(active=True).values_list('name', flat=True))
|
||||
self.assertEqual(group.children.count(), 0)
|
||||
hosts = set(group.hosts.values_list('name', flat=True))
|
||||
host_names = set(['lb.example.com'])
|
||||
self.assertEqual(hosts, host_names)
|
||||
if overwrite:
|
||||
@ -920,7 +799,7 @@ class InventoryImportTest(BaseCommandMixin, BaseLiveServerTest):
|
||||
# Check hosts in dotcom group.
|
||||
group = new_inv.groups.get(name='dotcom')
|
||||
self.assertEqual(group.hosts.count(), 65)
|
||||
for host in group.hosts.filter(active=True, name__startswith='web'):
|
||||
for host in group.hosts.filter( name__startswith='web'):
|
||||
self.assertEqual(host.variables_dict.get('ansible_ssh_user', ''), 'example')
|
||||
# Check hosts in dotnet group.
|
||||
group = new_inv.groups.get(name='dotnet')
|
||||
@ -928,7 +807,7 @@ class InventoryImportTest(BaseCommandMixin, BaseLiveServerTest):
|
||||
# Check hosts in dotorg group.
|
||||
group = new_inv.groups.get(name='dotorg')
|
||||
self.assertEqual(group.hosts.count(), 61)
|
||||
for host in group.hosts.filter(active=True):
|
||||
for host in group.hosts:
|
||||
if host.name.startswith('mx.'):
|
||||
continue
|
||||
self.assertEqual(host.variables_dict.get('ansible_ssh_user', ''), 'example')
|
||||
@ -936,7 +815,7 @@ class InventoryImportTest(BaseCommandMixin, BaseLiveServerTest):
|
||||
# Check hosts in dotus group.
|
||||
group = new_inv.groups.get(name='dotus')
|
||||
self.assertEqual(group.hosts.count(), 10)
|
||||
for host in group.hosts.filter(active=True):
|
||||
for host in group.hosts:
|
||||
if int(host.name[2:4]) % 2 == 0:
|
||||
self.assertEqual(host.variables_dict.get('even_odd', ''), 'even')
|
||||
else:
|
||||
@ -1063,7 +942,7 @@ class InventoryImportTest(BaseCommandMixin, BaseLiveServerTest):
|
||||
self.assertNotEqual(new_inv.groups.count(), 0)
|
||||
self.assertNotEqual(new_inv.total_hosts, 0)
|
||||
self.assertNotEqual(new_inv.total_groups, 0)
|
||||
self.assertElapsedLessThan(30)
|
||||
self.assertElapsedLessThan(60)
|
||||
|
||||
def test_splunk_inventory(self):
|
||||
new_inv = self.organizations[0].inventories.create(name='splunk')
|
||||
@ -1082,7 +961,7 @@ class InventoryImportTest(BaseCommandMixin, BaseLiveServerTest):
|
||||
self.assertNotEqual(new_inv.groups.count(), 0)
|
||||
self.assertNotEqual(new_inv.total_hosts, 0)
|
||||
self.assertNotEqual(new_inv.total_groups, 0)
|
||||
self.assertElapsedLessThan(120)
|
||||
self.assertElapsedLessThan(600)
|
||||
|
||||
def _get_ngroups_for_nhosts(self, n):
|
||||
if n > 0:
|
||||
@ -1090,7 +969,7 @@ class InventoryImportTest(BaseCommandMixin, BaseLiveServerTest):
|
||||
else:
|
||||
return 0
|
||||
|
||||
def _check_largeinv_import(self, new_inv, nhosts, nhosts_inactive=0):
|
||||
def _check_largeinv_import(self, new_inv, nhosts):
|
||||
self._start_time = time.time()
|
||||
inv_file = os.path.join(os.path.dirname(__file__), '..', '..', 'data', 'largeinv.py')
|
||||
ngroups = self._get_ngroups_for_nhosts(nhosts)
|
||||
@ -1103,12 +982,11 @@ class InventoryImportTest(BaseCommandMixin, BaseLiveServerTest):
|
||||
# Check that inventory is populated as expected within a reasonable
|
||||
# amount of time. Computed fields should also be updated.
|
||||
new_inv = Inventory.objects.get(pk=new_inv.pk)
|
||||
self.assertEqual(new_inv.hosts.filter(active=True).count(), nhosts)
|
||||
self.assertEqual(new_inv.groups.filter(active=True).count(), ngroups)
|
||||
self.assertEqual(new_inv.hosts.filter(active=False).count(), nhosts_inactive)
|
||||
self.assertEqual(new_inv.hosts.count(), nhosts)
|
||||
self.assertEqual(new_inv.groups.count(), ngroups)
|
||||
self.assertEqual(new_inv.total_hosts, nhosts)
|
||||
self.assertEqual(new_inv.total_groups, ngroups)
|
||||
self.assertElapsedLessThan(45)
|
||||
self.assertElapsedLessThan(120)
|
||||
|
||||
@unittest.skipIf(getattr(settings, 'LOCAL_DEVELOPMENT', False),
|
||||
'Skip this test in local development environments, '
|
||||
@ -1119,10 +997,10 @@ class InventoryImportTest(BaseCommandMixin, BaseLiveServerTest):
|
||||
self.assertEqual(new_inv.groups.count(), 0)
|
||||
nhosts = 2000
|
||||
# Test initial import into empty inventory.
|
||||
self._check_largeinv_import(new_inv, nhosts, 0)
|
||||
self._check_largeinv_import(new_inv, nhosts)
|
||||
# Test re-importing and overwriting.
|
||||
self._check_largeinv_import(new_inv, nhosts, 0)
|
||||
self._check_largeinv_import(new_inv, nhosts)
|
||||
# Test re-importing with only half as many hosts.
|
||||
self._check_largeinv_import(new_inv, nhosts / 2, nhosts / 2)
|
||||
self._check_largeinv_import(new_inv, nhosts / 2)
|
||||
# Test re-importing that clears all hosts.
|
||||
self._check_largeinv_import(new_inv, 0, nhosts)
|
||||
self._check_largeinv_import(new_inv, 0)
|
||||
|
||||
@ -2,7 +2,6 @@
|
||||
# All Rights Reserved.
|
||||
|
||||
# Python
|
||||
import datetime
|
||||
import glob
|
||||
import json
|
||||
import os
|
||||
@ -14,7 +13,6 @@ import time
|
||||
from django.conf import settings
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.test.utils import override_settings
|
||||
from django.utils.timezone import now
|
||||
|
||||
# AWX
|
||||
from awx.main.models import * # noqa
|
||||
@ -47,9 +45,9 @@ class InventoryTest(BaseTest):
|
||||
self.setup_instances()
|
||||
self.setup_users()
|
||||
self.organizations = self.make_organizations(self.super_django_user, 3)
|
||||
self.organizations[0].admins.add(self.normal_django_user)
|
||||
self.organizations[0].users.add(self.other_django_user)
|
||||
self.organizations[0].users.add(self.normal_django_user)
|
||||
self.organizations[0].admin_role.members.add(self.normal_django_user)
|
||||
self.organizations[0].member_role.members.add(self.other_django_user)
|
||||
self.organizations[0].member_role.members.add(self.normal_django_user)
|
||||
|
||||
self.inventory_a = Inventory.objects.create(name='inventory-a', description='foo', organization=self.organizations[0])
|
||||
self.inventory_b = Inventory.objects.create(name='inventory-b', description='bar', organization=self.organizations[1])
|
||||
@ -58,10 +56,7 @@ class InventoryTest(BaseTest):
|
||||
|
||||
# create a permission here on the 'other' user so they have edit access on the org
|
||||
# we may add another permission type later.
|
||||
self.perm_read = Permission.objects.create(
|
||||
inventory = self.inventory_b,
|
||||
user = self.other_django_user,
|
||||
permission_type = 'read')
|
||||
self.inventory_b.auditor_role.members.add(self.other_django_user)
|
||||
|
||||
def tearDown(self):
|
||||
super(InventoryTest, self).tearDown()
|
||||
@ -69,7 +64,7 @@ class InventoryTest(BaseTest):
|
||||
|
||||
def test_get_inventory_list(self):
|
||||
url = reverse('api:inventory_list')
|
||||
qs = Inventory.objects.filter(active=True).distinct()
|
||||
qs = Inventory.objects.distinct()
|
||||
|
||||
# Check list view with invalid authentication.
|
||||
self.check_invalid_auth(url)
|
||||
@ -78,11 +73,11 @@ class InventoryTest(BaseTest):
|
||||
self.check_get_list(url, self.super_django_user, qs)
|
||||
|
||||
# an org admin can list inventories but is filtered to what he adminsters
|
||||
normal_qs = qs.filter(organization__admins__in=[self.normal_django_user])
|
||||
normal_qs = qs.filter(organization__admin_role__members=self.normal_django_user)
|
||||
self.check_get_list(url, self.normal_django_user, normal_qs)
|
||||
|
||||
# a user who is on a team who has a read permissions on an inventory can see filtered inventories
|
||||
other_qs = qs.filter(permissions__user__in=[self.other_django_user])
|
||||
other_qs = Inventory.accessible_objects(self.other_django_user, {'read': True}).distinct()
|
||||
self.check_get_list(url, self.other_django_user, other_qs)
|
||||
|
||||
# a regular user not part of anything cannot see any inventories
|
||||
@ -226,6 +221,8 @@ class InventoryTest(BaseTest):
|
||||
self.inventory_a.groups.create(name='group-a')
|
||||
self.inventory_b.hosts.create(name='host-b')
|
||||
self.inventory_b.groups.create(name='group-b')
|
||||
a_pk = self.inventory_a.pk
|
||||
b_pk = self.inventory_b.pk
|
||||
|
||||
# Check put to detail view with invalid authentication.
|
||||
self.check_invalid_auth(url_a, methods=('delete',))
|
||||
@ -248,45 +245,33 @@ class InventoryTest(BaseTest):
|
||||
self.delete(url_a, expect=204)
|
||||
self.delete(url_b, expect=403)
|
||||
|
||||
# Verify that the inventory is marked inactive, along with all its
|
||||
# hosts and groups.
|
||||
self.inventory_a = Inventory.objects.get(pk=self.inventory_a.pk)
|
||||
self.assertFalse(self.inventory_a.active)
|
||||
self.assertFalse(self.inventory_a.hosts.filter(active=True).count())
|
||||
self.assertFalse(self.inventory_a.groups.filter(active=True).count())
|
||||
# Verify that the inventory was deleted
|
||||
assert Inventory.objects.filter(pk=a_pk).count() == 0
|
||||
|
||||
# a super user can delete inventory records
|
||||
with self.current_user(self.super_django_user):
|
||||
self.delete(url_a, expect=404)
|
||||
self.delete(url_b, expect=204)
|
||||
|
||||
# Verify that the inventory is marked inactive, along with all its
|
||||
# hosts and groups.
|
||||
self.inventory_b = Inventory.objects.get(pk=self.inventory_b.pk)
|
||||
self.assertFalse(self.inventory_b.active)
|
||||
self.assertFalse(self.inventory_b.hosts.filter(active=True).count())
|
||||
self.assertFalse(self.inventory_b.groups.filter(active=True).count())
|
||||
# Verify that the inventory was deleted
|
||||
assert Inventory.objects.filter(pk=b_pk).count() == 0
|
||||
|
||||
def test_inventory_access_deleted_permissions(self):
|
||||
temp_org = self.make_organizations(self.super_django_user, 1)[0]
|
||||
temp_org.admins.add(self.normal_django_user)
|
||||
temp_org.users.add(self.other_django_user)
|
||||
temp_org.users.add(self.normal_django_user)
|
||||
temp_org.admin_role.members.add(self.normal_django_user)
|
||||
temp_org.member_role.members.add(self.other_django_user)
|
||||
temp_org.member_role.members.add(self.normal_django_user)
|
||||
temp_inv = temp_org.inventories.create(name='Delete Org Inventory')
|
||||
temp_inv.groups.create(name='Delete Org Inventory Group')
|
||||
|
||||
temp_perm_read = Permission.objects.create(
|
||||
inventory = temp_inv,
|
||||
user = self.other_django_user,
|
||||
permission_type = 'read'
|
||||
)
|
||||
temp_inv.auditor_role.members.add(self.other_django_user)
|
||||
|
||||
reverse('api:organization_detail', args=(temp_org.pk,))
|
||||
inventory_detail = reverse('api:inventory_detail', args=(temp_inv.pk,))
|
||||
permission_detail = reverse('api:permission_detail', args=(temp_perm_read.pk,))
|
||||
auditor_role_users_list = reverse('api:role_users_list', args=(temp_inv.auditor_role.pk,))
|
||||
|
||||
self.get(inventory_detail, expect=200, auth=self.get_other_credentials())
|
||||
self.delete(permission_detail, expect=204, auth=self.get_super_credentials())
|
||||
self.post(auditor_role_users_list, data={'disassociate': True, "id": self.other_django_user.id}, expect=204, auth=self.get_super_credentials())
|
||||
self.get(inventory_detail, expect=403, auth=self.get_other_credentials())
|
||||
|
||||
def test_create_inventory_script(self):
|
||||
@ -341,10 +326,8 @@ class InventoryTest(BaseTest):
|
||||
self.post(hosts, data=new_host_b, expect=403, auth=self.get_nobody_credentials())
|
||||
|
||||
# a normal user with inventory edit permissions (on any inventory) can create hosts
|
||||
Permission.objects.create(
|
||||
user = self.other_django_user,
|
||||
inventory = Inventory.objects.get(pk=inv.pk),
|
||||
permission_type = PERM_INVENTORY_WRITE)
|
||||
|
||||
inv.admin_role.members.add(self.other_django_user)
|
||||
host_data3 = self.post(hosts, data=new_host_c, expect=201, auth=self.get_other_credentials())
|
||||
|
||||
# Port should be split out into host variables, other variables kept intact.
|
||||
@ -399,11 +382,6 @@ class InventoryTest(BaseTest):
|
||||
|
||||
# a normal user with inventory edit permissions (on any inventory) can create groups
|
||||
# already done!
|
||||
#edit_perm = Permission.objects.create(
|
||||
# user = self.other_django_user,
|
||||
# inventory = Inventory.objects.get(pk=inv.pk),
|
||||
# permission_type = PERM_INVENTORY_WRITE
|
||||
#)
|
||||
self.post(groups, data=new_group_c, expect=201, auth=self.get_other_credentials())
|
||||
|
||||
# hostnames must be unique inside an organization
|
||||
@ -423,9 +401,10 @@ class InventoryTest(BaseTest):
|
||||
del_children_url = reverse('api:group_children_list', args=(del_group.pk,))
|
||||
nondel_url = reverse('api:group_detail',
|
||||
args=(Group.objects.get(name='nondel').pk,))
|
||||
del_group.mark_inactive()
|
||||
assert(inv.accessible_by(self.normal_django_user, {'read': True}))
|
||||
del_group.delete()
|
||||
nondel_detail = self.get(nondel_url, expect=200, auth=self.get_normal_credentials())
|
||||
self.post(del_children_url, data=nondel_detail, expect=403, auth=self.get_normal_credentials())
|
||||
self.post(del_children_url, data=nondel_detail, expect=400, auth=self.get_normal_credentials())
|
||||
|
||||
|
||||
#################################################
|
||||
@ -662,11 +641,7 @@ class InventoryTest(BaseTest):
|
||||
gx5 = Group.objects.create(name='group-X5', inventory=inva)
|
||||
gx5.parents.add(gx4)
|
||||
|
||||
Permission.objects.create(
|
||||
inventory = inva,
|
||||
user = self.other_django_user,
|
||||
permission_type = PERM_INVENTORY_WRITE
|
||||
)
|
||||
inva.admin_role.members.add(self.other_django_user)
|
||||
|
||||
# data used for testing listing all hosts that are transitive members of a group
|
||||
g2 = Group.objects.get(name='web4')
|
||||
@ -747,13 +722,11 @@ class InventoryTest(BaseTest):
|
||||
# removed group should be automatically marked inactive once it no longer has any parents.
|
||||
removed_group = Group.objects.get(pk=result['id'])
|
||||
self.assertTrue(removed_group.parents.count())
|
||||
self.assertTrue(removed_group.active)
|
||||
for parent in removed_group.parents.all():
|
||||
parent_children_url = reverse('api:group_children_list', args=(parent.pk,))
|
||||
data = {'id': removed_group.pk, 'disassociate': 1}
|
||||
self.post(parent_children_url, data, expect=204, auth=self.get_super_credentials())
|
||||
removed_group = Group.objects.get(pk=result['id'])
|
||||
#self.assertFalse(removed_group.active) # FIXME: Disabled for now because automatically deleting group with no parents is also disabled.
|
||||
|
||||
# Removing a group from a hierarchy should migrate its children to the
|
||||
# parent. The group itself will be deleted (marked inactive), and all
|
||||
@ -766,7 +739,6 @@ class InventoryTest(BaseTest):
|
||||
with self.current_user(self.super_django_user):
|
||||
self.post(url, data, expect=204)
|
||||
gx3 = Group.objects.get(pk=gx3.pk)
|
||||
#self.assertFalse(gx3.active) # FIXME: Disabled for now....
|
||||
self.assertFalse(gx3 in gx2.children.all())
|
||||
#self.assertTrue(gx4 in gx2.children.all())
|
||||
|
||||
@ -944,13 +916,10 @@ class InventoryTest(BaseTest):
|
||||
# Mark group C inactive. Its child groups and hosts should now also be
|
||||
# attached to group A. Group D hosts should be unchanged. Group C
|
||||
# should also no longer have any group or host relationships.
|
||||
g_c.mark_inactive()
|
||||
g_c.delete()
|
||||
self.assertTrue(g_d in g_a.children.all())
|
||||
self.assertTrue(h_c in g_a.hosts.all())
|
||||
self.assertFalse(h_d in g_a.hosts.all())
|
||||
self.assertFalse(g_c.parents.all())
|
||||
self.assertFalse(g_c.children.all())
|
||||
self.assertFalse(g_c.hosts.all())
|
||||
|
||||
def test_safe_delete_recursion(self):
|
||||
# First hierarchy
|
||||
@ -989,11 +958,9 @@ class InventoryTest(BaseTest):
|
||||
self.assertTrue(other_sub_group in sub_group.children.all())
|
||||
|
||||
# Now recursively remove its parent and the reference from subgroup should remain
|
||||
other_top_group.mark_inactive_recursive()
|
||||
other_top_group = Group.objects.get(pk=other_top_group.pk)
|
||||
other_top_group.delete_recursive()
|
||||
self.assertTrue(s2 in sub_group.all_hosts.all())
|
||||
self.assertTrue(other_sub_group in sub_group.children.all())
|
||||
self.assertFalse(other_top_group.active)
|
||||
|
||||
def test_group_parents_and_children(self):
|
||||
# Test for various levels of group parent/child relations, with hosts,
|
||||
@ -1129,59 +1096,6 @@ class InventoryTest(BaseTest):
|
||||
self.assertEqual(response['hosts']['total'], 8)
|
||||
self.assertEqual(response['hosts']['failed'], 8)
|
||||
|
||||
def test_dashboard_inventory_graph_view(self):
|
||||
url = reverse('api:dashboard_inventory_graph_view')
|
||||
# Test with zero hosts.
|
||||
with self.current_user(self.super_django_user):
|
||||
response = self.get(url)
|
||||
self.assertFalse(sum([x[1] for x in response['hosts']]))
|
||||
# Create hosts in inventory_a, with created one day apart, and check
|
||||
# the time series results.
|
||||
dtnow = now()
|
||||
hostnames = list('abcdefg')
|
||||
for x in xrange(len(hostnames) - 1, -1, -1):
|
||||
hostname = hostnames[x]
|
||||
created = dtnow - datetime.timedelta(days=x, seconds=60)
|
||||
self.inventory_a.hosts.create(name=hostname, created=created)
|
||||
with self.current_user(self.super_django_user):
|
||||
response = self.get(url)
|
||||
for n, d in enumerate(reversed(response['hosts'])):
|
||||
self.assertEqual(d[1], max(len(hostnames) - n, 0))
|
||||
# Create more hosts a day apart in inventory_b and check the time
|
||||
# series results.
|
||||
hostnames2 = list('hijklmnop')
|
||||
for x in xrange(len(hostnames2) - 1, -1, -1):
|
||||
hostname = hostnames2[x]
|
||||
created = dtnow - datetime.timedelta(days=x, seconds=120)
|
||||
self.inventory_b.hosts.create(name=hostname, created=created)
|
||||
with self.current_user(self.super_django_user):
|
||||
response = self.get(url)
|
||||
for n, d in enumerate(reversed(response['hosts'])):
|
||||
self.assertEqual(d[1], max(len(hostnames2) - n, 0) + max(len(hostnames) - n, 0))
|
||||
# Now create some hosts in inventory_a with the same hostnames already
|
||||
# used in inventory_b; duplicate hostnames should only be counted the
|
||||
# first time they were seen in inventory_b.
|
||||
hostnames3 = list('lmnop')
|
||||
for x in xrange(len(hostnames3) - 1, -1, -1):
|
||||
hostname = hostnames3[x]
|
||||
created = dtnow - datetime.timedelta(days=x, seconds=180)
|
||||
self.inventory_a.hosts.create(name=hostname, created=created)
|
||||
with self.current_user(self.super_django_user):
|
||||
response = self.get(url)
|
||||
for n, d in enumerate(reversed(response['hosts'])):
|
||||
self.assertEqual(d[1], max(len(hostnames2) - n, 0) + max(len(hostnames) - n, 0))
|
||||
# Delete recently added hosts and verify the count drops.
|
||||
hostnames4 = list('defg')
|
||||
for host in Host.objects.filter(name__in=hostnames4):
|
||||
host.mark_inactive()
|
||||
with self.current_user(self.super_django_user):
|
||||
response = self.get(url)
|
||||
for n, d in enumerate(reversed(response['hosts'])):
|
||||
count = max(len(hostnames2) - n, 0) + max(len(hostnames) - n, 0)
|
||||
if n == 0:
|
||||
count -= 4
|
||||
self.assertEqual(d[1], count)
|
||||
|
||||
|
||||
@override_settings(CELERY_ALWAYS_EAGER=True,
|
||||
CELERY_EAGER_PROPAGATES_EXCEPTIONS=True,
|
||||
@ -1195,9 +1109,9 @@ class InventoryUpdatesTest(BaseTransactionTest):
|
||||
self.setup_instances()
|
||||
self.setup_users()
|
||||
self.organization = self.make_organizations(self.super_django_user, 1)[0]
|
||||
self.organization.admins.add(self.normal_django_user)
|
||||
self.organization.users.add(self.other_django_user)
|
||||
self.organization.users.add(self.normal_django_user)
|
||||
self.organization.admin_role.members.add(self.normal_django_user)
|
||||
self.organization.member_role.members.add(self.other_django_user)
|
||||
self.organization.member_role.members.add(self.normal_django_user)
|
||||
self.inventory = self.organization.inventories.create(name='Cloud Inventory')
|
||||
self.group = self.inventory.groups.create(name='Cloud Group')
|
||||
self.inventory2 = self.organization.inventories.create(name='Cloud Inventory 2')
|
||||
@ -1270,7 +1184,7 @@ class InventoryUpdatesTest(BaseTransactionTest):
|
||||
url = reverse('api:inventory_source_hosts_list', args=(inventory_source.pk,))
|
||||
response = self.get(url, expect=200)
|
||||
self.assertNotEqual(response['count'], 0)
|
||||
for host in inventory.hosts.filter(active=True):
|
||||
for host in inventory.hosts.all():
|
||||
source_pks = host.inventory_sources.values_list('pk', flat=True)
|
||||
self.assertTrue(inventory_source.pk in source_pks)
|
||||
self.assertTrue(host.has_inventory_sources)
|
||||
@ -1284,12 +1198,12 @@ class InventoryUpdatesTest(BaseTransactionTest):
|
||||
url = reverse('api:host_inventory_sources_list', args=(host.pk,))
|
||||
response = self.get(url, expect=200)
|
||||
self.assertNotEqual(response['count'], 0)
|
||||
for group in inventory.groups.filter(active=True):
|
||||
for group in inventory.groups.all():
|
||||
source_pks = group.inventory_sources.values_list('pk', flat=True)
|
||||
self.assertTrue(inventory_source.pk in source_pks)
|
||||
self.assertTrue(group.has_inventory_sources)
|
||||
self.assertTrue(group.children.filter(active=True).exists() or
|
||||
group.hosts.filter(active=True).exists())
|
||||
self.assertTrue(group.children.exists() or
|
||||
group.hosts.exists())
|
||||
# Make sure EC2 instance ID groups and RDS groups are excluded.
|
||||
if inventory_source.source == 'ec2' and not instance_id_group_ok:
|
||||
self.assertFalse(re.match(r'^i-[0-9a-f]{8}$', group.name, re.I),
|
||||
@ -1307,7 +1221,7 @@ class InventoryUpdatesTest(BaseTransactionTest):
|
||||
self.assertNotEqual(response['count'], 0)
|
||||
# Try to set a source on a child group that was imported. Should not
|
||||
# be allowed.
|
||||
for group in inventory_source.group.children.filter(active=True):
|
||||
for group in inventory_source.group.children.all():
|
||||
inv_src_2 = group.inventory_source
|
||||
inv_src_url2 = reverse('api:inventory_source_detail', args=(inv_src_2.pk,))
|
||||
with self.current_user(self.super_django_user):
|
||||
@ -1554,16 +1468,9 @@ class InventoryUpdatesTest(BaseTransactionTest):
|
||||
self.post(inv_src_update_url, {}, expect=403)
|
||||
# If given read permission to the inventory, other user should be able
|
||||
# to see the inventory source and update view, but not start an update.
|
||||
other_perms_url = reverse('api:user_permissions_list',
|
||||
args=(self.other_django_user.pk,))
|
||||
other_perms_data = {
|
||||
'name': 'read only inventory permission for other',
|
||||
'user': self.other_django_user.pk,
|
||||
'inventory': self.inventory.pk,
|
||||
'permission_type': 'read',
|
||||
}
|
||||
user_roles_list_url = reverse('api:user_roles_list', args=(self.other_django_user.pk,))
|
||||
with self.current_user(self.super_django_user):
|
||||
self.post(other_perms_url, other_perms_data, expect=201)
|
||||
self.post(user_roles_list_url, {"id": self.inventory.auditor_role.id}, expect=204)
|
||||
with self.current_user(self.other_django_user):
|
||||
self.get(inv_src_url, expect=200)
|
||||
response = self.get(inv_src_update_url, expect=200)
|
||||
@ -1571,14 +1478,8 @@ class InventoryUpdatesTest(BaseTransactionTest):
|
||||
self.post(inv_src_update_url, {}, expect=403)
|
||||
# Once given write permission, the normal user is able to update the
|
||||
# inventory source.
|
||||
other_perms_data = {
|
||||
'name': 'read-write inventory permission for other',
|
||||
'user': self.other_django_user.pk,
|
||||
'inventory': self.inventory.pk,
|
||||
'permission_type': 'write',
|
||||
}
|
||||
with self.current_user(self.super_django_user):
|
||||
self.post(other_perms_url, other_perms_data, expect=201)
|
||||
self.post(user_roles_list_url, {"id": self.inventory.admin_role.id}, expect=204)
|
||||
with self.current_user(self.other_django_user):
|
||||
self.get(inv_src_url, expect=200)
|
||||
response = self.get(inv_src_update_url, expect=200)
|
||||
@ -1663,7 +1564,7 @@ class InventoryUpdatesTest(BaseTransactionTest):
|
||||
inventory_source.overwrite = True
|
||||
inventory_source.save()
|
||||
self.check_inventory_source(inventory_source, initial=False)
|
||||
for host in self.inventory.hosts.filter(active=True):
|
||||
for host in self.inventory.hosts.all():
|
||||
self.assertEqual(host.variables_dict['ec2_instance_type'], instance_type)
|
||||
|
||||
# Try invalid instance filters that should be ignored:
|
||||
@ -1797,12 +1698,12 @@ class InventoryUpdatesTest(BaseTransactionTest):
|
||||
inventory_source.save()
|
||||
self.check_inventory_source(inventory_source, initial=False)
|
||||
# Verify that only the desired groups are returned.
|
||||
child_names = self.group.children.filter(active=True).values_list('name', flat=True)
|
||||
child_names = self.group.children.values_list('name', flat=True)
|
||||
self.assertTrue('ec2' in child_names)
|
||||
self.assertTrue('regions' in child_names)
|
||||
self.assertTrue(self.group.children.get(name='regions').children.filter(active=True).count())
|
||||
self.assertTrue(self.group.children.get(name='regions').children.count())
|
||||
self.assertTrue('types' in child_names)
|
||||
self.assertTrue(self.group.children.get(name='types').children.filter(active=True).count())
|
||||
self.assertTrue(self.group.children.get(name='types').children.count())
|
||||
self.assertFalse('keys' in child_names)
|
||||
self.assertFalse('security_groups' in child_names)
|
||||
self.assertFalse('tags' in child_names)
|
||||
@ -1819,27 +1720,27 @@ class InventoryUpdatesTest(BaseTransactionTest):
|
||||
self.check_inventory_source(inventory_source, initial=False, instance_id_group_ok=True)
|
||||
# Verify that only the desired groups are returned.
|
||||
# Skip vpcs as selected inventory may or may not have any.
|
||||
child_names = self.group.children.filter(active=True).values_list('name', flat=True)
|
||||
child_names = self.group.children.values_list('name', flat=True)
|
||||
self.assertTrue('ec2' in child_names)
|
||||
self.assertFalse('tag_none' in child_names)
|
||||
self.assertTrue('regions' in child_names)
|
||||
self.assertTrue(self.group.children.get(name='regions').children.filter(active=True).count())
|
||||
self.assertTrue(self.group.children.get(name='regions').children.count())
|
||||
self.assertTrue('types' in child_names)
|
||||
self.assertTrue(self.group.children.get(name='types').children.filter(active=True).count())
|
||||
self.assertTrue(self.group.children.get(name='types').children.count())
|
||||
self.assertTrue('keys' in child_names)
|
||||
self.assertTrue(self.group.children.get(name='keys').children.filter(active=True).count())
|
||||
self.assertTrue(self.group.children.get(name='keys').children.count())
|
||||
self.assertTrue('security_groups' in child_names)
|
||||
self.assertTrue(self.group.children.get(name='security_groups').children.filter(active=True).count())
|
||||
self.assertTrue(self.group.children.get(name='security_groups').children.count())
|
||||
self.assertTrue('tags' in child_names)
|
||||
self.assertTrue(self.group.children.get(name='tags').children.filter(active=True).count())
|
||||
self.assertTrue(self.group.children.get(name='tags').children.count())
|
||||
# Only check for tag_none as a child of tags if there is a tag_none group;
|
||||
# the test inventory *may* have tags set for all hosts.
|
||||
if self.inventory.groups.filter(name='tag_none').exists():
|
||||
self.assertTrue('tag_none' in self.group.children.get(name='tags').children.values_list('name', flat=True))
|
||||
self.assertTrue('images' in child_names)
|
||||
self.assertTrue(self.group.children.get(name='images').children.filter(active=True).count())
|
||||
self.assertTrue(self.group.children.get(name='images').children.count())
|
||||
self.assertTrue('instances' in child_names)
|
||||
self.assertTrue(self.group.children.get(name='instances').children.filter(active=True).count())
|
||||
self.assertTrue(self.group.children.get(name='instances').children.count())
|
||||
# Sync again with overwrite set to False after renaming a group that
|
||||
# was created by the sync. With overwrite false, the renamed group and
|
||||
# the original group (created again by the sync) will both exist.
|
||||
@ -1853,7 +1754,7 @@ class InventoryUpdatesTest(BaseTransactionTest):
|
||||
inventory_source.overwrite = False
|
||||
inventory_source.save()
|
||||
self.check_inventory_source(inventory_source, initial=False, instance_id_group_ok=True)
|
||||
child_names = self.group.children.filter(active=True).values_list('name', flat=True)
|
||||
child_names = self.group.children.values_list('name', flat=True)
|
||||
self.assertTrue(region_group_original_name in self.group.children.get(name='regions').children.values_list('name', flat=True))
|
||||
self.assertTrue(region_group.name in self.group.children.get(name='regions').children.values_list('name', flat=True))
|
||||
# Replacement text should not be left in inventory source name.
|
||||
|
||||
@ -15,7 +15,7 @@ import yaml
|
||||
|
||||
__all__ = ['JobTemplateLaunchTest', 'JobTemplateLaunchPasswordsTest']
|
||||
|
||||
class JobTemplateLaunchTest(BaseJobTestMixin, django.test.TestCase):
|
||||
class JobTemplateLaunchTest(BaseJobTestMixin, django.test.TransactionTestCase):
|
||||
def setUp(self):
|
||||
super(JobTemplateLaunchTest, self).setUp()
|
||||
|
||||
@ -96,7 +96,7 @@ class JobTemplateLaunchTest(BaseJobTestMixin, django.test.TestCase):
|
||||
def test_credential_explicit(self):
|
||||
# Explicit, credential
|
||||
with self.current_user(self.user_sue):
|
||||
self.cred_sue.mark_inactive()
|
||||
self.cred_sue.delete()
|
||||
response = self.post(self.launch_url, {'credential': self.cred_doug.pk}, expect=202)
|
||||
j = Job.objects.get(pk=response['job'])
|
||||
self.assertEqual(j.status, 'new')
|
||||
@ -105,7 +105,7 @@ class JobTemplateLaunchTest(BaseJobTestMixin, django.test.TestCase):
|
||||
def test_credential_explicit_via_credential_id(self):
|
||||
# Explicit, credential
|
||||
with self.current_user(self.user_sue):
|
||||
self.cred_sue.mark_inactive()
|
||||
self.cred_sue.delete()
|
||||
response = self.post(self.launch_url, {'credential_id': self.cred_doug.pk}, expect=202)
|
||||
j = Job.objects.get(pk=response['job'])
|
||||
self.assertEqual(j.status, 'new')
|
||||
@ -131,15 +131,16 @@ class JobTemplateLaunchTest(BaseJobTestMixin, django.test.TestCase):
|
||||
# Can't launch a job template without a credential defined (or if we
|
||||
# pass an invalid/inactive credential value).
|
||||
with self.current_user(self.user_sue):
|
||||
self.cred_sue.mark_inactive()
|
||||
self.cred_sue.delete()
|
||||
self.post(self.launch_url, {}, expect=400)
|
||||
self.post(self.launch_url, {'credential': 0}, expect=400)
|
||||
self.post(self.launch_url, {'credential_id': 0}, expect=400)
|
||||
self.post(self.launch_url, {'credential': 'one'}, expect=400)
|
||||
self.post(self.launch_url, {'credential_id': 'one'}, expect=400)
|
||||
self.cred_doug.mark_inactive()
|
||||
self.post(self.launch_url, {'credential': self.cred_doug.pk}, expect=400)
|
||||
self.post(self.launch_url, {'credential_id': self.cred_doug.pk}, expect=400)
|
||||
cred_doug_pk = self.cred_doug.pk
|
||||
self.cred_doug.delete()
|
||||
self.post(self.launch_url, {'credential': cred_doug_pk}, expect=400)
|
||||
self.post(self.launch_url, {'credential_id': cred_doug_pk}, expect=400)
|
||||
|
||||
def test_explicit_unowned_cred(self):
|
||||
# Explicitly specify a credential that we don't have access to
|
||||
@ -174,11 +175,11 @@ class JobTemplateLaunchTest(BaseJobTestMixin, django.test.TestCase):
|
||||
|
||||
def test_deleted_credential_fail(self):
|
||||
# Job Templates with deleted credentials cannot be launched.
|
||||
self.cred_sue.mark_inactive()
|
||||
self.cred_sue.delete()
|
||||
with self.current_user(self.user_sue):
|
||||
self.post(self.launch_url, {}, expect=400)
|
||||
|
||||
class JobTemplateLaunchPasswordsTest(BaseJobTestMixin, django.test.TestCase):
|
||||
class JobTemplateLaunchPasswordsTest(BaseJobTestMixin, django.test.TransactionTestCase):
|
||||
def setUp(self):
|
||||
super(JobTemplateLaunchPasswordsTest, self).setUp()
|
||||
|
||||
@ -202,7 +203,7 @@ class JobTemplateLaunchPasswordsTest(BaseJobTestMixin, django.test.TestCase):
|
||||
passwords_required = ['ssh_password', 'become_password', 'ssh_key_unlock']
|
||||
# Job Templates with deleted credentials cannot be launched.
|
||||
with self.current_user(self.user_sue):
|
||||
self.cred_sue_ask.mark_inactive()
|
||||
self.cred_sue_ask.delete()
|
||||
response = self.post(self.launch_url, {'credential_id': self.cred_sue_ask_many.pk}, expect=400)
|
||||
for p in passwords_required:
|
||||
self.assertIn(p, response['passwords_needed_to_start'])
|
||||
|
||||
@ -183,7 +183,7 @@ TEST_SURVEY_REQUIREMENTS = '''
|
||||
}
|
||||
'''
|
||||
|
||||
class JobTemplateTest(BaseJobTestMixin, django.test.TestCase):
|
||||
class JobTemplateTest(BaseJobTestMixin, django.test.TransactionTestCase):
|
||||
|
||||
JOB_TEMPLATE_FIELDS = ('id', 'type', 'url', 'related', 'summary_fields',
|
||||
'created', 'modified', 'name', 'description',
|
||||
@ -265,7 +265,7 @@ class JobTemplateTest(BaseJobTestMixin, django.test.TestCase):
|
||||
|
||||
# Chuck is temporarily assigned to ops east team to help them running some playbooks
|
||||
# even though he's in a different group and org entirely he'll now see their job templates
|
||||
self.team_ops_east.users.add(self.user_chuck)
|
||||
self.team_ops_east.deprecated_users.add(self.user_chuck)
|
||||
with self.current_user(self.user_chuck):
|
||||
resp = self.get(url, expect=200)
|
||||
#print [x['name'] for x in resp['results']]
|
||||
@ -492,7 +492,7 @@ class JobTemplateTest(BaseJobTestMixin, django.test.TestCase):
|
||||
with self.current_user(self.user_doug):
|
||||
self.get(detail_url, expect=403)
|
||||
|
||||
class JobTest(BaseJobTestMixin, django.test.TestCase):
|
||||
class JobTest(BaseJobTestMixin, django.test.TransactionTestCase):
|
||||
|
||||
def test_get_job_list(self):
|
||||
url = reverse('api:job_list')
|
||||
@ -1083,7 +1083,7 @@ class JobTransactionTest(BaseJobTestMixin, django.test.LiveServerTestCase):
|
||||
self.assertEqual(job.status, 'successful', job.result_stdout)
|
||||
self.assertFalse(errors)
|
||||
|
||||
class JobTemplateSurveyTest(BaseJobTestMixin, django.test.TestCase):
|
||||
class JobTemplateSurveyTest(BaseJobTestMixin, django.test.TransactionTestCase):
|
||||
def setUp(self):
|
||||
super(JobTemplateSurveyTest, self).setUp()
|
||||
# TODO: Test non-enterprise license
|
||||
|
||||
@ -19,7 +19,7 @@ class LicenseTests(BaseTest):
|
||||
self.setup_users()
|
||||
u = self.super_django_user
|
||||
org = Organization.objects.create(name='o1', created_by=u)
|
||||
org.admins.add(self.normal_django_user)
|
||||
org.admin_role.members.add(self.normal_django_user)
|
||||
self.inventory = Inventory.objects.create(name='hi', organization=org, created_by=u)
|
||||
Host.objects.create(name='a1', inventory=self.inventory, created_by=u)
|
||||
Host.objects.create(name='a2', inventory=self.inventory, created_by=u)
|
||||
|
||||
@ -88,12 +88,12 @@ class OrganizationsTest(BaseTest):
|
||||
# nobody_user is a user not a member of any organizations
|
||||
|
||||
for x in self.organizations:
|
||||
x.admins.add(self.super_django_user)
|
||||
x.users.add(self.super_django_user)
|
||||
x.users.add(self.other_django_user)
|
||||
x.admin_role.members.add(self.super_django_user)
|
||||
x.member_role.members.add(self.super_django_user)
|
||||
x.member_role.members.add(self.other_django_user)
|
||||
|
||||
self.organizations[0].users.add(self.normal_django_user)
|
||||
self.organizations[1].admins.add(self.normal_django_user)
|
||||
self.organizations[0].member_role.members.add(self.normal_django_user)
|
||||
self.organizations[1].admin_role.members.add(self.normal_django_user)
|
||||
|
||||
def test_get_organization_list(self):
|
||||
url = reverse('api:organization_list')
|
||||
@ -136,7 +136,7 @@ class OrganizationsTest(BaseTest):
|
||||
# no admin rights? get empty list
|
||||
with self.current_user(self.other_django_user):
|
||||
response = self.get(url, expect=200)
|
||||
self.check_pagination_and_size(response, self.other_django_user.organizations.count(), previous=None, next=None)
|
||||
self.check_pagination_and_size(response, len(self.organizations), previous=None, next=None)
|
||||
|
||||
# not a member of any orgs? get empty list
|
||||
with self.current_user(self.nobody_django_user):
|
||||
@ -283,14 +283,14 @@ class OrganizationsTest(BaseTest):
|
||||
# find projects attached to the first org
|
||||
projects0_url = orgs['results'][0]['related']['projects']
|
||||
projects1_url = orgs['results'][1]['related']['projects']
|
||||
projects2_url = orgs['results'][2]['related']['projects']
|
||||
|
||||
# get all the projects on the first org
|
||||
projects0 = self.get(projects0_url, expect=200, auth=self.get_super_credentials())
|
||||
a_project = projects0['results'][-1]
|
||||
|
||||
# attempt to add the project to the 7th org and see what happens
|
||||
self.post(projects1_url, a_project, expect=204, auth=self.get_super_credentials())
|
||||
#self.post(projects1_url, a_project, expect=204, auth=self.get_super_credentials())
|
||||
self.post(projects1_url, a_project, expect=400, auth=self.get_super_credentials())
|
||||
projects1 = self.get(projects0_url, expect=200, auth=self.get_super_credentials())
|
||||
self.assertEquals(projects1['count'], 3)
|
||||
|
||||
@ -300,32 +300,19 @@ class OrganizationsTest(BaseTest):
|
||||
|
||||
# test that by posting a pk + disassociate: True we can remove a relationship
|
||||
projects1 = self.get(projects1_url, expect=200, auth=self.get_super_credentials())
|
||||
self.assertEquals(projects1['count'], 6)
|
||||
self.assertEquals(projects1['count'], 5)
|
||||
a_project['disassociate'] = True
|
||||
self.post(projects1_url, a_project, expect=204, auth=self.get_super_credentials())
|
||||
self.post(projects1_url, a_project, expect=400, auth=self.get_super_credentials())
|
||||
projects1 = self.get(projects1_url, expect=200, auth=self.get_super_credentials())
|
||||
self.assertEquals(projects1['count'], 5)
|
||||
|
||||
a_project = projects1['results'][-1]
|
||||
a_project['disassociate'] = 1
|
||||
projects1 = self.get(projects1_url, expect=200, auth=self.get_super_credentials())
|
||||
self.post(projects1_url, a_project, expect=204, auth=self.get_normal_credentials())
|
||||
self.post(projects1_url, a_project, expect=400, auth=self.get_normal_credentials())
|
||||
projects1 = self.get(projects1_url, expect=200, auth=self.get_super_credentials())
|
||||
self.assertEquals(projects1['count'], 4)
|
||||
self.assertEquals(projects1['count'], 5)
|
||||
|
||||
new_project_a = self.make_projects(self.normal_django_user, 1)[0]
|
||||
new_project_b = self.make_projects(self.other_django_user, 1)[0]
|
||||
|
||||
# admin of org can add projects that he can read
|
||||
self.post(projects1_url, dict(id=new_project_a.pk), expect=204, auth=self.get_normal_credentials())
|
||||
# but not those he cannot
|
||||
self.post(projects1_url, dict(id=new_project_b.pk), expect=403, auth=self.get_normal_credentials())
|
||||
|
||||
# and can't post a project he can read to an org he cannot
|
||||
# self.post(projects2_url, dict(id=new_project_a.pk), expect=403, auth=self.get_normal_credentials())
|
||||
|
||||
# and can't do post a project he can read to an organization he cannot
|
||||
self.post(projects2_url, dict(id=new_project_a.pk), expect=403, auth=self.get_normal_credentials())
|
||||
|
||||
|
||||
def test_post_item_subobjects_users(self):
|
||||
@ -342,7 +329,7 @@ class OrganizationsTest(BaseTest):
|
||||
|
||||
# post a completely new user to verify we can add users to the subcollection directly
|
||||
new_user = dict(username='NewUser9000', password='NewPassword9000')
|
||||
which_org = self.normal_django_user.admin_of_organizations.all()[0]
|
||||
which_org = Organization.accessible_objects(self.normal_django_user, {'read': True, 'write': True})[0]
|
||||
url = reverse('api:organization_users_list', args=(which_org.pk,))
|
||||
self.post(url, new_user, expect=201, auth=self.get_normal_credentials())
|
||||
|
||||
@ -436,10 +423,8 @@ class OrganizationsTest(BaseTest):
|
||||
self.delete(urls[0], expect=204, auth=self.get_super_credentials())
|
||||
|
||||
# check that when we have deleted an object it comes back 404 via GET
|
||||
# but that it's still in the database as inactive
|
||||
self.get(urls[1], expect=404, auth=self.get_normal_credentials())
|
||||
org1 = Organization.objects.get(pk=urldata1['id'])
|
||||
self.assertEquals(org1.active, False)
|
||||
assert Organization.objects.filter(pk=urldata1['id']).count() == 0
|
||||
|
||||
# also check that DELETE on the collection doesn't work
|
||||
self.delete(self.collection(), expect=405, auth=self.get_super_credentials())
|
||||
|
||||
@ -59,8 +59,8 @@ class ProjectsTest(BaseTransactionTest):
|
||||
self.organizations[1].projects.add(project)
|
||||
for project in self.projects[9:10]:
|
||||
self.organizations[2].projects.add(project)
|
||||
self.organizations[0].projects.add(self.projects[-1])
|
||||
self.organizations[9].projects.add(self.projects[-2])
|
||||
#self.organizations[0].projects.add(self.projects[-1])
|
||||
#self.organizations[9].projects.add(self.projects[-2])
|
||||
|
||||
# get the URL for various organization records
|
||||
self.a_detail_url = "%s%s" % (self.collection(), self.organizations[0].pk)
|
||||
@ -75,10 +75,10 @@ class ProjectsTest(BaseTransactionTest):
|
||||
for x in self.organizations:
|
||||
# NOTE: superuser does not have to be explicitly added to admin group
|
||||
# x.admins.add(self.super_django_user)
|
||||
x.users.add(self.super_django_user)
|
||||
x.member_role.members.add(self.super_django_user)
|
||||
|
||||
self.organizations[0].users.add(self.normal_django_user)
|
||||
self.organizations[1].admins.add(self.normal_django_user)
|
||||
self.organizations[0].member_role.members.add(self.normal_django_user)
|
||||
self.organizations[1].admin_role.members.add(self.normal_django_user)
|
||||
|
||||
self.team1 = Team.objects.create(
|
||||
name = 'team1', organization = self.organizations[0]
|
||||
@ -89,7 +89,9 @@ class ProjectsTest(BaseTransactionTest):
|
||||
)
|
||||
|
||||
# create some teams in the first org
|
||||
self.team1.projects.add(self.projects[0])
|
||||
#self.team1.projects.add(self.projects[0])
|
||||
self.projects[0].teams.add(self.team1)
|
||||
#self.team1.projects.add(self.projects[0])
|
||||
self.team2.projects.add(self.projects[1])
|
||||
self.team2.projects.add(self.projects[2])
|
||||
self.team2.projects.add(self.projects[3])
|
||||
@ -97,8 +99,8 @@ class ProjectsTest(BaseTransactionTest):
|
||||
self.team2.projects.add(self.projects[5])
|
||||
self.team1.save()
|
||||
self.team2.save()
|
||||
self.team1.users.add(self.normal_django_user)
|
||||
self.team2.users.add(self.other_django_user)
|
||||
self.team1.member_role.members.add(self.normal_django_user)
|
||||
self.team2.member_role.members.add(self.other_django_user)
|
||||
|
||||
def test_playbooks(self):
|
||||
def write_test_file(project, name, content):
|
||||
@ -162,14 +164,14 @@ class ProjectsTest(BaseTransactionTest):
|
||||
set(Project.get_local_path_choices()))
|
||||
|
||||
# return local paths are only the ones not used by any active project.
|
||||
qs = Project.objects.filter(active=True)
|
||||
qs = Project.objects
|
||||
used_paths = qs.values_list('local_path', flat=True)
|
||||
self.assertFalse(set(response['project_local_paths']) & set(used_paths))
|
||||
for project in self.projects:
|
||||
local_path = project.local_path
|
||||
response = self.get(url, expect=200, auth=self.get_super_credentials())
|
||||
self.assertTrue(local_path not in response['project_local_paths'])
|
||||
project.mark_inactive()
|
||||
project.delete()
|
||||
response = self.get(url, expect=200, auth=self.get_super_credentials())
|
||||
self.assertTrue(local_path in response['project_local_paths'])
|
||||
|
||||
@ -215,7 +217,7 @@ class ProjectsTest(BaseTransactionTest):
|
||||
self.assertEquals(results['count'], 10)
|
||||
# org admin
|
||||
results = self.get(projects, expect=200, auth=self.get_normal_credentials())
|
||||
self.assertEquals(results['count'], 9)
|
||||
self.assertEquals(results['count'], 6)
|
||||
# user on a team
|
||||
results = self.get(projects, expect=200, auth=self.get_other_credentials())
|
||||
self.assertEquals(results['count'], 5)
|
||||
@ -296,31 +298,6 @@ class ProjectsTest(BaseTransactionTest):
|
||||
got = self.get(proj_playbooks, expect=200, auth=self.get_super_credentials())
|
||||
self.assertEqual(got, self.projects[2].playbooks)
|
||||
|
||||
# can list member organizations for projects
|
||||
proj_orgs = reverse('api:project_organizations_list', args=(self.projects[0].pk,))
|
||||
# only usable as superuser
|
||||
got = self.get(proj_orgs, expect=200, auth=self.get_normal_credentials())
|
||||
got = self.get(proj_orgs, expect=200, auth=self.get_super_credentials())
|
||||
self.get(proj_orgs, expect=403, auth=self.get_other_credentials())
|
||||
self.assertEquals(got['count'], 1)
|
||||
self.assertEquals(got['results'][0]['url'], reverse('api:organization_detail', args=(self.organizations[0].pk,)))
|
||||
|
||||
# post to create new org associated with this project.
|
||||
self.post(proj_orgs, data={'name': 'New Org'}, expect=201, auth=self.get_super_credentials())
|
||||
got = self.get(proj_orgs, expect=200, auth=self.get_super_credentials())
|
||||
self.assertEquals(got['count'], 2)
|
||||
|
||||
# Verify that creatorship doesn't imply access if access is removed
|
||||
a_new_proj = self.make_project(created_by=self.other_django_user, playbook_content=TEST_PLAYBOOK)
|
||||
self.organizations[0].admins.add(self.other_django_user)
|
||||
self.organizations[0].projects.add(a_new_proj)
|
||||
proj_detail = reverse('api:project_detail', args=(a_new_proj.pk,))
|
||||
self.patch(proj_detail, data=dict(description="test"), expect=200, auth=self.get_other_credentials())
|
||||
self.organizations[0].admins.remove(self.other_django_user)
|
||||
self.patch(proj_detail, data=dict(description="test_now"), expect=403, auth=self.get_other_credentials())
|
||||
self.delete(proj_detail, expect=403, auth=self.get_other_credentials())
|
||||
a_new_proj.delete()
|
||||
|
||||
# =====================================================================
|
||||
# TEAMS
|
||||
|
||||
@ -337,7 +314,7 @@ class ProjectsTest(BaseTransactionTest):
|
||||
self.assertEquals(got['url'], reverse('api:team_detail', args=(self.team1.pk,)))
|
||||
got = self.get(team1, expect=200, auth=self.get_normal_credentials())
|
||||
got = self.get(team1, expect=403, auth=self.get_other_credentials())
|
||||
self.team1.users.add(User.objects.get(username='other'))
|
||||
self.team1.member_role.members.add(User.objects.get(username='other'))
|
||||
self.team1.save()
|
||||
got = self.get(team1, expect=200, auth=self.get_other_credentials())
|
||||
got = self.get(team1, expect=403, auth=self.get_nobody_credentials())
|
||||
@ -402,7 +379,7 @@ class ProjectsTest(BaseTransactionTest):
|
||||
# =====================================================================
|
||||
# TEAM PROJECTS
|
||||
|
||||
team = Team.objects.filter(active=True, organization__pk=self.organizations[1].pk)[0]
|
||||
team = Team.objects.filter( organization__pk=self.organizations[1].pk)[0]
|
||||
team_projects = reverse('api:team_projects_list', args=(team.pk,))
|
||||
|
||||
p1 = self.projects[0]
|
||||
@ -419,10 +396,10 @@ class ProjectsTest(BaseTransactionTest):
|
||||
# =====================================================================
|
||||
# TEAMS USER MEMBERSHIP
|
||||
|
||||
team = Team.objects.filter(active=True, organization__pk=self.organizations[1].pk)[0]
|
||||
team = Team.objects.filter( organization__pk=self.organizations[1].pk)[0]
|
||||
team_users = reverse('api:team_users_list', args=(team.pk,))
|
||||
for x in team.users.all():
|
||||
team.users.remove(x)
|
||||
for x in team.member_role.members.all():
|
||||
team.member_role.members.remove(x)
|
||||
team.save()
|
||||
|
||||
# can list uses on teams
|
||||
@ -446,7 +423,7 @@ class ProjectsTest(BaseTransactionTest):
|
||||
self.post(team_users, data=dict(username='attempted_superuser_create', password='thepassword',
|
||||
is_superuser=True), expect=201, auth=self.get_super_credentials())
|
||||
|
||||
self.assertEqual(Team.objects.get(pk=team.pk).users.count(), 5)
|
||||
self.assertEqual(Team.objects.get(pk=team.pk).member_role.members.count(), all_users['count'] + 1)
|
||||
|
||||
# can remove users from teams
|
||||
for x in all_users['results']:
|
||||
@ -454,7 +431,7 @@ class ProjectsTest(BaseTransactionTest):
|
||||
self.post(team_users, data=y, expect=403, auth=self.get_nobody_credentials())
|
||||
self.post(team_users, data=y, expect=204, auth=self.get_normal_credentials())
|
||||
|
||||
self.assertEquals(Team.objects.get(pk=team.pk).users.count(), 1) # Leaving just the super user we created
|
||||
self.assertEquals(Team.objects.get(pk=team.pk).member_role.members.count(), 1) # Leaving just the super user we created
|
||||
|
||||
# =====================================================================
|
||||
# USER TEAMS
|
||||
@ -465,9 +442,12 @@ class ProjectsTest(BaseTransactionTest):
|
||||
self.get(url, expect=401)
|
||||
self.get(url, expect=401, auth=self.get_invalid_credentials())
|
||||
self.get(url, expect=403, auth=self.get_nobody_credentials())
|
||||
other.organizations.add(Organization.objects.get(pk=self.organizations[1].pk))
|
||||
self.organizations[1].member_role.members.add(other)
|
||||
# Normal user can only see some teams that other user is a part of,
|
||||
# since normal user is not an admin of that organization.
|
||||
my_teams1 = self.get(url, expect=200, auth=self.get_normal_credentials())
|
||||
my_teams2 = self.get(url, expect=200, auth=self.get_other_credentials())
|
||||
|
||||
my_teams1 = self.get(url, expect=200, auth=self.get_normal_credentials())
|
||||
self.assertEqual(my_teams1['count'], 1)
|
||||
# Other user should be able to see all his own teams.
|
||||
@ -622,7 +602,7 @@ class ProjectsTest(BaseTransactionTest):
|
||||
# Test post as organization admin where team is part of org, but user
|
||||
# creating credential is not a member of the team. UI may pass user
|
||||
# as an empty string instead of None.
|
||||
normal_org = self.normal_django_user.admin_of_organizations.all()[0]
|
||||
normal_org = self.organizations[1] # normal user is an admin of this
|
||||
org_team = normal_org.teams.create(name='new empty team')
|
||||
with self.current_user(self.normal_django_user):
|
||||
data = {
|
||||
@ -787,7 +767,7 @@ class ProjectsTest(BaseTransactionTest):
|
||||
# User is still a team member
|
||||
self.get(reverse('api:project_detail', args=(project.pk,)), expect=200, auth=self.get_other_credentials())
|
||||
|
||||
team.users.remove(self.other_django_user)
|
||||
team.member_role.members.remove(self.other_django_user)
|
||||
|
||||
# User is no longer a team member and has no permissions
|
||||
self.get(reverse('api:project_detail', args=(project.pk,)), expect=403, auth=self.get_other_credentials())
|
||||
@ -1262,7 +1242,7 @@ class ProjectUpdatesTest(BaseTransactionTest):
|
||||
else:
|
||||
self.check_project_update(project, should_fail=should_still_fail)
|
||||
# Test that we can delete project updates.
|
||||
for pu in project.project_updates.filter(active=True):
|
||||
for pu in project.project_updates.all():
|
||||
pu_url = reverse('api:project_update_detail', args=(pu.pk,))
|
||||
with self.current_user(self.super_django_user):
|
||||
self.delete(pu_url, expect=204)
|
||||
@ -1351,7 +1331,7 @@ class ProjectUpdatesTest(BaseTransactionTest):
|
||||
'scm_url': scm_url,
|
||||
}
|
||||
org = self.make_organizations(self.super_django_user, 1)[0]
|
||||
org.admins.add(self.normal_django_user)
|
||||
org.admin_role.members.add(self.normal_django_user)
|
||||
with self.current_user(self.super_django_user):
|
||||
del_proj = self.post(projects_url, project_data, expect=201)
|
||||
del_proj = Project.objects.get(pk=del_proj["id"])
|
||||
|
||||
@ -54,12 +54,12 @@ class ScheduleTest(BaseTest):
|
||||
self.setup_instances()
|
||||
self.setup_users()
|
||||
self.organizations = self.make_organizations(self.super_django_user, 2)
|
||||
self.organizations[0].admins.add(self.normal_django_user)
|
||||
self.organizations[0].users.add(self.other_django_user)
|
||||
self.organizations[0].users.add(self.normal_django_user)
|
||||
self.organizations[0].admin_role.members.add(self.normal_django_user)
|
||||
self.organizations[0].member_role.members.add(self.other_django_user)
|
||||
self.organizations[0].member_role.members.add(self.normal_django_user)
|
||||
|
||||
self.diff_org_user = self.make_user('fred')
|
||||
self.organizations[1].users.add(self.diff_org_user)
|
||||
self.organizations[1].member_role.members.add(self.diff_org_user)
|
||||
|
||||
self.cloud_source = Credential.objects.create(kind='awx', user=self.super_django_user,
|
||||
username='Dummy', password='Dummy')
|
||||
@ -71,11 +71,7 @@ class ScheduleTest(BaseTest):
|
||||
self.first_inventory_source.source = 'ec2'
|
||||
self.first_inventory_source.save()
|
||||
|
||||
Permission.objects.create(
|
||||
inventory = self.first_inventory,
|
||||
user = self.other_django_user,
|
||||
permission_type = 'read'
|
||||
)
|
||||
self.first_inventory.auditor_role.members.add(self.other_django_user)
|
||||
|
||||
self.second_inventory = Inventory.objects.create(name='test_inventory_2', description='for org 0', organization=self.organizations[0])
|
||||
self.second_inventory.hosts.create(name='host_2')
|
||||
@ -139,11 +135,7 @@ class ScheduleTest(BaseTest):
|
||||
self.post(first_url, data=unauth_schedule, expect=403)
|
||||
|
||||
#give normal user write access and then they can post
|
||||
Permission.objects.create(
|
||||
user = self.other_django_user,
|
||||
inventory = self.first_inventory,
|
||||
permission_type = PERM_INVENTORY_WRITE
|
||||
)
|
||||
self.first_inventory.admin_role.members.add(self.other_django_user)
|
||||
auth_schedule = unauth_schedule
|
||||
with self.current_user(self.other_django_user):
|
||||
self.post(first_url, data=auth_schedule, expect=201)
|
||||
|
||||
@ -87,10 +87,12 @@ class InventoryScriptTest(BaseScriptTest):
|
||||
host = inventory.hosts.create(name='host-%02d-%02d.example.com' % (n, x),
|
||||
inventory=inventory,
|
||||
variables=variables)
|
||||
if x in (3, 7):
|
||||
host.mark_inactive()
|
||||
#if x in (3, 7):
|
||||
# host.delete()
|
||||
# continue
|
||||
hosts.append(host)
|
||||
|
||||
|
||||
# add localhost just to make sure it's thrown into all (Ansible github bug)
|
||||
local = inventory.hosts.create(name='localhost', inventory=inventory, variables={})
|
||||
hosts.append(local)
|
||||
@ -105,8 +107,9 @@ class InventoryScriptTest(BaseScriptTest):
|
||||
group = inventory.groups.create(name='group-%d' % x,
|
||||
inventory=inventory,
|
||||
variables=variables)
|
||||
if x == 2:
|
||||
group.mark_inactive()
|
||||
#if x == 2:
|
||||
# #group.delete()
|
||||
# #continue
|
||||
groups.append(group)
|
||||
group.hosts.add(hosts[x])
|
||||
group.hosts.add(hosts[x + 5])
|
||||
@ -116,6 +119,13 @@ class InventoryScriptTest(BaseScriptTest):
|
||||
group.hosts.add(local)
|
||||
self.groups.extend(groups)
|
||||
|
||||
hosts[3].delete()
|
||||
hosts[7].delete()
|
||||
groups[2].delete()
|
||||
|
||||
|
||||
|
||||
|
||||
def tearDown(self):
|
||||
super(InventoryScriptTest, self).tearDown()
|
||||
self.stop_redis()
|
||||
@ -144,12 +154,11 @@ class InventoryScriptTest(BaseScriptTest):
|
||||
|
||||
def test_list_with_inventory_id_as_argument(self):
|
||||
inventory = self.inventories[0]
|
||||
self.assertTrue(inventory.active)
|
||||
rc, stdout, stderr = self.run_inventory_script(list=True,
|
||||
inventory=inventory.pk)
|
||||
self.assertEqual(rc, 0, stderr)
|
||||
data = json.loads(stdout)
|
||||
groups = inventory.groups.filter(active=True)
|
||||
groups = inventory.groups
|
||||
groupnames = [ x for x in groups.values_list('name', flat=True)]
|
||||
|
||||
# it's ok for all to be here because due to an Ansible inventory workaround
|
||||
@ -161,20 +170,17 @@ class InventoryScriptTest(BaseScriptTest):
|
||||
# variable data or parent/child relationships.
|
||||
for k,v in data.items():
|
||||
if k != 'all':
|
||||
self.assertTrue(isinstance(v, dict))
|
||||
self.assertTrue(isinstance(v['children'], (list,tuple)))
|
||||
self.assertTrue(isinstance(v['hosts'], (list,tuple)))
|
||||
self.assertTrue(isinstance(v['vars'], (dict)))
|
||||
group = inventory.groups.get(active=True, name=k)
|
||||
hosts = group.hosts.filter(active=True)
|
||||
assert isinstance(v, dict)
|
||||
assert isinstance(v['children'], (list,tuple))
|
||||
assert isinstance(v['hosts'], (list,tuple))
|
||||
assert isinstance(v['vars'], (dict))
|
||||
group = inventory.groups.get(name=k)
|
||||
hosts = group.hosts
|
||||
hostnames = hosts.values_list('name', flat=True)
|
||||
self.assertEqual(set(v['hosts']), set(hostnames))
|
||||
else:
|
||||
self.assertTrue(v['hosts'] == ['localhost'])
|
||||
assert v['hosts'] == ['host-00-02.example.com', 'localhost']
|
||||
|
||||
for group in inventory.groups.filter(active=False):
|
||||
self.assertFalse(group.name in data.keys(),
|
||||
'deleted group %s should not be in data' % group)
|
||||
# Command line argument for inventory ID should take precedence over
|
||||
# environment variable.
|
||||
inventory_pks = set(map(lambda x: x.pk, self.inventories))
|
||||
@ -187,43 +193,41 @@ class InventoryScriptTest(BaseScriptTest):
|
||||
|
||||
def test_list_with_inventory_id_in_environment(self):
|
||||
inventory = self.inventories[1]
|
||||
self.assertTrue(inventory.active)
|
||||
os.environ['INVENTORY_ID'] = str(inventory.pk)
|
||||
rc, stdout, stderr = self.run_inventory_script(list=True)
|
||||
self.assertEqual(rc, 0, stderr)
|
||||
data = json.loads(stdout)
|
||||
groups = inventory.groups.filter(active=True)
|
||||
groups = inventory.groups
|
||||
groupnames = list(groups.values_list('name', flat=True)) + ['all']
|
||||
self.assertEqual(set(data.keys()), set(groupnames))
|
||||
# Groups for this inventory should have hosts, variable data, and one
|
||||
# parent/child relationship.
|
||||
for k,v in data.items():
|
||||
self.assertTrue(isinstance(v, dict))
|
||||
assert isinstance(v, dict)
|
||||
if k == 'all':
|
||||
self.assertEqual(v.get('vars', {}), inventory.variables_dict)
|
||||
continue
|
||||
group = inventory.groups.get(active=True, name=k)
|
||||
hosts = group.hosts.filter(active=True)
|
||||
group = inventory.groups.get(name=k)
|
||||
hosts = group.hosts
|
||||
hostnames = hosts.values_list('name', flat=True)
|
||||
self.assertEqual(set(v.get('hosts', [])), set(hostnames))
|
||||
if group.variables:
|
||||
self.assertEqual(v.get('vars', {}), group.variables_dict)
|
||||
if k == 'group-3':
|
||||
children = group.children.filter(active=True)
|
||||
children = group.children
|
||||
childnames = children.values_list('name', flat=True)
|
||||
self.assertEqual(set(v.get('children', [])), set(childnames))
|
||||
else:
|
||||
self.assertTrue(len(v['children']) == 0)
|
||||
assert len(v['children']) == 0
|
||||
|
||||
def test_list_with_hostvars_inline(self):
|
||||
inventory = self.inventories[1]
|
||||
self.assertTrue(inventory.active)
|
||||
rc, stdout, stderr = self.run_inventory_script(list=True,
|
||||
inventory=inventory.pk,
|
||||
hostvars=True)
|
||||
self.assertEqual(rc, 0, stderr)
|
||||
data = json.loads(stdout)
|
||||
groups = inventory.groups.filter(active=True)
|
||||
groups = inventory.groups
|
||||
groupnames = list(groups.values_list('name', flat=True))
|
||||
groupnames.extend(['all', '_meta'])
|
||||
self.assertEqual(set(data.keys()), set(groupnames))
|
||||
@ -231,28 +235,28 @@ class InventoryScriptTest(BaseScriptTest):
|
||||
# Groups for this inventory should have hosts, variable data, and one
|
||||
# parent/child relationship.
|
||||
for k,v in data.items():
|
||||
self.assertTrue(isinstance(v, dict))
|
||||
assert isinstance(v, dict)
|
||||
if k == 'all':
|
||||
self.assertEqual(v.get('vars', {}), inventory.variables_dict)
|
||||
continue
|
||||
if k == '_meta':
|
||||
continue
|
||||
group = inventory.groups.get(active=True, name=k)
|
||||
hosts = group.hosts.filter(active=True)
|
||||
group = inventory.groups.get(name=k)
|
||||
hosts = group.hosts
|
||||
hostnames = hosts.values_list('name', flat=True)
|
||||
all_hostnames.update(hostnames)
|
||||
self.assertEqual(set(v.get('hosts', [])), set(hostnames))
|
||||
assert set(v.get('hosts', [])) == set(hostnames)
|
||||
if group.variables:
|
||||
self.assertEqual(v.get('vars', {}), group.variables_dict)
|
||||
assert v.get('vars', {}) == group.variables_dict
|
||||
if k == 'group-3':
|
||||
children = group.children.filter(active=True)
|
||||
children = group.children
|
||||
childnames = children.values_list('name', flat=True)
|
||||
self.assertEqual(set(v.get('children', [])), set(childnames))
|
||||
assert set(v.get('children', [])) == set(childnames)
|
||||
else:
|
||||
self.assertTrue(len(v['children']) == 0)
|
||||
assert len(v['children']) == 0
|
||||
# Check hostvars in ['_meta']['hostvars'] dict.
|
||||
for hostname in all_hostnames:
|
||||
self.assertTrue(hostname in data['_meta']['hostvars'])
|
||||
assert hostname in data['_meta']['hostvars']
|
||||
host = inventory.hosts.get(name=hostname)
|
||||
self.assertEqual(data['_meta']['hostvars'][hostname],
|
||||
host.variables_dict)
|
||||
@ -262,13 +266,12 @@ class InventoryScriptTest(BaseScriptTest):
|
||||
inventory=inventory.pk)
|
||||
self.assertEqual(rc, 0, stderr)
|
||||
data = json.loads(stdout)
|
||||
self.assertTrue('_meta' in data)
|
||||
assert '_meta' in data
|
||||
|
||||
def test_valid_host(self):
|
||||
# Host without variable data.
|
||||
inventory = self.inventories[0]
|
||||
self.assertTrue(inventory.active)
|
||||
host = inventory.hosts.filter(active=True)[2]
|
||||
host = inventory.hosts.all()[2]
|
||||
os.environ['INVENTORY_ID'] = str(inventory.pk)
|
||||
rc, stdout, stderr = self.run_inventory_script(host=host.name)
|
||||
self.assertEqual(rc, 0, stderr)
|
||||
@ -276,8 +279,7 @@ class InventoryScriptTest(BaseScriptTest):
|
||||
self.assertEqual(data, {})
|
||||
# Host with variable data.
|
||||
inventory = self.inventories[1]
|
||||
self.assertTrue(inventory.active)
|
||||
host = inventory.hosts.filter(active=True)[4]
|
||||
host = inventory.hosts.all()[4]
|
||||
os.environ['INVENTORY_ID'] = str(inventory.pk)
|
||||
rc, stdout, stderr = self.run_inventory_script(host=host.name)
|
||||
self.assertEqual(rc, 0, stderr)
|
||||
@ -287,8 +289,7 @@ class InventoryScriptTest(BaseScriptTest):
|
||||
def test_invalid_host(self):
|
||||
# Valid host, but not part of the specified inventory.
|
||||
inventory = self.inventories[0]
|
||||
self.assertTrue(inventory.active)
|
||||
host = Host.objects.filter(active=True).exclude(inventory=inventory)[0]
|
||||
host = Host.objects.exclude(inventory=inventory)[0]
|
||||
os.environ['INVENTORY_ID'] = str(inventory.pk)
|
||||
rc, stdout, stderr = self.run_inventory_script(host=host.name)
|
||||
self.assertNotEqual(rc, 0, stderr)
|
||||
@ -320,16 +321,15 @@ class InventoryScriptTest(BaseScriptTest):
|
||||
|
||||
def test_with_deleted_inventory(self):
|
||||
inventory = self.inventories[0]
|
||||
inventory.mark_inactive()
|
||||
self.assertFalse(inventory.active)
|
||||
os.environ['INVENTORY_ID'] = str(inventory.pk)
|
||||
pk = inventory.pk
|
||||
inventory.delete()
|
||||
os.environ['INVENTORY_ID'] = str(pk)
|
||||
rc, stdout, stderr = self.run_inventory_script(list=True)
|
||||
self.assertNotEqual(rc, 0, stderr)
|
||||
self.assertEqual(json.loads(stdout), {'failed': True})
|
||||
|
||||
def test_without_list_or_host_argument(self):
|
||||
inventory = self.inventories[0]
|
||||
self.assertTrue(inventory.active)
|
||||
os.environ['INVENTORY_ID'] = str(inventory.pk)
|
||||
rc, stdout, stderr = self.run_inventory_script()
|
||||
self.assertNotEqual(rc, 0, stderr)
|
||||
@ -337,7 +337,6 @@ class InventoryScriptTest(BaseScriptTest):
|
||||
|
||||
def test_with_both_list_and_host_arguments(self):
|
||||
inventory = self.inventories[0]
|
||||
self.assertTrue(inventory.active)
|
||||
os.environ['INVENTORY_ID'] = str(inventory.pk)
|
||||
rc, stdout, stderr = self.run_inventory_script(list=True, host='blah')
|
||||
self.assertNotEqual(rc, 0, stderr)
|
||||
@ -345,8 +344,7 @@ class InventoryScriptTest(BaseScriptTest):
|
||||
|
||||
def test_with_disabled_hosts(self):
|
||||
inventory = self.inventories[1]
|
||||
self.assertTrue(inventory.active)
|
||||
for host in inventory.hosts.filter(active=True, enabled=True):
|
||||
for host in inventory.hosts.filter(enabled=True):
|
||||
host.enabled = False
|
||||
host.save(update_fields=['enabled'])
|
||||
os.environ['INVENTORY_ID'] = str(inventory.pk)
|
||||
@ -354,49 +352,49 @@ class InventoryScriptTest(BaseScriptTest):
|
||||
rc, stdout, stderr = self.run_inventory_script(list=True)
|
||||
self.assertEqual(rc, 0, stderr)
|
||||
data = json.loads(stdout)
|
||||
groups = inventory.groups.filter(active=True)
|
||||
groups = inventory.groups
|
||||
groupnames = list(groups.values_list('name', flat=True)) + ['all']
|
||||
self.assertEqual(set(data.keys()), set(groupnames))
|
||||
for k,v in data.items():
|
||||
self.assertTrue(isinstance(v, dict))
|
||||
assert isinstance(v, dict)
|
||||
if k == 'all':
|
||||
self.assertEqual(v.get('vars', {}), inventory.variables_dict)
|
||||
continue
|
||||
group = inventory.groups.get(active=True, name=k)
|
||||
hosts = group.hosts.filter(active=True, enabled=True)
|
||||
group = inventory.groups.get(name=k)
|
||||
hosts = group.hosts.filter(enabled=True)
|
||||
hostnames = hosts.values_list('name', flat=True)
|
||||
self.assertEqual(set(v.get('hosts', [])), set(hostnames))
|
||||
self.assertFalse(hostnames)
|
||||
if group.variables:
|
||||
self.assertEqual(v.get('vars', {}), group.variables_dict)
|
||||
if k == 'group-3':
|
||||
children = group.children.filter(active=True)
|
||||
children = group.children
|
||||
childnames = children.values_list('name', flat=True)
|
||||
self.assertEqual(set(v.get('children', [])), set(childnames))
|
||||
else:
|
||||
self.assertTrue(len(v['children']) == 0)
|
||||
assert len(v['children']) == 0
|
||||
# Load inventory list with all hosts.
|
||||
rc, stdout, stderr = self.run_inventory_script(list=True, all=True)
|
||||
self.assertEqual(rc, 0, stderr)
|
||||
data = json.loads(stdout)
|
||||
groups = inventory.groups.filter(active=True)
|
||||
groups = inventory.groups
|
||||
groupnames = list(groups.values_list('name', flat=True)) + ['all']
|
||||
self.assertEqual(set(data.keys()), set(groupnames))
|
||||
for k,v in data.items():
|
||||
self.assertTrue(isinstance(v, dict))
|
||||
assert isinstance(v, dict)
|
||||
if k == 'all':
|
||||
self.assertEqual(v.get('vars', {}), inventory.variables_dict)
|
||||
continue
|
||||
group = inventory.groups.get(active=True, name=k)
|
||||
hosts = group.hosts.filter(active=True)
|
||||
group = inventory.groups.get(name=k)
|
||||
hosts = group.hosts
|
||||
hostnames = hosts.values_list('name', flat=True)
|
||||
self.assertEqual(set(v.get('hosts', [])), set(hostnames))
|
||||
self.assertTrue(hostnames)
|
||||
assert hostnames
|
||||
if group.variables:
|
||||
self.assertEqual(v.get('vars', {}), group.variables_dict)
|
||||
if k == 'group-3':
|
||||
children = group.children.filter(active=True)
|
||||
children = group.children
|
||||
childnames = children.values_list('name', flat=True)
|
||||
self.assertEqual(set(v.get('children', [])), set(childnames))
|
||||
else:
|
||||
self.assertTrue(len(v['children']) == 0)
|
||||
assert len(v['children']) == 0
|
||||
|
||||
@ -592,26 +592,8 @@ class RunJobTest(BaseJobExecutionTest):
|
||||
new_group.children.remove(self.group)
|
||||
new_group = Group.objects.get(pk=new_group.pk)
|
||||
self.assertFalse(new_group.has_active_failures)
|
||||
# Mark host inactive (should clear flag on parent group and inventory)
|
||||
self.host.mark_inactive()
|
||||
self.group = Group.objects.get(pk=self.group.pk)
|
||||
self.assertFalse(self.group.has_active_failures)
|
||||
self.inventory = Inventory.objects.get(pk=self.inventory.pk)
|
||||
self.assertFalse(self.inventory.has_active_failures)
|
||||
# Un-mark host as inactive (need to force update of flag on group and
|
||||
# inventory)
|
||||
host = self.host
|
||||
host.name = '_'.join(host.name.split('_')[3:]) or 'undeleted host'
|
||||
host.active = True
|
||||
host.save()
|
||||
host.update_computed_fields()
|
||||
self.group = Group.objects.get(pk=self.group.pk)
|
||||
self.assertTrue(self.group.has_active_failures)
|
||||
self.inventory = Inventory.objects.get(pk=self.inventory.pk)
|
||||
self.assertTrue(self.inventory.has_active_failures)
|
||||
# Delete host. (should clear flag)
|
||||
# Delete host (should clear flag on parent group and inventory)
|
||||
self.host.delete()
|
||||
self.host = None
|
||||
self.group = Group.objects.get(pk=self.group.pk)
|
||||
self.assertFalse(self.group.has_active_failures)
|
||||
self.inventory = Inventory.objects.get(pk=self.inventory.pk)
|
||||
@ -619,30 +601,7 @@ class RunJobTest(BaseJobExecutionTest):
|
||||
|
||||
def test_update_has_active_failures_when_job_removed(self):
|
||||
job = self.test_run_job_that_fails()
|
||||
# Mark job as inactive (should clear flags).
|
||||
job.mark_inactive()
|
||||
self.host = Host.objects.get(pk=self.host.pk)
|
||||
self.assertFalse(self.host.has_active_failures)
|
||||
self.group = Group.objects.get(pk=self.group.pk)
|
||||
self.assertFalse(self.group.has_active_failures)
|
||||
self.inventory = Inventory.objects.get(pk=self.inventory.pk)
|
||||
self.assertFalse(self.inventory.has_active_failures)
|
||||
# Un-mark job as inactive (need to force update of flag)
|
||||
job.active = True
|
||||
job.save()
|
||||
# Need to manually update last_job on host...
|
||||
host = Host.objects.get(pk=self.host.pk)
|
||||
host.last_job = job
|
||||
host.last_job_host_summary = JobHostSummary.objects.get(job=job, host=host)
|
||||
host.save()
|
||||
self.inventory.update_computed_fields()
|
||||
self.host = Host.objects.get(pk=self.host.pk)
|
||||
self.assertTrue(self.host.has_active_failures)
|
||||
self.group = Group.objects.get(pk=self.group.pk)
|
||||
self.assertTrue(self.group.has_active_failures)
|
||||
self.inventory = Inventory.objects.get(pk=self.inventory.pk)
|
||||
self.assertTrue(self.inventory.has_active_failures)
|
||||
# Delete job entirely.
|
||||
# Delete (should clear flags).
|
||||
job.delete()
|
||||
self.host = Host.objects.get(pk=self.host.pk)
|
||||
self.assertFalse(self.host.has_active_failures)
|
||||
@ -662,8 +621,8 @@ class RunJobTest(BaseJobExecutionTest):
|
||||
self.host = Host.objects.get(pk=self.host.pk)
|
||||
self.assertEqual(self.host.last_job, job1)
|
||||
self.assertEqual(self.host.last_job_host_summary.job, job1)
|
||||
# Mark job1 inactive (should update host.last_job to None).
|
||||
job1.mark_inactive()
|
||||
# Delete job1 (should update host.last_job to None).
|
||||
job1.delete()
|
||||
self.host = Host.objects.get(pk=self.host.pk)
|
||||
self.assertEqual(self.host.last_job, None)
|
||||
self.assertEqual(self.host.last_job_host_summary, None)
|
||||
|
||||
@ -100,7 +100,7 @@ class AuthTokenProxyTest(BaseTest):
|
||||
self.setup_users()
|
||||
self.setup_instances()
|
||||
self.organizations = self.make_organizations(self.super_django_user, 2)
|
||||
self.organizations[0].admins.add(self.normal_django_user)
|
||||
self.organizations[0].admin_role.members.add(self.normal_django_user)
|
||||
|
||||
self.assertIn('REMOTE_ADDR', settings.REMOTE_HOST_HEADERS)
|
||||
self.assertIn('REMOTE_HOST', settings.REMOTE_HOST_HEADERS)
|
||||
@ -174,10 +174,10 @@ class UsersTest(BaseTest):
|
||||
super(UsersTest, self).setUp()
|
||||
self.setup_users()
|
||||
self.organizations = self.make_organizations(self.super_django_user, 2)
|
||||
self.organizations[0].admins.add(self.normal_django_user)
|
||||
self.organizations[0].users.add(self.other_django_user)
|
||||
self.organizations[0].users.add(self.normal_django_user)
|
||||
self.organizations[1].users.add(self.other_django_user)
|
||||
self.organizations[0].admin_role.members.add(self.normal_django_user)
|
||||
self.organizations[0].member_role.members.add(self.other_django_user)
|
||||
self.organizations[0].member_role.members.add(self.normal_django_user)
|
||||
self.organizations[1].member_role.members.add(self.other_django_user)
|
||||
|
||||
def test_user_creation_fails_without_password(self):
|
||||
url = reverse('api:user_list')
|
||||
@ -196,7 +196,7 @@ class UsersTest(BaseTest):
|
||||
self.post(url, expect=201, data=new_user2, auth=self.get_normal_credentials())
|
||||
self.post(url, expect=400, data=new_user2, auth=self.get_normal_credentials())
|
||||
# Normal user cannot add users after his org is marked inactive.
|
||||
self.organizations[0].mark_inactive()
|
||||
self.organizations[0].delete()
|
||||
new_user3 = dict(username='blippy3')
|
||||
self.post(url, expect=403, data=new_user3, auth=self.get_normal_credentials())
|
||||
|
||||
@ -316,7 +316,7 @@ class UsersTest(BaseTest):
|
||||
remote_addr=remote_addr)
|
||||
|
||||
# Token auth should be denied if the user is inactive.
|
||||
self.normal_django_user.mark_inactive()
|
||||
self.normal_django_user.delete()
|
||||
response = self.get(user_me_url, expect=401, auth=auth_token2,
|
||||
remote_addr=remote_addr)
|
||||
self.assertEqual(response['detail'], 'User inactive or deleted')
|
||||
@ -422,7 +422,7 @@ class UsersTest(BaseTest):
|
||||
# Normal user can no longer see all users after the organization he
|
||||
# admins is marked inactive, nor can he see any other users that were
|
||||
# in that org, so he only sees himself.
|
||||
self.organizations[0].mark_inactive()
|
||||
self.organizations[0].delete()
|
||||
data3 = self.get(url, expect=200, auth=self.get_normal_credentials())
|
||||
self.assertEquals(data3['count'], 1)
|
||||
|
||||
@ -790,8 +790,8 @@ class UsersTest(BaseTest):
|
||||
self.check_get_list(url, self.super_django_user, qs)
|
||||
|
||||
# Filter by related organizations admins username.
|
||||
url = '%s?organizations__admins__username__startswith=norm' % base_url
|
||||
qs = base_qs.filter(organizations__admins__username__startswith='norm')
|
||||
url = '%s?organizationsadmin_role__members__username__startswith=norm' % base_url
|
||||
qs = base_qs.filter(organizationsadmin_role__members__username__startswith='norm')
|
||||
self.assertTrue(qs.count())
|
||||
self.check_get_list(url, self.super_django_user, qs)
|
||||
|
||||
@ -839,11 +839,11 @@ class UsersTest(BaseTest):
|
||||
self.check_get_list(url, self.super_django_user, base_qs, expect=400)
|
||||
|
||||
# Filter by invalid field across lookups.
|
||||
url = '%s?organizations__users__teams__laser=green' % base_url
|
||||
url = '%s?organizations__member_role.members__teams__laser=green' % base_url
|
||||
self.check_get_list(url, self.super_django_user, base_qs, expect=400)
|
||||
|
||||
# Filter by invalid relation within lookups.
|
||||
url = '%s?organizations__users__llamas__name=freddie' % base_url
|
||||
url = '%s?organizations__member_role.members__llamas__name=freddie' % base_url
|
||||
self.check_get_list(url, self.super_django_user, base_qs, expect=400)
|
||||
|
||||
# Filter by invalid query string field names.
|
||||
@ -1020,13 +1020,13 @@ class LdapTest(BaseTest):
|
||||
for org_name, org_result in settings.AUTH_LDAP_ORGANIZATION_MAP_RESULT.items():
|
||||
org = Organization.objects.get(name=org_name)
|
||||
if org_result.get('admins', False):
|
||||
self.assertTrue(user in org.admins.all())
|
||||
self.assertTrue(user in org.admin_role.members.all())
|
||||
else:
|
||||
self.assertFalse(user in org.admins.all())
|
||||
self.assertFalse(user in org.admin_role.members.all())
|
||||
if org_result.get('users', False):
|
||||
self.assertTrue(user in org.users.all())
|
||||
self.assertTrue(user in org.member_role.members.all())
|
||||
else:
|
||||
self.assertFalse(user in org.users.all())
|
||||
self.assertFalse(user in org.member_role.members.all())
|
||||
# Try again with different test mapping.
|
||||
self.use_test_setting('ORGANIZATION_MAP', {},
|
||||
from_name='ORGANIZATION_MAP_2')
|
||||
@ -1038,13 +1038,13 @@ class LdapTest(BaseTest):
|
||||
for org_name, org_result in settings.AUTH_LDAP_ORGANIZATION_MAP_RESULT.items():
|
||||
org = Organization.objects.get(name=org_name)
|
||||
if org_result.get('admins', False):
|
||||
self.assertTrue(user in org.admins.all())
|
||||
self.assertTrue(user in org.admin_role.members.all())
|
||||
else:
|
||||
self.assertFalse(user in org.admins.all())
|
||||
self.assertFalse(user in org.admin_role.members.all())
|
||||
if org_result.get('users', False):
|
||||
self.assertTrue(user in org.users.all())
|
||||
self.assertTrue(user in org.member_role.members.all())
|
||||
else:
|
||||
self.assertFalse(user in org.users.all())
|
||||
self.assertFalse(user in org.member_role.members.all())
|
||||
|
||||
def test_ldap_team_mapping(self):
|
||||
for name in ('USER_SEARCH', 'ALWAYS_UPDATE_USER', 'USER_ATTR_MAP',
|
||||
@ -1062,9 +1062,9 @@ class LdapTest(BaseTest):
|
||||
for team_name, team_result in settings.AUTH_LDAP_TEAM_MAP_RESULT.items():
|
||||
team = Team.objects.get(name=team_name)
|
||||
if team_result.get('users', False):
|
||||
self.assertTrue(user in team.users.all())
|
||||
self.assertTrue(user in team.member_role.members.all())
|
||||
else:
|
||||
self.assertFalse(user in team.users.all())
|
||||
self.assertFalse(user in team.member_role.members.all())
|
||||
# Try again with different test mapping.
|
||||
self.use_test_setting('TEAM_MAP', {}, from_name='TEAM_MAP_2')
|
||||
self.use_test_setting('TEAM_MAP_RESULT', {},
|
||||
@ -1075,9 +1075,9 @@ class LdapTest(BaseTest):
|
||||
for team_name, team_result in settings.AUTH_LDAP_TEAM_MAP_RESULT.items():
|
||||
team = Team.objects.get(name=team_name)
|
||||
if team_result.get('users', False):
|
||||
self.assertTrue(user in team.users.all())
|
||||
self.assertTrue(user in team.member_role.members.all())
|
||||
else:
|
||||
self.assertFalse(user in team.users.all())
|
||||
self.assertFalse(user in team.member_role.members.all())
|
||||
|
||||
def test_prevent_changing_ldap_user_fields(self):
|
||||
for name in ('USER_SEARCH', 'ALWAYS_UPDATE_USER', 'USER_ATTR_MAP',
|
||||
|
||||
@ -30,7 +30,7 @@ __all__ = ['get_object_or_400', 'get_object_or_403', 'camelcase_to_underscore',
|
||||
'get_ansible_version', 'get_ssh_version', 'get_awx_version', 'update_scm_url',
|
||||
'get_type_for_model', 'get_model_for_type', 'to_python_boolean',
|
||||
'ignore_inventory_computed_fields', 'ignore_inventory_group_removal',
|
||||
'_inventory_updates', 'get_pk_from_dict']
|
||||
'_inventory_updates', 'get_pk_from_dict', 'getattrd', 'NoDefaultProvided']
|
||||
|
||||
|
||||
def get_object_or_400(klass, *args, **kwargs):
|
||||
@ -524,3 +524,21 @@ def timedelta_total_seconds(timedelta):
|
||||
timedelta.microseconds + 0.0 +
|
||||
(timedelta.seconds + timedelta.days * 24 * 3600) * 10 ** 6) / 10 ** 6
|
||||
|
||||
|
||||
class NoDefaultProvided(object):
|
||||
pass
|
||||
|
||||
def getattrd(obj, name, default=NoDefaultProvided):
|
||||
"""
|
||||
Same as getattr(), but allows dot notation lookup
|
||||
Discussed in:
|
||||
http://stackoverflow.com/questions/11975781
|
||||
"""
|
||||
|
||||
try:
|
||||
return reduce(getattr, name.split("."), obj)
|
||||
except AttributeError:
|
||||
if default != NoDefaultProvided:
|
||||
return default
|
||||
raise
|
||||
|
||||
|
||||
@ -209,7 +209,6 @@ REST_FRAMEWORK = {
|
||||
'awx.api.permissions.ModelAccessPermission',
|
||||
),
|
||||
'DEFAULT_FILTER_BACKENDS': (
|
||||
'awx.api.filters.ActiveOnlyBackend',
|
||||
'awx.api.filters.TypeFilterBackend',
|
||||
'awx.api.filters.FieldLookupBackend',
|
||||
'rest_framework.filters.SearchFilter',
|
||||
|
||||
@ -210,11 +210,11 @@ def on_populate_user(sender, **kwargs):
|
||||
remove = bool(org_opts.get('remove', False))
|
||||
admins_opts = org_opts.get('admins', None)
|
||||
remove_admins = bool(org_opts.get('remove_admins', remove))
|
||||
_update_m2m_from_groups(user, ldap_user, org.admins, admins_opts,
|
||||
_update_m2m_from_groups(user, ldap_user, org.admin_role.members, admins_opts,
|
||||
remove_admins)
|
||||
users_opts = org_opts.get('users', None)
|
||||
remove_users = bool(org_opts.get('remove_users', remove))
|
||||
_update_m2m_from_groups(user, ldap_user, org.users, users_opts,
|
||||
_update_m2m_from_groups(user, ldap_user, org.member_role.members, users_opts,
|
||||
remove_users)
|
||||
|
||||
# Update team membership based on group memberships.
|
||||
@ -226,7 +226,7 @@ def on_populate_user(sender, **kwargs):
|
||||
team, created = Team.objects.get_or_create(name=team_name, organization=org)
|
||||
users_opts = team_opts.get('users', None)
|
||||
remove = bool(team_opts.get('remove', False))
|
||||
_update_m2m_from_groups(user, ldap_user, team.users, users_opts,
|
||||
_update_m2m_from_groups(user, ldap_user, team.member_role.users, users_opts,
|
||||
remove)
|
||||
|
||||
# Update user profile to store LDAP DN.
|
||||
|
||||
@ -90,7 +90,7 @@ def update_user_orgs(backend, details, user=None, *args, **kwargs):
|
||||
org = Organization.objects.get_or_create(name=org_name)[0]
|
||||
else:
|
||||
try:
|
||||
org = Organization.objects.filter(active=True).order_by('pk')[0]
|
||||
org = Organization.objects.order_by('pk')[0]
|
||||
except IndexError:
|
||||
continue
|
||||
|
||||
@ -98,12 +98,12 @@ def update_user_orgs(backend, details, user=None, *args, **kwargs):
|
||||
remove = bool(org_opts.get('remove', False))
|
||||
admins_expr = org_opts.get('admins', None)
|
||||
remove_admins = bool(org_opts.get('remove_admins', remove))
|
||||
_update_m2m_from_expression(user, org.admins, admins_expr, remove_admins)
|
||||
_update_m2m_from_expression(user, org.admin_role.members, admins_expr, remove_admins)
|
||||
|
||||
# Update org users from expression(s).
|
||||
users_expr = org_opts.get('users', None)
|
||||
remove_users = bool(org_opts.get('remove_users', remove))
|
||||
_update_m2m_from_expression(user, org.users, users_expr, remove_users)
|
||||
_update_m2m_from_expression(user, org.member_role.members, users_expr, remove_users)
|
||||
|
||||
|
||||
def update_user_teams(backend, details, user=None, *args, **kwargs):
|
||||
@ -126,7 +126,7 @@ def update_user_teams(backend, details, user=None, *args, **kwargs):
|
||||
org = Organization.objects.get_or_create(name=team_opts['organization'])[0]
|
||||
else:
|
||||
try:
|
||||
org = Organization.objects.filter(active=True).order_by('pk')[0]
|
||||
org = Organization.objects.order_by('pk')[0]
|
||||
except IndexError:
|
||||
continue
|
||||
|
||||
@ -134,4 +134,4 @@ def update_user_teams(backend, details, user=None, *args, **kwargs):
|
||||
team = Team.objects.get_or_create(name=team_name, organization=org)[0]
|
||||
users_expr = team_opts.get('users', None)
|
||||
remove = bool(team_opts.get('remove', False))
|
||||
_update_m2m_from_expression(user, team.users, users_expr, remove)
|
||||
_update_m2m_from_expression(user, team.member_role.members, users_expr, remove)
|
||||
|
||||
@ -634,6 +634,13 @@ dd {
|
||||
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 5px rgba(255, 88, 80, 0.6);
|
||||
}
|
||||
|
||||
.form-control.ng-dirty.ng-invalid + .select2 .select2-selection,
|
||||
.form-control.ng-dirty.ng-invalid + .select2 .select2-selection:focus {
|
||||
border-color: rgba(255, 88, 80, 0.8) !important;
|
||||
outline: 0 !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.form-control.ng-dirty.ng-pristine {
|
||||
border-color: @default-second-border;
|
||||
box-shadow: none;
|
||||
@ -2008,15 +2015,33 @@ tr td button i {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.form-control + .select2 .select2-selection {
|
||||
border-color: @default-second-border !important;
|
||||
background-color: #f6f6f6 !important;
|
||||
color: @default-data-txt !important;
|
||||
transition: border-color 0.3s !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.form-control:active, .form-control:focus {
|
||||
box-shadow: none;
|
||||
border-color: #167ec4;
|
||||
}
|
||||
|
||||
.form-control:active + .select2 .select2-selection, .form-control:focus + .select2 .select2-selection {
|
||||
box-shadow: none !important;
|
||||
border-color: #167ec4 !important;
|
||||
}
|
||||
|
||||
.form-control.ng-dirty.ng-invalid, .form-control.ng-dirty.ng-invalid:focus {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.form-control.ng-dirty.ng-invalid + .select2 .select2-selection, .form-control.ng-dirty.ng-invalid:focus + .select2 .select2-selection {
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
|
||||
.error {
|
||||
opacity: 1;
|
||||
transition: opacity 0.2s;
|
||||
@ -2041,3 +2066,7 @@ tr td button i {
|
||||
.select2-container--disabled {
|
||||
opacity: .35;
|
||||
}
|
||||
|
||||
body.is-modalOpen {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@ -41,6 +41,9 @@ table, tbody {
|
||||
|
||||
.List-tableHeader:last-of-type {
|
||||
border-top-right-radius: 5px;
|
||||
}
|
||||
|
||||
.List-tableHeader--actions {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
@ -320,6 +323,11 @@ table, tbody {
|
||||
height: 34px;
|
||||
}
|
||||
|
||||
.List-searchWidget--compact {
|
||||
max-width: ~"calc(100% - 91px)";
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.List-searchRow {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
@ -0,0 +1,212 @@
|
||||
@import "../../shared/branding/colors.default.less";
|
||||
|
||||
/** @define AddPermissions */
|
||||
|
||||
.AddPermissions-backDrop {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 1041;
|
||||
opacity: 0.2;
|
||||
transition: 0.5s opacity;
|
||||
background: @login-backdrop;
|
||||
}
|
||||
|
||||
.AddPermissions-dialog {
|
||||
margin: 30px auto;
|
||||
margin-top: 95px;
|
||||
}
|
||||
|
||||
.AddPermissions-content {
|
||||
max-width: 750px;
|
||||
margin: 0 auto;
|
||||
border: 0;
|
||||
box-shadow: none;
|
||||
background-color: @login-bg;
|
||||
border-radius: 4px;
|
||||
transition: opacity 0.5s;
|
||||
z-index: 1042;
|
||||
position: relative;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.AddPermissions-header {
|
||||
padding: 20px;
|
||||
padding-bottom: 10px;
|
||||
padding-top: 15px;
|
||||
}
|
||||
|
||||
.AddPermissions-body {
|
||||
padding: 0px 20px;
|
||||
}
|
||||
|
||||
.AddPermissions-footer {
|
||||
display: flex;
|
||||
flex-wrap: wrap-reverse;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
padding-bottom: 0px;
|
||||
padding-top: 20px;
|
||||
}
|
||||
|
||||
.AddPermissions-list .List-searchRow {
|
||||
height: 0px;
|
||||
}
|
||||
|
||||
.AddPermissions-list .List-searchWidget {
|
||||
height: 66px;
|
||||
}
|
||||
|
||||
.AddPermissions-list .List-tableHeader:last-child {
|
||||
border-top-right-radius: 5px;
|
||||
}
|
||||
|
||||
.AddPermissions-list select-all {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.AddPermissions-title {
|
||||
margin-top: 5px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.AddPermissions-buttons {
|
||||
margin-left: auto;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.AddPermissions-directions {
|
||||
margin-top: 10px;
|
||||
margin-bottom: 20px;
|
||||
color: #848992;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.AddPermissions-directionNumber {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
border-radius: 50%;
|
||||
background-color: @default-list-header-bg;
|
||||
padding: 2px 6px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.AddPermissions-separator {
|
||||
margin-top: 20px 0px;
|
||||
width: 100%;
|
||||
border-bottom: 1px solid @default-second-border;
|
||||
}
|
||||
|
||||
.AddPermissions-roleRow {
|
||||
display: flex;
|
||||
margin-bottom: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.AddPermissions-roleName {
|
||||
width: 30%;
|
||||
padding-right: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.AddPermissions-roleNameVal {
|
||||
font-size: 14px;
|
||||
max-width: ~"calc(100% - 46px)";
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.AddPermissions-roleType {
|
||||
border-radius: 5px;
|
||||
padding: 0px 6px;
|
||||
border: 1px solid @default-second-border;
|
||||
font-size: 10px;
|
||||
color: @default-interface-txt;
|
||||
text-transform: uppercase;
|
||||
background-color: @default-bg;
|
||||
margin-left: 6px;
|
||||
}
|
||||
|
||||
.AddPermissions-roleSelect {
|
||||
width: ~"calc(70% - 40px)";
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
.AddPermissions-roleSelect .Form-dropDown {
|
||||
height: inherit !important;
|
||||
}
|
||||
|
||||
.AddPermissions-roleRemove {
|
||||
border-radius: 50%;
|
||||
padding: 5px 3px;
|
||||
line-height: 11px;
|
||||
color: @default-icon;
|
||||
background-color: @default-tertiary-bg;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.AddPermissions-roleRemove:hover {
|
||||
background-color: @default-err;
|
||||
color: @default-bg;
|
||||
}
|
||||
|
||||
.AddPermissions-selectHide {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.AddPermissions .select2-search__field {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.AddPermissions-keyToggle {
|
||||
margin-left: auto;
|
||||
text-transform: uppercase;
|
||||
padding: 3px 9px;
|
||||
font-size: 12px;
|
||||
background-color: @default-bg;
|
||||
border-radius: 5px;
|
||||
color: @default-interface-txt;
|
||||
border: 1px solid @default-second-border;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.AddPermissions-keyToggle:hover {
|
||||
background-color: @default-tertiary-bg;
|
||||
}
|
||||
|
||||
.AddPermissions-keyToggle.is-active {
|
||||
background-color: @default-link;
|
||||
border-color: @default-link;
|
||||
color: @default-bg;
|
||||
}
|
||||
|
||||
.AddPermissions-keyPane {
|
||||
margin: 20px 0;
|
||||
border-radius: 5px;
|
||||
padding: 15px;
|
||||
padding-bottom: 0px;
|
||||
border: 1px solid @default-second-border;
|
||||
color: @default-interface-txt;
|
||||
}
|
||||
|
||||
.AddPermissions-keyRow {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.AddPermissions-keyName {
|
||||
flex: 1 0 auto;
|
||||
text-transform: uppercase;
|
||||
font-weight: bold;
|
||||
padding-bottom: 3px;
|
||||
}
|
||||
|
||||
.AddPermissions-keyDescription {
|
||||
flex: 1 0 auto;
|
||||
}
|
||||
@ -0,0 +1,177 @@
|
||||
/*************************************************
|
||||
* Copyright (c) 2015 Ansible, Inc.
|
||||
*
|
||||
* All Rights Reserved
|
||||
*************************************************/
|
||||
|
||||
/**
|
||||
* @ngdoc function
|
||||
* @name controllers.function:Access
|
||||
* @description
|
||||
* Controller for handling permissions adding
|
||||
*/
|
||||
|
||||
export default ['$rootScope', '$scope', 'GetBasePath', 'Rest', '$q', 'Wait', 'ProcessErrors', function (rootScope, scope, GetBasePath, Rest, $q, Wait, ProcessErrors) {
|
||||
var manuallyUpdateChecklists = function(list, id, isSelected) {
|
||||
var elemScope = angular
|
||||
.element("#" +
|
||||
list + "s_table #" + id + ".List-tableRow input")
|
||||
.scope();
|
||||
if (elemScope) {
|
||||
elemScope.isSelected = !!isSelected;
|
||||
}
|
||||
};
|
||||
|
||||
scope.allSelected = [];
|
||||
|
||||
// the object permissions are being added to
|
||||
scope.object = scope[scope.$parent.list
|
||||
.iterator + "_obj"];
|
||||
|
||||
// array for all possible roles for the object
|
||||
scope.roles = Object
|
||||
.keys(scope.object.summary_fields.roles)
|
||||
.map(function(key) {
|
||||
return {
|
||||
value: scope.object.summary_fields
|
||||
.roles[key].id,
|
||||
label: scope.object.summary_fields
|
||||
.roles[key].name };
|
||||
});
|
||||
|
||||
// TODO: get working with api
|
||||
// array w roles and descriptions for key
|
||||
scope.roleKey = Object
|
||||
.keys(scope.object.summary_fields.roles)
|
||||
.map(function(key) {
|
||||
return {
|
||||
name: scope.object.summary_fields
|
||||
.roles[key].name,
|
||||
description: scope.object.summary_fields
|
||||
.roles[key].description };
|
||||
});
|
||||
|
||||
scope.showKeyPane = false;
|
||||
|
||||
scope.toggleKeyPane = function() {
|
||||
scope.showKeyPane = !scope.showKeyPane;
|
||||
};
|
||||
|
||||
// handle form tab changes
|
||||
scope.toggleFormTabs = function(list) {
|
||||
scope.usersSelected = (list === 'users');
|
||||
scope.teamsSelected = !scope.usersSelected;
|
||||
};
|
||||
|
||||
// manually handle selection/deselection of user/team checkboxes
|
||||
scope.$on("selectedOrDeselected", function(e, val) {
|
||||
val = val.value;
|
||||
if (val.isSelected) {
|
||||
// deselected, so remove from the allSelected list
|
||||
scope.allSelected = scope.allSelected.filter(function(i) {
|
||||
// return all but the object who has the id and type
|
||||
// of the element to deselect
|
||||
return (!(val.id === i.id && val.type === i.type));
|
||||
});
|
||||
} else {
|
||||
// selected, so add to the allSelected list
|
||||
scope.allSelected.push({
|
||||
name: function() {
|
||||
if (val.type === "user") {
|
||||
return (val.first_name &&
|
||||
val.last_name) ?
|
||||
val.first_name + " " +
|
||||
val.last_name :
|
||||
val.username;
|
||||
} else {
|
||||
return val .name;
|
||||
}
|
||||
},
|
||||
type: val.type,
|
||||
roles: [],
|
||||
id: val.id
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// used to handle changes to the itemsSelected scope var on "next page",
|
||||
// "sorting etc."
|
||||
scope.$on("itemsSelected", function(e, inList) {
|
||||
// compile a list of objects that needed to be checked in the lists
|
||||
scope.updateLists = scope.allSelected.filter(function(inMemory) {
|
||||
var notInList = true;
|
||||
inList.forEach(function(val) {
|
||||
// if the object is part of the allSelected list and is
|
||||
// selected,
|
||||
// you don't need to add it updateLists
|
||||
if (inMemory.id === val.id &&
|
||||
inMemory.type === val.type) {
|
||||
notInList = false;
|
||||
}
|
||||
});
|
||||
return notInList;
|
||||
});
|
||||
});
|
||||
|
||||
// handle changes to the updatedLists by manually selected those values in
|
||||
// the UI
|
||||
scope.$watch("updateLists", function(toUpdate) {
|
||||
(toUpdate || []).forEach(function(obj) {
|
||||
manuallyUpdateChecklists(obj.type, obj.id, true);
|
||||
});
|
||||
|
||||
delete scope.updateLists;
|
||||
});
|
||||
|
||||
// remove selected user/team
|
||||
scope.removeObject = function(obj) {
|
||||
manuallyUpdateChecklists(obj.type, obj.id, false);
|
||||
|
||||
scope.allSelected = scope.allSelected.filter(function(i) {
|
||||
return (!(obj.id === i.id && obj.type === i.type));
|
||||
});
|
||||
};
|
||||
|
||||
// update post url list
|
||||
scope.$watch("allSelected", function(val) {
|
||||
scope.posts = _
|
||||
.flatten((val || [])
|
||||
.map(function (owner) {
|
||||
var url = GetBasePath(owner.type + "s") + owner.id +
|
||||
"/roles/";
|
||||
|
||||
return (owner.roles || [])
|
||||
.map(function (role) {
|
||||
return {url: url,
|
||||
id: role.value};
|
||||
});
|
||||
}));
|
||||
}, true);
|
||||
|
||||
// post roles to api
|
||||
scope.updatePermissions = function() {
|
||||
Wait('start');
|
||||
|
||||
var requests = scope.posts
|
||||
.map(function(post) {
|
||||
Rest.setUrl(post.url);
|
||||
return Rest.post({"id": post.id});
|
||||
});
|
||||
|
||||
$q.all(requests)
|
||||
.then(function () {
|
||||
Wait('stop');
|
||||
rootScope.$broadcast("refreshList", "permission");
|
||||
scope.closeModal();
|
||||
}, function (error) {
|
||||
Wait('stop');
|
||||
rootScope.$broadcast("refreshList", "permission");
|
||||
scope.closeModal();
|
||||
ProcessErrors(null, error.data, error.status, null, {
|
||||
hdr: 'Error!',
|
||||
msg: 'Failed to post role(s): POST returned status' +
|
||||
error.status
|
||||
});
|
||||
});
|
||||
};
|
||||
}];
|
||||
@ -0,0 +1,58 @@
|
||||
/*************************************************
|
||||
* Copyright (c) 2015 Ansible, Inc.
|
||||
*
|
||||
* All Rights Reserved
|
||||
*************************************************/
|
||||
import addPermissionsController from './addPermissions.controller';
|
||||
|
||||
/* jshint unused: vars */
|
||||
export default
|
||||
[ 'templateUrl',
|
||||
'Wait',
|
||||
function(templateUrl, Wait) {
|
||||
return {
|
||||
restrict: 'E',
|
||||
scope: true,
|
||||
controller: addPermissionsController,
|
||||
templateUrl: templateUrl('access/addPermissions/addPermissions'),
|
||||
link: function(scope, element, attrs, ctrl) {
|
||||
scope.toggleFormTabs('users');
|
||||
|
||||
$("body").addClass("is-modalOpen");
|
||||
|
||||
$("body").append(element);
|
||||
|
||||
Wait('start');
|
||||
|
||||
scope.$broadcast("linkLists");
|
||||
|
||||
setTimeout(function() {
|
||||
$('#add-permissions-modal').modal("show");
|
||||
}, 200);
|
||||
|
||||
$('.modal[aria-hidden=false]').each(function () {
|
||||
if ($(this).attr('id') !== 'add-permissions-modal') {
|
||||
$(this).modal('hide');
|
||||
}
|
||||
});
|
||||
|
||||
scope.closeModal = function() {
|
||||
$("body").removeClass("is-modalOpen");
|
||||
$('#add-permissions-modal').on('hidden.bs.modal',
|
||||
function () {
|
||||
$('.AddPermissions').remove();
|
||||
});
|
||||
$('#add-permissions-modal').modal('hide');
|
||||
};
|
||||
|
||||
scope.$on('closePermissionsModal', function() {
|
||||
scope.closeModal();
|
||||
});
|
||||
|
||||
Wait('stop');
|
||||
|
||||
window.scrollTo(0,0);
|
||||
}
|
||||
};
|
||||
}
|
||||
];
|
||||
@ -0,0 +1,118 @@
|
||||
<div id="add-permissions-modal" class="AddPermissions modal fade">
|
||||
<div class="AddPermissions-backDrop is-loggedOut"></div>
|
||||
<div class="AddPermissions-dialog">
|
||||
<div class="AddPermissions-content is-loggedOut">
|
||||
<div class="AddPermissions-header">
|
||||
<div class="List-header">
|
||||
<div class="List-title">
|
||||
<div class="List-titleText ng-binding">
|
||||
{{ object.name }}
|
||||
<div class="List-titleLockup"></div>
|
||||
Add Permissions
|
||||
</div>
|
||||
</div>
|
||||
<div class="Form-exitHolder">
|
||||
<button class="Form-exit" ng-click="closeModal()">
|
||||
<i class="fa fa-times-circle"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="AddPermissions-body">
|
||||
<div class="AddPermissions-directions">
|
||||
<span class="AddPermissions-directionNumber">
|
||||
1.
|
||||
</span>
|
||||
Please select Users / Teams from the lists below.
|
||||
</div>
|
||||
|
||||
<div class="Form-tabHolder">
|
||||
<div id="users_tab" class="Form-tab"
|
||||
ng-click="toggleFormTabs('users')"
|
||||
ng-class="{'is-selected': usersSelected }">
|
||||
Users
|
||||
</div>
|
||||
<div id="teams_tab" class="Form-tab"
|
||||
ng-click="toggleFormTabs('teams')"
|
||||
ng-class="{'is-selected': teamsSelected }"
|
||||
>
|
||||
Teams
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="AddPermissions-list" ng-show="usersSelected">
|
||||
<add-permissions-list type="users">
|
||||
</add-permissions-list>
|
||||
</div>
|
||||
<div class="AddPermissions-list" ng-show="teamsSelected">
|
||||
<add-permissions-list type="teams">
|
||||
</add-permissions-list>
|
||||
</div>
|
||||
|
||||
<div class="AddPermissions-separator"
|
||||
ng-show="allSelected && allSelected.length > 0"></div>
|
||||
|
||||
<div class="AddPermissions-directions"
|
||||
ng-show="allSelected && allSelected.length > 0">
|
||||
<span class="AddPermissions-directionNumber">
|
||||
2.
|
||||
</span>
|
||||
Please assign roles to the selected users/teams
|
||||
<div class="AddPermissions-keyToggle"
|
||||
ng-class="{'is-active': showKeyPane}"
|
||||
ng-click="toggleKeyPane()">
|
||||
Key
|
||||
</div>
|
||||
</div>
|
||||
<div class="AddPermissions-keyPane"
|
||||
ng-show="showKeyPane">
|
||||
<div class="AddPermissions-keyRow"
|
||||
ng-repeat="key in roleKey">
|
||||
<div class="AddPermissions-keyName">
|
||||
{{ key.name }}
|
||||
</div>
|
||||
<div class="AddPermissions-keyDescription">
|
||||
{{ key.description || "No description provided" }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<form name="userForm" novalidate>
|
||||
<ng-form name="userRoleForm">
|
||||
<div class="AddPermissions-roleRow"
|
||||
ng-repeat="obj in allSelected">
|
||||
<div class="AddPermissions-roleName">
|
||||
<span class="AddPermissions-roleNameVal">
|
||||
{{ obj.name }}
|
||||
</span>
|
||||
<span class="AddPermissions-roleType">
|
||||
{{ obj.type }}
|
||||
</span>
|
||||
</div>
|
||||
<role-select class="AddPermissions-roleSelect">
|
||||
</role-select>
|
||||
<button class="AddPermissions-roleRemove"
|
||||
ng-click="removeObject(obj)">
|
||||
<i class="fa fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
</ng-form>
|
||||
</form>
|
||||
</div>
|
||||
<div class="AddPermissions-footer">
|
||||
<div class="buttons Form-buttons AddPermissions-buttons">
|
||||
<button type="button"
|
||||
class="btn btn-sm Form-saveButton"
|
||||
ng-click="updatePermissions()"
|
||||
ng-disabled="userRoleForm.$invalid || !allSelected || !allSelected.length">
|
||||
Save
|
||||
</button>
|
||||
<button type="button"
|
||||
class="btn btn-sm Form-cancelButton"
|
||||
ng-click="closeModal()">
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -0,0 +1,58 @@
|
||||
/*************************************************
|
||||
* Copyright (c) 2015 Ansible, Inc.
|
||||
*
|
||||
* All Rights Reserved
|
||||
*************************************************/
|
||||
|
||||
/* jshint unused: vars */
|
||||
export default
|
||||
['addPermissionsTeamsList', 'addPermissionsUsersList', 'generateList', 'GetBasePath', 'SelectionInit', 'SearchInit',
|
||||
'PaginateInit', function(addPermissionsTeamsList,
|
||||
addPermissionsUsersList, generateList,
|
||||
GetBasePath, SelectionInit, SearchInit, PaginateInit) {
|
||||
return {
|
||||
restrict: 'E',
|
||||
scope: {
|
||||
},
|
||||
template: "<div class='addPermissionsList-inner'></div>",
|
||||
link: function(scope, element, attrs, ctrl) {
|
||||
scope.$on("linkLists", function(e) {
|
||||
var generator = generateList,
|
||||
list = addPermissionsTeamsList,
|
||||
url = GetBasePath("teams"),
|
||||
set = "teams",
|
||||
id = "addPermissionsTeamsList",
|
||||
mode = "edit";
|
||||
|
||||
if (attrs.type === 'users') {
|
||||
list = addPermissionsUsersList;
|
||||
url = GetBasePath("users") + "?is_superuser=false";
|
||||
set = "users";
|
||||
id = "addPermissionsUsersList";
|
||||
mode = "edit";
|
||||
}
|
||||
|
||||
scope.id = id;
|
||||
|
||||
scope.$watch("selectedItems", function() {
|
||||
scope.$emit("itemsSelected", scope.selectedItems);
|
||||
});
|
||||
|
||||
element.find(".addPermissionsList-inner")
|
||||
.attr("id", id);
|
||||
|
||||
generator.inject(list, { id: id,
|
||||
title: false, mode: mode, scope: scope });
|
||||
|
||||
SearchInit({ scope: scope, set: set,
|
||||
list: list, url: url });
|
||||
|
||||
PaginateInit({ scope: scope,
|
||||
list: list, url: url, pageSize: 5 });
|
||||
|
||||
scope.search(list.iterator);
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
];
|
||||
@ -0,0 +1,15 @@
|
||||
/*************************************************
|
||||
* Copyright (c) 2015 Ansible, Inc.
|
||||
*
|
||||
* All Rights Reserved
|
||||
*************************************************/
|
||||
|
||||
import addPermissionsListDirective from './addPermissionsList.directive';
|
||||
import teamsList from './permissionsTeams.list';
|
||||
import usersList from './permissionsUsers.list';
|
||||
|
||||
export default
|
||||
angular.module('addPermissionsListModule', [])
|
||||
.directive('addPermissionsList', addPermissionsListDirective)
|
||||
.factory('addPermissionsTeamsList', teamsList)
|
||||
.factory('addPermissionsUsersList', usersList);
|
||||
@ -0,0 +1,27 @@
|
||||
/*************************************************
|
||||
* Copyright (c) 2015 Ansible, Inc.
|
||||
*
|
||||
* All Rights Reserved
|
||||
*************************************************/
|
||||
|
||||
|
||||
export default function() {
|
||||
return {
|
||||
|
||||
name: 'teams',
|
||||
iterator: 'team',
|
||||
listTitleBadge: false,
|
||||
multiSelect: true,
|
||||
multiSelectExtended: true,
|
||||
index: false,
|
||||
hover: true,
|
||||
|
||||
fields: {
|
||||
name: {
|
||||
key: true,
|
||||
label: 'name'
|
||||
},
|
||||
},
|
||||
|
||||
};
|
||||
}
|
||||
@ -0,0 +1,37 @@
|
||||
/*************************************************
|
||||
* Copyright (c) 2015 Ansible, Inc.
|
||||
*
|
||||
* All Rights Reserved
|
||||
*************************************************/
|
||||
|
||||
|
||||
export default function() {
|
||||
return {
|
||||
|
||||
name: 'users',
|
||||
iterator: 'user',
|
||||
title: false,
|
||||
listTitleBadge: false,
|
||||
multiSelect: true,
|
||||
multiSelectExtended: true,
|
||||
index: false,
|
||||
hover: true,
|
||||
|
||||
fields: {
|
||||
first_name: {
|
||||
label: 'First Name',
|
||||
columnClass: 'col-md-3 col-sm-3 hidden-xs'
|
||||
},
|
||||
last_name: {
|
||||
label: 'Last Name',
|
||||
columnClass: 'col-md-3 col-sm-3 hidden-xs'
|
||||
},
|
||||
username: {
|
||||
key: true,
|
||||
label: 'Username',
|
||||
columnClass: 'col-md-3 col-sm-3 col-xs-9'
|
||||
},
|
||||
},
|
||||
|
||||
};
|
||||
}
|
||||
14
awx/ui/client/src/access/addPermissions/main.js
Normal file
14
awx/ui/client/src/access/addPermissions/main.js
Normal file
@ -0,0 +1,14 @@
|
||||
/*************************************************
|
||||
* Copyright (c) 2015 Ansible, Inc.
|
||||
*
|
||||
* All Rights Reserved
|
||||
*************************************************/
|
||||
|
||||
import addPermissionsDirective from './addPermissions.directive';
|
||||
import roleSelect from './roleSelect.directive';
|
||||
import addPermissionsList from './addPermissionsList/main';
|
||||
|
||||
export default
|
||||
angular.module('AddPermissions', [addPermissionsList.name])
|
||||
.directive('addPermissions', addPermissionsDirective)
|
||||
.directive('roleSelect', roleSelect);
|
||||
@ -0,0 +1,25 @@
|
||||
/*************************************************
|
||||
* Copyright (c) 2015 Ansible, Inc.
|
||||
*
|
||||
* All Rights Reserved
|
||||
*************************************************/
|
||||
|
||||
/* jshint unused: vars */
|
||||
export default
|
||||
[
|
||||
'CreateSelect2',
|
||||
function(CreateSelect2) {
|
||||
return {
|
||||
restrict: 'E',
|
||||
scope: false,
|
||||
template: '<select ng-cloak class="AddPermissions-selectHide roleSelect2 form-control" ng-model="obj.roles" ng-options="role.label for role in roles track by role.value" multiple required></select>',
|
||||
link: function(scope, element, attrs, ctrl) {
|
||||
CreateSelect2({
|
||||
element: '.roleSelect2',
|
||||
multiple: true,
|
||||
placeholder: 'Select roles'
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
];
|
||||
12
awx/ui/client/src/access/main.js
Normal file
12
awx/ui/client/src/access/main.js
Normal file
@ -0,0 +1,12 @@
|
||||
/*************************************************
|
||||
* Copyright (c) 2015 Ansible, Inc.
|
||||
*
|
||||
* All Rights Reserved
|
||||
*************************************************/
|
||||
|
||||
import roleList from './roleList.directive';
|
||||
import addPermissions from './addPermissions/main';
|
||||
|
||||
export default
|
||||
angular.module('access', [addPermissions.name])
|
||||
.directive('roleList', roleList);
|
||||
72
awx/ui/client/src/access/roleList.block.less
Normal file
72
awx/ui/client/src/access/roleList.block.less
Normal file
@ -0,0 +1,72 @@
|
||||
/** @define RoleList */
|
||||
@import "../shared/branding/colors.default.less";
|
||||
|
||||
.RoleList {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.RoleList-tagContainer {
|
||||
display: flex;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.RoleList-tag {
|
||||
border-radius: 5px;
|
||||
padding: 2px 10px;
|
||||
margin: 4px 0px;
|
||||
border: 1px solid @default-second-border;
|
||||
font-size: 12px;
|
||||
color: @default-interface-txt;
|
||||
text-transform: uppercase;
|
||||
background-color: @default-bg;
|
||||
margin-right: 5px;
|
||||
max-width: 100%;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.RoleList-tag--deletable {
|
||||
margin-right: 0px;
|
||||
border-top-right-radius: 0px;
|
||||
border-bottom-right-radius: 0px;
|
||||
border-right: 0;
|
||||
max-wdith: ~"calc(100% - 23px)";
|
||||
}
|
||||
|
||||
.RoleList-deleteContainer {
|
||||
border: 1px solid @default-second-border;
|
||||
border-top-right-radius: 5px;
|
||||
border-bottom-right-radius: 5px;
|
||||
padding: 0 5px;
|
||||
margin: 4px 0px;
|
||||
margin-right: 5px;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.RoleList-tagDelete {
|
||||
font-size: 13px;
|
||||
color: @default-icon;
|
||||
}
|
||||
|
||||
.RoleList-name {
|
||||
flex: initial;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.RoleList-tag--deletable > .RoleList-name {
|
||||
max-width: ~"calc(100% - 23px)";
|
||||
}
|
||||
|
||||
.RoleList-deleteContainer:hover, {
|
||||
border-color: @default-err;
|
||||
background-color: @default-err;
|
||||
}
|
||||
|
||||
.RoleList-deleteContainer:hover > .RoleList-tagDelete {
|
||||
color: @default-bg;
|
||||
}
|
||||
44
awx/ui/client/src/access/roleList.directive.js
Normal file
44
awx/ui/client/src/access/roleList.directive.js
Normal file
@ -0,0 +1,44 @@
|
||||
/* jshint unused: vars */
|
||||
export default
|
||||
[ 'templateUrl',
|
||||
function(templateUrl) {
|
||||
return {
|
||||
restrict: 'E',
|
||||
scope: false,
|
||||
templateUrl: templateUrl('access/roleList'),
|
||||
link: function(scope, element, attrs) {
|
||||
// given a list of roles (things like "project
|
||||
// auditor") which are pulled from two different
|
||||
// places in summary fields, and creates a
|
||||
// concatenated/sorted list
|
||||
scope.roles = []
|
||||
.concat(scope.permission.summary_fields
|
||||
.direct_access.map(function(i) {
|
||||
return {
|
||||
name: i.role.name,
|
||||
roleId: i.role.id,
|
||||
resourceName: i.role.resource_name,
|
||||
explicit: true
|
||||
};
|
||||
}))
|
||||
.concat(scope.permission.summary_fields
|
||||
.indirect_access.map(function(i) {
|
||||
return {
|
||||
name: i.role.name,
|
||||
roleId: i.role.id,
|
||||
explicit: false
|
||||
};
|
||||
}))
|
||||
.sort(function(a, b) {
|
||||
if (a.name
|
||||
.toLowerCase() > b.name
|
||||
.toLowerCase()) {
|
||||
return 1;
|
||||
} else {
|
||||
return -1;
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
];
|
||||
13
awx/ui/client/src/access/roleList.partial.html
Normal file
13
awx/ui/client/src/access/roleList.partial.html
Normal file
@ -0,0 +1,13 @@
|
||||
<div class="RoleList-tagContainer"
|
||||
ng-repeat="role in roles">
|
||||
<div class="RoleList-tag"
|
||||
ng-class="{'RoleList-tag--deletable': role.explicit}">
|
||||
<span class="RoleList-name">{{ role.name }}</span>
|
||||
</div>
|
||||
<div class="RoleList-deleteContainer"
|
||||
ng-if="role.explicit"
|
||||
ng-click="deletePermission(permission.id, role.roleId, permission.username, role.name, role.resourceName)">
|
||||
<i ng-if="role.explicit"
|
||||
class="fa fa-times RoleList-tagDelete"></i>
|
||||
</div>
|
||||
</div>
|
||||
@ -32,6 +32,7 @@ import permissions from './permissions/main';
|
||||
import managementJobs from './management-jobs/main';
|
||||
import jobDetail from './job-detail/main';
|
||||
import notifications from './notifications/main';
|
||||
import access from './access/main';
|
||||
|
||||
// modules
|
||||
import about from './about/main';
|
||||
@ -104,6 +105,7 @@ var tower = angular.module('Tower', [
|
||||
jobDetail.name,
|
||||
notifications.name,
|
||||
standardOut.name,
|
||||
access.name,
|
||||
JobTemplates.name,
|
||||
'templates',
|
||||
'Utilities',
|
||||
@ -765,16 +767,42 @@ var tower = angular.module('Tower', [
|
||||
}]);
|
||||
}])
|
||||
|
||||
.run(['$q', '$compile', '$cookieStore', '$rootScope', '$log', '$state', 'CheckLicense',
|
||||
'$location', 'Authorization', 'LoadBasePaths', 'Timer', 'ClearScope', 'Socket',
|
||||
'LoadConfig', 'Store', 'ShowSocketHelp', 'pendoService',
|
||||
function (
|
||||
$q, $compile, $cookieStore, $rootScope, $log, $state, CheckLicense,
|
||||
$location, Authorization, LoadBasePaths, Timer, ClearScope, Socket,
|
||||
LoadConfig, Store, ShowSocketHelp, pendoService)
|
||||
{
|
||||
.run(['$q', '$compile', '$cookieStore', '$rootScope', '$log', 'CheckLicense', '$location', 'Authorization', 'LoadBasePaths', 'Timer', 'ClearScope', 'Socket',
|
||||
'LoadConfig', 'Store', 'ShowSocketHelp', 'pendoService', 'Prompt', 'Rest', 'Wait', 'ProcessErrors', '$state', 'GetBasePath',
|
||||
function ($q, $compile, $cookieStore, $rootScope, $log, CheckLicense, $location, Authorization, LoadBasePaths, Timer, ClearScope, Socket,
|
||||
LoadConfig, Store, ShowSocketHelp, pendoService, Prompt, Rest, Wait, ProcessErrors, $state, GetBasePath) {
|
||||
var sock;
|
||||
|
||||
$rootScope.addPermission = function (scope) {
|
||||
$compile("<add-permissions class='AddPermissions'></add-permissions>")(scope);
|
||||
}
|
||||
|
||||
$rootScope.deletePermission = function (user, role, userName,
|
||||
roleName, resourceName) {
|
||||
var action = function () {
|
||||
$('#prompt-modal').modal('hide');
|
||||
Wait('start');
|
||||
var url = GetBasePath("users") + user + "/roles/";
|
||||
Rest.setUrl(url);
|
||||
Rest.post({"disassociate": true, "id": role})
|
||||
.success(function () {
|
||||
Wait('stop');
|
||||
$rootScope.$broadcast("refreshList", "permission");
|
||||
})
|
||||
.error(function (data, status) {
|
||||
ProcessErrors($rootScope, data, status, null, { hdr: 'Error!',
|
||||
msg: 'Could not disacssociate user from role. Call to ' + url + ' failed. DELETE returned status: ' + status });
|
||||
});
|
||||
};
|
||||
|
||||
Prompt({
|
||||
hdr: 'Remove Role from ' + resourceName,
|
||||
body: '<div class="Prompt-bodyQuery">Confirm the removal of the <span class="Prompt-emphasis">' + roleName + '</span> role associated with ' + userName + '.</div>',
|
||||
action: action,
|
||||
actionText: 'REMOVE'
|
||||
});
|
||||
};
|
||||
|
||||
function activateTab() {
|
||||
// Make the correct tab active
|
||||
var base = $location.path().replace(/^\//, '').split('/')[0];
|
||||
@ -908,6 +936,7 @@ var tower = angular.module('Tower', [
|
||||
|
||||
|
||||
$rootScope.$on("$stateChangeStart", function (event, next, nextParams, prev) {
|
||||
$rootScope.$broadcast("closePermissionsModal");
|
||||
// this line removes the query params attached to a route
|
||||
if(prev && prev.$$route &&
|
||||
prev.$$route.name === 'systemTracking'){
|
||||
|
||||
@ -649,7 +649,6 @@ export function ProjectsEdit($scope, $rootScope, $compile, $location, $log,
|
||||
data.summary_fields[form.fields[fld].sourceModel][form.fields[fld].sourceField];
|
||||
}
|
||||
}
|
||||
|
||||
relatedSets = form.relatedSets(data.related);
|
||||
|
||||
|
||||
|
||||
@ -210,36 +210,40 @@ export function TeamsEdit($scope, $rootScope, $compile, $location, $log,
|
||||
$scope.$emit("RefreshTeamsList");
|
||||
|
||||
// return a promise from the options request with the permission type choices (including adhoc) as a param
|
||||
var permissionsChoice = fieldChoices({
|
||||
scope: $scope,
|
||||
url: 'api/v1/' + base + '/' + id + '/permissions/',
|
||||
field: 'permission_type'
|
||||
});
|
||||
// var permissionsChoice = fieldChoices({
|
||||
// scope: $scope,
|
||||
// url: 'api/v1/' + base + '/' + id + '/permissions/',
|
||||
// field: 'permission_type'
|
||||
// });
|
||||
|
||||
// manipulate the choices from the options request to be set on
|
||||
// scope and be usable by the list form
|
||||
permissionsChoice.then(function (choices) {
|
||||
choices =
|
||||
fieldLabels({
|
||||
choices: choices
|
||||
});
|
||||
_.map(choices, function(n, key) {
|
||||
$scope.permission_label[key] = n;
|
||||
});
|
||||
});
|
||||
// // manipulate the choices from the options request to be set on
|
||||
// // scope and be usable by the list form
|
||||
// permissionsChoice.then(function (choices) {
|
||||
// choices =
|
||||
// fieldLabels({
|
||||
// choices: choices
|
||||
// });
|
||||
// _.map(choices, function(n, key) {
|
||||
// $scope.permission_label[key] = n;
|
||||
// });
|
||||
// });
|
||||
|
||||
// manipulate the choices from the options request to be usable
|
||||
// by the search option for permission_type, you can't inject the
|
||||
// list until this is done!
|
||||
permissionsChoice.then(function (choices) {
|
||||
form.related.permissions.fields.permission_type.searchOptions =
|
||||
permissionsSearchSelect({
|
||||
choices: choices
|
||||
});
|
||||
// permissionsChoice.then(function (choices) {
|
||||
// form.related.permissions.fields.permission_type.searchOptions =
|
||||
// permissionsSearchSelect({
|
||||
// choices: choices
|
||||
// });
|
||||
generator.inject(form, { mode: 'edit', related: true, scope: $scope });
|
||||
generator.reset();
|
||||
$scope.$emit('loadTeam');
|
||||
});
|
||||
// });
|
||||
|
||||
generator.inject(form, { mode: 'edit', related: true, scope: $scope });
|
||||
generator.reset();
|
||||
$scope.$emit('loadTeam');
|
||||
|
||||
$scope.team_id = id;
|
||||
|
||||
|
||||
@ -240,37 +240,38 @@ export function UsersEdit($scope, $rootScope, $compile, $location, $log,
|
||||
|
||||
$scope.$emit("RefreshUsersList");
|
||||
|
||||
// return a promise from the options request with the permission type choices (including adhoc) as a param
|
||||
var permissionsChoice = fieldChoices({
|
||||
scope: $scope,
|
||||
url: 'api/v1/' + base + '/' + id + '/permissions/',
|
||||
field: 'permission_type'
|
||||
});
|
||||
|
||||
// manipulate the choices from the options request to be set on
|
||||
// scope and be usable by the list form
|
||||
permissionsChoice.then(function (choices) {
|
||||
choices =
|
||||
fieldLabels({
|
||||
choices: choices
|
||||
});
|
||||
_.map(choices, function(n, key) {
|
||||
$scope.permission_label[key] = n;
|
||||
});
|
||||
});
|
||||
// // return a promise from the options request with the permission type choices (including adhoc) as a param
|
||||
// var permissionsChoice = fieldChoices({
|
||||
// scope: $scope,
|
||||
// url: 'api/v1/' + base + '/' + id + '/permissions/',
|
||||
// field: 'permission_type'
|
||||
// });
|
||||
//
|
||||
// // manipulate the choices from the options request to be set on
|
||||
// // scope and be usable by the list form
|
||||
// permissionsChoice.then(function (choices) {
|
||||
// choices =
|
||||
// fieldLabels({
|
||||
// choices: choices
|
||||
// });
|
||||
// _.map(choices, function(n, key) {
|
||||
// $scope.permission_label[key] = n;
|
||||
// });
|
||||
// });
|
||||
|
||||
// manipulate the choices from the options request to be usable
|
||||
// by the search option for permission_type, you can't inject the
|
||||
// list until this is done!
|
||||
permissionsChoice.then(function (choices) {
|
||||
form.related.permissions.fields.permission_type.searchOptions =
|
||||
permissionsSearchSelect({
|
||||
choices: choices
|
||||
});
|
||||
generator.inject(form, { mode: 'edit', related: true, scope: $scope });
|
||||
generator.reset();
|
||||
$scope.$emit("loadForm");
|
||||
});
|
||||
// permissionsChoice.then(function (choices) {
|
||||
// form.related.permissions.fields.permission_type.searchOptions =
|
||||
// permissionsSearchSelect({
|
||||
// choices: choices
|
||||
// });
|
||||
// });
|
||||
|
||||
generator.inject(form, { mode: 'edit', related: true, scope: $scope });
|
||||
generator.reset();
|
||||
$scope.$emit("loadForm");
|
||||
|
||||
if ($scope.removeFormReady) {
|
||||
$scope.removeFormReady();
|
||||
|
||||
@ -7,7 +7,6 @@
|
||||
import sanitizeFilter from './shared/xss-sanitizer.filter';
|
||||
import capitalizeFilter from './shared/capitalize.filter';
|
||||
import longDateFilter from './shared/long-date.filter';
|
||||
|
||||
export {
|
||||
sanitizeFilter,
|
||||
capitalizeFilter,
|
||||
|
||||
@ -278,83 +278,38 @@ angular.module('ProjectFormDefinition', ['SchedulesListDefinition'])
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
schedules: {
|
||||
permissions: {
|
||||
type: 'collection',
|
||||
title: 'Schedules',
|
||||
iterator: 'schedule',
|
||||
title: 'Permissions',
|
||||
iterator: 'permission',
|
||||
index: false,
|
||||
open: false,
|
||||
|
||||
searchType: 'select',
|
||||
actions: {
|
||||
refresh: {
|
||||
mode: 'all',
|
||||
awToolTip: "Refresh the page",
|
||||
ngClick: "refreshSchedules()",
|
||||
actionClass: 'btn List-buttonDefault',
|
||||
buttonContent: 'REFRESH',
|
||||
ngHide: 'scheduleLoading == false && schedule_active_search == false && schedule_total_rows < 1'
|
||||
},
|
||||
add: {
|
||||
mode: 'all',
|
||||
ngClick: 'addSchedule()',
|
||||
awToolTip: 'Add a new schedule',
|
||||
ngClick: "addPermission",
|
||||
label: 'Add',
|
||||
awToolTip: 'Add a permission',
|
||||
actionClass: 'btn List-buttonSubmit',
|
||||
buttonContent: '+ ADD'
|
||||
}
|
||||
},
|
||||
|
||||
fields: {
|
||||
name: {
|
||||
username: {
|
||||
key: true,
|
||||
label: 'Name',
|
||||
ngClick: "editSchedule(schedule.id)",
|
||||
columnClass: "col-md-3 col-sm-3 col-xs-3"
|
||||
label: 'User',
|
||||
linkBase: 'users',
|
||||
class: 'col-lg-3 col-md-3 col-sm-3 col-xs-4'
|
||||
},
|
||||
dtstart: {
|
||||
label: 'First Run',
|
||||
filter: "longDate",
|
||||
searchable: false,
|
||||
columnClass: "col-md-2 col-sm-3 hidden-xs"
|
||||
},
|
||||
next_run: {
|
||||
label: 'Next Run',
|
||||
filter: "longDate",
|
||||
searchable: false,
|
||||
columnClass: "col-md-2 col-sm-3 col-xs-3"
|
||||
},
|
||||
dtend: {
|
||||
label: 'Final Run',
|
||||
filter: "longDate",
|
||||
searchable: false,
|
||||
columnClass: "col-md-2 col-sm-3 hidden-xs"
|
||||
}
|
||||
},
|
||||
fieldActions: {
|
||||
"play": {
|
||||
mode: "all",
|
||||
ngClick: "toggleSchedule($event, schedule.id)",
|
||||
awToolTip: "{{ schedule.play_tip }}",
|
||||
dataTipWatch: "schedule.play_tip",
|
||||
iconClass: "{{ 'fa icon-schedule-enabled-' + schedule.enabled }}",
|
||||
dataPlacement: "top"
|
||||
},
|
||||
edit: {
|
||||
label: 'Edit',
|
||||
ngClick: "editSchedule(schedule.id)",
|
||||
icon: 'icon-edit',
|
||||
awToolTip: 'Edit schedule',
|
||||
dataPlacement: 'top'
|
||||
},
|
||||
"delete": {
|
||||
label: 'Delete',
|
||||
ngClick: "deleteSchedule(schedule.id)",
|
||||
icon: 'icon-trash',
|
||||
awToolTip: 'Delete schedule',
|
||||
dataPlacement: 'top'
|
||||
role: {
|
||||
label: 'Role',
|
||||
type: 'role',
|
||||
noSort: true,
|
||||
class: 'col-lg-9 col-md-9 col-sm-9 col-xs-8'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
relatedSets: function(urls) {
|
||||
@ -363,9 +318,9 @@ angular.module('ProjectFormDefinition', ['SchedulesListDefinition'])
|
||||
iterator: 'organization',
|
||||
url: urls.organizations
|
||||
},
|
||||
schedules: {
|
||||
iterator: 'schedule',
|
||||
url: urls.schedules
|
||||
permissions: {
|
||||
iterator: 'permission',
|
||||
url: urls.resource_access_list
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@ -158,69 +158,69 @@ export default
|
||||
}
|
||||
},
|
||||
|
||||
permissions: {
|
||||
type: 'collection',
|
||||
title: 'Permissions',
|
||||
iterator: 'permission',
|
||||
open: false,
|
||||
index: false,
|
||||
|
||||
actions: {
|
||||
add: {
|
||||
ngClick: "add('permissions')",
|
||||
label: 'Add',
|
||||
awToolTip: 'Add a permission for this user',
|
||||
ngShow: 'PermissionAddAllowed',
|
||||
actionClass: 'btn List-buttonSubmit',
|
||||
buttonContent: '+ ADD'
|
||||
}
|
||||
},
|
||||
|
||||
fields: {
|
||||
name: {
|
||||
key: true,
|
||||
label: 'Name',
|
||||
ngClick: "edit('permissions', permission.id, permission.name)"
|
||||
},
|
||||
inventory: {
|
||||
label: 'Inventory',
|
||||
sourceModel: 'inventory',
|
||||
sourceField: 'name',
|
||||
ngBind: 'permission.summary_fields.inventory.name'
|
||||
},
|
||||
project: {
|
||||
label: 'Project',
|
||||
sourceModel: 'project',
|
||||
sourceField: 'name',
|
||||
ngBind: 'permission.summary_fields.project.name'
|
||||
},
|
||||
permission_type: {
|
||||
label: 'Permission',
|
||||
ngBind: 'getPermissionText()',
|
||||
searchType: 'select'
|
||||
}
|
||||
},
|
||||
|
||||
fieldActions: {
|
||||
edit: {
|
||||
label: 'Edit',
|
||||
ngClick: "edit('permissions', permission.id, permission.name)",
|
||||
icon: 'icon-edit',
|
||||
awToolTip: 'Edit the permission',
|
||||
'class': 'btn btn-default'
|
||||
},
|
||||
|
||||
"delete": {
|
||||
label: 'Delete',
|
||||
ngClick: "delete('permissions', permission.id, permission.name, 'permission')",
|
||||
icon: 'icon-trash',
|
||||
"class": 'btn-danger',
|
||||
awToolTip: 'Delete the permission',
|
||||
ngShow: 'PermissionAddAllowed'
|
||||
}
|
||||
}
|
||||
|
||||
},
|
||||
// permissions: {
|
||||
// type: 'collection',
|
||||
// title: 'Permissions',
|
||||
// iterator: 'permission',
|
||||
// open: false,
|
||||
// index: false,
|
||||
//
|
||||
// actions: {
|
||||
// add: {
|
||||
// ngClick: "add('permissions')",
|
||||
// label: 'Add',
|
||||
// awToolTip: 'Add a permission for this user',
|
||||
// ngShow: 'PermissionAddAllowed',
|
||||
// actionClass: 'btn List-buttonSubmit',
|
||||
// buttonContent: '+ ADD'
|
||||
// }
|
||||
// },
|
||||
//
|
||||
// fields: {
|
||||
// name: {
|
||||
// key: true,
|
||||
// label: 'Name',
|
||||
// ngClick: "edit('permissions', permission.id, permission.name)"
|
||||
// },
|
||||
// inventory: {
|
||||
// label: 'Inventory',
|
||||
// sourceModel: 'inventory',
|
||||
// sourceField: 'name',
|
||||
// ngBind: 'permission.summary_fields.inventory.name'
|
||||
// },
|
||||
// project: {
|
||||
// label: 'Project',
|
||||
// sourceModel: 'project',
|
||||
// sourceField: 'name',
|
||||
// ngBind: 'permission.summary_fields.project.name'
|
||||
// },
|
||||
// permission_type: {
|
||||
// label: 'Permission',
|
||||
// ngBind: 'getPermissionText()',
|
||||
// searchType: 'select'
|
||||
// }
|
||||
// },
|
||||
//
|
||||
// fieldActions: {
|
||||
// edit: {
|
||||
// label: 'Edit',
|
||||
// ngClick: "edit('permissions', permission.id, permission.name)",
|
||||
// icon: 'icon-edit',
|
||||
// awToolTip: 'Edit the permission',
|
||||
// 'class': 'btn btn-default'
|
||||
// },
|
||||
//
|
||||
// "delete": {
|
||||
// label: 'Delete',
|
||||
// ngClick: "delete('permissions', permission.id, permission.name, 'permission')",
|
||||
// icon: 'icon-trash',
|
||||
// "class": 'btn-danger',
|
||||
// awToolTip: 'Delete the permission',
|
||||
// ngShow: 'PermissionAddAllowed'
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// },
|
||||
|
||||
admin_of_organizations: { // Assumes a plural name (e.g. things)
|
||||
type: 'collection',
|
||||
|
||||
@ -32,7 +32,7 @@ export default
|
||||
// Which page are we on?
|
||||
if (Empty(next) && previous) {
|
||||
// no next page, but there is a previous page
|
||||
scope[iterator + '_page'] = scope[iterator + '_num_pages'];
|
||||
scope[iterator + '_page'] = /page=\d+/.test(previous) ? parseInt(previous.match(/page=(\d+)/)[1]) + 1 : 2;
|
||||
} else if (next && Empty(previous)) {
|
||||
// next page available, but no previous page
|
||||
scope[iterator + '_page'] = 1;
|
||||
|
||||
@ -230,10 +230,15 @@ export default
|
||||
url += (url.match(/\/$/)) ? '?' : '&';
|
||||
url += scope[iterator + 'SearchParams'];
|
||||
url += (scope[iterator + '_page_size']) ? '&page_size=' + scope[iterator + '_page_size'] : "";
|
||||
scope[iterator + '_active_search'] = true;
|
||||
RefreshRelated({ scope: scope, set: set, iterator: iterator, url: url });
|
||||
};
|
||||
|
||||
|
||||
scope.$on("refreshList", function(e, iterator) {
|
||||
scope.search(iterator);
|
||||
});
|
||||
|
||||
scope.sort = function (iterator, fld) {
|
||||
var sort_order, icon, direction, set;
|
||||
|
||||
|
||||
@ -85,7 +85,9 @@ export default
|
||||
}
|
||||
Store('sessionTime', x);
|
||||
|
||||
$rootScope.lastUser = $cookieStore.get('current_user').id;
|
||||
if ($cookieStore.get('current_user')) {
|
||||
$rootScope.lastUser = $cookieStore.get('current_user').id;
|
||||
}
|
||||
$cookieStore.remove('token_expires');
|
||||
$cookieStore.remove('current_user');
|
||||
$cookieStore.remove('token');
|
||||
|
||||
@ -136,6 +136,10 @@
|
||||
.MainMenu-itemText--username {
|
||||
padding-left: 13px;
|
||||
margin-top: -4px;
|
||||
max-width: 85px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.MainMenu-itemImage {
|
||||
|
||||
@ -614,7 +614,8 @@ angular.module('Utilities', ['RestServices', 'Utilities', 'sanitizeFilter'])
|
||||
|
||||
var element = params.element,
|
||||
options = params.opts,
|
||||
multiple = (params.multiple!==undefined) ? params.multiple : true;
|
||||
multiple = (params.multiple!==undefined) ? params.multiple : true,
|
||||
placeholder = params.placeholder;
|
||||
|
||||
$.fn.select2.amd.require([
|
||||
'select2/utils',
|
||||
@ -632,6 +633,7 @@ angular.module('Utilities', ['RestServices', 'Utilities', 'sanitizeFilter'])
|
||||
}, Dropdown);
|
||||
|
||||
$(element).select2({
|
||||
placeholder: placeholder,
|
||||
multiple: multiple,
|
||||
containerCssClass: 'Form-dropDown',
|
||||
width: '100%',
|
||||
|
||||
@ -1548,7 +1548,9 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat
|
||||
|
||||
html += "<div class=\"buttons Form-buttons\" ";
|
||||
html += "id=\"" + this.form.name + "_controls\" ";
|
||||
|
||||
if (options.mode === 'edit' && this.form.tabs) {
|
||||
html += "ng-show=\"" + this.form.name + "Selected\"; "
|
||||
}
|
||||
html += ">\n";
|
||||
|
||||
if (this.form.horizontal) {
|
||||
@ -1723,32 +1725,52 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat
|
||||
html += "<tr class=\"List-tableHeaderRow\">\n";
|
||||
html += (collection.index === undefined || collection.index !== false) ? "<th class=\"col-xs-1\">#</th>\n" : "";
|
||||
for (fld in collection.fields) {
|
||||
html += "<th class=\"List-tableHeader list-header\" id=\"" + collection.iterator + '-' + fld + "-header\" " +
|
||||
"ng-click=\"sort('" + collection.iterator + "', '" + fld + "')\">" +
|
||||
collection.fields[fld].label;
|
||||
html += " <i class=\"";
|
||||
if (collection.fields[fld].key) {
|
||||
if (collection.fields[fld].desc) {
|
||||
html += "fa fa-sort-down";
|
||||
} else {
|
||||
html += "fa fa-sort-up";
|
||||
}
|
||||
html += "<th class=\"List-tableHeader list-header ";
|
||||
html += (collection.fields[fld].class) ? collection.fields[fld].class : "";
|
||||
html += "\" id=\"" + collection.iterator + '-' + fld + "-header\" ";
|
||||
|
||||
if (!collection.fields[fld].noSort) {
|
||||
html += "ng-click=\"sort('" + collection.iterator + "', '" + fld + "')\">"
|
||||
} else {
|
||||
html += "fa fa-sort";
|
||||
html += ">";
|
||||
}
|
||||
html += "\"></i></a></th>\n";
|
||||
|
||||
|
||||
html += collection.fields[fld].label;
|
||||
|
||||
if (!collection.fields[fld].noSort) {
|
||||
html += " <i class=\"";
|
||||
|
||||
|
||||
if (collection.fields[fld].key) {
|
||||
if (collection.fields[fld].desc) {
|
||||
html += "fa fa-sort-down";
|
||||
} else {
|
||||
html += "fa fa-sort-up";
|
||||
}
|
||||
} else {
|
||||
html += "fa fa-sort";
|
||||
}
|
||||
html += "\"></i>"
|
||||
}
|
||||
|
||||
html += "</a></th>\n";
|
||||
}
|
||||
if (collection.fieldActions) {
|
||||
html += "<th class=\"List-tableHeader List-tableHeader--actions\">Actions</th>\n";
|
||||
}
|
||||
html += "<th class=\"List-tableHeader\">Actions</th>\n";
|
||||
html += "</tr>\n";
|
||||
html += "</thead>";
|
||||
html += "<tbody>\n";
|
||||
|
||||
html += "<tr class=\"List-tableHeaderRow\" ng-repeat=\"" + collection.iterator + " in " + itm + "\" ";
|
||||
html += "<tr class=\"List-tableRow\" ng-repeat=\"" + collection.iterator + " in " + itm + "\" ";
|
||||
html += "ng-class-odd=\"'List-tableRow--oddRow'\" ";
|
||||
html += "ng-class-even=\"'List-tableRow--evenRow'\" ";
|
||||
html += "id=\"{{ " + collection.iterator + ".id }}\">\n";
|
||||
if (collection.index === undefined || collection.index !== false) {
|
||||
html += "<td class=\"List-tableCell\">{{ $index + ((" + collection.iterator + "_page - 1) * " +
|
||||
html += "<td class=\"List-tableCell";
|
||||
html += (collection.fields[fld].class) ? collection.fields[fld].class : "";
|
||||
html += "\">{{ $index + ((" + collection.iterator + "_page - 1) * " +
|
||||
collection.iterator + "_page_size) + 1 }}.</td>\n";
|
||||
}
|
||||
cnt = 1;
|
||||
@ -1765,31 +1787,33 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat
|
||||
}
|
||||
|
||||
// Row level actions
|
||||
html += "<td class=\"List-tableCell List-actionButtonCell actions\">";
|
||||
for (act in collection.fieldActions) {
|
||||
fAction = collection.fieldActions[act];
|
||||
html += "<button id=\"" + ((fAction.id) ? fAction.id : act + "-action") + "\" ";
|
||||
html += (fAction.href) ? "href=\"" + fAction.href + "\" " : "";
|
||||
html += (fAction.ngClick) ? this.attr(fAction, 'ngClick') : "";
|
||||
html += (fAction.ngHref) ? this.attr(fAction, 'ngHref') : "";
|
||||
html += (fAction.ngShow) ? this.attr(fAction, 'ngShow') : "";
|
||||
html += " class=\"List-actionButton ";
|
||||
html += (act === 'delete') ? "List-actionButton--delete" : "";
|
||||
html += "\"";
|
||||
html += ">";
|
||||
if (fAction.iconClass) {
|
||||
html += "<i class=\"" + fAction.iconClass + "\"></i>";
|
||||
} else {
|
||||
html += SelectIcon({
|
||||
action: act
|
||||
});
|
||||
if (collection.fieldActions) {
|
||||
html += "<td class=\"List-tableCell List-actionButtonCell actions\">";
|
||||
for (act in collection.fieldActions) {
|
||||
fAction = collection.fieldActions[act];
|
||||
html += "<button id=\"" + ((fAction.id) ? fAction.id : act + "-action") + "\" ";
|
||||
html += (fAction.href) ? "href=\"" + fAction.href + "\" " : "";
|
||||
html += (fAction.ngClick) ? this.attr(fAction, 'ngClick') : "";
|
||||
html += (fAction.ngHref) ? this.attr(fAction, 'ngHref') : "";
|
||||
html += (fAction.ngShow) ? this.attr(fAction, 'ngShow') : "";
|
||||
html += " class=\"List-actionButton ";
|
||||
html += (act === 'delete') ? "List-actionButton--delete" : "";
|
||||
html += "\"";
|
||||
html += ">";
|
||||
if (fAction.iconClass) {
|
||||
html += "<i class=\"" + fAction.iconClass + "\"></i>";
|
||||
} else {
|
||||
html += SelectIcon({
|
||||
action: act
|
||||
});
|
||||
}
|
||||
// html += SelectIcon({ action: act });
|
||||
//html += (fAction.label) ? "<span class=\"list-action-label\"> " + fAction.label + "</span>": "";
|
||||
html += "</button>";
|
||||
}
|
||||
// html += SelectIcon({ action: act });
|
||||
//html += (fAction.label) ? "<span class=\"list-action-label\"> " + fAction.label + "</span>": "";
|
||||
html += "</button>";
|
||||
html += "</td>";
|
||||
html += "</tr>\n";
|
||||
}
|
||||
html += "</td>";
|
||||
html += "</tr>\n";
|
||||
|
||||
// Message for loading
|
||||
html += "<tr ng-show=\"" + collection.iterator + "Loading == true\">\n";
|
||||
|
||||
@ -449,6 +449,8 @@ angular.module('GeneratorHelpers', [systemStatus.name])
|
||||
|
||||
if (field.type !== undefined && field.type === 'DropDown') {
|
||||
html = DropDown(params);
|
||||
} else if (field.type === 'role') {
|
||||
html += "<td class=\"List-tableCell\"><role-list class=\"RoleList\"></role-list></td>";
|
||||
} else if (field.type === 'badgeCount') {
|
||||
html = BadgeCount(params);
|
||||
} else if (field.type === 'badgeOnly') {
|
||||
@ -520,7 +522,7 @@ angular.module('GeneratorHelpers', [systemStatus.name])
|
||||
list: list,
|
||||
field: field,
|
||||
fld: fld,
|
||||
base: base
|
||||
base: field.linkBase || base
|
||||
}) + ' ';
|
||||
});
|
||||
}
|
||||
@ -532,7 +534,7 @@ angular.module('GeneratorHelpers', [systemStatus.name])
|
||||
list: list,
|
||||
field: field,
|
||||
fld: fld,
|
||||
base: base
|
||||
base: field.linkBase || base
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -633,6 +635,7 @@ angular.module('GeneratorHelpers', [systemStatus.name])
|
||||
var iterator = params.iterator,
|
||||
form = params.template,
|
||||
size = params.size,
|
||||
mini = params.mini,
|
||||
includeSize = (params.includeSize === undefined) ? true : params.includeSize,
|
||||
ngShow = (params.ngShow) ? params.ngShow : false,
|
||||
i, html = '',
|
||||
@ -666,6 +669,7 @@ angular.module('GeneratorHelpers', [systemStatus.name])
|
||||
|
||||
if (includeSize) {
|
||||
html += "<div class=\"List-searchWidget ";
|
||||
html += (mini) ? "List-searchWidget--compact " : "";
|
||||
html += (size) ? size : "col-lg-4 col-md-8 col-sm-12 col-xs-12";
|
||||
html += "\" id=\"search-widget-container" + modifier + "\">\n";
|
||||
}
|
||||
|
||||
@ -416,6 +416,7 @@ export default ['$location', '$compile', '$rootScope', 'SearchWidget', 'Paginate
|
||||
function buildTable() {
|
||||
var extraClasses = list['class'];
|
||||
var multiSelect = list.multiSelect ? 'multi-select-list' : null;
|
||||
var multiSelectExtended = list.multiSelectExtended ? 'true' : 'false';
|
||||
|
||||
if (options.mode === 'summary') {
|
||||
extraClasses += ' table-summary';
|
||||
@ -425,7 +426,8 @@ export default ['$location', '$compile', '$rootScope', 'SearchWidget', 'Paginate
|
||||
.attr('id', list.name + '_table')
|
||||
.addClass('List-table')
|
||||
.addClass(extraClasses)
|
||||
.attr('multi-select-list', multiSelect);
|
||||
.attr('multi-select-list', multiSelect)
|
||||
.attr('is-extended', multiSelectExtended);
|
||||
|
||||
}
|
||||
|
||||
@ -460,7 +462,7 @@ export default ['$location', '$compile', '$rootScope', 'SearchWidget', 'Paginate
|
||||
}
|
||||
|
||||
if (list.multiSelect) {
|
||||
innerTable += '<td class="col-xs-1 select-column List-tableCell"><select-list-item item=\"' + list.iterator + '\"></select-list-item></td>';
|
||||
innerTable += '<td class="col-xs-1 select-column List- List-staticColumn--smallStatus"><select-list-item item=\"' + list.iterator + '\"></select-list-item></td>';
|
||||
}
|
||||
|
||||
// Change layout if a lookup list, place radio buttons before labels
|
||||
@ -609,7 +611,7 @@ export default ['$location', '$compile', '$rootScope', 'SearchWidget', 'Paginate
|
||||
|
||||
function buildSelectAll() {
|
||||
return $('<th>')
|
||||
.addClass('col-xs-1 select-column List-tableHeader')
|
||||
.addClass('col-xs-1 select-column List-tableHeader List-staticColumn--smallStatus')
|
||||
.append(
|
||||
$('<select-all>')
|
||||
.attr('selections-empty', 'selectedItems.length === 0')
|
||||
@ -665,10 +667,9 @@ export default ['$location', '$compile', '$rootScope', 'SearchWidget', 'Paginate
|
||||
}
|
||||
}
|
||||
if (options.mode === 'select') {
|
||||
html += "<th class=\"List-tableHeader col-lg-1 col-md-1 col-sm-2 col-xs-2\">Select</th>";
|
||||
}
|
||||
else if (options.mode === 'edit' && list.fieldActions) {
|
||||
html += "<th class=\"List-tableHeader actions-column";
|
||||
html += "<th class=\"List-tableHeader col-lg-1 col-md-1 col-sm-2 col-xs-2\">Select</th>";
|
||||
} else if (options.mode === 'edit' && list.fieldActions) {
|
||||
html += "<th class=\"List-tableHeader List-tableHeader--actions actions-column";
|
||||
html += (list.fieldActions && list.fieldActions.columnClass) ? " " + list.fieldActions.columnClass : "";
|
||||
html += "\">";
|
||||
html += (list.fieldActions.label === undefined || list.fieldActions.label) ? "Actions" : "";
|
||||
|
||||
@ -30,7 +30,7 @@ export default
|
||||
item: '=item'
|
||||
},
|
||||
require: '^multiSelectList',
|
||||
template: '<input type="checkbox" data-multi-select-list-item ng-model="isSelected">',
|
||||
template: '<input type="checkbox" data-multi-select-list-item ng-model="isSelected" ng-change="userInteractionSelect()">',
|
||||
link: function(scope, element, attrs, multiSelectList) {
|
||||
|
||||
scope.isSelected = false;
|
||||
@ -52,6 +52,10 @@ export default
|
||||
multiSelectList.deregisterItem(scope.decoratedItem);
|
||||
});
|
||||
|
||||
scope.userInteractionSelect = function() {
|
||||
scope.$emit("selectedOrDeselected", scope.decoratedItem);
|
||||
}
|
||||
|
||||
}
|
||||
};
|
||||
}];
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user