Merge branch 'rbac' of github.com:ansible/ansible-tower into devel

This commit is contained in:
Wayne Witzel III 2016-03-22 14:10:42 -04:00
commit 2494cc56df
103 changed files with 8113 additions and 2163 deletions

View File

@ -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

View File

@ -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))

View File

@ -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

View File

@ -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')

View File

@ -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)),

View File

@ -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

View File

@ -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()

View File

@ -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)

View File

@ -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):

View 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

View File

@ -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:

View File

@ -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.

View 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',
),
]

View 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),
),
]

View 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),
]

View File

@ -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 = [

View 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)

File diff suppressed because it is too large Load Diff

View 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

View File

@ -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

View File

@ -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:

View File

@ -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()

View File

@ -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.

View File

@ -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

View File

@ -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
View 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

View File

@ -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):

View File

@ -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
View 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

View File

@ -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)

View File

@ -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.

View File

@ -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:

View File

@ -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

View File

@ -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)

View File

@ -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')

View File

@ -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()

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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

View 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

View 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

View 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})

View 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

View 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({})

View 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

View 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

View 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

View 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})

View File

@ -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',

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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.

View File

@ -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'])

View File

@ -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

View File

@ -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)

View File

@ -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())

View File

@ -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"])

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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',

View File

@ -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

View File

@ -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',

View File

@ -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.

View File

@ -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)

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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
});
});
};
}];

View File

@ -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);
}
};
}
];

View File

@ -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>

View File

@ -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);
});
}
};
}
];

View File

@ -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);

View File

@ -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'
},
},
};
}

View File

@ -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'
},
},
};
}

View 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);

View File

@ -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'
});
}
};
}
];

View 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);

View 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;
}

View 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;
}
});
}
};
}
];

View 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>

View File

@ -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'){

View File

@ -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);

View File

@ -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;

View File

@ -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();

View File

@ -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,

View File

@ -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: '&#43; 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
}
};
}

View File

@ -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: '&#43; 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: '&#43; 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',

View File

@ -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;

View File

@ -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;

View File

@ -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');

View File

@ -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 {

View File

@ -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%',

View File

@ -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";

View File

@ -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";
}

View File

@ -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" : "";

View File

@ -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