From 21d3051ec4c7692fac85bacd2931275b6715fbb1 Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Thu, 18 Aug 2016 15:14:31 -0400 Subject: [PATCH 01/51] Include user edit/start/delete/copy capabilities to summary fields This patch generically adds a map of capabilities that the current user has on an object, namely can they start, delete, editor, or copy the object. The intent is that these flags will be used by the UI to disable buttons that do those things. #2479 --- awx/api/serializers.py | 7 +++++++ awx/main/access.py | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/awx/api/serializers.py b/awx/api/serializers.py index af3e6ae4ac..acc0b48e8e 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -37,6 +37,7 @@ from polymorphic import PolymorphicModel # AWX from awx.main.constants import SCHEDULEABLE_PROVIDERS from awx.main.models import * # noqa +from awx.main.access import get_user_capabilities from awx.main.fields import ImplicitRoleField from awx.main.utils import get_type_for_model, get_model_for_type, build_url, timestamp_apiformat, camelcase_to_underscore, getattrd from awx.main.conf import tower_settings @@ -330,6 +331,12 @@ class BaseSerializer(serializers.ModelSerializer): } if len(roles) > 0: summary_fields['object_roles'] = roles + view = self.context.get('view', None) + if view and view.request and view.request.user: + user_capabilities = get_user_capabilities(view.request.user, obj) + if user_capabilities: + summary_fields['user_capabilities'] = user_capabilities + return summary_fields def get_created(self, obj): diff --git a/awx/main/access.py b/awx/main/access.py index e5ca8fa0ec..3dfc777d5f 100644 --- a/awx/main/access.py +++ b/awx/main/access.py @@ -116,6 +116,18 @@ def check_user_access(user, model_class, action, *args, **kwargs): return result return False +def get_user_capabilities(user, instance): + ''' + Returns a dictionary of capabilities the user has on the particular + instance. *NOTE* This is not a direct mapping of can_* methods into this + dictionary, it is intended to munge some queries in a way that is + convenient for the user interface to consume and hide or show various + actions in the interface. + ''' + for access_class in access_registry.get(type(instance), []): + return access_class(user).get_user_capabilities(instance) + return None + def check_superuser(func): ''' check_superuser is a decorator that provides a simple short circuit @@ -207,6 +219,28 @@ class BaseAccess(object): elif "features" not in validation_info: raise LicenseForbids("Features not found in active license.") + def get_user_capabilities(self, obj): + user_capabilities = {} + + if isinstance(obj, JobTemplate): + user_capabilities['copy'] = self.user.can_access(type(obj), 'add', { 'reference_obj': obj }) + print(type(obj)) + + for method in ['change', 'delete', 'start']: + try: + if isinstance(obj, Group) and method is 'start' and obj.inventory_source: + obj = obj.inventory_source + + if method in ['change']: # 3 args + user_capabilities[method] = self.user.can_access(type(obj), method, obj, {}) + else: # 2 args + user_capabilities[method] = self.user.can_access(type(obj), method, obj) + except Exception as exc: + user_capabilities[method] = False + print(exc) + + return user_capabilities + class UserAccess(BaseAccess): ''' From eddc1a8ed26526c2642eea731de0fbe3c8ba5ea4 Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Thu, 11 Aug 2016 13:55:38 -0400 Subject: [PATCH 02/51] implement two methods for can_edit field for testing --- awx/api/serializers.py | 6 ++++++ awx/api/views.py | 18 ++++++++++++++++++ awx/main/models/projects.py | 3 +++ 3 files changed, 27 insertions(+) diff --git a/awx/api/serializers.py b/awx/api/serializers.py index 2d2e38a8f5..f492b06ee9 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -330,6 +330,12 @@ class BaseSerializer(serializers.ModelSerializer): } if len(roles) > 0: summary_fields['object_roles'] = roles + if hasattr(obj, 'get_can_edit'): + request = self.context.get('request', None) + if request and request.user is not None: + summary_fields['can_edit'] = obj.get_can_edit(request.user) + elif hasattr(obj, 'can_edit'): + summary_fields['can_edit'] = obj.can_edit return summary_fields def get_created(self, obj): diff --git a/awx/api/views.py b/awx/api/views.py index 39db4cd23c..e2ae7bfe3f 100644 --- a/awx/api/views.py +++ b/awx/api/views.py @@ -1521,6 +1521,24 @@ class InventoryList(ListCreateAPIView): qs = qs.select_related('admin_role', 'read_role', 'update_role', 'use_role', 'adhoc_role') return qs + def list(self, request, *args, **kwargs): + queryset = self.filter_queryset(self.get_queryset()) + + page = self.paginate_queryset(queryset) + readable_ids = [obj.id for obj in page] + editable_ids = Inventory.accessible_objects(request.user, 'admin_role').filter(pk__in=readable_ids).values_list('pk', flat=True) + for obj in page: + if obj.pk in editable_ids: + obj.can_edit = True + else: + obj.can_edit = False + if page is not None: + serializer = self.get_serializer(page, many=True) + return self.get_paginated_response(serializer.data) + + serializer = self.get_serializer(queryset, many=True) + return Response(serializer.data) + class InventoryDetail(RetrieveUpdateDestroyAPIView): model = Inventory diff --git a/awx/main/models/projects.py b/awx/main/models/projects.py index 93c4a42e36..d68f3db3ae 100644 --- a/awx/main/models/projects.py +++ b/awx/main/models/projects.py @@ -105,6 +105,9 @@ class ProjectOptions(models.Model): on_delete=models.SET_NULL, ) + def get_can_edit(self, user): + return user in self.admin_role + def clean_scm_type(self): return self.scm_type or '' From e4504f789aab88ec68ebf08e0c2720aee1953bf1 Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Fri, 26 Aug 2016 13:16:06 -0400 Subject: [PATCH 03/51] intermediary state, not yet known to be working --- awx/api/serializers.py | 34 +++++++++++++++++++++++++++------- awx/main/access.py | 12 ++++++------ awx/main/models/projects.py | 6 +++--- 3 files changed, 36 insertions(+), 16 deletions(-) diff --git a/awx/api/serializers.py b/awx/api/serializers.py index 47564414b1..99fa68d6d9 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -331,11 +331,12 @@ class BaseSerializer(serializers.ModelSerializer): } if len(roles) > 0: summary_fields['object_roles'] = roles - view = self.context.get('view', None) - if view and view.request and view.request.user: - user_capabilities = get_user_capabilities(view.request.user, obj) - if user_capabilities: - summary_fields['user_capabilities'] = user_capabilities + if hasattr(self, 'show_capabilities'): + view = self.context.get('view', None) + if view and view.request and view.request.user: + user_capabilities = get_user_capabilities(view.request.user, obj, self.show_capabilities) + if user_capabilities: + summary_fields['user_capabilities'] = user_capabilities return summary_fields @@ -502,6 +503,7 @@ class BaseFactSerializer(BaseSerializer): return ret class UnifiedJobTemplateSerializer(BaseSerializer): + show_capabilities = ['start', 'delete'] class Meta: model = UnifiedJobTemplate @@ -683,6 +685,7 @@ class UserSerializer(BaseSerializer): ldap_dn = serializers.CharField(source='profile.ldap_dn', read_only=True) external_account = serializers.SerializerMethodField(help_text='Set if the account is managed by an external service') is_system_auditor = serializers.BooleanField(default=False) + show_capabilities = ['edit', 'delete'] class Meta: model = User @@ -808,6 +811,7 @@ class UserSerializer(BaseSerializer): class OrganizationSerializer(BaseSerializer): + show_capabilities = ['edit', 'delete'] class Meta: model = Organization @@ -892,6 +896,7 @@ class ProjectSerializer(UnifiedJobTemplateSerializer, ProjectOptionsSerializer): status = serializers.ChoiceField(choices=Project.PROJECT_STATUS_CHOICES, read_only=True) last_update_failed = serializers.BooleanField(read_only=True) last_updated = serializers.DateTimeField(read_only=True) + show_capabilities = ['start', 'schedule', 'edit', 'delete'] class Meta: model = Project @@ -1007,6 +1012,7 @@ class BaseSerializerWithVariables(BaseSerializer): class InventorySerializer(BaseSerializerWithVariables): + show_capabilities = ['edit', 'delete'] class Meta: model = Inventory @@ -1057,6 +1063,7 @@ class InventoryDetailSerializer(InventorySerializer): class InventoryScriptSerializer(InventorySerializer): + show_capabilities = ['edit', 'delete'] class Meta: fields = () @@ -1458,6 +1465,7 @@ class InventoryUpdateCancelSerializer(InventoryUpdateSerializer): class TeamSerializer(BaseSerializer): + show_capabilities = ['edit', 'delete'] class Meta: model = Team @@ -1543,6 +1551,10 @@ class ResourceAccessListElementSerializer(UserSerializer): 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 self.context['view'].request is not None: + requesting_user = self.context['view'].request.user + else: + requesting_user = None if 'summary_fields' not in ret: ret['summary_fields'] = {} @@ -1553,6 +1565,12 @@ class ResourceAccessListElementSerializer(UserSerializer): role_dict['resource_name'] = role.content_object.name role_dict['resource_type'] = role.content_type.name role_dict['related'] = reverse_gfk(role.content_object) + # Special implementation of unattach user capabilities to show/hide X in UI + role_dict['user_capabilities'] = { + 'unattach': requesting_user.can_access( + type(role.content_object), 'unattach', role.content_object, + role, 'roles', data) + } except: pass return { 'role': role_dict, 'descendant_roles': get_roles_on_resource(obj, role)} @@ -1579,8 +1597,6 @@ class ResourceAccessListElementSerializer(UserSerializer): team_content_type = ContentType.objects.get_for_model(Team) content_type = ContentType.objects.get_for_model(obj) - - content_type = ContentType.objects.get_for_model(obj) direct_permissive_role_ids = Role.objects.filter(content_type=content_type, object_id=obj.id).values_list('id', flat=True) all_permissive_role_ids = Role.objects.filter(content_type=content_type, object_id=obj.id).values_list('ancestors__id', flat=True) @@ -1625,6 +1641,7 @@ class ResourceAccessListElementSerializer(UserSerializer): class CredentialSerializer(BaseSerializer): + show_capabilities = ['edit', 'delete'] class Meta: model = Credential @@ -1829,6 +1846,7 @@ class JobOptionsSerializer(BaseSerializer): class JobTemplateSerializer(UnifiedJobTemplateSerializer, JobOptionsSerializer): + show_capabilities = ['start', 'schedule', 'copy', 'edit', 'delete'] status = serializers.ChoiceField(choices=JobTemplate.JOB_TEMPLATE_STATUS_CHOICES, read_only=True, required=False) @@ -2134,6 +2152,7 @@ class AdHocCommandRelaunchSerializer(AdHocCommandSerializer): class SystemJobTemplateSerializer(UnifiedJobTemplateSerializer): + show_capabilities = ['start', 'schedule', 'edit'] class Meta: model = SystemJobTemplate @@ -2411,6 +2430,7 @@ class JobLaunchSerializer(BaseSerializer): return attrs class NotificationTemplateSerializer(BaseSerializer): + show_capabilities = ['edit', 'delete'] class Meta: model = NotificationTemplate diff --git a/awx/main/access.py b/awx/main/access.py index b8505746df..a5c75c87ee 100644 --- a/awx/main/access.py +++ b/awx/main/access.py @@ -219,19 +219,19 @@ class BaseAccess(object): elif "features" not in validation_info: raise LicenseForbids("Features not found in active license.") - def get_user_capabilities(self, obj): + def get_user_capabilities(self, obj, method_list=['edit', 'delete']): user_capabilities = {} - if hasattr(obj, 'get_can_edit'): - user_capabilities['change'] = obj.get_can_edit(self.user) - elif hasattr(obj, 'can_edit'): - user_capabilities['change'] = obj.can_edit + # if hasattr(obj, 'get_can_edit'): + # user_capabilities['change'] = obj.get_can_edit(self.user) + # elif hasattr(obj, 'can_edit'): + # user_capabilities['change'] = obj.can_edit if isinstance(obj, JobTemplate): user_capabilities['copy'] = self.user.can_access(type(obj), 'add', { 'reference_obj': obj }) print(type(obj)) - for method in ['change', 'delete', 'start']: + for method in method_list: try: if isinstance(obj, Group) and method is 'start' and obj.inventory_source: obj = obj.inventory_source diff --git a/awx/main/models/projects.py b/awx/main/models/projects.py index d68f3db3ae..a07e1f24ed 100644 --- a/awx/main/models/projects.py +++ b/awx/main/models/projects.py @@ -105,9 +105,6 @@ class ProjectOptions(models.Model): on_delete=models.SET_NULL, ) - def get_can_edit(self, user): - return user in self.admin_role - def clean_scm_type(self): return self.scm_type or '' @@ -328,6 +325,9 @@ class Project(UnifiedJobTemplate, ProjectOptions, ResourceMixin): kwargs['scm_delete_on_update'] = True return kwargs + def get_can_edit(self, user): + return user in self.admin_role + def create_project_update(self, **kwargs): return self.create_unified_job(**kwargs) From 8f1e9bd20d8340275f2c2b08970224432cfdd87a Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Fri, 26 Aug 2016 17:05:52 -0400 Subject: [PATCH 04/51] fields generally working as intended now --- awx/api/serializers.py | 12 ++++------ awx/main/access.py | 54 ++++++++++++++++++++++++++++++++---------- 2 files changed, 46 insertions(+), 20 deletions(-) diff --git a/awx/api/serializers.py b/awx/api/serializers.py index 99fa68d6d9..69824258ea 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -1012,7 +1012,7 @@ class BaseSerializerWithVariables(BaseSerializer): class InventorySerializer(BaseSerializerWithVariables): - show_capabilities = ['edit', 'delete'] + show_capabilities = ['edit', 'delete', 'adhoc'] class Meta: model = Inventory @@ -1063,13 +1063,14 @@ class InventoryDetailSerializer(InventorySerializer): class InventoryScriptSerializer(InventorySerializer): - show_capabilities = ['edit', 'delete'] + show_capabilities = ['copy', 'edit', 'delete'] class Meta: fields = () class HostSerializer(BaseSerializerWithVariables): + show_capabilities = ['edit', 'delete'] class Meta: model = Host @@ -1180,6 +1181,7 @@ class HostSerializer(BaseSerializerWithVariables): class GroupSerializer(BaseSerializerWithVariables): + show_capabilities = ['start', 'copy', 'schedule', 'edit', 'delete'] class Meta: model = Group @@ -1565,12 +1567,6 @@ class ResourceAccessListElementSerializer(UserSerializer): role_dict['resource_name'] = role.content_object.name role_dict['resource_type'] = role.content_type.name role_dict['related'] = reverse_gfk(role.content_object) - # Special implementation of unattach user capabilities to show/hide X in UI - role_dict['user_capabilities'] = { - 'unattach': requesting_user.can_access( - type(role.content_object), 'unattach', role.content_object, - role, 'roles', data) - } except: pass return { 'role': role_dict, 'descendant_roles': get_roles_on_resource(obj, role)} diff --git a/awx/main/access.py b/awx/main/access.py index a5c75c87ee..52c1ff7250 100644 --- a/awx/main/access.py +++ b/awx/main/access.py @@ -116,7 +116,7 @@ def check_user_access(user, model_class, action, *args, **kwargs): return result return False -def get_user_capabilities(user, instance): +def get_user_capabilities(user, instance, method_list): ''' Returns a dictionary of capabilities the user has on the particular instance. *NOTE* This is not a direct mapping of can_* methods into this @@ -125,7 +125,7 @@ def get_user_capabilities(user, instance): actions in the interface. ''' for access_class in access_registry.get(type(instance), []): - return access_class(user).get_user_capabilities(instance) + return access_class(user).get_user_capabilities(instance, method_list) return None def check_superuser(func): @@ -227,21 +227,51 @@ class BaseAccess(object): # elif hasattr(obj, 'can_edit'): # user_capabilities['change'] = obj.can_edit - if isinstance(obj, JobTemplate): - user_capabilities['copy'] = self.user.can_access(type(obj), 'add', { 'reference_obj': obj }) print(type(obj)) - for method in method_list: - try: - if isinstance(obj, Group) and method is 'start' and obj.inventory_source: - obj = obj.inventory_source + for display_method in ['edit', 'delete', 'start', 'schedule', 'copy']: + # Custom ordering of methods used so we can reuse earlier calcs + if display_method not in method_list: + continue - if method in ['change']: # 3 args - user_capabilities[method] = self.user.can_access(type(obj), method, obj, {}) + # Aliases for going form UI language to API language + if display_method == 'edit': + method = 'change' + elif display_method == 'copy': + method = 'add' + elif display_method == 'schedule' and 'edit' in user_capabilities: + user_capabilities['schedule'] = user_capabilities['edit'] + continue + else: + method = display_method + + # Build the fields used for the calculation + data = None + sub_obj = None + if method == 'add': + data = {} + + try: + if isinstance(obj, (Group, Host)): + if method == 'start': + if obj.inventory_source: + obj = obj.inventory_source + else: + user_capabilities[method] = False + continue + else: + obj = obj.inventory + if isinstance(obj, JobTemplate): + data = {'reference_obj': obj} + + if data is not None: # 3 args + user_capabilities[display_method] = self.user.can_access(type(obj), method, obj, data) else: # 2 args - user_capabilities[method] = self.user.can_access(type(obj), method, obj) + user_capabilities[display_method] = self.user.can_access(type(obj), method, obj) + + except Exception as exc: - user_capabilities[method] = False + user_capabilities[display_method] = False print(exc) return user_capabilities From 381e8aacddf730a674d7c48604fd508ad78204f5 Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Mon, 29 Aug 2016 16:57:30 -0400 Subject: [PATCH 05/51] more organization of access method preprocessing --- awx/main/access.py | 33 +++++++++++-------- .../functional/api/test_adding_options.py | 14 ++++++++ 2 files changed, 33 insertions(+), 14 deletions(-) create mode 100644 awx/main/tests/functional/api/test_adding_options.py diff --git a/awx/main/access.py b/awx/main/access.py index 52c1ff7250..f80f3a7865 100644 --- a/awx/main/access.py +++ b/awx/main/access.py @@ -222,6 +222,7 @@ class BaseAccess(object): def get_user_capabilities(self, obj, method_list=['edit', 'delete']): user_capabilities = {} + # TODO: pull data from the custom cache, which won't be exactly like this # if hasattr(obj, 'get_can_edit'): # user_capabilities['change'] = obj.get_can_edit(self.user) # elif hasattr(obj, 'can_edit'): @@ -242,31 +243,35 @@ class BaseAccess(object): elif display_method == 'schedule' and 'edit' in user_capabilities: user_capabilities['schedule'] = user_capabilities['edit'] continue + elif display_method == 'delete' and not isinstance(obj, User): + user_capabilities['delete'] = user_capabilities['edit'] + continue else: method = display_method - # Build the fields used for the calculation + # Preprocessing before the access method is called data = None sub_obj = None if method == 'add': data = {} - try: - if isinstance(obj, (Group, Host)): - if method == 'start': - if obj.inventory_source: - obj = obj.inventory_source - else: - user_capabilities[method] = False - continue + if isinstance(obj, (Group, Host)): + if method == 'start': + if obj.inventory_source: + obj = obj.inventory_source else: - obj = obj.inventory - if isinstance(obj, JobTemplate): - data = {'reference_obj': obj} + user_capabilities[method] = False + continue + else: + obj = obj.inventory + if isinstance(obj, JobTemplate): + data = {'reference_obj': obj} - if data is not None: # 3 args + try: + + if method in ['change', 'start', 'delete']: # 3 args user_capabilities[display_method] = self.user.can_access(type(obj), method, obj, data) - else: # 2 args + elif method == 'add': # 2 args user_capabilities[display_method] = self.user.can_access(type(obj), method, obj) diff --git a/awx/main/tests/functional/api/test_adding_options.py b/awx/main/tests/functional/api/test_adding_options.py new file mode 100644 index 0000000000..e03450ac7f --- /dev/null +++ b/awx/main/tests/functional/api/test_adding_options.py @@ -0,0 +1,14 @@ +import pytest + +from django.core.urlresolvers import reverse + +@pytest.fixture +def test_inventory_group_add(inventory, alice, bob, options): + inventory.admin_role.add(alice) + response = options(reverse('api:inventory_detail', args=[inventory.pk]), alice) + print ' resp: ' + str(response.data) + assert 'POST' in response.data + + inventory.read_role.add(bob) + + From 0151967e9c3fa96c09f74644cab45628cf377bf6 Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Tue, 30 Aug 2016 10:11:51 -0400 Subject: [PATCH 06/51] selectively show POST in options for inventory sublists --- awx/api/permissions.py | 3 +++ .../functional/api/test_adding_options.py | 26 ++++++++++++------- 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/awx/api/permissions.py b/awx/api/permissions.py index 6e1320e2d8..d1f909cd0b 100644 --- a/awx/api/permissions.py +++ b/awx/api/permissions.py @@ -49,6 +49,9 @@ class ModelAccessPermission(permissions.BasePermission): if not check_user_access(request.user, view.parent_model, 'read', parent_obj): return False + if hasattr(view, 'parent_key'): + if not check_user_access(request.user, view.model, 'add', {view.parent_key: parent_obj.pk}): + return False return True elif getattr(view, 'is_job_start', False): if not obj: diff --git a/awx/main/tests/functional/api/test_adding_options.py b/awx/main/tests/functional/api/test_adding_options.py index e03450ac7f..08ecf27a4f 100644 --- a/awx/main/tests/functional/api/test_adding_options.py +++ b/awx/main/tests/functional/api/test_adding_options.py @@ -2,13 +2,21 @@ import pytest from django.core.urlresolvers import reverse -@pytest.fixture -def test_inventory_group_add(inventory, alice, bob, options): - inventory.admin_role.add(alice) - response = options(reverse('api:inventory_detail', args=[inventory.pk]), alice) - print ' resp: ' + str(response.data) - assert 'POST' in response.data +@pytest.mark.django_db +def test_inventory_group_host_can_add(inventory, alice, options): + inventory.admin_role.members.add(alice) - inventory.read_role.add(bob) - - + response = options(reverse('api:inventory_hosts_list', args=[inventory.pk]), alice) + assert 'POST' in response.data['actions'] + response = options(reverse('api:inventory_groups_list', args=[inventory.pk]), alice) + assert 'POST' in response.data['actions'] + + +@pytest.mark.django_db +def test_inventory_group_host_can_not_add(inventory, bob, options): + inventory.read_role.members.add(bob) + + response = options(reverse('api:inventory_hosts_list', args=[inventory.pk]), bob) + assert 'POST' not in response.data['actions'] + response = options(reverse('api:inventory_groups_list', args=[inventory.pk]), bob) + assert 'POST' not in response.data['actions'] From a32dd5b535f02614f03836ee77c6f8b60b3feb5c Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Tue, 30 Aug 2016 14:02:57 -0400 Subject: [PATCH 07/51] introduce summary_fields for users, showing user_capabilities --- awx/api/serializers.py | 2 +- awx/main/access.py | 9 ++++----- awx/main/tests/functional/api/test_adding_options.py | 11 ++++++++++- 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/awx/api/serializers.py b/awx/api/serializers.py index 69824258ea..44eabc22d5 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -690,7 +690,7 @@ class UserSerializer(BaseSerializer): class Meta: model = User fields = ('*', '-name', '-description', '-modified', - '-summary_fields', 'username', 'first_name', 'last_name', + 'username', 'first_name', 'last_name', 'email', 'is_superuser', 'is_system_auditor', 'password', 'ldap_dn', 'external_account') def to_representation(self, obj): diff --git a/awx/main/access.py b/awx/main/access.py index f80f3a7865..01eb84eedb 100644 --- a/awx/main/access.py +++ b/awx/main/access.py @@ -228,8 +228,6 @@ class BaseAccess(object): # elif hasattr(obj, 'can_edit'): # user_capabilities['change'] = obj.can_edit - print(type(obj)) - for display_method in ['edit', 'delete', 'start', 'schedule', 'copy']: # Custom ordering of methods used so we can reuse earlier calcs if display_method not in method_list: @@ -251,7 +249,6 @@ class BaseAccess(object): # Preprocessing before the access method is called data = None - sub_obj = None if method == 'add': data = {} @@ -269,10 +266,12 @@ class BaseAccess(object): try: - if method in ['change', 'start', 'delete']: # 3 args + if method in ['change', 'start']: # 3 args user_capabilities[display_method] = self.user.can_access(type(obj), method, obj, data) - elif method == 'add': # 2 args + elif method in ['delete']: # 2 args user_capabilities[display_method] = self.user.can_access(type(obj), method, obj) + elif method in ['add']: # 2 args with data + user_capabilities[display_method] = self.user.can_access(type(obj), method, data) except Exception as exc: diff --git a/awx/main/tests/functional/api/test_adding_options.py b/awx/main/tests/functional/api/test_adding_options.py index 08ecf27a4f..e271c20188 100644 --- a/awx/main/tests/functional/api/test_adding_options.py +++ b/awx/main/tests/functional/api/test_adding_options.py @@ -11,7 +11,6 @@ def test_inventory_group_host_can_add(inventory, alice, options): response = options(reverse('api:inventory_groups_list', args=[inventory.pk]), alice) assert 'POST' in response.data['actions'] - @pytest.mark.django_db def test_inventory_group_host_can_not_add(inventory, bob, options): inventory.read_role.members.add(bob) @@ -20,3 +19,13 @@ def test_inventory_group_host_can_not_add(inventory, bob, options): assert 'POST' not in response.data['actions'] response = options(reverse('api:inventory_groups_list', args=[inventory.pk]), bob) assert 'POST' not in response.data['actions'] + +@pytest.mark.django_db +def test_user_list_can_add(org_member, org_admin, options): + response = options(reverse('api:user_list'), org_admin) + assert 'POST' in response.data['actions'] + +@pytest.mark.django_db +def test_user_list_can_not_add(org_member, org_admin, options): + response = options(reverse('api:user_list'), org_member) + assert 'POST' not in response.data['actions'] From 766a7420a1d343b3a9bc5771cdb20fb06716dde8 Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Tue, 30 Aug 2016 15:42:52 -0400 Subject: [PATCH 08/51] implement user capabilities in access_list --- awx/api/serializers.py | 4 ++++ awx/main/access.py | 9 +++++++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/awx/api/serializers.py b/awx/api/serializers.py index 44eabc22d5..3be4efd387 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -1567,6 +1567,8 @@ class ResourceAccessListElementSerializer(UserSerializer): role_dict['resource_name'] = role.content_object.name role_dict['resource_type'] = role.content_type.name role_dict['related'] = reverse_gfk(role.content_object) + role_dict['user_capabilities'] = {'unattach': requesting_user.can_access( + Role, 'unattach', role, user, 'members', data={}, skip_sub_obj_read_check=False)} except: pass return { 'role': role_dict, 'descendant_roles': get_roles_on_resource(obj, role)} @@ -1585,6 +1587,8 @@ class ResourceAccessListElementSerializer(UserSerializer): role_dict['resource_name'] = role.content_object.name role_dict['resource_type'] = role.content_type.name role_dict['related'] = reverse_gfk(role.content_object) + role_dict['user_capabilities'] = {'unattach': requesting_user.can_access( + Role, 'unattach', role, team_role, 'parents', data={}, skip_sub_obj_read_check=False)} except: pass ret.append({ 'role': role_dict, 'descendant_roles': get_roles_on_resource(obj, team_role)}) diff --git a/awx/main/access.py b/awx/main/access.py index 01eb84eedb..bed42abd29 100644 --- a/awx/main/access.py +++ b/awx/main/access.py @@ -1751,8 +1751,13 @@ class RoleAccess(BaseAccess): @check_superuser def can_unattach(self, obj, sub_obj, relationship, data=None, skip_sub_obj_read_check=False): - if not skip_sub_obj_read_check and relationship in ['members', 'member_role.parents']: - if not check_user_access(self.user, sub_obj.__class__, 'read', sub_obj): + if not skip_sub_obj_read_check and relationship in ['members', 'member_role.parents', 'parents']: + # If we are unattaching a team Role, check the Team read access + if relationship == 'parents': + sub_obj_resource = sub_obj.content_object + else: + sub_obj_resource = sub_obj + if not check_user_access(self.user, sub_obj_resource.__class__, 'read', sub_obj_resource): return False if isinstance(obj.content_object, ResourceMixin) and \ From 953b192ff8c5a52862a3e008a877bda3d9845a8f Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Tue, 30 Aug 2016 16:56:57 -0400 Subject: [PATCH 09/51] polishing of some remaining bugs --- awx/api/serializers.py | 3 ++- awx/main/access.py | 16 ++++++++++------ 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/awx/api/serializers.py b/awx/api/serializers.py index 3be4efd387..2982fe4012 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -503,7 +503,6 @@ class BaseFactSerializer(BaseSerializer): return ret class UnifiedJobTemplateSerializer(BaseSerializer): - show_capabilities = ['start', 'delete'] class Meta: model = UnifiedJobTemplate @@ -545,6 +544,7 @@ class UnifiedJobTemplateSerializer(BaseSerializer): class UnifiedJobSerializer(BaseSerializer): + show_capabilities = ['start', 'delete'] result_stdout = serializers.SerializerMethodField() @@ -1286,6 +1286,7 @@ class GroupVariableDataSerializer(BaseVariableDataSerializer): class CustomInventoryScriptSerializer(BaseSerializer): script = serializers.CharField(trim_whitespace=False) + show_capabilities = ['edit', 'delete'] class Meta: model = CustomInventoryScript diff --git a/awx/main/access.py b/awx/main/access.py index bed42abd29..68565db802 100644 --- a/awx/main/access.py +++ b/awx/main/access.py @@ -234,6 +234,7 @@ class BaseAccess(object): continue # Aliases for going form UI language to API language + # speedups in certain cases by deferring to earlier property if display_method == 'edit': method = 'change' elif display_method == 'copy': @@ -241,7 +242,7 @@ class BaseAccess(object): elif display_method == 'schedule' and 'edit' in user_capabilities: user_capabilities['schedule'] = user_capabilities['edit'] continue - elif display_method == 'delete' and not isinstance(obj, User): + elif display_method == 'delete' and not isinstance(obj, (User, UnifiedJob)): user_capabilities['delete'] = user_capabilities['edit'] continue else: @@ -265,15 +266,12 @@ class BaseAccess(object): data = {'reference_obj': obj} try: - - if method in ['change', 'start']: # 3 args + if method in ['change']: # 3 args user_capabilities[display_method] = self.user.can_access(type(obj), method, obj, data) - elif method in ['delete']: # 2 args + elif method in ['delete', 'start']: # 2 args user_capabilities[display_method] = self.user.can_access(type(obj), method, obj) elif method in ['add']: # 2 args with data user_capabilities[display_method] = self.user.can_access(type(obj), method, data) - - except Exception as exc: user_capabilities[display_method] = False print(exc) @@ -888,6 +886,12 @@ class ProjectUpdateAccess(BaseAccess): # Project updates cascade delete with project, admin role descends from org admin return self.user in obj.project.admin_role + def can_start(self, obj): + # for relaunching + if obj and obj.project: + return self.user in obj.project.update_role + return False + @check_superuser def can_delete(self, obj): return obj and self.user in obj.project.admin_role From fa08c8d4bb47cf7d6183dd41885c1d33fbb4b999 Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Wed, 31 Aug 2016 16:53:47 -0400 Subject: [PATCH 10/51] wrap up some more implementation, and get caching seriously working --- awx/api/serializers.py | 1 - awx/api/views.py | 5 ++--- awx/main/access.py | 20 +++++++++++--------- awx/main/models/projects.py | 3 --- 4 files changed, 13 insertions(+), 16 deletions(-) diff --git a/awx/api/serializers.py b/awx/api/serializers.py index 2982fe4012..1e499b5b37 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -2153,7 +2153,6 @@ class AdHocCommandRelaunchSerializer(AdHocCommandSerializer): class SystemJobTemplateSerializer(UnifiedJobTemplateSerializer): - show_capabilities = ['start', 'schedule', 'edit'] class Meta: model = SystemJobTemplate diff --git a/awx/api/views.py b/awx/api/views.py index ee317285e1..7c9c8673c9 100644 --- a/awx/api/views.py +++ b/awx/api/views.py @@ -1528,10 +1528,9 @@ class InventoryList(ListCreateAPIView): readable_ids = [obj.id for obj in page] editable_ids = Inventory.accessible_objects(request.user, 'admin_role').filter(pk__in=readable_ids).values_list('pk', flat=True) for obj in page: + obj.capabilities_cache = {'edit': False, 'adhoc': False} if obj.pk in editable_ids: - obj.can_edit = True - else: - obj.can_edit = False + obj.capabilities_cache['edit'] = True if page is not None: serializer = self.get_serializer(page, many=True) return self.get_paginated_response(serializer.data) diff --git a/awx/main/access.py b/awx/main/access.py index 68565db802..9ff3564ff8 100644 --- a/awx/main/access.py +++ b/awx/main/access.py @@ -219,18 +219,18 @@ class BaseAccess(object): elif "features" not in validation_info: raise LicenseForbids("Features not found in active license.") - def get_user_capabilities(self, obj, method_list=['edit', 'delete']): + def get_user_capabilities(self, obj, method_list): user_capabilities = {} - # TODO: pull data from the custom cache, which won't be exactly like this - # if hasattr(obj, 'get_can_edit'): - # user_capabilities['change'] = obj.get_can_edit(self.user) - # elif hasattr(obj, 'can_edit'): - # user_capabilities['change'] = obj.can_edit - - for display_method in ['edit', 'delete', 'start', 'schedule', 'copy']: + for display_method in ['edit', 'delete', 'start', 'schedule', 'copy', 'adhoc']: # Custom ordering of methods used so we can reuse earlier calcs if display_method not in method_list: + print ' Programming error: declared unavailable method' + continue + + # Grab the answer from the cache, if available + if hasattr(obj, 'capabilities_cache') and display_method in obj.capabilities_cache: + user_capabilities[display_method] = obj.capabilities_cache[display_method] continue # Aliases for going form UI language to API language @@ -245,6 +245,8 @@ class BaseAccess(object): elif display_method == 'delete' and not isinstance(obj, (User, UnifiedJob)): user_capabilities['delete'] = user_capabilities['edit'] continue + elif display_method == 'adhoc': + method = 'run_ad_hoc_commands' else: method = display_method @@ -268,7 +270,7 @@ class BaseAccess(object): try: if method in ['change']: # 3 args user_capabilities[display_method] = self.user.can_access(type(obj), method, obj, data) - elif method in ['delete', 'start']: # 2 args + elif method in ['delete', 'start', 'adhoc']: # 2 args user_capabilities[display_method] = self.user.can_access(type(obj), method, obj) elif method in ['add']: # 2 args with data user_capabilities[display_method] = self.user.can_access(type(obj), method, data) diff --git a/awx/main/models/projects.py b/awx/main/models/projects.py index a07e1f24ed..93c4a42e36 100644 --- a/awx/main/models/projects.py +++ b/awx/main/models/projects.py @@ -325,9 +325,6 @@ class Project(UnifiedJobTemplate, ProjectOptions, ResourceMixin): kwargs['scm_delete_on_update'] = True return kwargs - def get_can_edit(self, user): - return user in self.admin_role - def create_project_update(self, **kwargs): return self.create_unified_job(**kwargs) From f5c095eb8a9f42f00e47d422b9dfaf32eb50d58e Mon Sep 17 00:00:00 2001 From: jangsutsr Date: Tue, 30 Aug 2016 21:07:23 -0400 Subject: [PATCH 11/51] update task failure traceback text --- awx/main/tasks.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/awx/main/tasks.py b/awx/main/tasks.py index b77275c0fd..c53fd90a29 100644 --- a/awx/main/tasks.py +++ b/awx/main/tasks.py @@ -656,7 +656,8 @@ class BaseTask(Task): if status == 'canceled': raise Exception("Task %s(pk:%s) was canceled (rc=%s)" % (str(self.model.__class__), str(pk), str(rc))) else: - raise Exception("Task %s(pk:%s) encountered an error (rc=%s)" % (str(self.model.__class__), str(pk), str(rc))) + raise Exception("Task %s(pk:%s) encountered an error (rc=%s), please see task stdout for details." % + (str(self.model.__class__), str(pk), str(rc))) if not hasattr(settings, 'CELERY_UNIT_TEST'): self.signal_finished(pk) From 398e9466f866d363be897405a2b67bbe4d1129aa Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Thu, 1 Sep 2016 15:05:33 -0400 Subject: [PATCH 12/51] refactor step 1, get access_class within method --- awx/api/views.py | 6 ++++++ awx/main/access.py | 30 +++++++++++++++++------------- 2 files changed, 23 insertions(+), 13 deletions(-) diff --git a/awx/api/views.py b/awx/api/views.py index 7c9c8673c9..d7a2291ec9 100644 --- a/awx/api/views.py +++ b/awx/api/views.py @@ -1515,6 +1515,7 @@ class InventoryList(ListCreateAPIView): model = Inventory serializer_class = InventorySerializer + capabilities_prefetch = ['admin', 'adhoc'] def get_queryset(self): qs = Inventory.accessible_objects(self.request.user, 'read_role') @@ -1522,15 +1523,20 @@ class InventoryList(ListCreateAPIView): return qs def list(self, request, *args, **kwargs): + if not hasattr(self, 'capabilities_prefetch'): + return super(ListCreateAPIView, self).list(request, *args, **kwargs) queryset = self.filter_queryset(self.get_queryset()) page = self.paginate_queryset(queryset) readable_ids = [obj.id for obj in page] editable_ids = Inventory.accessible_objects(request.user, 'admin_role').filter(pk__in=readable_ids).values_list('pk', flat=True) + adhoc_ids = Inventory.accessible_objects(request.user, 'adhoc_role').filter(pk__in=readable_ids).values_list('pk', flat=True) for obj in page: obj.capabilities_cache = {'edit': False, 'adhoc': False} if obj.pk in editable_ids: obj.capabilities_cache['edit'] = True + if obj.pk in adhoc_ids: + obj.capabilities_cache['adhoc'] = True if page is not None: serializer = self.get_serializer(page, many=True) return self.get_paginated_response(serializer.data) diff --git a/awx/main/access.py b/awx/main/access.py index 9ff3564ff8..cc624265f2 100644 --- a/awx/main/access.py +++ b/awx/main/access.py @@ -225,7 +225,6 @@ class BaseAccess(object): for display_method in ['edit', 'delete', 'start', 'schedule', 'copy', 'adhoc']: # Custom ordering of methods used so we can reuse earlier calcs if display_method not in method_list: - print ' Programming error: declared unavailable method' continue # Grab the answer from the cache, if available @@ -255,28 +254,33 @@ class BaseAccess(object): if method == 'add': data = {} + access_instance = self + obj_check = obj if isinstance(obj, (Group, Host)): if method == 'start': if obj.inventory_source: - obj = obj.inventory_source + obj_check = obj.inventory_source else: user_capabilities[method] = False continue else: - obj = obj.inventory + obj_check = obj.inventory + access_class = access_registry.get(type(obj_check), [])[0] + access_instance = access_class(self.user) if isinstance(obj, JobTemplate): data = {'reference_obj': obj} - try: - if method in ['change']: # 3 args - user_capabilities[display_method] = self.user.can_access(type(obj), method, obj, data) - elif method in ['delete', 'start', 'adhoc']: # 2 args - user_capabilities[display_method] = self.user.can_access(type(obj), method, obj) - elif method in ['add']: # 2 args with data - user_capabilities[display_method] = self.user.can_access(type(obj), method, data) - except Exception as exc: - user_capabilities[display_method] = False - print(exc) + # try: + access_method = getattr(access_instance, "can_%s" % method) + if method in ['change']: # 3 args + user_capabilities[display_method] = access_method(obj_check, data) + elif method in ['delete', 'start', 'run_ad_hoc_commands']: # 2 args + user_capabilities[display_method] = access_method(obj_check) + elif method in ['add']: # 2 args with data + user_capabilities[display_method] = access_method(data) + # except Exception as exc: + # user_capabilities[display_method] = False + # print(exc) return user_capabilities From 0406431337b22ec303830134d7fb75b135c9198e Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Thu, 1 Sep 2016 17:20:58 -0400 Subject: [PATCH 13/51] refactor step 2, move cacher to utils, generics ListView --- awx/api/generics.py | 7 +++++++ awx/api/views.py | 25 +++---------------------- awx/main/utils.py | 27 ++++++++++++++++++++++++++- 3 files changed, 36 insertions(+), 23 deletions(-) diff --git a/awx/api/generics.py b/awx/api/generics.py index 51598979d8..1a3e5e2910 100644 --- a/awx/api/generics.py +++ b/awx/api/generics.py @@ -235,6 +235,13 @@ class ListAPIView(generics.ListAPIView, GenericAPIView): def get_queryset(self): return self.request.user.get_queryset(self.model) + def paginate_queryset(self, queryset): + page = super(ListAPIView, self).paginate_queryset(queryset) + # Queries RBAC info & stores into list objects + if hasattr(self, 'capabilities_prefetch') and page is not None: + cache_list_capabilities(page, self.capabilities_prefetch, self.model, self.request.user) + return page + def get_description_context(self): opts = self.model._meta if 'username' in opts.get_all_field_names(): diff --git a/awx/api/views.py b/awx/api/views.py index d7a2291ec9..dc9d411086 100644 --- a/awx/api/views.py +++ b/awx/api/views.py @@ -963,6 +963,7 @@ class ProjectList(ListCreateAPIView): model = Project serializer_class = ProjectSerializer + capabilities_prefetch = ['admin', 'update'] def get_queryset(self): projects_qs = Project.accessible_objects(self.request.user, 'read_role') @@ -1156,6 +1157,7 @@ class UserList(ListCreateAPIView): model = User serializer_class = UserSerializer + capabilities_prefetch = ['admin'] def post(self, request, *args, **kwargs): ret = super(UserList, self).post( request, *args, **kwargs) @@ -1522,28 +1524,6 @@ class InventoryList(ListCreateAPIView): qs = qs.select_related('admin_role', 'read_role', 'update_role', 'use_role', 'adhoc_role') return qs - def list(self, request, *args, **kwargs): - if not hasattr(self, 'capabilities_prefetch'): - return super(ListCreateAPIView, self).list(request, *args, **kwargs) - queryset = self.filter_queryset(self.get_queryset()) - - page = self.paginate_queryset(queryset) - readable_ids = [obj.id for obj in page] - editable_ids = Inventory.accessible_objects(request.user, 'admin_role').filter(pk__in=readable_ids).values_list('pk', flat=True) - adhoc_ids = Inventory.accessible_objects(request.user, 'adhoc_role').filter(pk__in=readable_ids).values_list('pk', flat=True) - for obj in page: - obj.capabilities_cache = {'edit': False, 'adhoc': False} - if obj.pk in editable_ids: - obj.capabilities_cache['edit'] = True - if obj.pk in adhoc_ids: - obj.capabilities_cache['adhoc'] = True - if page is not None: - serializer = self.get_serializer(page, many=True) - return self.get_paginated_response(serializer.data) - - serializer = self.get_serializer(queryset, many=True) - return Response(serializer.data) - class InventoryDetail(RetrieveUpdateDestroyAPIView): model = Inventory @@ -2224,6 +2204,7 @@ class JobTemplateList(ListCreateAPIView): model = JobTemplate serializer_class = JobTemplateSerializer always_allow_superuser = False + capabilities_prefetch = ['admin', 'execute'] def post(self, request, *args, **kwargs): ret = super(JobTemplateList, self).post(request, *args, **kwargs) diff --git a/awx/main/utils.py b/awx/main/utils.py index 63235ffca3..f65698f052 100644 --- a/awx/main/utils.py +++ b/awx/main/utils.py @@ -33,7 +33,7 @@ logger = logging.getLogger('awx.main.utils') __all__ = ['get_object_or_400', 'get_object_or_403', 'camelcase_to_underscore', 'memoize', 'get_ansible_version', 'get_ssh_version', 'get_awx_version', 'update_scm_url', - 'get_type_for_model', 'get_model_for_type', 'to_python_boolean', + 'get_type_for_model', 'get_model_for_type', 'cache_list_capabilities', 'to_python_boolean', 'ignore_inventory_computed_fields', 'ignore_inventory_group_removal', '_inventory_updates', 'get_pk_from_dict', 'getattrd', 'NoDefaultProvided', 'get_current_apps', 'set_current_apps'] @@ -409,6 +409,31 @@ def get_model_for_type(type): return ct_model +def cache_list_capabilities(page, role_types, model, user): + ''' + Given a `page` list of objects, the specified roles for the specified user + are save on each object in the list, using 1 query for each role type + ''' + page_ids = [obj.id for obj in page] + id_lists = {} + for role_type in role_types: + # Role name translation to UI names for methods + display_method = role_type + if role_type == 'admin': + display_method = 'edit' + elif role_type in ['execute', 'update']: + display_method = 'start' + # Query for union of page objects & role accessible_objects + id_lists[display_method] = model.accessible_objects( + user, '%s_role' % role_type).filter(pk__in=page_ids).values_list('pk', flat=True) + # Save data item-by-item + for obj in page: + obj.capabilities_cache = {display_method: False for display_method in id_lists.keys()} + for display_method, id_list in id_lists.iteritems(): + if obj.pk in id_list: + obj.capabilities_cache[display_method] = True + + def get_system_task_capacity(): ''' Measure system memory and use it as a baseline for determining the system's capacity From 1ca7ce1bd435b6a3d1e1b50a8522fd454d23502f Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Fri, 2 Sep 2016 17:07:42 -0400 Subject: [PATCH 14/51] JT OPTIONS based on RBAC, refactoring toward combinational prefetching --- awx/api/serializers.py | 1 + awx/main/access.py | 56 ++++++++++++++++++------------------------ awx/main/utils.py | 18 ++++++++------ 3 files changed, 36 insertions(+), 39 deletions(-) diff --git a/awx/api/serializers.py b/awx/api/serializers.py index 1e499b5b37..05491a829f 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -2544,6 +2544,7 @@ class LabelSerializer(BaseSerializer): return res class ScheduleSerializer(BaseSerializer): + show_capabilities = ['edit', 'delete'] class Meta: model = Schedule diff --git a/awx/main/access.py b/awx/main/access.py index cc624265f2..a00acb6214 100644 --- a/awx/main/access.py +++ b/awx/main/access.py @@ -222,8 +222,8 @@ class BaseAccess(object): def get_user_capabilities(self, obj, method_list): user_capabilities = {} + # Custom ordering to loop through methods so we can reuse earlier calcs for display_method in ['edit', 'delete', 'start', 'schedule', 'copy', 'adhoc']: - # Custom ordering of methods used so we can reuse earlier calcs if display_method not in method_list: continue @@ -233,54 +233,38 @@ class BaseAccess(object): continue # Aliases for going form UI language to API language - # speedups in certain cases by deferring to earlier property if display_method == 'edit': method = 'change' elif display_method == 'copy': method = 'add' - elif display_method == 'schedule' and 'edit' in user_capabilities: - user_capabilities['schedule'] = user_capabilities['edit'] - continue - elif display_method == 'delete' and not isinstance(obj, (User, UnifiedJob)): - user_capabilities['delete'] = user_capabilities['edit'] - continue elif display_method == 'adhoc': method = 'run_ad_hoc_commands' else: method = display_method + # Shortcuts in certain cases by deferring to earlier property + if display_method == 'schedule' and 'edit' in user_capabilities: + user_capabilities['schedule'] = user_capabilities['edit'] + continue + elif display_method == 'delete' and not isinstance(obj, (User, UnifiedJob)): + user_capabilities['delete'] = user_capabilities['edit'] + continue + # Preprocessing before the access method is called data = None - if method == 'add': - data = {} - - access_instance = self - obj_check = obj - if isinstance(obj, (Group, Host)): - if method == 'start': - if obj.inventory_source: - obj_check = obj.inventory_source - else: - user_capabilities[method] = False - continue - else: - obj_check = obj.inventory - access_class = access_registry.get(type(obj_check), [])[0] - access_instance = access_class(self.user) if isinstance(obj, JobTemplate): data = {'reference_obj': obj} + elif method == 'add': + data = {} - # try: - access_method = getattr(access_instance, "can_%s" % method) + # Compute permission + access_method = getattr(self, "can_%s" % method) if method in ['change']: # 3 args - user_capabilities[display_method] = access_method(obj_check, data) + user_capabilities[display_method] = access_method(obj, data) elif method in ['delete', 'start', 'run_ad_hoc_commands']: # 2 args - user_capabilities[display_method] = access_method(obj_check) + user_capabilities[display_method] = access_method(obj) elif method in ['add']: # 2 args with data user_capabilities[display_method] = access_method(data) - # except Exception as exc: - # user_capabilities[display_method] = False - # print(exc) return user_capabilities @@ -603,6 +587,12 @@ class GroupAccess(BaseAccess): "active_jobs": active_jobs}) return True + def can_start(self, obj): + # Used as another alias to inventory_source start access + if obj and obj.inventory_source: + return self.user.can_access(InventorySource, 'start', obj.inventory_source) + return False + class InventorySourceAccess(BaseAccess): ''' I can see inventory sources whenever I can see their group or inventory. @@ -938,7 +928,9 @@ class JobTemplateAccess(BaseAccess): Users who are able to create deploy jobs can also run normal and check (dry run) jobs. ''' if not data: # So the browseable API will work - return True + return ( + Project.accessible_objects(self.user, 'use_role').exists() or + Inventory.accessible_objects(self.user, 'use_role').exists()) # if reference_obj is provided, determine if it can be coppied reference_obj = data.pop('reference_obj', None) diff --git a/awx/main/utils.py b/awx/main/utils.py index f65698f052..df30faf2f3 100644 --- a/awx/main/utils.py +++ b/awx/main/utils.py @@ -416,6 +416,9 @@ def cache_list_capabilities(page, role_types, model, user): ''' page_ids = [obj.id for obj in page] id_lists = {} + for obj in page: + obj.capabilities_cache = {} + for role_type in role_types: # Role name translation to UI names for methods display_method = role_type @@ -423,14 +426,15 @@ def cache_list_capabilities(page, role_types, model, user): display_method = 'edit' elif role_type in ['execute', 'update']: display_method = 'start' + # Query for union of page objects & role accessible_objects - id_lists[display_method] = model.accessible_objects( - user, '%s_role' % role_type).filter(pk__in=page_ids).values_list('pk', flat=True) - # Save data item-by-item - for obj in page: - obj.capabilities_cache = {display_method: False for display_method in id_lists.keys()} - for display_method, id_list in id_lists.iteritems(): - if obj.pk in id_list: + ids_with_role = set(model.accessible_objects( + user, '%s_role' % role_type).filter(pk__in=page_ids).values_list('pk', flat=True)) + + # Save data item-by-item + for obj in page: + obj.capabilities_cache[display_method] = False + if obj.pk in ids_with_role: obj.capabilities_cache[display_method] = True From 9da00c2d38b02bec4de977f93858440f0c45619f Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Wed, 7 Sep 2016 17:08:07 -0400 Subject: [PATCH 15/51] RBAC copy/edit displays test refactor --- awx/api/serializers.py | 18 +- awx/main/access.py | 13 +- .../functional/api/test_adding_options.py | 31 --- .../tests/functional/api/test_job_template.py | 128 +---------- .../functional/api/test_rbac_displays.py | 212 ++++++++++++++++++ awx/main/tests/unit/api/test_serializers.py | 37 ++- awx/main/tests/unit/test_access.py | 24 ++ awx/main/utils.py | 1 - 8 files changed, 278 insertions(+), 186 deletions(-) delete mode 100644 awx/main/tests/functional/api/test_adding_options.py create mode 100644 awx/main/tests/functional/api/test_rbac_displays.py diff --git a/awx/api/serializers.py b/awx/api/serializers.py index 05491a829f..9bc5e3ecc9 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -1539,6 +1539,7 @@ class RoleSerializer(BaseSerializer): class ResourceAccessListElementSerializer(UserSerializer): + show_capabilities = [] # Clear fields from UserSerializer parent class def to_representation(self, user): ''' @@ -1564,14 +1565,12 @@ class ResourceAccessListElementSerializer(UserSerializer): def format_role_perm(role): role_dict = { 'id': role.id, 'name': role.name, 'description': role.description} - try: + if role.content_type is not None: role_dict['resource_name'] = role.content_object.name role_dict['resource_type'] = role.content_type.name role_dict['related'] = reverse_gfk(role.content_object) - role_dict['user_capabilities'] = {'unattach': requesting_user.can_access( - Role, 'unattach', role, user, 'members', data={}, skip_sub_obj_read_check=False)} - except: - pass + role_dict['user_capabilities'] = {'unattach': requesting_user.can_access( + Role, 'unattach', role, user, 'members', data={}, skip_sub_obj_read_check=False)} return { 'role': role_dict, 'descendant_roles': get_roles_on_resource(obj, role)} def format_team_role_perm(team_role, permissive_role_ids): @@ -1584,14 +1583,12 @@ class ResourceAccessListElementSerializer(UserSerializer): 'team_id': team_role.object_id, 'team_name': team_role.content_object.name } - try: + if role.content_type is not None: role_dict['resource_name'] = role.content_object.name role_dict['resource_type'] = role.content_type.name role_dict['related'] = reverse_gfk(role.content_object) - role_dict['user_capabilities'] = {'unattach': requesting_user.can_access( - Role, 'unattach', role, team_role, 'parents', data={}, skip_sub_obj_read_check=False)} - except: - pass + role_dict['user_capabilities'] = {'unattach': requesting_user.can_access( + Role, 'unattach', role, team_role, 'parents', data={}, skip_sub_obj_read_check=False)} ret.append({ 'role': role_dict, 'descendant_roles': get_roles_on_resource(obj, team_role)}) return ret @@ -1885,6 +1882,7 @@ class JobTemplateSerializer(UnifiedJobTemplateSerializer, JobOptionsSerializer): d['survey'] = dict(title=obj.survey_spec['name'], description=obj.survey_spec['description']) request = self.context.get('request', None) + # Remove the can_copy and can_edit fields when dependencies are fully removed # Check for conditions that would create a validation error if coppied validation_errors, resources_needed_to_start = obj.resource_validation_data() diff --git a/awx/main/access.py b/awx/main/access.py index a00acb6214..b46954db24 100644 --- a/awx/main/access.py +++ b/awx/main/access.py @@ -249,13 +249,16 @@ class BaseAccess(object): elif display_method == 'delete' and not isinstance(obj, (User, UnifiedJob)): user_capabilities['delete'] = user_capabilities['edit'] continue + if display_method == 'copy' and isinstance(obj, JobTemplate): + validation_errors, resources_needed_to_start = obj.resource_validation_data() + if validation_errors: + user_capabilities['copy'] = False + continue # Preprocessing before the access method is called - data = None - if isinstance(obj, JobTemplate): - data = {'reference_obj': obj} - elif method == 'add': - data = {} + data = {} + if method == 'add' and isinstance(obj, JobTemplate): + data['reference_obj'] = obj # Compute permission access_method = getattr(self, "can_%s" % method) diff --git a/awx/main/tests/functional/api/test_adding_options.py b/awx/main/tests/functional/api/test_adding_options.py deleted file mode 100644 index e271c20188..0000000000 --- a/awx/main/tests/functional/api/test_adding_options.py +++ /dev/null @@ -1,31 +0,0 @@ -import pytest - -from django.core.urlresolvers import reverse - -@pytest.mark.django_db -def test_inventory_group_host_can_add(inventory, alice, options): - inventory.admin_role.members.add(alice) - - response = options(reverse('api:inventory_hosts_list', args=[inventory.pk]), alice) - assert 'POST' in response.data['actions'] - response = options(reverse('api:inventory_groups_list', args=[inventory.pk]), alice) - assert 'POST' in response.data['actions'] - -@pytest.mark.django_db -def test_inventory_group_host_can_not_add(inventory, bob, options): - inventory.read_role.members.add(bob) - - response = options(reverse('api:inventory_hosts_list', args=[inventory.pk]), bob) - assert 'POST' not in response.data['actions'] - response = options(reverse('api:inventory_groups_list', args=[inventory.pk]), bob) - assert 'POST' not in response.data['actions'] - -@pytest.mark.django_db -def test_user_list_can_add(org_member, org_admin, options): - response = options(reverse('api:user_list'), org_admin) - assert 'POST' in response.data['actions'] - -@pytest.mark.django_db -def test_user_list_can_not_add(org_member, org_admin, options): - response = options(reverse('api:user_list'), org_member) - assert 'POST' not in response.data['actions'] diff --git a/awx/main/tests/functional/api/test_job_template.py b/awx/main/tests/functional/api/test_job_template.py index 88437a0037..68a7e7aecd 100644 --- a/awx/main/tests/functional/api/test_job_template.py +++ b/awx/main/tests/functional/api/test_job_template.py @@ -3,12 +3,11 @@ import mock # AWX from awx.api.serializers import JobTemplateSerializer, JobLaunchSerializer -from awx.main.models.jobs import JobTemplate, Job +from awx.main.models.jobs import Job from awx.main.models.projects import ProjectOptions from awx.main.migrations import _save_password_keys as save_password_keys # Django -from django.test.client import RequestFactory from django.core.urlresolvers import reverse from django.apps import apps @@ -141,131 +140,7 @@ def test_job_template_role_user(post, organization_factory, job_template_factory response = post(url, dict(id=jt_objects.job_template.execute_role.pk), objects.superusers.admin) assert response.status_code == 204 -# Test protection against limited set of validation problems -@pytest.mark.django_db -def test_bad_data_copy_edit(admin_user, project): - """ - If a required resource (inventory here) was deleted, copying not allowed - because doing so would caues a validation error - """ - - jt_res = JobTemplate.objects.create( - job_type='run', - project=project, - inventory=None, ask_inventory_on_launch=False, # not allowed - credential=None, ask_credential_on_launch=True, - name='deploy-job-template' - ) - serializer = JobTemplateSerializer(jt_res) - request = RequestFactory().get('/api/v1/job_templates/12/') - request.user = admin_user - serializer.context['request'] = request - response = serializer.to_representation(jt_res) - assert not response['summary_fields']['can_copy'] - assert response['summary_fields']['can_edit'] - -# Tests for correspondence between view info and actual access - -@pytest.mark.django_db -def test_admin_copy_edit(jt_copy_edit, admin_user): - "Absent a validation error, system admins can do everything" - - # Serializer can_copy/can_edit fields - serializer = JobTemplateSerializer(jt_copy_edit) - request = RequestFactory().get('/api/v1/job_templates/12/') - request.user = admin_user - serializer.context['request'] = request - response = serializer.to_representation(jt_copy_edit) - assert response['summary_fields']['can_copy'] - assert response['summary_fields']['can_edit'] - -@pytest.mark.django_db -def test_org_admin_copy_edit(jt_copy_edit, org_admin): - "Organization admins SHOULD be able to copy a JT firmly in their org" - - # Serializer can_copy/can_edit fields - serializer = JobTemplateSerializer(jt_copy_edit) - request = RequestFactory().get('/api/v1/job_templates/12/') - request.user = org_admin - serializer.context['request'] = request - response = serializer.to_representation(jt_copy_edit) - assert response['summary_fields']['can_copy'] - assert response['summary_fields']['can_edit'] - -@pytest.mark.django_db -def test_org_admin_foreign_cred_no_copy_edit(jt_copy_edit, org_admin, machine_credential): - """ - Organization admins without access to the 3 related resources: - SHOULD NOT be able to copy JT - SHOULD be able to edit that job template, for nonsensitive changes - """ - - # Attach credential to JT that org admin can not use - jt_copy_edit.credential = machine_credential - jt_copy_edit.save() - - # Serializer can_copy/can_edit fields - serializer = JobTemplateSerializer(jt_copy_edit) - request = RequestFactory().get('/api/v1/job_templates/12/') - request.user = org_admin - serializer.context['request'] = request - response = serializer.to_representation(jt_copy_edit) - assert not response['summary_fields']['can_copy'] - assert response['summary_fields']['can_edit'] - -@pytest.mark.django_db -def test_jt_admin_copy_edit(jt_copy_edit, rando): - """ - JT admins wihout access to associated resources SHOULD NOT be able to copy - SHOULD be able to make nonsensitive changes""" - - # random user given JT admin access only - jt_copy_edit.admin_role.members.add(rando) - jt_copy_edit.save() - - # Serializer can_copy/can_edit fields - serializer = JobTemplateSerializer(jt_copy_edit) - request = RequestFactory().get('/api/v1/job_templates/12/') - request.user = rando - serializer.context['request'] = request - response = serializer.to_representation(jt_copy_edit) - assert not response['summary_fields']['can_copy'] - assert response['summary_fields']['can_edit'] - -@pytest.mark.django_db -def test_proj_jt_admin_copy_edit(jt_copy_edit, rando): - "JT admins with access to associated resources SHOULD be able to copy" - - # random user given JT and project admin abilities - jt_copy_edit.admin_role.members.add(rando) - jt_copy_edit.save() - jt_copy_edit.project.admin_role.members.add(rando) - jt_copy_edit.project.save() - - # Serializer can_copy/can_edit fields - serializer = JobTemplateSerializer(jt_copy_edit) - request = RequestFactory().get('/api/v1/job_templates/12/') - request.user = rando - serializer.context['request'] = request - response = serializer.to_representation(jt_copy_edit) - assert response['summary_fields']['can_copy'] - assert response['summary_fields']['can_edit'] - -# Functional tests - create new JT with all returned fields, as the UI does - -@pytest.mark.django_db -@mock.patch.object(ProjectOptions, "playbooks", project_playbooks) -def test_org_admin_copy_edit_functional(jt_copy_edit, org_admin, get, post): - get_response = get(reverse('api:job_template_detail', args=[jt_copy_edit.pk]), user=org_admin) - assert get_response.status_code == 200 - assert get_response.data['summary_fields']['can_copy'] - - post_data = get_response.data - post_data['name'] = '%s @ 12:19:47 pm' % post_data['name'] - post_response = post(reverse('api:job_template_list', args=[]), user=org_admin, data=post_data) - assert post_response.status_code == 201 - assert post_response.data['name'] == 'copy-edit-job-template @ 12:19:47 pm' @pytest.mark.django_db @mock.patch.object(ProjectOptions, "playbooks", project_playbooks) @@ -277,7 +152,6 @@ def test_jt_admin_copy_edit_functional(jt_copy_edit, rando, get, post): get_response = get(reverse('api:job_template_detail', args=[jt_copy_edit.pk]), user=rando) assert get_response.status_code == 200 - assert not get_response.data['summary_fields']['can_copy'] post_data = get_response.data post_data['name'] = '%s @ 12:19:47 pm' % post_data['name'] diff --git a/awx/main/tests/functional/api/test_rbac_displays.py b/awx/main/tests/functional/api/test_rbac_displays.py new file mode 100644 index 0000000000..aaf97b3782 --- /dev/null +++ b/awx/main/tests/functional/api/test_rbac_displays.py @@ -0,0 +1,212 @@ +import pytest + +from django.core.urlresolvers import reverse +from django.test.client import RequestFactory + +from awx.main.models.jobs import JobTemplate +from awx.main.models import Role +from awx.api.serializers import JobTemplateSerializer +from awx.main.access import access_registry + + +# This file covers special-cases of displays of user_capabilities +# general functionality should be covered fully by unit tests, see: +# awx/main/tests/unit/api/test_serializers.py :: +# TestJobTemplateSerializerGetSummaryFields.test_copy_edit_standard +# awx/main/tests/unit/test_access.py :: +# test_user_capabilities_method + +class FakeView(object): + pass + +@pytest.fixture +def jt_copy_edit(job_template_factory, project): + objects = job_template_factory( + 'copy-edit-job-template', + project=project) + return objects.job_template + + +@pytest.mark.django_db +def test_inventory_group_host_can_add(inventory, alice, options): + inventory.admin_role.members.add(alice) + + response = options(reverse('api:inventory_hosts_list', args=[inventory.pk]), alice) + assert 'POST' in response.data['actions'] + response = options(reverse('api:inventory_groups_list', args=[inventory.pk]), alice) + assert 'POST' in response.data['actions'] + +@pytest.mark.django_db +def test_inventory_group_host_can_not_add(inventory, bob, options): + inventory.read_role.members.add(bob) + + response = options(reverse('api:inventory_hosts_list', args=[inventory.pk]), bob) + assert 'POST' not in response.data['actions'] + response = options(reverse('api:inventory_groups_list', args=[inventory.pk]), bob) + assert 'POST' not in response.data['actions'] + +@pytest.mark.django_db +def test_user_list_can_add(org_member, org_admin, options): + response = options(reverse('api:user_list'), org_admin) + assert 'POST' in response.data['actions'] + +@pytest.mark.django_db +def test_user_list_can_not_add(org_member, org_admin, options): + response = options(reverse('api:user_list'), org_member) + assert 'POST' not in response.data['actions'] + + +def fake_context(user): + request = RequestFactory().get('/api/v1/resource/42/') + request.user = user + fake_view = FakeView() + fake_view.request = request + context = {} + context['view'] = fake_view + context['request'] = request + return context + +# Test protection against limited set of validation problems + +@pytest.mark.django_db +def test_bad_data_copy_edit(admin_user, project): + """ + If a required resource (inventory here) was deleted, copying not allowed + because doing so would caues a validation error + """ + + jt_res = JobTemplate.objects.create( + job_type='run', + project=project, + inventory=None, ask_inventory_on_launch=False, # not allowed + credential=None, ask_credential_on_launch=True, + name='deploy-job-template' + ) + serializer = JobTemplateSerializer(jt_res) + serializer.context = fake_context(admin_user) + response = serializer.to_representation(jt_res) + assert not response['summary_fields']['user_capabilities']['copy'] + assert response['summary_fields']['user_capabilities']['edit'] + +# Tests for correspondence between view info and intended access + +@pytest.mark.django_db +def test_sys_admin_copy_edit(jt_copy_edit, admin_user): + "Absent a validation error, system admins can do everything" + serializer = JobTemplateSerializer(jt_copy_edit) + serializer.context = fake_context(admin_user) + response = serializer.to_representation(jt_copy_edit) + assert response['summary_fields']['user_capabilities']['copy'] + assert response['summary_fields']['user_capabilities']['edit'] + +@pytest.mark.django_db +def test_org_admin_copy_edit(jt_copy_edit, org_admin): + "Organization admins SHOULD be able to copy a JT firmly in their org" + serializer = JobTemplateSerializer(jt_copy_edit) + serializer.context = fake_context(org_admin) + response = serializer.to_representation(jt_copy_edit) + assert response['summary_fields']['user_capabilities']['copy'] + assert response['summary_fields']['user_capabilities']['edit'] + +@pytest.mark.django_db +def test_org_admin_foreign_cred_no_copy_edit(jt_copy_edit, org_admin, machine_credential): + """ + Organization admins without access to the 3 related resources: + SHOULD NOT be able to copy JT + SHOULD be able to edit that job template, for nonsensitive changes + """ + + # Attach credential to JT that org admin can not use + jt_copy_edit.credential = machine_credential + jt_copy_edit.save() + + serializer = JobTemplateSerializer(jt_copy_edit) + serializer.context = fake_context(org_admin) + response = serializer.to_representation(jt_copy_edit) + assert not response['summary_fields']['user_capabilities']['copy'] + assert response['summary_fields']['user_capabilities']['edit'] + +@pytest.mark.django_db +def test_jt_admin_copy_edit(jt_copy_edit, rando): + """ + JT admins wihout access to associated resources SHOULD NOT be able to copy + SHOULD be able to make nonsensitive changes""" + + # random user given JT admin access only + jt_copy_edit.admin_role.members.add(rando) + jt_copy_edit.save() + + serializer = JobTemplateSerializer(jt_copy_edit) + serializer.context = fake_context(rando) + response = serializer.to_representation(jt_copy_edit) + assert not response['summary_fields']['user_capabilities']['copy'] + assert response['summary_fields']['user_capabilities']['edit'] + +@pytest.mark.django_db +def test_proj_jt_admin_copy_edit(jt_copy_edit, rando): + "JT admins with access to associated resources SHOULD be able to copy" + + # random user given JT and project admin abilities + jt_copy_edit.admin_role.members.add(rando) + jt_copy_edit.save() + jt_copy_edit.project.admin_role.members.add(rando) + jt_copy_edit.project.save() + + serializer = JobTemplateSerializer(jt_copy_edit) + serializer.context = fake_context(rando) + response = serializer.to_representation(jt_copy_edit) + assert response['summary_fields']['user_capabilities']['copy'] + assert response['summary_fields']['user_capabilities']['edit'] + + +@pytest.mark.django_db +class TestAccessListCapabilities: + @pytest.fixture + def mock_access_method(self, mocker): + "Mocking this requires extra work because of the logging statement" + mock_method = mocker.MagicMock() + mock_method.return_value = 'foobar' + mock_method.__name__ = 'bars' + return mock_method + + def _assert_one_in_list(self, data, sublist='direct_access'): + assert len(data['results']) == 1 + assert len(data['results'][0]['summary_fields'][sublist]) == 1 + + def test_access_list_direct_access_capability(self, inventory, rando, get, mocker, mock_access_method): + """Test that the access_list serializer shows the exact output of the + RoleAccess.can_attach method in the direct_access list""" + inventory.admin_role.members.add(rando) + with mocker.patch.object(access_registry[Role][0], 'can_unattach', mock_access_method): + response = get(reverse('api:inventory_access_list', args=(inventory.id,)), rando) + self._assert_one_in_list(response.data) + direct_access_list = response.data['results'][0]['summary_fields']['direct_access'] + assert direct_access_list[0]['role']['user_capabilities']['unattach'] == 'foobar' + + def test_access_list_indirect_access_capability(self, inventory, admin_user, get, mocker, mock_access_method): + """Test the display of unattach access for a singleton permission""" + with mocker.patch.object(access_registry[Role][0], 'can_unattach', mock_access_method): + response = get(reverse('api:inventory_access_list', args=(inventory.id,)), admin_user) + self._assert_one_in_list(response.data, sublist='indirect_access') + indirect_access_list = response.data['results'][0]['summary_fields']['indirect_access'] + assert indirect_access_list[0]['role']['user_capabilities']['unattach'] == 'foobar' + + def test_access_list_team_direct_access_capability(self, inventory, team, team_member, get, mocker, mock_access_method): + """Test the display of unattach access for team-based permissions + this happens in a difference place in the serializer code from the user permission""" + team.member_role.children.add(inventory.admin_role) + with mocker.patch.object(access_registry[Role][0], 'can_unattach', mock_access_method): + response = get(reverse('api:inventory_access_list', args=(inventory.id,)), team_member) + self._assert_one_in_list(response.data) + direct_access_list = response.data['results'][0]['summary_fields']['direct_access'] + assert direct_access_list[0]['role']['user_capabilities']['unattach'] == 'foobar' + + +@pytest.mark.django_db +def test_team_roles_unattach(mocker): + pass + +@pytest.mark.django_db +def test_user_roles_unattach(mocker): + pass + diff --git a/awx/main/tests/unit/api/test_serializers.py b/awx/main/tests/unit/api/test_serializers.py index 2496ba9a2d..1d64a99246 100644 --- a/awx/main/tests/unit/api/test_serializers.py +++ b/awx/main/tests/unit/api/test_serializers.py @@ -11,7 +11,9 @@ from awx.api.serializers import ( JobOptionsSerializer, CustomInventoryScriptSerializer, ) +from awx.api.views import JobTemplateDetail from awx.main.models import ( + Role, Label, Job, CustomInventoryScript, @@ -123,21 +125,32 @@ class TestJobTemplateSerializerGetSummaryFields(GetSummaryFieldsMixin): summary = self._mock_and_run(JobTemplateSerializer, job_template) assert 'survey' not in summary - @pytest.mark.skip(reason="RBAC needs to land") - def test_can_copy_true(self, mocker, job_template): - pass + def test_copy_edit_standard(self, mocker, job_template_factory): + """Verify that the exact output of the access.py methods + are put into the serializer user_capabilities""" - @pytest.mark.skip(reason="RBAC needs to land") - def test_can_copy_false(self, mocker, job_template): - pass + jt_obj = job_template_factory('testJT', project='proj1', persisted=False).job_template + jt_obj.id = 5 + jt_obj.admin_role = Role(id=9, role_field='admin_role') + jt_obj.execute_role = Role(id=8, role_field='execute_role') + jt_obj.read_role = Role(id=7, role_field='execute_role') + user = User(username="auser") + serializer = JobTemplateSerializer(job_template) + serializer.show_capabilities = ['copy', 'edit'] + serializer._summary_field_labels = lambda self: [] + serializer._recent_jobs = lambda self: [] + request = APIRequestFactory().get('/api/v1/job_templates/42/') + request.user = user + view = JobTemplateDetail() + view.request = request + serializer.context['view'] = view - @pytest.mark.skip(reason="RBAC needs to land") - def test_can_edit_true(self, mocker, job_template): - pass + with mocker.patch("awx.main.access.JobTemplateAccess.can_change", return_value='foobar'): + with mocker.patch("awx.main.access.JobTemplateAccess.can_add", return_value='foo'): + response = serializer.get_summary_fields(jt_obj) - @pytest.mark.skip(reason="RBAC needs to land") - def test_can_edit_false(self, mocker, job_template): - pass + assert response['user_capabilities']['copy'] == 'foo' + assert response['user_capabilities']['edit'] == 'foobar' @mock.patch('awx.api.serializers.UnifiedJobTemplateSerializer.get_related', lambda x,y: {}) @mock.patch('awx.api.serializers.JobOptionsSerializer.get_related', lambda x,y: {}) diff --git a/awx/main/tests/unit/test_access.py b/awx/main/tests/unit/test_access.py index 000d91268c..2b111f5b4f 100644 --- a/awx/main/tests/unit/test_access.py +++ b/awx/main/tests/unit/test_access.py @@ -110,3 +110,27 @@ def test_jt_can_add_bad_data(user_unit): access = JobTemplateAccess(user_unit) assert not access.can_add({'asdf': 'asdf'}) +@pytest.mark.django_db +def test_user_capabilities_method(): + """Unit test to verify that the user_capabilities method will defer + to the appropriate sub-class methods of the access classes. + Note that normal output is True/False, but a string is returned + in these tests to establish uniqueness. + """ + + class FooAccess(BaseAccess): + def can_change(self, obj, data): + return 'bar' + + def can_add(self, data): + return 'foobar' + + user = User(username='auser') + foo_access = FooAccess(user) + foo = object() + foo_capabilities = foo_access.get_user_capabilities(foo, ['edit', 'copy']) + assert foo_capabilities == { + 'edit': 'bar', + 'copy': 'foobar' + } + diff --git a/awx/main/utils.py b/awx/main/utils.py index df30faf2f3..06227c59e3 100644 --- a/awx/main/utils.py +++ b/awx/main/utils.py @@ -415,7 +415,6 @@ def cache_list_capabilities(page, role_types, model, user): are save on each object in the list, using 1 query for each role type ''' page_ids = [obj.id for obj in page] - id_lists = {} for obj in page: obj.capabilities_cache = {} From 6b0df43f3b8bc5378d802e8f6b8926cad95c6a06 Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Thu, 8 Sep 2016 11:09:17 -0400 Subject: [PATCH 16/51] add unattach field to user and team roles list --- awx/api/serializers.py | 25 ++++-- awx/api/views.py | 4 +- awx/main/access.py | 16 +++- .../functional/api/test_rbac_displays.py | 90 ++++++++++++++----- 4 files changed, 104 insertions(+), 31 deletions(-) diff --git a/awx/api/serializers.py b/awx/api/serializers.py index 9bc5e3ecc9..417b44db87 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -331,10 +331,16 @@ class BaseSerializer(serializers.ModelSerializer): } if len(roles) > 0: summary_fields['object_roles'] = roles + + # Advance display of RBAC capabilities if hasattr(self, 'show_capabilities'): view = self.context.get('view', None) + parent_obj = None + if hasattr(view, 'parent_model'): + parent_obj = view.get_parent_object() if view and view.request and view.request.user: - user_capabilities = get_user_capabilities(view.request.user, obj, self.show_capabilities) + user_capabilities = get_user_capabilities( + view.request.user, obj, method_list=self.show_capabilities, parent_obj=parent_obj) if user_capabilities: summary_fields['user_capabilities'] = user_capabilities @@ -1537,6 +1543,9 @@ class RoleSerializer(BaseSerializer): return ret +class RoleSerializerWithParentAccess(RoleSerializer): + show_capabilities = ['unattach'] + class ResourceAccessListElementSerializer(UserSerializer): show_capabilities = [] # Clear fields from UserSerializer parent class @@ -1569,8 +1578,11 @@ class ResourceAccessListElementSerializer(UserSerializer): role_dict['resource_name'] = role.content_object.name role_dict['resource_type'] = role.content_type.name role_dict['related'] = reverse_gfk(role.content_object) - role_dict['user_capabilities'] = {'unattach': requesting_user.can_access( - Role, 'unattach', role, user, 'members', data={}, skip_sub_obj_read_check=False)} + role_dict['user_capabilities'] = {'unattach': requesting_user.can_access( + Role, 'unattach', role, user, 'members', data={}, skip_sub_obj_read_check=False)} + else: + # Singleton roles should not be managed from this view, as per copy/edit rework spec + role_dict['user_capabilities'] = {'unattach': False} return { 'role': role_dict, 'descendant_roles': get_roles_on_resource(obj, role)} def format_team_role_perm(team_role, permissive_role_ids): @@ -1587,8 +1599,11 @@ class ResourceAccessListElementSerializer(UserSerializer): role_dict['resource_name'] = role.content_object.name role_dict['resource_type'] = role.content_type.name role_dict['related'] = reverse_gfk(role.content_object) - role_dict['user_capabilities'] = {'unattach': requesting_user.can_access( - Role, 'unattach', role, team_role, 'parents', data={}, skip_sub_obj_read_check=False)} + role_dict['user_capabilities'] = {'unattach': requesting_user.can_access( + Role, 'unattach', role, team_role, 'parents', data={}, skip_sub_obj_read_check=False)} + else: + # Singleton roles should not be managed from this view, as per copy/edit rework spec + role_dict['user_capabilities'] = {'unattach': False} ret.append({ 'role': role_dict, 'descendant_roles': get_roles_on_resource(obj, team_role)}) return ret diff --git a/awx/api/views.py b/awx/api/views.py index dc9d411086..579566766c 100644 --- a/awx/api/views.py +++ b/awx/api/views.py @@ -864,7 +864,7 @@ class TeamUsersList(BaseUsersList): class TeamRolesList(SubListCreateAttachDetachAPIView): model = Role - serializer_class = RoleSerializer + serializer_class = RoleSerializerWithParentAccess metadata_class = RoleMetadata parent_model = Team relationship='member_role.children' @@ -1197,7 +1197,7 @@ class UserTeamsList(ListAPIView): class UserRolesList(SubListCreateAttachDetachAPIView): model = Role - serializer_class = RoleSerializer + serializer_class = RoleSerializerWithParentAccess metadata_class = RoleMetadata parent_model = User relationship='roles' diff --git a/awx/main/access.py b/awx/main/access.py index b46954db24..bd581893d3 100644 --- a/awx/main/access.py +++ b/awx/main/access.py @@ -116,7 +116,7 @@ def check_user_access(user, model_class, action, *args, **kwargs): return result return False -def get_user_capabilities(user, instance, method_list): +def get_user_capabilities(user, instance, **kwargs): ''' Returns a dictionary of capabilities the user has on the particular instance. *NOTE* This is not a direct mapping of can_* methods into this @@ -125,7 +125,7 @@ def get_user_capabilities(user, instance, method_list): actions in the interface. ''' for access_class in access_registry.get(type(instance), []): - return access_class(user).get_user_capabilities(instance, method_list) + return access_class(user).get_user_capabilities(instance, **kwargs) return None def check_superuser(func): @@ -219,11 +219,11 @@ class BaseAccess(object): elif "features" not in validation_info: raise LicenseForbids("Features not found in active license.") - def get_user_capabilities(self, obj, method_list): + def get_user_capabilities(self, obj, method_list=[], parent_obj=None): user_capabilities = {} # Custom ordering to loop through methods so we can reuse earlier calcs - for display_method in ['edit', 'delete', 'start', 'schedule', 'copy', 'adhoc']: + for display_method in ['edit', 'delete', 'start', 'schedule', 'copy', 'adhoc', 'unattach']: if display_method not in method_list: continue @@ -268,6 +268,14 @@ class BaseAccess(object): user_capabilities[display_method] = access_method(obj) elif method in ['add']: # 2 args with data user_capabilities[display_method] = access_method(data) + elif method in ['attach', 'unattach']: # parent/sub-object call + if type(parent_obj) == Team: + relationship = 'parents' + parent_obj = parent_obj.member_role + else: + relationship = 'members' + user_capabilities[display_method] = access_method( + obj, parent_obj, relationship, skip_sub_obj_read_check=True, data=data) return user_capabilities diff --git a/awx/main/tests/functional/api/test_rbac_displays.py b/awx/main/tests/functional/api/test_rbac_displays.py index aaf97b3782..c203e45b4b 100644 --- a/awx/main/tests/functional/api/test_rbac_displays.py +++ b/awx/main/tests/functional/api/test_rbac_displays.py @@ -159,54 +159,104 @@ def test_proj_jt_admin_copy_edit(jt_copy_edit, rando): assert response['summary_fields']['user_capabilities']['edit'] +@pytest.fixture +def mock_access_method(mocker): + mock_method = mocker.MagicMock() + mock_method.return_value = 'foobar' + mock_method.__name__ = 'bars' # Required for a logging statement + return mock_method + + @pytest.mark.django_db class TestAccessListCapabilities: - @pytest.fixture - def mock_access_method(self, mocker): - "Mocking this requires extra work because of the logging statement" - mock_method = mocker.MagicMock() - mock_method.return_value = 'foobar' - mock_method.__name__ = 'bars' - return mock_method + """ + Test that the access_list serializer shows the exact output of the RoleAccess.can_attach + - looks at /api/v1/inventories/N/access_list/ + - test for types: direct, indirect, and team access + """ + + extra_kwargs = dict(skip_sub_obj_read_check=False, data={}) def _assert_one_in_list(self, data, sublist='direct_access'): + "Establish that exactly 1 type of access exists so we know the entry is the right one" assert len(data['results']) == 1 assert len(data['results'][0]['summary_fields'][sublist]) == 1 - def test_access_list_direct_access_capability(self, inventory, rando, get, mocker, mock_access_method): - """Test that the access_list serializer shows the exact output of the - RoleAccess.can_attach method in the direct_access list""" + def test_access_list_direct_access_capability( + self, inventory, rando, get, mocker, mock_access_method): inventory.admin_role.members.add(rando) + with mocker.patch.object(access_registry[Role][0], 'can_unattach', mock_access_method): response = get(reverse('api:inventory_access_list', args=(inventory.id,)), rando) + + mock_access_method.assert_called_once_with(inventory.admin_role, rando, 'members', **self.extra_kwargs) self._assert_one_in_list(response.data) direct_access_list = response.data['results'][0]['summary_fields']['direct_access'] assert direct_access_list[0]['role']['user_capabilities']['unattach'] == 'foobar' - def test_access_list_indirect_access_capability(self, inventory, admin_user, get, mocker, mock_access_method): - """Test the display of unattach access for a singleton permission""" + def test_access_list_indirect_access_capability( + self, inventory, organization, org_admin, get, mocker, mock_access_method): with mocker.patch.object(access_registry[Role][0], 'can_unattach', mock_access_method): - response = get(reverse('api:inventory_access_list', args=(inventory.id,)), admin_user) + response = get(reverse('api:inventory_access_list', args=(inventory.id,)), org_admin) + + mock_access_method.assert_called_once_with(organization.admin_role, org_admin, 'members', **self.extra_kwargs) self._assert_one_in_list(response.data, sublist='indirect_access') indirect_access_list = response.data['results'][0]['summary_fields']['indirect_access'] assert indirect_access_list[0]['role']['user_capabilities']['unattach'] == 'foobar' - def test_access_list_team_direct_access_capability(self, inventory, team, team_member, get, mocker, mock_access_method): - """Test the display of unattach access for team-based permissions - this happens in a difference place in the serializer code from the user permission""" + def test_access_list_team_direct_access_capability( + self, inventory, team, team_member, get, mocker, mock_access_method): team.member_role.children.add(inventory.admin_role) + with mocker.patch.object(access_registry[Role][0], 'can_unattach', mock_access_method): response = get(reverse('api:inventory_access_list', args=(inventory.id,)), team_member) + + mock_access_method.assert_called_once_with(inventory.admin_role, team.member_role, 'parents', **self.extra_kwargs) self._assert_one_in_list(response.data) direct_access_list = response.data['results'][0]['summary_fields']['direct_access'] assert direct_access_list[0]['role']['user_capabilities']['unattach'] == 'foobar' @pytest.mark.django_db -def test_team_roles_unattach(mocker): - pass +def test_team_roles_unattach(mocker, team, team_member, inventory, mock_access_method, get): + team.member_role.children.add(inventory.admin_role) + + with mocker.patch.object(access_registry[Role][0], 'can_unattach', mock_access_method): + response = get(reverse('api:team_roles_list', args=(team.id,)), team_member) + + # Did we assess whether team_member can remove team's permission to the inventory? + mock_access_method.assert_called_once_with( + inventory.admin_role, team.member_role, 'parents', skip_sub_obj_read_check=True, data={}) + assert response.data['results'][0]['summary_fields']['user_capabilities']['unattach'] == 'foobar' @pytest.mark.django_db -def test_user_roles_unattach(mocker): - pass +def test_user_roles_unattach(mocker, organization, alice, bob, mock_access_method, get): + # Add to same organization so that alice and bob can see each other + organization.member_role.members.add(alice) + organization.member_role.members.add(bob) + + with mocker.patch.object(access_registry[Role][0], 'can_unattach', mock_access_method): + response = get(reverse('api:user_roles_list', args=(alice.id,)), bob) + + # Did we assess whether bob can remove alice's permission to the inventory? + mock_access_method.assert_called_once_with( + organization.member_role, alice, 'members', skip_sub_obj_read_check=True, data={}) + assert response.data['results'][0]['summary_fields']['user_capabilities']['unattach'] == 'foobar' + +@pytest.mark.django_db +def test_team_roles_unattach_functional(team, team_member, inventory, get): + team.member_role.children.add(inventory.admin_role) + response = get(reverse('api:team_roles_list', args=(team.id,)), team_member) + # Team member should be able to remove access to inventory, becauase + # the inventory admin_role grants that ability + assert response.data['results'][0]['summary_fields']['user_capabilities']['unattach'] == True + +@pytest.mark.django_db +def test_user_roles_unattach_functional(organization, alice, bob, get): + # Add to same organization so that alice and bob can see each other + organization.member_role.members.add(alice) + organization.member_role.members.add(bob) + response = get(reverse('api:user_roles_list', args=(alice.id,)), bob) + # Org members can not revoke the membership of other members + assert response.data['results'][0]['summary_fields']['user_capabilities']['unattach'] == False From 6e653c29e08bd27edf84e48c419ab33a2261346e Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Fri, 2 Sep 2016 11:31:18 -0400 Subject: [PATCH 17/51] ui implementation of crud-based ui hiding --- .../client/src/access/roleList.partial.html | 6 +- awx/ui/client/src/app.js | 6 ++ awx/ui/client/src/controllers/Credentials.js | 33 ++++++++ awx/ui/client/src/controllers/Projects.js | 32 ++++++- awx/ui/client/src/controllers/Teams.js | 32 ++++++- awx/ui/client/src/controllers/Users.js | 32 ++++++- awx/ui/client/src/forms/Credentials.js | 83 +++++++++++++------ awx/ui/client/src/forms/Inventories.js | 25 ++++-- awx/ui/client/src/forms/JobTemplates.js | 79 ++++++++++++------ awx/ui/client/src/forms/Organizations.js | 19 +++-- awx/ui/client/src/forms/Projects.js | 50 +++++++---- awx/ui/client/src/forms/Teams.js | 22 +++-- awx/ui/client/src/forms/Users.js | 35 +++++--- awx/ui/client/src/helpers/Parse.js | 2 +- awx/ui/client/src/helpers/ProjectPath.js | 14 ++-- .../add/inventory-add.controller.js | 9 ++ .../edit/inventory-edit.controller.js | 11 ++- .../list/inventory-list.controller.js | 12 +++ .../inventory-scripts/add/add.controller.js | 13 ++- .../inventory-scripts/edit/edit.controller.js | 11 +++ .../inventory-scripts.form.js | 17 +++- .../inventory-scripts.list.js | 17 +++- .../inventory-scripts/list/list.controller.js | 12 +++ .../add/job-templates-add.controller.js | 9 ++ .../edit/job-templates-edit.controller.js | 10 +++ .../list/job-templates-list.controller.js | 13 ++- .../survey-maker/survey-maker.block.less | 6 ++ awx/ui/client/src/lists/AllJobs.js | 6 +- awx/ui/client/src/lists/CompletedJobs.js | 5 +- awx/ui/client/src/lists/Credentials.js | 18 +++- awx/ui/client/src/lists/Inventories.js | 16 +++- awx/ui/client/src/lists/InventoryGroups.js | 18 ++-- awx/ui/client/src/lists/JobTemplates.js | 16 +++- awx/ui/client/src/lists/Projects.js | 23 +++-- awx/ui/client/src/lists/Schedules.js | 6 +- awx/ui/client/src/lists/Teams.js | 18 +++- awx/ui/client/src/lists/Users.js | 18 +++- .../src/notifications/notifications.list.js | 3 +- .../add/organizations-add.controller.js | 9 ++ .../edit/organizations-edit.controller.js | 10 +++ .../list/organizations-list.controller.js | 13 +++ .../list/organizations-list.partial.html | 11 +++ .../src/partials/survey-maker-modal.html | 17 ++-- awx/ui/client/src/shared/form-generator.js | 15 ++++ 44 files changed, 669 insertions(+), 163 deletions(-) diff --git a/awx/ui/client/src/access/roleList.partial.html b/awx/ui/client/src/access/roleList.partial.html index 1478c9dc37..365a20f061 100644 --- a/awx/ui/client/src/access/roleList.partial.html +++ b/awx/ui/client/src/access/roleList.partial.html @@ -1,13 +1,13 @@
-
{{ entry.name }} diff --git a/awx/ui/client/src/app.js b/awx/ui/client/src/app.js index d9101f6d22..512ad43595 100644 --- a/awx/ui/client/src/app.js +++ b/awx/ui/client/src/app.js @@ -840,6 +840,12 @@ var tower = angular.module('Tower', [ // If browser refresh, set the user_is_superuser value $rootScope.user_is_superuser = Authorization.getUserInfo('is_superuser'); $rootScope.user_is_system_auditor = Authorization.getUserInfo('is_system_auditor'); + + Rest.setUrl($rootScope.current_user.related.admin_of_organizations); + Rest.get() + .success(function(data) { + $rootScope.current_user_admin_orgs = data.results.map(i => i.name); + }); // state the user refreshes we want to open the socket, except if the user is on the login page, which should happen after the user logs in (see the AuthService module for that call to OpenSocket) if (!_.contains($location.$$url, '/login')) { ConfigService.getConfig().then(function() { diff --git a/awx/ui/client/src/controllers/Credentials.js b/awx/ui/client/src/controllers/Credentials.js index 66b016dc50..53c4c001b4 100644 --- a/awx/ui/client/src/controllers/Credentials.js +++ b/awx/ui/client/src/controllers/Credentials.js @@ -17,6 +17,18 @@ export function CredentialsList($scope, $rootScope, $location, $log, SelectionInit, GetChoices, Wait, $state, $filter) { ClearScope(); + $scope.canAdd = false; + $scope.canEdit = false; + + Rest.setUrl(GetBasePath('credentials')); + Rest.options() + .success(function(data) { + if (data.actions.POST) { + $scope.canAdd = true; + $scope.canEdit = true; + } + }); + Wait('start'); var list = CredentialList, @@ -139,6 +151,15 @@ export function CredentialsAdd($scope, $rootScope, $compile, $location, $log, LookUpInit, OrganizationList, GetBasePath, GetChoices, Empty, KindChange, OwnerChange, FormSave, $state, CreateSelect2) { + Rest.setUrl(GetBasePath('credentials')); + Rest.options() + .success(function(data) { + if (!data.actions.POST) { + $state.go("^"); + Alert('Permission Error', 'You do not have permission to add a credential.', 'alert-info'); + } + }); + ClearScope(); // Inject dynamic view @@ -337,6 +358,7 @@ export function CredentialsEdit($scope, $rootScope, $compile, $location, $log, } ClearScope(); + var defaultUrl = GetBasePath('credentials'), generator = GenerateForm, form = CredentialForm, @@ -344,6 +366,17 @@ export function CredentialsEdit($scope, $rootScope, $compile, $location, $log, master = {}, id = $stateParams.credential_id, relatedSets = {}; + + $scope.canEdit = false; + + Rest.setUrl(GetBasePath('credentials') + id); + Rest.options() + .success(function(data) { + if (data.actions.PUT) { + $scope.canEdit = true; + } + }); + generator.inject(form, { mode: 'edit', related: true, scope: $scope }); generator.reset(); $scope.id = id; diff --git a/awx/ui/client/src/controllers/Projects.js b/awx/ui/client/src/controllers/Projects.js index 60692d581e..5c0803c3ce 100644 --- a/awx/ui/client/src/controllers/Projects.js +++ b/awx/ui/client/src/controllers/Projects.js @@ -16,9 +16,20 @@ export function ProjectsList ($scope, $rootScope, $location, $log, $stateParams, PaginateInit, ReturnToCaller, ClearScope, ProcessErrors, GetBasePath, SelectionInit, ProjectUpdate, Refresh, Wait, GetChoices, Empty, Find, GetProjectIcon, GetProjectToolTip, $filter, $state) { - ClearScope(); + $scope.canAdd = false; + $scope.canEdit = false; + + Rest.setUrl(GetBasePath('projects')); + Rest.options() + .success(function(data) { + if (data.actions.POST) { + $scope.canAdd = true; + $scope.canEdit = true; + } + }); + Wait('start'); var list = ProjectList, @@ -379,6 +390,15 @@ export function ProjectsAdd(Refresh, $scope, $rootScope, $compile, $location, $l OrganizationList, CredentialList, GetChoices, DebugForm, Wait, $state, CreateSelect2) { + Rest.setUrl(GetBasePath('projects')); + Rest.options() + .success(function(data) { + if (!data.actions.POST) { + $state.go("^"); + Alert('Permission Error', 'You do not have permission to add a project.', 'alert-info'); + } + }); + ClearScope(); // Inject dynamic view @@ -568,6 +588,16 @@ export function ProjectsEdit($scope, $rootScope, $compile, $location, $log, id = $stateParams.id, relatedSets = {}; + $scope.canEdit = false; + + Rest.setUrl(GetBasePath('projects') + id); + Rest.options() + .success(function(data) { + if (data.actions.PUT) { + $scope.canEdit = true; + } + }); + // remove "type" field from search options CredentialList = _.cloneDeep(CredentialList); CredentialList.fields.kind.noSearch = true; diff --git a/awx/ui/client/src/controllers/Teams.js b/awx/ui/client/src/controllers/Teams.js index a8eed62bc5..f78a5f13c9 100644 --- a/awx/ui/client/src/controllers/Teams.js +++ b/awx/ui/client/src/controllers/Teams.js @@ -15,9 +15,20 @@ export function TeamsList($scope, $rootScope, $location, $log, $stateParams, Rest, Alert, TeamList, GenerateList, Prompt, SearchInit, PaginateInit, ReturnToCaller, ClearScope, ProcessErrors, SetTeamListeners, GetBasePath, SelectionInit, Wait, $state, Refresh, $filter) { - ClearScope(); + $scope.canAdd = false; + $scope.canEdit = false; + + Rest.setUrl(GetBasePath('teams')); + Rest.options() + .success(function(data) { + if (data.actions.POST) { + $scope.canAdd = true; + $scope.canEdit = true; + } + }); + var list = TeamList, defaultUrl = GetBasePath('teams'), generator = GenerateList, @@ -137,6 +148,15 @@ export function TeamsAdd($scope, $rootScope, $compile, $location, $log, ClearScope('htmlTemplate'); //Garbage collection. Don't leave behind any listeners/watchers from the prior //$scope. + Rest.setUrl(GetBasePath('teams')); + Rest.options() + .success(function(data) { + if (!data.actions.POST) { + $state.go("^"); + Alert('Permission Error', 'You do not have permission to add a team.', 'alert-info'); + } + }); + // Inject dynamic view var defaultUrl = GetBasePath('teams'), form = TeamForm, @@ -206,6 +226,16 @@ export function TeamsEdit($scope, $rootScope, $location, relatedSets = {}, set; + $scope.canEdit = false; + + Rest.setUrl(GetBasePath('teams') + id); + Rest.options() + .success(function(data) { + if (data.actions.PUT) { + $scope.canEdit = true; + } + }); + $scope.team_id = id; diff --git a/awx/ui/client/src/controllers/Users.js b/awx/ui/client/src/controllers/Users.js index 103e1d1c84..5e76269983 100644 --- a/awx/ui/client/src/controllers/Users.js +++ b/awx/ui/client/src/controllers/Users.js @@ -35,9 +35,20 @@ export function UsersList($scope, $rootScope, $location, $log, $stateParams, Rest, Alert, UserList, GenerateList, Prompt, SearchInit, PaginateInit, ReturnToCaller, ClearScope, ProcessErrors, GetBasePath, SelectionInit, Wait, $state, Refresh, $filter) { - ClearScope(); + $scope.canAdd = false; + $scope.canEdit = false; + + Rest.setUrl(GetBasePath('users')); + Rest.options() + .success(function(data) { + if (data.actions.POST) { + $scope.canAdd = true; + $scope.canEdit = true; + } + }); + var list = UserList, defaultUrl = GetBasePath('users'), generator = GenerateList, @@ -148,6 +159,15 @@ export function UsersAdd($scope, $rootScope, $compile, $location, $log, ReturnToCaller, ClearScope, GetBasePath, LookUpInit, OrganizationList, ResetForm, Wait, CreateSelect2, $state) { + Rest.setUrl(GetBasePath('users')); + Rest.options() + .success(function(data) { + if (!data.actions.POST) { + $state.go("^"); + Alert('Permission Error', 'You do not have permission to add a user.', 'alert-info'); + } + }); + ClearScope(); // Inject dynamic view @@ -272,6 +292,16 @@ export function UsersEdit($scope, $rootScope, $location, relatedSets = {}, set; + $scope.canEdit = false; + + Rest.setUrl(GetBasePath('users') + id); + Rest.options() + .success(function(data) { + if (data.actions.PUT) { + $scope.canEdit = true; + } + }); + generator.inject(form, { mode: 'edit', related: true, scope: $scope }); generator.reset(); diff --git a/awx/ui/client/src/forms/Credentials.js b/awx/ui/client/src/forms/Credentials.js index d503f53905..4f2b262359 100644 --- a/awx/ui/client/src/forms/Credentials.js +++ b/awx/ui/client/src/forms/Credentials.js @@ -32,13 +32,15 @@ export default type: 'text', addRequired: true, editRequired: true, - autocomplete: false + autocomplete: false, + ngDisabled: '!canEdit' }, description: { label: 'Description', type: 'text', addRequired: false, - editRequired: false + editRequired: false, + ngDisabled: '!canEdit' }, organization: { addRequired: false, @@ -52,7 +54,8 @@ export default awPopOver: "

If no organization is given, the credential can only be used by the user that creates the credential. Organization admins and system administrators can assign an organization so that roles for the credential can be assigned to users and teams in that organization.

", dataTitle: 'Organization ', dataPlacement: 'bottom', - dataContainer: "body" + dataContainer: "body", + ngDisabled: '!canEdit' }, kind: { label: 'Type', @@ -83,7 +86,8 @@ export default dataTitle: 'Type', dataPlacement: 'right', dataContainer: "body", - hasSubForm: true + hasSubForm: true, + ngDisabled: '!canEdit' }, access_key: { label: 'Access Key', @@ -96,12 +100,13 @@ export default autocomplete: false, apiField: 'username', subForm: 'credentialSubForm', + ngDisabled: '!canEdit' }, secret_key: { label: 'Secret Key', type: 'sensitive', ngShow: "kind.value == 'aws'", - ngDisabled: "secret_key_ask", + ngDisabled: "secret_key_ask || !canEdit", awRequiredWhen: { reqExpression: "aws_required", init: false @@ -123,7 +128,8 @@ export default dataTitle: 'STS Token', dataPlacement: 'right', dataContainer: "body", - subForm: 'credentialSubForm' + subForm: 'credentialSubForm', + ngDisabled: '!canEdit' }, "host": { labelBind: 'hostLabel', @@ -139,7 +145,8 @@ export default reqExpression: 'host_required', init: false }, - subForm: 'credentialSubForm' + subForm: 'credentialSubForm', + ngDisabled: '!canEdit' }, "subscription": { label: "Subscription ID", @@ -156,7 +163,8 @@ export default dataTitle: 'Subscription ID', dataPlacement: 'right', dataContainer: "body", - subForm: 'credentialSubForm' + subForm: 'credentialSubForm', + ngDisabled: '!canEdit' }, "username": { labelBind: 'usernameLabel', @@ -168,7 +176,8 @@ export default init: false }, autocomplete: false, - subForm: "credentialSubForm" + subForm: "credentialSubForm", + ngDisabled: '!canEdit' }, "email_address": { labelBind: 'usernameLabel', @@ -183,7 +192,8 @@ export default dataTitle: 'Email', dataPlacement: 'right', dataContainer: "body", - subForm: 'credentialSubForm' + subForm: 'credentialSubForm', + ngDisabled: '!canEdit' }, "api_key": { label: 'API Key', @@ -196,7 +206,8 @@ export default autocomplete: false, hasShowInputButton: true, clear: false, - subForm: 'credentialSubForm' + subForm: 'credentialSubForm', + ngDisabled: '!canEdit' }, "password": { labelBind: 'passwordLabel', @@ -209,13 +220,14 @@ export default reqExpression: "password_required", init: false }, - subForm: "credentialSubForm" + subForm: "credentialSubForm", + ngDisabled: '!canEdit' }, "ssh_password": { label: 'Password', type: 'sensitive', ngShow: "kind.value == 'ssh'", - ngDisabled: "ssh_password_ask", + ngDisabled: "ssh_password_ask || !canEdit", addRequired: false, editRequired: false, subCheckbox: { @@ -247,7 +259,8 @@ export default dataTitle: 'Private Key', dataPlacement: 'right', dataContainer: "body", - subForm: "credentialSubForm" + subForm: "credentialSubForm", + ngDisabled: '!canEdit' }, "ssh_key_unlock": { label: 'Private Key Passphrase', @@ -255,7 +268,7 @@ export default ngShow: "kind.value == 'ssh' || kind.value == 'scm'", addRequired: false, editRequired: false, - ngDisabled: "keyEntered === false || ssh_key_unlock_ask", + ngDisabled: "keyEntered === false || ssh_key_unlock_ask || !canEdit", subCheckbox: { variable: 'ssh_key_unlock_ask', ngShow: "kind.value == 'ssh'", @@ -278,7 +291,8 @@ export default "sudo | su | pbrun | pfexec | runas
(defaults to sudo)

", dataPlacement: 'right', dataContainer: "body", - subForm: 'credentialSubForm' + subForm: 'credentialSubForm', + ngDisabled: '!canEdit' }, "become_username": { labelBind: 'becomeUsernameLabel', @@ -287,13 +301,14 @@ export default addRequired: false, editRequired: false, autocomplete: false, - subForm: 'credentialSubForm' + subForm: 'credentialSubForm', + ngDisabled: '!canEdit' }, "become_password": { labelBind: 'becomePasswordLabel', type: 'sensitive', ngShow: "(kind.value == 'ssh' && (become_method && become_method.value)) ", - ngDisabled: "become_password_ask", + ngDisabled: "become_password_ask || !canEdit", addRequired: false, editRequired: false, subCheckbox: { @@ -309,7 +324,8 @@ export default type: 'text', label: 'Client ID', subForm: 'credentialSubForm', - ngShow: "kind.value === 'azure_rm'" + ngShow: "kind.value === 'azure_rm'", + ngDisabled: '!canEdit' }, secret:{ type: 'sensitive', @@ -317,20 +333,23 @@ export default autocomplete: false, label: 'Client Secret', subForm: 'credentialSubForm', - ngShow: "kind.value === 'azure_rm'" + ngShow: "kind.value === 'azure_rm'", + ngDisabled: '!canEdit' }, tenant: { type: 'text', label: 'Tenant ID', subForm: 'credentialSubForm', - ngShow: "kind.value === 'azure_rm'" + ngShow: "kind.value === 'azure_rm'", + ngDisabled: '!canEdit' }, authorize: { label: 'Authorize', type: 'checkbox', ngChange: "toggleCallback('host_config_key')", subForm: 'credentialSubForm', - ngShow: "kind.value === 'net'" + ngShow: "kind.value === 'net'", + ngDisabled: '!canEdit' }, authorize_password: { label: 'Authorize Password', @@ -339,6 +358,7 @@ export default autocomplete: false, subForm: 'credentialSubForm', ngShow: "authorize && authorize !== 'false'", + ngDisabled: '!canEdit' }, "project": { labelBind: 'projectLabel', @@ -355,7 +375,8 @@ export default reqExpression: 'project_required', init: false }, - subForm: 'credentialSubForm' + subForm: 'credentialSubForm', + ngDisabled: '!canEdit' }, "domain": { labelBind: 'domainLabel', @@ -371,13 +392,14 @@ export default dataContainer: "body", addRequired: false, editRequired: false, - subForm: 'credentialSubForm' + subForm: 'credentialSubForm', + ngDisabled: '!canEdit' }, "vault_password": { label: "Vault Password", type: 'sensitive', ngShow: "kind.value == 'ssh'", - ngDisabled: "vault_password_ask", + ngDisabled: "vault_password_ask || !canEdit", addRequired: false, editRequired: false, subCheckbox: { @@ -394,11 +416,17 @@ export default buttons: { cancel: { ngClick: 'formCancel()', + ngShow: 'canEdit' + }, + close: { + ngClick: 'formCancel()', + ngShow: '!canEdit' }, save: { label: 'Save', ngClick: 'formSave()', //$scope.function to call on click, optional - ngDisabled: true //Disable when $pristine or $invalid, optional + ngDisabled: true, + ngShow: 'canEdit' //Disable when $pristine or $invalid, optional } }, @@ -421,7 +449,8 @@ export default label: 'Add', awToolTip: 'Add a permission', actionClass: 'btn List-buttonSubmit', - buttonContent: '+ ADD' + buttonContent: '+ ADD', + ngShow: 'canEdit' } }, diff --git a/awx/ui/client/src/forms/Inventories.js b/awx/ui/client/src/forms/Inventories.js index 6467bf28d3..c1d3c172d0 100644 --- a/awx/ui/client/src/forms/Inventories.js +++ b/awx/ui/client/src/forms/Inventories.js @@ -26,14 +26,16 @@ export default type: 'text', addRequired: true, editRequired: true, - capitalize: false + capitalize: false, + ngDisabled: '!canEdit' }, inventory_description: { realName: 'description', label: 'Description', type: 'text', addRequired: false, - editRequired: false + editRequired: false, + ngDisabled: '!canEdit' }, organization: { label: 'Organization', @@ -44,7 +46,8 @@ export default awRequiredWhen: { reqExpression: "organizationrequired", init: "true" - } + }, + ngDisabled: '!canEdit' }, variables: { label: 'Variables', @@ -63,17 +66,24 @@ export default '

View YAML examples at docs.ansible.com

', dataTitle: 'Inventory Variables', dataPlacement: 'right', - dataContainer: 'body' + dataContainer: 'body', + ngDisabled: '!canEdit' // TODO: get working } }, buttons: { cancel: { - ngClick: 'formCancel()' + ngClick: 'formCancel()', + ngShow: 'canEdit' + }, + close: { + ngClick: 'formCancel()', + ngHide: 'canEdit' }, save: { ngClick: 'formSave()', - ngDisabled: true + ngDisabled: true, + ngShow: 'canEdit' } }, @@ -94,7 +104,8 @@ export default label: 'Add', awToolTip: 'Add a permission', actionClass: 'btn List-buttonSubmit', - buttonContent: '+ ADD' + buttonContent: '+ ADD', + ngShow: 'canEdit' } }, diff --git a/awx/ui/client/src/forms/JobTemplates.js b/awx/ui/client/src/forms/JobTemplates.js index 5d56907c88..d7a6c81bfa 100644 --- a/awx/ui/client/src/forms/JobTemplates.js +++ b/awx/ui/client/src/forms/JobTemplates.js @@ -27,14 +27,16 @@ export default type: 'text', addRequired: true, editRequired: true, - column: 1 + column: 1, + ngDisabled: '!canEdit' }, description: { label: 'Description', type: 'text', addRequired: false, editRequired: false, - column: 1 + column: 1, + ngDisabled: '!canEdit' }, job_type: { label: 'Job Type', @@ -56,7 +58,8 @@ export default variable: 'ask_job_type_on_launch', ngShow: "!job_type.value || job_type.value !== 'scan'", text: 'Prompt on launch' - } + }, + ngDisabled: '!canEdit' }, inventory: { label: 'Inventory', @@ -78,7 +81,8 @@ export default variable: 'ask_inventory_on_launch', ngShow: "!job_type.value || job_type.value !== 'scan'", text: 'Prompt on launch' - } + }, + ngDisabled: '!canEdit' }, project: { label: 'Project', @@ -100,12 +104,13 @@ export default dataTitle: 'Project', dataPlacement: 'right', dataContainer: "body", + ngDisabled: '!canEdit' }, playbook: { label: 'Playbook', type:'select', ngOptions: 'book for book in playbook_options track by book', - ngDisabled: "job_type.value === 'scan' && project_name === 'Default'", + ngDisabled: "(job_type.value === 'scan' && project_name === 'Default') || !canEdit", id: 'playbook-select', awRequiredWhen: { reqExpression: "playbookrequired", @@ -138,7 +143,8 @@ export default subCheckbox: { variable: 'ask_credential_on_launch', text: 'Prompt on launch' - } + }, + ngDisabled: '!canEdit' }, cloud_credential: { label: 'Cloud Credential', @@ -153,7 +159,8 @@ export default "running playbook, allowing provisioning into the cloud without manually passing parameters to the included modules.

", dataTitle: 'Cloud Credential', dataPlacement: 'right', - dataContainer: "body" + dataContainer: "body", + ngDisabled: '!canEdit' }, network_credential: { label: 'Network Credential', @@ -167,7 +174,8 @@ export default awPopOver: "

Network credentials are used by Ansible networking modules to connect to and manage networking devices.

", dataTitle: 'Network Credential', dataPlacement: 'right', - dataContainer: "body" + dataContainer: "body", + ngDisabled: '!canEdit' }, forks: { label: 'Forks', @@ -186,7 +194,8 @@ export default ' target=\"_blank\">ansible configuration file.

', dataTitle: 'Forks', dataPlacement: 'right', - dataContainer: "body" + dataContainer: "body", + ngDisabled: '!canEdit' // TODO: get working }, limit: { label: 'Limit', @@ -203,7 +212,8 @@ export default subCheckbox: { variable: 'ask_limit_on_launch', text: 'Prompt on launch' - } + }, + ngDisabled: '!canEdit' }, verbosity: { label: 'Verbosity', @@ -216,7 +226,8 @@ export default awPopOver: "

Control the level of output ansible will produce as the playbook executes.

", dataTitle: 'Verbosity', dataPlacement: 'right', - dataContainer: "body" + dataContainer: "body", + ngDisabled: '!canEdit' }, job_tags: { label: 'Job Tags', @@ -235,7 +246,8 @@ export default subCheckbox: { variable: 'ask_tags_on_launch', text: 'Prompt on launch' - } + }, + ngDisabled: '!canEdit' }, skip_tags: { label: 'Skip Tags', @@ -254,7 +266,8 @@ export default subCheckbox: { variable: 'ask_skip_tags_on_launch', text: 'Prompt on launch' - } + }, + ngDisabled: '!canEdit' }, checkbox_group: { label: 'Options', @@ -270,7 +283,8 @@ export default dataPlacement: 'right', dataTitle: 'Become Privilege Escalation', dataContainer: "body", - labelClass: 'stack-inline' + labelClass: 'stack-inline', + ngDisabled: '!canEdit' }, { name: 'allow_callbacks', label: 'Allow Provisioning Callbacks', @@ -284,7 +298,8 @@ export default dataPlacement: 'right', dataTitle: 'Allow Provisioning Callbacks', dataContainer: "body", - labelClass: 'stack-inline' + labelClass: 'stack-inline', + ngDisabled: '!canEdit' }] }, callback_url: { @@ -299,7 +314,8 @@ export default awPopOverWatch: "callback_help", dataPlacement: 'top', dataTitle: 'Provisioning Callback URL', - dataContainer: "body" + dataContainer: "body", + ngDisabled: '!canEdit' }, host_config_key: { label: 'Host Config Key', @@ -312,7 +328,8 @@ export default awPopOverWatch: "callback_help", dataPlacement: 'right', dataTitle: "Host Config Key", - dataContainer: "body" + dataContainer: "body", + ngDisabled: '!canEdit' }, labels: { label: 'Labels', @@ -325,7 +342,8 @@ export default dataTitle: 'Labels', dataPlacement: 'right', awPopOver: "

Optional labels that describe this job template, such as 'dev' or 'test'. Labels can be used to group and filter job templates and completed jobs in the Tower display.

", - dataContainer: 'body' + dataContainer: 'body', + ngDisabled: '!canEdit' }, variables: { label: 'Extra Variables', @@ -348,14 +366,15 @@ export default subCheckbox: { variable: 'ask_variables_on_launch', text: 'Prompt on launch' - } + }, + ngDisabled: '!canEdit' // TODO: get working } }, buttons: { //for now always generates @@ -31,13 +32,23 @@
+
-
+
-
+
-
+
PREVIEW
@@ -56,13 +56,13 @@ {{question.question_description}}
- +   -
+
@@ -80,9 +80,10 @@
- - - + + + +
diff --git a/awx/ui/client/src/shared/form-generator.js b/awx/ui/client/src/shared/form-generator.js index 66c4f750b8..821857e3f6 100644 --- a/awx/ui/client/src/shared/form-generator.js +++ b/awx/ui/client/src/shared/form-generator.js @@ -865,6 +865,7 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat html += ">"; html += " Date: Fri, 2 Sep 2016 13:38:10 -0400 Subject: [PATCH 18/51] conditionally show add button for job templates --- awx/ui/client/src/lists/JobTemplates.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/awx/ui/client/src/lists/JobTemplates.js b/awx/ui/client/src/lists/JobTemplates.js index 398a490d65..ce75bf3dd6 100644 --- a/awx/ui/client/src/lists/JobTemplates.js +++ b/awx/ui/client/src/lists/JobTemplates.js @@ -55,7 +55,8 @@ export default basePaths: ['job_templates'], awToolTip: 'Create a new template', actionClass: 'btn List-buttonSubmit', - buttonContent: '+ ADD' + buttonContent: '+ ADD', + ngShow: 'canAdd' } }, From 1f586091e2334e3012189d0d03e6555b1f7cf4eb Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Tue, 6 Sep 2016 17:10:16 -0400 Subject: [PATCH 19/51] groups and hosts rbac conditional showing in ui implementation --- awx/ui/client/src/forms/Groups.js | 41 +++++++++++++------ awx/ui/client/src/forms/Hosts.js | 14 +++++-- .../manage/groups/groups-edit.controller.js | 15 ++++++- .../manage/groups/groups-list.controller.js | 16 +++++++- .../manage/hosts/hosts-edit.controller.js | 14 ++++++- .../manage/hosts/hosts-list.controller.js | 18 +++++++- .../manage/inventory-manage.controller.js | 35 +++++++++------- awx/ui/client/src/lists/InventoryGroups.js | 14 ++++++- awx/ui/client/src/lists/InventoryHosts.js | 19 +++++++-- 9 files changed, 142 insertions(+), 44 deletions(-) diff --git a/awx/ui/client/src/forms/Groups.js b/awx/ui/client/src/forms/Groups.js index e3327d442e..ece74417a4 100644 --- a/awx/ui/client/src/forms/Groups.js +++ b/awx/ui/client/src/forms/Groups.js @@ -26,14 +26,16 @@ export default type: 'text', addRequired: true, editRequired: true, - tab: 'properties' + tab: 'properties', + ngDisabled: '!canEdit' }, description: { label: 'Description', type: 'text', addRequired: false, editRequired: false, - tab: 'properties' + tab: 'properties', + ngDisabled: '!canEdit' }, variables: { label: 'Variables', @@ -65,7 +67,8 @@ export default ngChange: 'sourceChange(source)', addRequired: false, editRequired: false, - ngModel: 'source' + ngModel: 'source', + ngDisabled: '!canEdit' }, credential: { label: 'Cloud Credential', @@ -77,7 +80,8 @@ export default awRequiredWhen: { reqExpression: "cloudCredentialRequired", init: "false" - } + }, + ngDisabled: '!canEdit' }, source_regions: { label: 'Regions', @@ -92,7 +96,8 @@ export default awPopOver: "

Click on the regions field to see a list of regions for your cloud provider. You can select multiple regions, " + "or choose All to include all regions. Tower will only be updated with Hosts associated with the selected regions." + "

", - dataContainer: 'body' + dataContainer: 'body', + ngDisabled: '!canEdit' }, instance_filters: { label: 'Instance Filters', @@ -112,7 +117,8 @@ export default "
tag:Name=test*
\n" + "

View the Describe Instances documentation " + "for a complete list of supported filters.

", - dataContainer: 'body' + dataContainer: 'body', + ngDisabled: '!canEdit' }, group_by: { label: 'Only Group By', @@ -137,7 +143,8 @@ export default "
  • VPC ID: vpcs » vpc-5ca1ab1e
  • " + "
  • Tag None: tags » tag_none
  • " + "

    If blank, all groups above are created except Instance ID.

    ", - dataContainer: 'body' + dataContainer: 'body', + ngDisabled: '!canEdit' }, inventory_script: { label : "Custom Inventory Script", @@ -149,6 +156,7 @@ export default addRequired: true, editRequired: true, ngRequired: "source && source.value === 'custom'", + ngDisabled: '!canEdit', }, custom_variables: { id: 'custom_variables', @@ -269,7 +277,8 @@ export default dataTitle: 'Overwrite', dataContainer: 'body', dataPlacement: 'right', - labelClass: 'checkbox-options' + labelClass: 'checkbox-options', + ngDisabled: '!canEdit' }, { name: 'overwrite_vars', label: 'Overwrite Variables', @@ -283,7 +292,8 @@ export default dataTitle: 'Overwrite Variables', dataContainer: 'body', dataPlacement: 'right', - labelClass: 'checkbox-options' + labelClass: 'checkbox-options', + ngDisabled: '!canEdit' }, { name: 'update_on_launch', label: 'Update on Launch', @@ -296,7 +306,8 @@ export default dataTitle: 'Update on Launch', dataContainer: 'body', dataPlacement: 'right', - labelClass: 'checkbox-options' + labelClass: 'checkbox-options', + ngDisabled: '!canEdit' }] }, update_cache_timeout: { @@ -321,11 +332,17 @@ export default buttons: { cancel: { - ngClick: 'formCancel()' + ngClick: 'formCancel()', + ngShow: 'canEdit' + }, + close: { + ngClick: 'formCancel()', + ngShow: '!canEdit' }, save: { ngClick: 'formSave()', - ngDisabled: true + ngDisabled: true, + ngShow: 'canEdit' } }, diff --git a/awx/ui/client/src/forms/Hosts.js b/awx/ui/client/src/forms/Hosts.js index 0da34d3e2e..99caaa2b6b 100644 --- a/awx/ui/client/src/forms/Hosts.js +++ b/awx/ui/client/src/forms/Hosts.js @@ -46,13 +46,15 @@ export default "", dataTitle: 'Host Name', dataPlacement: 'right', - dataContainer: 'body' + dataContainer: 'body', + ngDisabled: '!canEdit' }, description: { label: 'Description', type: 'text', addRequired: false, - editRequired: false + editRequired: false, + ngDisabled: '!canEdit' }, variables: { label: 'Variables', @@ -83,10 +85,16 @@ export default buttons: { cancel: { ngClick: 'formCancel()', + ngShow: 'canEdit' + }, + close: { + ngClick: 'formCancel()', + ngShow: '!canEdit' }, save: { ngClick: 'formSave()', - ngDisabled: true + ngDisabled: true, + ngShow: 'canEdit' } }, diff --git a/awx/ui/client/src/inventories/manage/groups/groups-edit.controller.js b/awx/ui/client/src/inventories/manage/groups/groups-edit.controller.js index 941789f39d..fbcd4a00c1 100644 --- a/awx/ui/client/src/inventories/manage/groups/groups-edit.controller.js +++ b/awx/ui/client/src/inventories/manage/groups/groups-edit.controller.js @@ -7,10 +7,21 @@ export default ['$state', '$stateParams', '$scope', 'GroupForm', 'CredentialList', 'inventoryScriptsListObject', 'ToggleNotification', 'ParseVariableString', 'ParseTypeChange', 'GenerateForm', 'LookUpInit', 'RelatedSearchInit', 'RelatedPaginateInit', 'NotificationsListInit', - 'GroupManageService','GetChoices', 'GetBasePath', 'CreateSelect2', 'GetSourceTypeOptions', 'groupData', 'inventorySourceData', + 'GroupManageService','GetChoices', 'GetBasePath', 'CreateSelect2', 'GetSourceTypeOptions', 'groupData', 'inventorySourceData', 'Rest', function($state, $stateParams, $scope, GroupForm, CredentialList, InventoryScriptsList, ToggleNotification, ParseVariableString, ParseTypeChange, GenerateForm, LookUpInit, RelatedSearchInit, RelatedPaginateInit, NotificationsListInit, - GroupManageService, GetChoices, GetBasePath, CreateSelect2, GetSourceTypeOptions, groupData, inventorySourceData){ + GroupManageService, GetChoices, GetBasePath, CreateSelect2, GetSourceTypeOptions, groupData, inventorySourceData, Rest){ + + $scope.canEdit = false; + + Rest.setUrl(GetBasePath('groups') + $stateParams.group_id); + Rest.options() + .success(function(data) { + if (data.actions.PUT) { + $scope.canEdit = true; + } + }); + var generator = GenerateForm, form = GroupForm(); diff --git a/awx/ui/client/src/inventories/manage/groups/groups-list.controller.js b/awx/ui/client/src/inventories/manage/groups/groups-list.controller.js index ca56eeddd4..2b3889de18 100644 --- a/awx/ui/client/src/inventories/manage/groups/groups-list.controller.js +++ b/awx/ui/client/src/inventories/manage/groups/groups-list.controller.js @@ -5,13 +5,25 @@ *************************************************/ export default ['$scope', '$rootScope', '$state', '$stateParams', 'InventoryGroups', 'generateList', 'InventoryUpdate', 'GroupManageService', 'GroupsCancelUpdate', 'ViewUpdateStatus', - 'InventoryManageService', 'groupsUrl', 'SearchInit', 'PaginateInit', 'GetSyncStatusMsg', 'GetHostsStatusMsg', + 'InventoryManageService', 'groupsUrl', 'SearchInit', 'PaginateInit', 'GetSyncStatusMsg', 'GetHostsStatusMsg', 'Rest', 'GetBasePath', function($scope, $rootScope, $state, $stateParams, InventoryGroups, generateList, InventoryUpdate, GroupManageService, GroupsCancelUpdate, ViewUpdateStatus, - InventoryManageService, groupsUrl, SearchInit, PaginateInit, GetSyncStatusMsg, GetHostsStatusMsg){ + InventoryManageService, groupsUrl, SearchInit, PaginateInit, GetSyncStatusMsg, GetHostsStatusMsg, Rest, GetBasePath){ var list = InventoryGroups, view = generateList, pageSize = 20; $scope.inventory_id = $stateParams.inventory_id; + + $scope.canAdd = false; + + Rest.setUrl(GetBasePath('inventory') + $scope.inventory_id + "/groups"); + Rest.options() + .success(function(data) { + if (data.actions.POST) { + $scope.canAdd = true; + } + }); + + // The ncy breadcrumb directive will look at this attribute when attempting to bind to the correct scope. // In this case, we don't want to incidentally bind to this scope when editing a host or a group. See: // https://github.com/ncuillery/angular-breadcrumb/issues/42 for a little more information on the diff --git a/awx/ui/client/src/inventories/manage/hosts/hosts-edit.controller.js b/awx/ui/client/src/inventories/manage/hosts/hosts-edit.controller.js index 9098e53333..435968ccce 100644 --- a/awx/ui/client/src/inventories/manage/hosts/hosts-edit.controller.js +++ b/awx/ui/client/src/inventories/manage/hosts/hosts-edit.controller.js @@ -5,8 +5,18 @@ *************************************************/ export default - ['$state', '$stateParams', '$scope', 'HostForm', 'ParseTypeChange', 'GenerateForm', 'HostManageService', 'host', - function($state, $stateParams, $scope, HostForm, ParseTypeChange, GenerateForm, HostManageService, host){ + ['$state', '$stateParams', '$scope', 'HostForm', 'ParseTypeChange', 'GenerateForm', 'HostManageService', 'host', 'GetBasePath', 'Rest', + function($state, $stateParams, $scope, HostForm, ParseTypeChange, GenerateForm, HostManageService, host, GetBasePath, Rest){ + $scope.canEdit = false; + + Rest.setUrl(GetBasePath('hosts') + $stateParams.host_id); + Rest.options() + .success(function(data) { + if (data.actions.PUT) { + $scope.canEdit = true; + } + }); + var generator = GenerateForm, form = HostForm; $scope.parseType = 'yaml'; diff --git a/awx/ui/client/src/inventories/manage/hosts/hosts-list.controller.js b/awx/ui/client/src/inventories/manage/hosts/hosts-list.controller.js index 3c48cf8603..88219d9715 100644 --- a/awx/ui/client/src/inventories/manage/hosts/hosts-list.controller.js +++ b/awx/ui/client/src/inventories/manage/hosts/hosts-list.controller.js @@ -5,12 +5,26 @@ *************************************************/ export default ['$scope', '$rootScope', '$state', '$stateParams', 'InventoryHosts', 'generateList', 'InventoryManageService', 'HostManageService', - 'hostsUrl', 'SearchInit', 'PaginateInit', 'SetStatus', 'Prompt', 'Wait', 'inventoryData', '$filter', + 'hostsUrl', 'SearchInit', 'PaginateInit', 'SetStatus', 'Prompt', 'Wait', 'inventoryData', '$filter', 'Rest', 'GetBasePath', function($scope, $rootScope, $state, $stateParams, InventoryHosts, generateList, InventoryManageService, HostManageService, - hostsUrl, SearchInit, PaginateInit, SetStatus, Prompt, Wait, inventoryData, $filter){ + hostsUrl, SearchInit, PaginateInit, SetStatus, Prompt, Wait, inventoryData, $filter, Rest, GetBasePath){ + var list = InventoryHosts, view = generateList, pageSize = 20; + + $scope.canAdd = false; + + $scope.inventory_id = $stateParams.inventory_id; + + Rest.setUrl(GetBasePath('inventory') + $scope.inventory_id + "/hosts"); + Rest.options() + .success(function(data) { + if (data.actions.POST) { + $scope.canAdd = true; + } + }); + // The ncy breadcrumb directive will look at this attribute when attempting to bind to the correct scope. // In this case, we don't want to incidentally bind to this scope when editing a host or a group. See: // https://github.com/ncuillery/angular-breadcrumb/issues/42 for a little more information on the diff --git a/awx/ui/client/src/inventories/manage/inventory-manage.controller.js b/awx/ui/client/src/inventories/manage/inventory-manage.controller.js index aceb174363..f3201d475a 100644 --- a/awx/ui/client/src/inventories/manage/inventory-manage.controller.js +++ b/awx/ui/client/src/inventories/manage/inventory-manage.controller.js @@ -3,21 +3,26 @@ * * All Rights Reserved *************************************************/ - export default - ['$scope', '$state', function($scope, $state){ - $scope.groupsSelected = false; - $scope.hostsSelected = false; - $scope.hostsSelectedItems = []; - $scope.groupsSelectedItems = []; - $scope.setAdhocPattern = function(){ - var pattern = _($scope.groupsSelectedItems) - .concat($scope.hostsSelectedItems) - .map(function(item){ - return item.name; - }).value().join(':'); - $state.go('inventoryManage.adhoc', {pattern: pattern}); - }; +export default + ['$scope', '$state', 'inventoryData', function($scope, $state, inventoryData){ + $scope.groupsSelected = false; + $scope.hostsSelected = false; + $scope.hostsSelectedItems = []; + $scope.groupsSelectedItems = []; + + $scope.canAdhoc = inventoryData.summary_fields.user_capabilities.adhoc; + + $scope.setAdhocPattern = function(){ + var pattern = _($scope.groupsSelectedItems) + .concat($scope.hostsSelectedItems) + .map(function(item){ + return item.name; + }).value().join(':'); + + $state.go('inventoryManage.adhoc', {pattern: pattern}); + }; + $scope.$watchGroup(['groupsSelected', 'hostsSelected'], function(newVals) { $scope.adhocCommandTooltip = (newVals[0] || newVals[1]) ? "Run a command on the selected inventory" : "Select an inventory source by clicking the check box beside it. The inventory source can be a single group or host, a selection of multiple hosts, or a selection of multiple groups."; }); - }]; + }]; diff --git a/awx/ui/client/src/lists/InventoryGroups.js b/awx/ui/client/src/lists/InventoryGroups.js index 66e74e97c0..d228982dd3 100644 --- a/awx/ui/client/src/lists/InventoryGroups.js +++ b/awx/ui/client/src/lists/InventoryGroups.js @@ -143,7 +143,8 @@ export default actionClass: 'btn List-buttonDefault', buttonContent: 'RUN COMMANDS', showTipWhenDisabled: true, - tooltipInnerClass: "Tooltip-wide" + tooltipInnerClass: "Tooltip-wide", + ngShow: 'canAdhoc' // TODO: set up a tip watcher and change text based on when // things are selected/not selected. This is started and // commented out in the inventory controller within the watchers. @@ -155,7 +156,8 @@ export default ngClick: "createGroup()", awToolTip: "Create a new group", actionClass: 'btn List-buttonSubmit', - buttonContent: '+ ADD GROUP' + buttonContent: '+ ADD GROUP', + ngShow: 'canAdd' } }, @@ -208,6 +210,14 @@ export default dataPlacement: "top", ngShow: "group.summary_fields.user_capabilities.edit" }, + view: { + //label: 'Edit', + mode: 'all', + ngClick: "editGroup(group.id)", + awToolTip: 'View group', + dataPlacement: "top", + ngShow: "!group.summary_fields.user_capabilities.edit" + }, "delete": { //label: 'Delete', mode: 'all', diff --git a/awx/ui/client/src/lists/InventoryHosts.js b/awx/ui/client/src/lists/InventoryHosts.js index 1c01a77021..1a01ae1ca2 100644 --- a/awx/ui/client/src/lists/InventoryHosts.js +++ b/awx/ui/client/src/lists/InventoryHosts.js @@ -78,21 +78,31 @@ export default mode: 'all', ngClick: "copyMoveHost(host.id)", awToolTip: 'Copy or move host to another group', - dataPlacement: "top" + dataPlacement: "top", + ngShow: 'host.summary_fields.user_capabilities.edit' }, edit: { //label: 'Edit', ngClick: "editHost(host.id)", icon: 'icon-edit', awToolTip: 'Edit host', - dataPlacement: 'top' + dataPlacement: 'top', + ngShow: 'host.summary_fields.user_capabilities.edit' + }, + view: { + //label: 'Edit', + ngClick: "editHost(host.id)", + awToolTip: 'View host', + dataPlacement: 'top', + ngShow: '!host.summary_fields.user_capabilities.edit' }, "delete": { //label: 'Delete', ngClick: "deleteHost(host.id, host.name)", icon: 'icon-trash', awToolTip: 'Delete host', - dataPlacement: 'top' + dataPlacement: 'top', + ngShow: 'host.summary_fields.user_capabilities.delete' } }, @@ -122,7 +132,8 @@ export default ngClick: "createHost()", awToolTip: "Create a new host", actionClass: 'btn List-buttonSubmit', - buttonContent: '+ ADD HOST' + buttonContent: '+ ADD HOST', + ngShow: 'canAdd' } } From ab3588fd934eb65ceedfb99642c0b949c89210ff Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Wed, 7 Sep 2016 15:27:11 -0400 Subject: [PATCH 20/51] more updates to ui crud --- awx/ui/client/src/controllers/Jobs.js | 9 +- .../inventory-scripts/edit/edit.controller.js | 7 +- .../inventory-scripts.form.js | 14 +-- awx/ui/client/src/lists/AllJobs.js | 7 +- awx/ui/client/src/lists/ScheduledJobs.js | 13 ++- awx/ui/client/src/lists/Schedules.js | 10 +- .../management-jobs/card/card.partial.html | 2 + .../src/notifications/add/add.controller.js | 13 ++- .../src/notifications/edit/edit.controller.js | 10 ++ .../list.controller.js | 12 ++ .../notificationTemplates.form.js | 104 ++++++++++++------ .../notificationTemplates.list.js | 20 +++- .../src/scheduler/scheduler.controller.js | 13 +++ .../src/scheduler/schedulerAdd.controller.js | 1 - 14 files changed, 180 insertions(+), 55 deletions(-) diff --git a/awx/ui/client/src/controllers/Jobs.js b/awx/ui/client/src/controllers/Jobs.js index 3f0e3bdcfd..0e2f46f736 100644 --- a/awx/ui/client/src/controllers/Jobs.js +++ b/awx/ui/client/src/controllers/Jobs.js @@ -13,7 +13,7 @@ export function JobsListController ($rootScope, $log, $scope, $compile, $stateParams, ClearScope, LoadSchedulesScope, - LoadJobsScope, AllJobsList, ScheduledJobsList, GetChoices, GetBasePath, Wait) { + LoadJobsScope, AllJobsList, ScheduledJobsList, GetChoices, GetBasePath, Wait, $state) { ClearScope(); @@ -61,6 +61,11 @@ export function JobsListController ($rootScope, $log, $scope, $compile, $statePa } } jobs_scope = $scope.$new(true); + + jobs_scope.viewJob = function (id) { + $state.transitionTo('jobDetail', {id: id}); + }; + jobs_scope.showJobType = true; LoadJobsScope({ parent_scope: $scope, @@ -153,4 +158,4 @@ export function JobsListController ($rootScope, $log, $scope, $compile, $statePa JobsListController.$inject = ['$rootScope', '$log', '$scope', '$compile', '$stateParams', 'ClearScope', 'LoadSchedulesScope', 'LoadJobsScope', -'AllJobsList', 'ScheduledJobsList', 'GetChoices', 'GetBasePath', 'Wait']; +'AllJobsList', 'ScheduledJobsList', 'GetChoices', 'GetBasePath', 'Wait', '$state']; diff --git a/awx/ui/client/src/inventory-scripts/edit/edit.controller.js b/awx/ui/client/src/inventory-scripts/edit/edit.controller.js index 41d3309cc1..40a6eda216 100644 --- a/awx/ui/client/src/inventory-scripts/edit/edit.controller.js +++ b/awx/ui/client/src/inventory-scripts/edit/edit.controller.js @@ -17,6 +17,7 @@ export default LookUpInit, OrganizationList, inventory_script, $scope, $state ) { + var generator = GenerateForm, id = inventory_script.id, form = inventoryScriptsFormObject, @@ -24,13 +25,15 @@ export default url = GetBasePath('inventory_scripts'); - $scope.canEdit = false; + $scope.canEditInvScripts = false; Rest.setUrl(GetBasePath('inventory_scripts') + id); Rest.options() .success(function(data) { if (data.actions.PUT) { - $scope.canEdit = true; + $scope.canEditInvScripts = true; + } else { + $scope.canEditInvScripts = false; } }); diff --git a/awx/ui/client/src/inventory-scripts/inventory-scripts.form.js b/awx/ui/client/src/inventory-scripts/inventory-scripts.form.js index 082126a975..6a2f538421 100644 --- a/awx/ui/client/src/inventory-scripts/inventory-scripts.form.js +++ b/awx/ui/client/src/inventory-scripts/inventory-scripts.form.js @@ -25,14 +25,14 @@ export default function() { addRequired: true, editRequired: true, capitalize: false, - ngDisabled: '!canEdit' + ngDisabled: '!canEditInvScripts' }, description: { label: 'Description', type: 'text', addRequired: false, editRequired: false, - ngDisabled: '!canEdit' + ngDisabled: '!canEditInvScripts' }, organization: { label: 'Organization', @@ -44,7 +44,7 @@ export default function() { sourceModel: 'organization', sourceField: 'name', ngClick: 'lookUpOrganization()', - ngDisabled: '!canEdit' + ngDisabled: '!canEditInvScripts' }, script: { label: 'Custom Script', @@ -54,7 +54,7 @@ export default function() { addRequired: true, editRequired: true, awDropFile: true, - ngDisabled: '!canEdit', + ngDisabled: '!canEditInvScripts', rows: 10, awPopOver: "

    Drag and drop your custom inventory script file here or create one in the field to import your custom inventory. " + "

    Script must begin with a hashbang sequence: i.e.... #!/usr/bin/env python

    ", @@ -67,16 +67,16 @@ export default function() { buttons: { //for now always generates @@ -27,6 +28,7 @@ diff --git a/awx/ui/client/src/notifications/add/add.controller.js b/awx/ui/client/src/notifications/add/add.controller.js index 0b58f2e257..122d822cf4 100644 --- a/awx/ui/client/src/notifications/add/add.controller.js +++ b/awx/ui/client/src/notifications/add/add.controller.js @@ -9,14 +9,23 @@ export default 'NotificationsFormObject', 'ProcessErrors', 'GetBasePath', 'Empty', 'GenerateForm', 'SearchInit' , 'PaginateInit', 'LookUpInit', 'OrganizationList', '$scope', '$state', 'CreateSelect2', 'GetChoices', - 'NotificationsTypeChange', 'ParseTypeChange', + 'NotificationsTypeChange', 'ParseTypeChange', 'Alert', function( $rootScope, pagination, $compile, SchedulerInit, Rest, Wait, NotificationsFormObject, ProcessErrors, GetBasePath, Empty, GenerateForm, SearchInit, PaginateInit, LookUpInit, OrganizationList, $scope, $state, CreateSelect2, GetChoices, - NotificationsTypeChange, ParseTypeChange + NotificationsTypeChange, ParseTypeChange, Alert ) { + Rest.setUrl(GetBasePath('projects')); + Rest.options() + .success(function(data) { + if (!data.actions.POST) { + $state.go("^"); + Alert('Permission Error', 'You do not have permission to add a notification template.', 'alert-info'); + } + }); + var generator = GenerateForm, form = NotificationsFormObject, url = GetBasePath('notification_templates'); diff --git a/awx/ui/client/src/notifications/edit/edit.controller.js b/awx/ui/client/src/notifications/edit/edit.controller.js index 44c50a71ce..0f9250ed29 100644 --- a/awx/ui/client/src/notifications/edit/edit.controller.js +++ b/awx/ui/client/src/notifications/edit/edit.controller.js @@ -25,6 +25,16 @@ export default master = {}, url = GetBasePath('notification_templates'); + $scope.canEdit = false; + + Rest.setUrl(GetBasePath('notification_templates') + id); + Rest.options() + .success(function(data) { + if (data.actions.PUT) { + $scope.canEdit = true; + } + }); + $scope.notification_template = notification_template; generator.inject(form, { mode: 'edit' , diff --git a/awx/ui/client/src/notifications/notification-templates-list/list.controller.js b/awx/ui/client/src/notifications/notification-templates-list/list.controller.js index 04265f1181..40570e7aa8 100644 --- a/awx/ui/client/src/notifications/notification-templates-list/list.controller.js +++ b/awx/ui/client/src/notifications/notification-templates-list/list.controller.js @@ -24,6 +24,18 @@ export default scope: scope }); + scope.canAdd = false; + scope.canEdit = false; + + Rest.setUrl(GetBasePath('notification_templates')); + Rest.options() + .success(function(data) { + if (data.actions.POST) { + scope.canAdd = true; + scope.canEdit = true;; + } + }); + scope.removePostRefresh = scope.$on('PostRefresh', function () { Wait('stop'); if (scope.notification_templates) { diff --git a/awx/ui/client/src/notifications/notificationTemplates.form.js b/awx/ui/client/src/notifications/notificationTemplates.form.js index cd0ff9d945..e4003e52b8 100644 --- a/awx/ui/client/src/notifications/notificationTemplates.form.js +++ b/awx/ui/client/src/notifications/notificationTemplates.form.js @@ -27,13 +27,15 @@ export default function() { type: 'text', addRequired: true, editRequired: true, - capitalize: false + capitalize: false, + ngDisabled: '!canEdit' }, description: { label: 'Description', type: 'text', addRequired: false, - editRequired: false + editRequired: false, + ngDisabled: '!canEdit' }, organization: { label: 'Organization', @@ -44,7 +46,8 @@ export default function() { awRequiredWhen: { reqExpression: "organizationrequired", init: "true" - } + }, + ngDisabled: '!canEdit' }, notification_type: { label: 'Type', @@ -54,13 +57,15 @@ export default function() { class: 'NotificationsForm-typeSelect', ngOptions: 'type.label for type in notification_type_options track by type.value', ngChange: 'typeChange()', - hasSubForm: true + hasSubForm: true, + ngDisabled: '!canEdit' }, username: { label: 'Username', type: 'text', ngShow: "notification_type.value == 'email' ", - subForm: 'typeSubForm' + subForm: 'typeSubForm', + ngDisabled: '!canEdit' }, host: { @@ -71,7 +76,8 @@ export default function() { init: "false" }, ngShow: "notification_type.value == 'email' ", - subForm: 'typeSubForm' + subForm: 'typeSubForm', + ngDisabled: '!canEdit' }, sender: { label: 'Sender Email', @@ -81,7 +87,8 @@ export default function() { init: "false" }, ngShow: "notification_type.value == 'email' ", - subForm: 'typeSubForm' + subForm: 'typeSubForm', + ngDisabled: '!canEdit' }, recipients: { label: 'Recipient List', @@ -97,7 +104,8 @@ export default function() { init: "false" }, ngShow: "notification_type.value == 'email' ", - subForm: 'typeSubForm' + subForm: 'typeSubForm', + ngDisabled: '!canEdit' }, password: { labelBind: 'passwordLabel', @@ -108,7 +116,8 @@ export default function() { init: "false" }, ngShow: "notification_type.value == 'email' || notification_type.value == 'irc' ", - subForm: 'typeSubForm' + subForm: 'typeSubForm', + ngDisabled: '!canEdit' }, port: { labelBind: 'portLabel', @@ -122,7 +131,8 @@ export default function() { init: "false" }, ngShow: "notification_type.value == 'email' || notification_type.value == 'irc'", - subForm: 'typeSubForm' + subForm: 'typeSubForm', + ngDisabled: '!canEdit' }, channels: { label: 'Destination Channels', @@ -138,7 +148,8 @@ export default function() { init: "false" }, ngShow: "notification_type.value == 'slack'", - subForm: 'typeSubForm' + subForm: 'typeSubForm', + ngDisabled: '!canEdit' }, rooms: { label: 'Destination Channels', @@ -154,7 +165,8 @@ export default function() { init: "false" }, ngShow: "notification_type.value == 'hipchat'", - subForm: 'typeSubForm' + subForm: 'typeSubForm', + ngDisabled: '!canEdit' }, token: { labelBind: 'tokenLabel', @@ -165,7 +177,8 @@ export default function() { init: "false" }, ngShow: "notification_type.value == 'slack' || notification_type.value == 'pagerduty' || notification_type.value == 'hipchat'", - subForm: 'typeSubForm' + subForm: 'typeSubForm', + ngDisabled: '!canEdit' }, account_token: { label: 'Account Token', @@ -176,7 +189,8 @@ export default function() { init: "false" }, ngShow: "notification_type.value == 'twilio' ", - subForm: 'typeSubForm' + subForm: 'typeSubForm', + ngDisabled: '!canEdit' }, from_number: { label: 'Source Phone Number', @@ -188,7 +202,8 @@ export default function() { init: "false" }, ngShow: "notification_type.value == 'twilio' ", - subForm: 'typeSubForm' + subForm: 'typeSubForm', + ngDisabled: '!canEdit' }, to_numbers: { label: 'Destination SMS Number', @@ -204,7 +219,8 @@ export default function() { init: "false" }, ngShow: "notification_type.value == 'twilio' ", - subForm: 'typeSubForm' + subForm: 'typeSubForm', + ngDisabled: '!canEdit' }, account_sid: { label: 'Account SID', @@ -214,7 +230,8 @@ export default function() { init: "false" }, ngShow: "notification_type.value == 'twilio' ", - subForm: 'typeSubForm' + subForm: 'typeSubForm', + ngDisabled: '!canEdit' }, subdomain: { label: 'Pagerduty subdomain', @@ -224,7 +241,8 @@ export default function() { init: "false" }, ngShow: "notification_type.value == 'pagerduty' ", - subForm: 'typeSubForm' + subForm: 'typeSubForm', + ngDisabled: '!canEdit' }, service_key: { label: 'API Service/Integration Key', @@ -234,7 +252,8 @@ export default function() { init: "false" }, ngShow: "notification_type.value == 'pagerduty' ", - subForm: 'typeSubForm' + subForm: 'typeSubForm', + ngDisabled: '!canEdit' }, client_name: { label: 'Client Identifier', @@ -244,7 +263,8 @@ export default function() { init: "false" }, ngShow: "notification_type.value == 'pagerduty' ", - subForm: 'typeSubForm' + subForm: 'typeSubForm', + ngDisabled: '!canEdit' }, message_from: { label: 'Label to be shown with notification', @@ -254,7 +274,8 @@ export default function() { init: "false" }, ngShow: "notification_type.value == 'hipchat' ", - subForm: 'typeSubForm' + subForm: 'typeSubForm', + ngDisabled: '!canEdit' }, api_url: { label: 'API URL', @@ -265,7 +286,8 @@ export default function() { init: "false" }, ngShow: "notification_type.value == 'hipchat' ", - subForm: 'typeSubForm' + subForm: 'typeSubForm', + ngDisabled: '!canEdit' }, color: { label: 'Notification Color', @@ -277,13 +299,15 @@ export default function() { init: "false" }, ngShow: "notification_type.value == 'hipchat' ", - subForm: 'typeSubForm' + subForm: 'typeSubForm', + ngDisabled: '!canEdit' }, notify: { label: 'Notify Channel', type: 'checkbox', ngShow: "notification_type.value == 'hipchat' ", - subForm: 'typeSubForm' + subForm: 'typeSubForm', + ngDisabled: '!canEdit' }, url: { label: 'Target URL', @@ -293,7 +317,8 @@ export default function() { init: "false" }, ngShow: "notification_type.value == 'webhook' ", - subForm: 'typeSubForm' + subForm: 'typeSubForm', + ngDisabled: '!canEdit' }, headers: { label: 'HTTP Headers', @@ -313,7 +338,8 @@ export default function() { '

    ', dataPlacement: 'right', ngShow: "notification_type.value == 'webhook' ", - subForm: 'typeSubForm' + subForm: 'typeSubForm', + ngDisabled: '!canEdit' }, server: { label: 'IRC Server Address', @@ -323,7 +349,8 @@ export default function() { init: "false" }, ngShow: "notification_type.value == 'irc' ", - subForm: 'typeSubForm' + subForm: 'typeSubForm', + ngDisabled: '!canEdit' }, nickname: { label: 'IRC Nick', @@ -333,7 +360,8 @@ export default function() { init: "false" }, ngShow: "notification_type.value == 'irc' ", - subForm: 'typeSubForm' + subForm: 'typeSubForm', + ngDisabled: '!canEdit' }, targets: { label: 'Destination Channels or Users', @@ -349,13 +377,15 @@ export default function() { init: "false" }, ngShow: "notification_type.value == 'irc' ", - subForm: 'typeSubForm' + subForm: 'typeSubForm', + ngDisabled: '!canEdit' }, use_ssl: { label: 'SSL Connection', type: 'checkbox', ngShow: "notification_type.value == 'irc'", - subForm: 'typeSubForm' + subForm: 'typeSubForm', + ngDisabled: '!canEdit' }, checkbox_group: { label: 'Options', @@ -367,13 +397,15 @@ export default function() { label: 'Use TLS', type: 'checkbox', ngShow: "notification_type.value == 'email' ", - labelClass: 'checkbox-options stack-inline' + labelClass: 'checkbox-options stack-inline', + ngDisabled: '!canEdit' }, { name: 'use_ssl', label: 'Use SSL', type: 'checkbox', ngShow: "notification_type.value == 'email'", - labelClass: 'checkbox-options stack-inline' + labelClass: 'checkbox-options stack-inline', + ngDisabled: '!canEdit' }] } }, @@ -381,9 +413,15 @@ export default function() { buttons: { //for now always generates
    -
    +
    -
    +
    -
    +
    PREVIEW
    @@ -56,13 +56,13 @@ {{question.question_description}}
    - +   -
    +
    @@ -80,10 +80,10 @@
    - - - - + + + +
    diff --git a/awx/ui/client/src/scheduler/scheduler.controller.js b/awx/ui/client/src/scheduler/scheduler.controller.js index bba15c65b6..16681e1b66 100644 --- a/awx/ui/client/src/scheduler/scheduler.controller.js +++ b/awx/ui/client/src/scheduler/scheduler.controller.js @@ -50,14 +50,12 @@ export default [ url += "schedules/"; $scope.canAdd = false; - $scope.canEdit = false; Rest.setUrl(url); Rest.options() .success(function(data) { if (data.actions.POST) { $scope.canAdd = true; - $scope.canEdit = true; } }); From a7cbfdc24264692f62cc467d6421ee8abeeb61cf Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Tue, 13 Sep 2016 16:14:26 -0400 Subject: [PATCH 27/51] update canAdd and move org admin scope var off of rootScope --- awx/ui/client/src/app.js | 5 ---- awx/ui/client/src/controllers/Credentials.js | 20 ------------- awx/ui/client/src/controllers/Projects.js | 13 ++++---- awx/ui/client/src/controllers/Teams.js | 13 ++++---- awx/ui/client/src/controllers/Users.js | 13 ++++---- .../list/inventory-list.controller.js | 13 ++++---- .../manage/groups/groups-list.controller.js | 13 ++++---- .../manage/hosts/hosts-list.controller.js | 17 +++++------ .../inventory-scripts/list/list.controller.js | 13 ++++---- .../list/job-templates-list.controller.js | 13 ++++---- awx/ui/client/src/lists/Credentials.js | 3 +- .../list.controller.js | 13 ++++---- .../shared/notification-list-init.factory.js | 12 ++++++-- .../list/organizations-list.controller.js | 13 ++++---- .../src/scheduler/scheduler.controller.js | 13 ++++---- awx/ui/client/src/shared/main.js | 4 ++- awx/ui/client/src/shared/rbacUiControl.js | 30 +++++++++++++++++++ 17 files changed, 101 insertions(+), 120 deletions(-) create mode 100644 awx/ui/client/src/shared/rbacUiControl.js diff --git a/awx/ui/client/src/app.js b/awx/ui/client/src/app.js index 512ad43595..bde0407b20 100644 --- a/awx/ui/client/src/app.js +++ b/awx/ui/client/src/app.js @@ -841,11 +841,6 @@ var tower = angular.module('Tower', [ $rootScope.user_is_superuser = Authorization.getUserInfo('is_superuser'); $rootScope.user_is_system_auditor = Authorization.getUserInfo('is_system_auditor'); - Rest.setUrl($rootScope.current_user.related.admin_of_organizations); - Rest.get() - .success(function(data) { - $rootScope.current_user_admin_orgs = data.results.map(i => i.name); - }); // state the user refreshes we want to open the socket, except if the user is on the login page, which should happen after the user logs in (see the AuthService module for that call to OpenSocket) if (!_.contains($location.$$url, '/login')) { ConfigService.getConfig().then(function() { diff --git a/awx/ui/client/src/controllers/Credentials.js b/awx/ui/client/src/controllers/Credentials.js index 1696756ed3..aed17aee32 100644 --- a/awx/ui/client/src/controllers/Credentials.js +++ b/awx/ui/client/src/controllers/Credentials.js @@ -17,16 +17,6 @@ export function CredentialsList($scope, $rootScope, $location, $log, SelectionInit, GetChoices, Wait, $state, $filter) { ClearScope(); - $scope.canAdd = false; - - Rest.setUrl(GetBasePath('credentials')); - Rest.options() - .success(function(data) { - if (data.actions.POST) { - $scope.canAdd = true; - } - }); - Wait('start'); var list = CredentialList, @@ -148,16 +138,6 @@ export function CredentialsAdd($scope, $rootScope, $compile, $location, $log, ReturnToCaller, ClearScope, GenerateList, SearchInit, PaginateInit, LookUpInit, OrganizationList, GetBasePath, GetChoices, Empty, KindChange, OwnerChange, FormSave, $state, CreateSelect2) { - - Rest.setUrl(GetBasePath('credentials')); - Rest.options() - .success(function(data) { - if (!data.actions.POST) { - $state.go("^"); - Alert('Permission Error', 'You do not have permission to add a credential.', 'alert-info'); - } - }); - ClearScope(); // Inject dynamic view diff --git a/awx/ui/client/src/controllers/Projects.js b/awx/ui/client/src/controllers/Projects.js index 2aa6d1ab9a..6c882cdd13 100644 --- a/awx/ui/client/src/controllers/Projects.js +++ b/awx/ui/client/src/controllers/Projects.js @@ -15,17 +15,14 @@ export function ProjectsList ($scope, $rootScope, $location, $log, $stateParams, Rest, Alert, ProjectList, GenerateList, Prompt, SearchInit, PaginateInit, ReturnToCaller, ClearScope, ProcessErrors, GetBasePath, SelectionInit, ProjectUpdate, Refresh, Wait, GetChoices, Empty, - Find, GetProjectIcon, GetProjectToolTip, $filter, $state) { + Find, GetProjectIcon, GetProjectToolTip, $filter, $state, rbacUiControlService) { ClearScope(); $scope.canAdd = false; - Rest.setUrl(GetBasePath('projects')); - Rest.options() - .success(function(data) { - if (data.actions.POST) { - $scope.canAdd = true; - } + rbacUiControlService.canAdd('projects') + .then(function(canAdd) { + $scope.canAdd = canAdd; }); Wait('start'); @@ -378,7 +375,7 @@ ProjectsList.$inject = ['$scope', '$rootScope', '$location', '$log', 'SearchInit', 'PaginateInit', 'ReturnToCaller', 'ClearScope', 'ProcessErrors', 'GetBasePath', 'SelectionInit', 'ProjectUpdate', 'Refresh', 'Wait', 'GetChoices', 'Empty', 'Find', - 'GetProjectIcon', 'GetProjectToolTip', '$filter', '$state' + 'GetProjectIcon', 'GetProjectToolTip', '$filter', '$state', 'rbacUiControlService' ]; diff --git a/awx/ui/client/src/controllers/Teams.js b/awx/ui/client/src/controllers/Teams.js index 08ed3c5f8f..ca30f84699 100644 --- a/awx/ui/client/src/controllers/Teams.js +++ b/awx/ui/client/src/controllers/Teams.js @@ -14,17 +14,14 @@ export function TeamsList($scope, $rootScope, $location, $log, $stateParams, Rest, Alert, TeamList, GenerateList, Prompt, SearchInit, PaginateInit, ReturnToCaller, ClearScope, ProcessErrors, SetTeamListeners, GetBasePath, - SelectionInit, Wait, $state, Refresh, $filter) { + SelectionInit, Wait, $state, Refresh, $filter, rbacUiControlService) { ClearScope(); $scope.canAdd = false; - Rest.setUrl(GetBasePath('teams')); - Rest.options() - .success(function(data) { - if (data.actions.POST) { - $scope.canAdd = true; - } + rbacUiControlService.canAdd('teams') + .then(function(canAdd) { + $scope.canAdd = canAdd; }); var list = TeamList, @@ -135,7 +132,7 @@ TeamsList.$inject = ['$scope', '$rootScope', '$location', '$log', '$stateParams', 'Rest', 'Alert', 'TeamList', 'generateList', 'Prompt', 'SearchInit', 'PaginateInit', 'ReturnToCaller', 'ClearScope', 'ProcessErrors', 'SetTeamListeners', 'GetBasePath', 'SelectionInit', 'Wait', - '$state', 'Refresh', '$filter' + '$state', 'Refresh', '$filter', 'rbacUiControlService' ]; diff --git a/awx/ui/client/src/controllers/Users.js b/awx/ui/client/src/controllers/Users.js index 7033e6333d..0a00ed97c3 100644 --- a/awx/ui/client/src/controllers/Users.js +++ b/awx/ui/client/src/controllers/Users.js @@ -34,17 +34,14 @@ function user_type_sync($scope) { export function UsersList($scope, $rootScope, $location, $log, $stateParams, Rest, Alert, UserList, GenerateList, Prompt, SearchInit, PaginateInit, ReturnToCaller, ClearScope, ProcessErrors, GetBasePath, SelectionInit, - Wait, $state, Refresh, $filter) { + Wait, $state, Refresh, $filter, rbacUiControlService) { ClearScope(); $scope.canAdd = false; - Rest.setUrl(GetBasePath('users')); - Rest.options() - .success(function(data) { - if (data.actions.POST) { - $scope.canAdd = true; - } + rbacUiControlService.canAdd('users') + .then(function(canAdd) { + $scope.canAdd = canAdd; }); var list = UserList, @@ -145,7 +142,7 @@ UsersList.$inject = ['$scope', '$rootScope', '$location', '$log', '$stateParams', 'Rest', 'Alert', 'UserList', 'generateList', 'Prompt', 'SearchInit', 'PaginateInit', 'ReturnToCaller', 'ClearScope', 'ProcessErrors', 'GetBasePath', 'SelectionInit', 'Wait', '$state', - 'Refresh', '$filter' + 'Refresh', '$filter', 'rbacUiControlService' ]; diff --git a/awx/ui/client/src/inventories/list/inventory-list.controller.js b/awx/ui/client/src/inventories/list/inventory-list.controller.js index d70217e6c5..a8d5fc73e7 100644 --- a/awx/ui/client/src/inventories/list/inventory-list.controller.js +++ b/awx/ui/client/src/inventories/list/inventory-list.controller.js @@ -14,16 +14,13 @@ function InventoriesList($scope, $rootScope, $location, $log, $stateParams, $compile, $filter, sanitizeFilter, Rest, Alert, InventoryList, generateList, Prompt, SearchInit, PaginateInit, ReturnToCaller, ClearScope, ProcessErrors, GetBasePath, Wait, - Find, Empty, $state) { + Find, Empty, $state, rbacUiControlService) { $scope.canAdd = false; - Rest.setUrl(GetBasePath('inventory')); - Rest.options() - .success(function(data) { - if (data.actions.POST) { - $scope.canAdd = true; - } + rbacUiControlService.canAdd('inventory') + .then(function(canAdd) { + $scope.canAdd = canAdd; }); var list = InventoryList, @@ -386,4 +383,4 @@ function InventoriesList($scope, $rootScope, $location, $log, export default ['$scope', '$rootScope', '$location', '$log', '$stateParams', '$compile', '$filter', 'sanitizeFilter', 'Rest', 'Alert', 'InventoryList', 'generateList', 'Prompt', 'SearchInit', 'PaginateInit', 'ReturnToCaller', - 'ClearScope', 'ProcessErrors', 'GetBasePath', 'Wait', 'Find', 'Empty', '$state', InventoriesList]; + 'ClearScope', 'ProcessErrors', 'GetBasePath', 'Wait', 'Find', 'Empty', '$state', 'rbacUiControlService', InventoriesList]; diff --git a/awx/ui/client/src/inventories/manage/groups/groups-list.controller.js b/awx/ui/client/src/inventories/manage/groups/groups-list.controller.js index 2b3889de18..a2f843793b 100644 --- a/awx/ui/client/src/inventories/manage/groups/groups-list.controller.js +++ b/awx/ui/client/src/inventories/manage/groups/groups-list.controller.js @@ -5,9 +5,9 @@ *************************************************/ export default ['$scope', '$rootScope', '$state', '$stateParams', 'InventoryGroups', 'generateList', 'InventoryUpdate', 'GroupManageService', 'GroupsCancelUpdate', 'ViewUpdateStatus', - 'InventoryManageService', 'groupsUrl', 'SearchInit', 'PaginateInit', 'GetSyncStatusMsg', 'GetHostsStatusMsg', 'Rest', 'GetBasePath', + 'InventoryManageService', 'groupsUrl', 'SearchInit', 'PaginateInit', 'GetSyncStatusMsg', 'GetHostsStatusMsg', 'Rest', 'GetBasePath', 'rbacUiControlService', function($scope, $rootScope, $state, $stateParams, InventoryGroups, generateList, InventoryUpdate, GroupManageService, GroupsCancelUpdate, ViewUpdateStatus, - InventoryManageService, groupsUrl, SearchInit, PaginateInit, GetSyncStatusMsg, GetHostsStatusMsg, Rest, GetBasePath){ + InventoryManageService, groupsUrl, SearchInit, PaginateInit, GetSyncStatusMsg, GetHostsStatusMsg, Rest, GetBasePath, rbacUiControlService){ var list = InventoryGroups, view = generateList, pageSize = 20; @@ -15,12 +15,9 @@ $scope.canAdd = false; - Rest.setUrl(GetBasePath('inventory') + $scope.inventory_id + "/groups"); - Rest.options() - .success(function(data) { - if (data.actions.POST) { - $scope.canAdd = true; - } + rbacUiControlService.canAdd(GetBasePath('inventory') + $scope.inventory_id + "/groups") + .then(function(canAdd) { + $scope.canAdd = canAdd; }); diff --git a/awx/ui/client/src/inventories/manage/hosts/hosts-list.controller.js b/awx/ui/client/src/inventories/manage/hosts/hosts-list.controller.js index 88219d9715..f42b9c39b3 100644 --- a/awx/ui/client/src/inventories/manage/hosts/hosts-list.controller.js +++ b/awx/ui/client/src/inventories/manage/hosts/hosts-list.controller.js @@ -5,24 +5,21 @@ *************************************************/ export default ['$scope', '$rootScope', '$state', '$stateParams', 'InventoryHosts', 'generateList', 'InventoryManageService', 'HostManageService', - 'hostsUrl', 'SearchInit', 'PaginateInit', 'SetStatus', 'Prompt', 'Wait', 'inventoryData', '$filter', 'Rest', 'GetBasePath', + 'hostsUrl', 'SearchInit', 'PaginateInit', 'SetStatus', 'Prompt', 'Wait', 'inventoryData', '$filter', 'Rest', 'GetBasePath', 'rbacUiControlService', function($scope, $rootScope, $state, $stateParams, InventoryHosts, generateList, InventoryManageService, HostManageService, - hostsUrl, SearchInit, PaginateInit, SetStatus, Prompt, Wait, inventoryData, $filter, Rest, GetBasePath){ + hostsUrl, SearchInit, PaginateInit, SetStatus, Prompt, Wait, inventoryData, $filter, Rest, GetBasePath, rbacUiControlService){ var list = InventoryHosts, view = generateList, pageSize = 20; - $scope.canAdd = false; - $scope.inventory_id = $stateParams.inventory_id; - Rest.setUrl(GetBasePath('inventory') + $scope.inventory_id + "/hosts"); - Rest.options() - .success(function(data) { - if (data.actions.POST) { - $scope.canAdd = true; - } + $scope.canAdd = false; + + rbacUiControlService.canAdd(GetBasePath('inventory') + $scope.inventory_id + "/hosts") + .then(function(canAdd) { + $scope.canAdd = canAdd; }); // The ncy breadcrumb directive will look at this attribute when attempting to bind to the correct scope. diff --git a/awx/ui/client/src/inventory-scripts/list/list.controller.js b/awx/ui/client/src/inventory-scripts/list/list.controller.js index f8ad33e7db..08e0db5f67 100644 --- a/awx/ui/client/src/inventory-scripts/list/list.controller.js +++ b/awx/ui/client/src/inventory-scripts/list/list.controller.js @@ -7,11 +7,11 @@ export default [ '$rootScope','Wait', 'generateList', 'inventoryScriptsListObject', 'GetBasePath' , 'SearchInit' , 'PaginateInit', 'Rest' , 'ProcessErrors', - 'Prompt', '$state', '$filter', + 'Prompt', '$state', '$filter', 'rbacUiControlService', function( $rootScope,Wait, GenerateList, inventoryScriptsListObject, GetBasePath, SearchInit, PaginateInit, - Rest, ProcessErrors, Prompt, $state, $filter + Rest, ProcessErrors, Prompt, $state, $filter, rbacUiControlService ) { var scope = $rootScope.$new(), defaultUrl = GetBasePath('inventory_scripts'), @@ -20,12 +20,9 @@ export default scope.canAdd = false; - Rest.setUrl(GetBasePath('inventory_scripts')); - Rest.options() - .success(function(data) { - if (data.actions.POST) { - scope.canAdd = true; - } + rbacUiControlService.canAdd("inventory_scripts") + .then(function(canAdd) { + scope.canAdd = canAdd; }); view.inject( list, { diff --git a/awx/ui/client/src/job-templates/list/job-templates-list.controller.js b/awx/ui/client/src/job-templates/list/job-templates-list.controller.js index a2d4eedf4e..4e50f43fec 100644 --- a/awx/ui/client/src/job-templates/list/job-templates-list.controller.js +++ b/awx/ui/client/src/job-templates/list/job-templates-list.controller.js @@ -10,25 +10,22 @@ export default 'Prompt', 'SearchInit', 'PaginateInit', 'ReturnToCaller', 'ClearScope', 'ProcessErrors', 'GetBasePath', 'JobTemplateForm', 'CredentialList', 'LookUpInit', 'InitiatePlaybookRun', 'Wait', '$compile', - '$state', '$filter', + '$state', '$filter', 'rbacUiControlService', function( $scope, $rootScope, $location, $log, $stateParams, Rest, Alert, JobTemplateList, GenerateList, Prompt, SearchInit, PaginateInit, ReturnToCaller, ClearScope, ProcessErrors, GetBasePath, JobTemplateForm, CredentialList, LookUpInit, InitiatePlaybookRun, - Wait, $compile, $state, $filter + Wait, $compile, $state, $filter, rbacUiControlService ) { ClearScope(); $scope.canAdd = false; - Rest.setUrl(GetBasePath('job_templates')); - Rest.options() - .success(function(data) { - if (data.actions.POST) { - $scope.canAdd = true; - } + rbacUiControlService.canAdd("job_templates") + .then(function(canAdd) { + $scope.canAdd = canAdd; }); var list = JobTemplateList, diff --git a/awx/ui/client/src/lists/Credentials.js b/awx/ui/client/src/lists/Credentials.js index bebfc7ccfb..d9496b1807 100644 --- a/awx/ui/client/src/lists/Credentials.js +++ b/awx/ui/client/src/lists/Credentials.js @@ -58,8 +58,7 @@ export default ngClick: 'addCredential()', awToolTip: 'Create a new credential', actionClass: 'btn List-buttonSubmit', - buttonContent: '+ ADD', - ngShow: 'canAdd' + buttonContent: '+ ADD' } }, diff --git a/awx/ui/client/src/notifications/notification-templates-list/list.controller.js b/awx/ui/client/src/notifications/notification-templates-list/list.controller.js index 09bf1393a8..7fc279534b 100644 --- a/awx/ui/client/src/notifications/notification-templates-list/list.controller.js +++ b/awx/ui/client/src/notifications/notification-templates-list/list.controller.js @@ -8,12 +8,12 @@ export default [ '$rootScope','Wait', 'generateList', 'NotificationTemplatesList', 'GetBasePath' , 'SearchInit' , 'PaginateInit', 'Rest' , 'ProcessErrors', 'Prompt', '$state', 'GetChoices', 'Empty', 'Find', - 'ngToast', '$compile', '$filter', + 'ngToast', '$compile', '$filter', 'rbacUiControlService', function( $rootScope,Wait, GenerateList, NotificationTemplatesList, GetBasePath, SearchInit, PaginateInit, Rest, ProcessErrors, Prompt, $state, GetChoices, Empty, Find, ngToast, - $compile, $filter) { + $compile, $filter, rbacUiControlService) { var scope = $rootScope.$new(), defaultUrl = GetBasePath('notification_templates'), list = NotificationTemplatesList, @@ -26,12 +26,9 @@ export default scope.canAdd = false; - Rest.setUrl(GetBasePath('notification_templates')); - Rest.options() - .success(function(data) { - if (data.actions.POST) { - scope.canAdd = true; - } + rbacUiControlService.canAdd("notification_templates") + .then(function(canAdd) { + scope.canAdd = canAdd; }); scope.removePostRefresh = scope.$on('PostRefresh', function () { diff --git a/awx/ui/client/src/notifications/shared/notification-list-init.factory.js b/awx/ui/client/src/notifications/shared/notification-list-init.factory.js index ba13bea4c9..e8a711e783 100644 --- a/awx/ui/client/src/notifications/shared/notification-list-init.factory.js +++ b/awx/ui/client/src/notifications/shared/notification-list-init.factory.js @@ -15,13 +15,21 @@ */ export default ['Wait', 'GetBasePath', 'ProcessErrors', 'Rest', 'GetChoices', - '$state', - function(Wait, GetBasePath, ProcessErrors, Rest, GetChoices, $state) { + '$state', '$rootScope', + function(Wait, GetBasePath, ProcessErrors, Rest, GetChoices, $state, $rootScope) { return function(params) { var scope = params.scope, url = params.url, id = params.id; + scope.current_user_admin_orgs = []; + + Rest.setUrl($rootScope.current_user.related.admin_of_organizations); + Rest.get() + .success(function(data) { + scope.current_user_admin_orgs = data.results.map(i => i.name); + }); + scope.addNotificationTemplate = function(){ $state.go('notifications.add'); }; diff --git a/awx/ui/client/src/organizations/list/organizations-list.controller.js b/awx/ui/client/src/organizations/list/organizations-list.controller.js index 00a5bb9569..13ea87ce82 100644 --- a/awx/ui/client/src/organizations/list/organizations-list.controller.js +++ b/awx/ui/client/src/organizations/list/organizations-list.controller.js @@ -8,23 +8,20 @@ export default ['$stateParams', '$scope', '$rootScope', '$location', '$log', '$compile', 'Rest', 'PaginateInit', 'SearchInit', 'OrganizationList', 'Alert', 'Prompt', 'ClearScope', 'ProcessErrors', 'GetBasePath', 'Wait', - '$state', 'generateList', 'Refresh', '$filter', + '$state', 'generateList', 'Refresh', '$filter', 'rbacUiControlService', function($stateParams, $scope, $rootScope, $location, $log, $compile, Rest, PaginateInit, SearchInit, OrganizationList, Alert, Prompt, ClearScope, ProcessErrors, GetBasePath, Wait, - $state, generateList, Refresh, $filter) { + $state, generateList, Refresh, $filter, rbacUiControlService) { ClearScope(); $scope.canAdd = false; - Rest.setUrl(GetBasePath('organizations')); - Rest.options() - .success(function(data) { - if (data.actions.POST) { - $scope.canAdd = true; - } + rbacUiControlService.canAdd("organizations") + .then(function(canAdd) { + $scope.canAdd = canAdd; }); var defaultUrl = GetBasePath('organizations'), diff --git a/awx/ui/client/src/scheduler/scheduler.controller.js b/awx/ui/client/src/scheduler/scheduler.controller.js index 16681e1b66..db8eaa7b29 100644 --- a/awx/ui/client/src/scheduler/scheduler.controller.js +++ b/awx/ui/client/src/scheduler/scheduler.controller.js @@ -14,11 +14,11 @@ export default [ '$scope', '$compile', '$location', '$stateParams', 'SchedulesList', 'Rest', 'ProcessErrors', 'ReturnToCaller', 'ClearScope', 'GetBasePath', 'Wait', - 'Find', 'LoadSchedulesScope', 'GetChoices', '$q', '$state', + 'Find', 'LoadSchedulesScope', 'GetChoices', '$q', '$state', 'rbacUiControlService', function ($scope, $compile, $location, $stateParams, SchedulesList, Rest, ProcessErrors, ReturnToCaller, ClearScope, GetBasePath, Wait, Find, LoadSchedulesScope, GetChoices, - $q, $state) { + $q, $state, rbacUiControlService) { var schedList = _.cloneDeep(SchedulesList); ClearScope(); @@ -51,12 +51,9 @@ export default [ $scope.canAdd = false; - Rest.setUrl(url); - Rest.options() - .success(function(data) { - if (data.actions.POST) { - $scope.canAdd = true; - } + rbacUiControlService.canAdd(url) + .then(function(canAdd) { + $scope.canAdd = canAdd; }); schedList.well = true; diff --git a/awx/ui/client/src/shared/main.js b/awx/ui/client/src/shared/main.js index 6279fbf1e7..60c812e12c 100644 --- a/awx/ui/client/src/shared/main.js +++ b/awx/ui/client/src/shared/main.js @@ -11,12 +11,14 @@ import lodashAsPromised from './lodash-as-promised'; import stringFilters from './string-filters/main'; import truncatedText from './truncated-text.directive'; import stateExtender from './stateExtender.provider'; +import rbacUiControl from './rbacUiControl'; export default angular.module('shared', [listGenerator.name, pagination.name, stringFilters.name, - 'ui.router' + 'ui.router', + rbacUiControl.name ]) .factory('lodashAsPromised', lodashAsPromised) .directive('truncatedText', truncatedText) diff --git a/awx/ui/client/src/shared/rbacUiControl.js b/awx/ui/client/src/shared/rbacUiControl.js new file mode 100644 index 0000000000..7ec06d66c2 --- /dev/null +++ b/awx/ui/client/src/shared/rbacUiControl.js @@ -0,0 +1,30 @@ +/************************************************* + * Copyright (c) 2015 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +export default + angular.module('rbacUiControl', []) + .service('rbacUiControlService', ['$q', 'GetBasePath', 'Rest', function($q, GetBasePath, Rest){ + this.canAdd = function(apiPath) { + var canAddVal = $q.defer(); + + if (apiPath.indexOf("api/v1") > -1) { + Rest.setUrl(apiPath); + } else { + Rest.setUrl(GetBasePath(apiPath)); + } + + Rest.options() + .success(function(data) { + if (data.actions.POST) { + canAddVal.resolve(true); + } else { + canAddVal.reject(false); + } + }); + + return canAddVal.promise; + }; + }]); From 2482f6c46b24c44cddad1ff3fa8eeb00a48aca23 Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Thu, 15 Sep 2016 13:20:19 -0400 Subject: [PATCH 28/51] add wait start and stop to add service --- awx/ui/client/src/shared/rbacUiControl.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/awx/ui/client/src/shared/rbacUiControl.js b/awx/ui/client/src/shared/rbacUiControl.js index 7ec06d66c2..4c8a00d827 100644 --- a/awx/ui/client/src/shared/rbacUiControl.js +++ b/awx/ui/client/src/shared/rbacUiControl.js @@ -6,7 +6,7 @@ export default angular.module('rbacUiControl', []) - .service('rbacUiControlService', ['$q', 'GetBasePath', 'Rest', function($q, GetBasePath, Rest){ + .service('rbacUiControlService', ['$q', 'GetBasePath', 'Rest', 'Wait', function($q, GetBasePath, Rest, Wait){ this.canAdd = function(apiPath) { var canAddVal = $q.defer(); @@ -16,6 +16,7 @@ export default Rest.setUrl(GetBasePath(apiPath)); } + Wait("start"); Rest.options() .success(function(data) { if (data.actions.POST) { @@ -23,6 +24,7 @@ export default } else { canAddVal.reject(false); } + Wait("stop"); }); return canAddVal.promise; From 113795bd40148a3d3cdd8ba84d2a9a4572d8b270 Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Thu, 15 Sep 2016 16:32:56 -0400 Subject: [PATCH 29/51] remove can_copy and can_edit fields no longer used by UI --- awx/api/serializers.py | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/awx/api/serializers.py b/awx/api/serializers.py index 417b44db87..868611333e 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -1897,20 +1897,6 @@ class JobTemplateSerializer(UnifiedJobTemplateSerializer, JobOptionsSerializer): d['survey'] = dict(title=obj.survey_spec['name'], description=obj.survey_spec['description']) request = self.context.get('request', None) - # Remove the can_copy and can_edit fields when dependencies are fully removed - # Check for conditions that would create a validation error if coppied - validation_errors, resources_needed_to_start = obj.resource_validation_data() - - if request is None or request.user is None: - d['can_copy'] = False - d['can_edit'] = False - elif request.user.is_superuser: - d['can_copy'] = not validation_errors - d['can_edit'] = True - else: - d['can_copy'] = (not validation_errors) and request.user.can_access(JobTemplate, 'add', {"reference_obj": obj}) - d['can_edit'] = request.user.can_access(JobTemplate, 'change', obj, {}) - d['recent_jobs'] = self._recent_jobs(obj) return d From 766dac9663c05546a9d490bccf903f8e5bb5aa71 Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Thu, 15 Sep 2016 17:23:24 -0400 Subject: [PATCH 30/51] flake8 fix and re-delete test_serializers --- awx/api/serializers.py | 2 - awx/main/tests/unit/api/test_serializers.py | 248 -------------------- 2 files changed, 250 deletions(-) delete mode 100644 awx/main/tests/unit/api/test_serializers.py diff --git a/awx/api/serializers.py b/awx/api/serializers.py index 9c04c95d6f..63533eb141 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -1902,8 +1902,6 @@ class JobTemplateSerializer(UnifiedJobTemplateSerializer, JobOptionsSerializer): d = super(JobTemplateSerializer, self).get_summary_fields(obj) if obj.survey_spec is not None and ('name' in obj.survey_spec and 'description' in obj.survey_spec): d['survey'] = dict(title=obj.survey_spec['name'], description=obj.survey_spec['description']) - request = self.context.get('request', None) - d['recent_jobs'] = self._recent_jobs(obj) return d diff --git a/awx/main/tests/unit/api/test_serializers.py b/awx/main/tests/unit/api/test_serializers.py deleted file mode 100644 index 1d64a99246..0000000000 --- a/awx/main/tests/unit/api/test_serializers.py +++ /dev/null @@ -1,248 +0,0 @@ -# Python -import pytest -import mock -from mock import PropertyMock -import json - -# AWX -from awx.api.serializers import ( - JobTemplateSerializer, - JobSerializer, - JobOptionsSerializer, - CustomInventoryScriptSerializer, -) -from awx.api.views import JobTemplateDetail -from awx.main.models import ( - Role, - Label, - Job, - CustomInventoryScript, - User, -) - -#DRF -from rest_framework.request import Request -from rest_framework import serializers -from rest_framework.test import ( - APIRequestFactory, - force_authenticate, -) - - -def mock_JT_resource_data(): - return ({}, []) - -@pytest.fixture -def job_template(mocker): - mock_jt = mocker.MagicMock(pk=5) - mock_jt.resource_validation_data = mock_JT_resource_data - return mock_jt - -@pytest.fixture -def job(mocker, job_template): - return mocker.MagicMock(pk=5, job_template=job_template) - -@pytest.fixture -def labels(mocker): - return [Label(id=x, name='label-%d' % x) for x in xrange(0, 25)] - -@pytest.fixture -def jobs(mocker): - return [Job(id=x, name='job-%d' % x) for x in xrange(0, 25)] - -class GetRelatedMixin: - def _assert(self, model_obj, related, resource_name, related_resource_name): - assert related_resource_name in related - assert related[related_resource_name] == '/api/v1/%s/%d/%s/' % (resource_name, model_obj.pk, related_resource_name) - - def _mock_and_run(self, serializer_class, model_obj): - serializer = serializer_class() - related = serializer.get_related(model_obj) - return related - - def _test_get_related(self, serializer_class, model_obj, resource_name, related_resource_name): - related = self._mock_and_run(serializer_class, model_obj) - self._assert(model_obj, related, resource_name, related_resource_name) - return related - -class GetSummaryFieldsMixin: - def _assert(self, summary, summary_field_name): - assert summary_field_name in summary - - def _mock_and_run(self, serializer_class, model_obj): - serializer = serializer_class() - return serializer.get_summary_fields(model_obj) - - def _test_get_summary_fields(self, serializer_class, model_obj, summary_field_name): - summary = self._mock_and_run(serializer_class, model_obj) - self._assert(summary, summary_field_name) - return summary - -@mock.patch('awx.api.serializers.UnifiedJobTemplateSerializer.get_related', lambda x,y: {}) -@mock.patch('awx.api.serializers.JobOptionsSerializer.get_related', lambda x,y: {}) -class TestJobTemplateSerializerGetRelated(GetRelatedMixin): - @pytest.mark.parametrize("related_resource_name", [ - 'jobs', - 'schedules', - 'activity_stream', - 'launch', - 'notification_templates_any', - 'notification_templates_success', - 'notification_templates_error', - 'survey_spec', - 'labels', - 'callback', - ]) - def test_get_related(self, job_template, related_resource_name): - self._test_get_related(JobTemplateSerializer, job_template, 'job_templates', related_resource_name) - - def test_callback_absent(self, job_template): - job_template.host_config_key = None - related = self._mock_and_run(JobTemplateSerializer, job_template) - assert 'callback' not in related - -class TestJobTemplateSerializerGetSummaryFields(GetSummaryFieldsMixin): - def test__recent_jobs(self, mocker, job_template, jobs): - - job_template.jobs.all = mocker.MagicMock(**{'order_by.return_value': jobs}) - job_template.jobs.all.return_value = job_template.jobs.all - - serializer = JobTemplateSerializer() - recent_jobs = serializer._recent_jobs(job_template) - - job_template.jobs.all.assert_called_once_with() - job_template.jobs.all.order_by.assert_called_once_with('-created') - assert len(recent_jobs) == 10 - for x in jobs[:10]: - assert recent_jobs == [{'id': x.id, 'status': x.status, 'finished': x.finished} for x in jobs[:10]] - - def test_survey_spec_exists(self, mocker, job_template): - job_template.survey_spec = {'name': 'blah', 'description': 'blah blah'} - self._test_get_summary_fields(JobTemplateSerializer, job_template, 'survey') - - def test_survey_spec_absent(self, mocker, job_template): - job_template.survey_spec = None - summary = self._mock_and_run(JobTemplateSerializer, job_template) - assert 'survey' not in summary - - def test_copy_edit_standard(self, mocker, job_template_factory): - """Verify that the exact output of the access.py methods - are put into the serializer user_capabilities""" - - jt_obj = job_template_factory('testJT', project='proj1', persisted=False).job_template - jt_obj.id = 5 - jt_obj.admin_role = Role(id=9, role_field='admin_role') - jt_obj.execute_role = Role(id=8, role_field='execute_role') - jt_obj.read_role = Role(id=7, role_field='execute_role') - user = User(username="auser") - serializer = JobTemplateSerializer(job_template) - serializer.show_capabilities = ['copy', 'edit'] - serializer._summary_field_labels = lambda self: [] - serializer._recent_jobs = lambda self: [] - request = APIRequestFactory().get('/api/v1/job_templates/42/') - request.user = user - view = JobTemplateDetail() - view.request = request - serializer.context['view'] = view - - with mocker.patch("awx.main.access.JobTemplateAccess.can_change", return_value='foobar'): - with mocker.patch("awx.main.access.JobTemplateAccess.can_add", return_value='foo'): - response = serializer.get_summary_fields(jt_obj) - - assert response['user_capabilities']['copy'] == 'foo' - assert response['user_capabilities']['edit'] == 'foobar' - -@mock.patch('awx.api.serializers.UnifiedJobTemplateSerializer.get_related', lambda x,y: {}) -@mock.patch('awx.api.serializers.JobOptionsSerializer.get_related', lambda x,y: {}) -class TestJobSerializerGetRelated(GetRelatedMixin): - @pytest.mark.parametrize("related_resource_name", [ - 'job_events', - 'job_plays', - 'job_tasks', - 'relaunch', - 'labels', - ]) - def test_get_related(self, mocker, job, related_resource_name): - self._test_get_related(JobSerializer, job, 'jobs', related_resource_name) - - def test_job_template_absent(self, mocker, job): - job.job_template = None - serializer = JobSerializer() - related = serializer.get_related(job) - assert 'job_template' not in related - - def test_job_template_present(self, job): - related = self._mock_and_run(JobSerializer, job) - assert 'job_template' in related - assert related['job_template'] == '/api/v1/%s/%d/' % ('job_templates', job.job_template.pk) - -@mock.patch('awx.api.serializers.BaseSerializer.to_representation', lambda self,obj: { - 'extra_vars': obj.extra_vars}) -class TestJobSerializerSubstitution(): - - def test_survey_password_hide(self, mocker): - job = mocker.MagicMock(**{ - 'display_extra_vars.return_value': '{\"secret_key\": \"$encrypted$\"}', - 'extra_vars.return_value': '{\"secret_key\": \"my_password\"}'}) - serializer = JobSerializer(job) - rep = serializer.to_representation(job) - extra_vars = json.loads(rep['extra_vars']) - assert extra_vars['secret_key'] == '$encrypted$' - job.display_extra_vars.assert_called_once_with() - assert 'my_password' not in extra_vars - -@mock.patch('awx.api.serializers.BaseSerializer.get_summary_fields', lambda x,y: {}) -class TestJobOptionsSerializerGetSummaryFields(GetSummaryFieldsMixin): - def test__summary_field_labels_10_max(self, mocker, job_template, labels): - job_template.labels.all = mocker.MagicMock(**{'order_by.return_value': labels}) - job_template.labels.all.return_value = job_template.labels.all - - serializer = JobOptionsSerializer() - summary_labels = serializer._summary_field_labels(job_template) - - job_template.labels.all.order_by.assert_called_with('name') - assert len(summary_labels['results']) == 10 - assert summary_labels['results'] == [{'id': x.id, 'name': x.name} for x in labels[:10]] - - def test_labels_exists(self, mocker, job_template): - self._test_get_summary_fields(JobOptionsSerializer, job_template, 'labels') - -class TestJobTemplateSerializerValidation(object): - - good_extra_vars = ["{\"test\": \"keys\"}", "---\ntest: key"] - bad_extra_vars = ["{\"test\": \"keys\"", "---\ntest: [2"] - - def test_validate_extra_vars(self): - serializer = JobTemplateSerializer() - for ev in self.good_extra_vars: - serializer.validate_extra_vars(ev) - for ev in self.bad_extra_vars: - with pytest.raises(serializers.ValidationError): - serializer.validate_extra_vars(ev) - -class TestCustomInventoryScriptSerializer(object): - - @pytest.mark.parametrize("superuser,sysaudit,admin_role,value", - ((True, False, False, '#!/python'), - (False, True, False, '#!/python'), - (False, False, True, '#!/python'), - (False, False, False, None))) - def test_to_representation_orphan(self, superuser, sysaudit, admin_role, value): - with mock.patch.object(CustomInventoryScriptSerializer, 'get_summary_fields', return_value={}): - User.add_to_class('is_system_auditor', sysaudit) - user = User(username="root", is_superuser=superuser) - roles = [user] if admin_role else [] - - with mock.patch('awx.main.models.CustomInventoryScript.admin_role', new_callable=PropertyMock, return_value=roles): - cis = CustomInventoryScript(pk=1, script='#!/python') - serializer = CustomInventoryScriptSerializer() - - factory = APIRequestFactory() - wsgi_request = factory.post("/inventory_script/1", {'id':1}, format="json") - force_authenticate(wsgi_request, user) - - request = Request(wsgi_request) - serializer.context['request'] = request - - representation = serializer.to_representation(cis) - assert representation['script'] == value From b0731181fffb3abfcd3ee935a7bdc501a1a3e61d Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Fri, 16 Sep 2016 10:39:38 -0400 Subject: [PATCH 31/51] fix disabled add forms --- awx/ui/client/src/controllers/Credentials.js | 15 +++- awx/ui/client/src/controllers/Projects.js | 6 ++ awx/ui/client/src/controllers/Teams.js | 6 ++ awx/ui/client/src/controllers/Users.js | 6 ++ awx/ui/client/src/forms/Credentials.js | 62 ++++++++-------- awx/ui/client/src/forms/Groups.js | 28 ++++---- awx/ui/client/src/forms/Hosts.js | 10 +-- awx/ui/client/src/forms/Inventories.js | 16 ++--- awx/ui/client/src/forms/JobTemplates.js | 54 +++++++------- awx/ui/client/src/forms/Organizations.js | 12 ++-- awx/ui/client/src/forms/Projects.js | 34 ++++----- awx/ui/client/src/forms/Teams.js | 14 ++-- awx/ui/client/src/forms/Users.js | 22 +++--- .../edit/inventory-edit.controller.js | 7 ++ .../manage/groups/groups-edit.controller.js | 6 ++ .../manage/hosts/hosts-edit.controller.js | 11 ++- .../inventory-scripts/edit/edit.controller.js | 7 ++ .../inventory-scripts.form.js | 14 ++-- .../edit/job-templates-edit.controller.js | 6 ++ .../src/notifications/edit/edit.controller.js | 7 ++ .../notificationTemplates.form.js | 70 +++++++++---------- .../edit/organizations-edit.controller.js | 6 ++ .../src/partials/survey-maker-modal.html | 18 ++--- 23 files changed, 256 insertions(+), 181 deletions(-) diff --git a/awx/ui/client/src/controllers/Credentials.js b/awx/ui/client/src/controllers/Credentials.js index aed17aee32..11e2b4e435 100644 --- a/awx/ui/client/src/controllers/Credentials.js +++ b/awx/ui/client/src/controllers/Credentials.js @@ -14,9 +14,14 @@ export function CredentialsList($scope, $rootScope, $location, $log, $stateParams, Rest, Alert, CredentialList, GenerateList, Prompt, SearchInit, PaginateInit, ReturnToCaller, ClearScope, ProcessErrors, GetBasePath, - SelectionInit, GetChoices, Wait, $state, $filter) { + SelectionInit, GetChoices, Wait, $state, $filter, rbacUiControlService) { ClearScope(); + rbacUiControlService.canAdd('credentials') + .then(function(canAdd) { + $scope.canAdd = canAdd; + }); + Wait('start'); var list = CredentialList, @@ -129,7 +134,7 @@ CredentialsList.$inject = ['$scope', '$rootScope', '$location', '$log', '$stateParams', 'Rest', 'Alert', 'CredentialList', 'generateList', 'Prompt', 'SearchInit', 'PaginateInit', 'ReturnToCaller', 'ClearScope', 'ProcessErrors', 'GetBasePath', 'SelectionInit', 'GetChoices', 'Wait', - '$state', '$filter' + '$state', '$filter', 'rbacUiControlService' ]; @@ -349,6 +354,12 @@ export function CredentialsEdit($scope, $rootScope, $compile, $location, $log, generator.reset(); $scope.id = id; + $scope.$watch('credential_obj.summary_fields.user_capabilities.edit', function(val) { + if (val === false) { + $scope.canAdd = false; + } + }); + $scope.canShareCredential = false; if ($rootScope.current_user.is_superuser) { diff --git a/awx/ui/client/src/controllers/Projects.js b/awx/ui/client/src/controllers/Projects.js index 6c882cdd13..f5db4a2b8c 100644 --- a/awx/ui/client/src/controllers/Projects.js +++ b/awx/ui/client/src/controllers/Projects.js @@ -574,6 +574,12 @@ export function ProjectsEdit($scope, $rootScope, $compile, $location, $log, ClearScope('htmlTemplate'); + $scope.$watch('project_obj.summary_fields.user_capabilities.edit', function(val) { + if (val === false) { + $scope.canAdd = false; + } + }); + // Inject dynamic view var form = ProjectsForm(), generator = GenerateForm, diff --git a/awx/ui/client/src/controllers/Teams.js b/awx/ui/client/src/controllers/Teams.js index ca30f84699..aded8e5824 100644 --- a/awx/ui/client/src/controllers/Teams.js +++ b/awx/ui/client/src/controllers/Teams.js @@ -223,6 +223,12 @@ export function TeamsEdit($scope, $rootScope, $location, $scope.team_id = id; + $scope.$watch('team_obj.summary_fields.user_capabilities.edit', function(val) { + if (val === false) { + $scope.canAdd = false; + } + }); + generator.inject(form, { mode: 'edit', related: true, scope: $scope }); generator.reset(); diff --git a/awx/ui/client/src/controllers/Users.js b/awx/ui/client/src/controllers/Users.js index 0a00ed97c3..eb55a1bbb2 100644 --- a/awx/ui/client/src/controllers/Users.js +++ b/awx/ui/client/src/controllers/Users.js @@ -294,6 +294,12 @@ export function UsersEdit($scope, $rootScope, $location, $scope.user_type = user_type_options[0]; $scope.$watch('user_type', user_type_sync($scope)); + $scope.$watch('user_obj.summary_fields.user_capabilities.edit', function(val) { + if (val === false) { + $scope.canAdd = false; + } + }); + var setScopeFields = function(data){ _(data) .pick(function(value, key){ diff --git a/awx/ui/client/src/forms/Credentials.js b/awx/ui/client/src/forms/Credentials.js index c75f30c8be..abcbbd476d 100644 --- a/awx/ui/client/src/forms/Credentials.js +++ b/awx/ui/client/src/forms/Credentials.js @@ -33,14 +33,14 @@ export default addRequired: true, editRequired: true, autocomplete: false, - ngDisabled: '!credential_obj.summary_fields.user_capabilities.edit' + ngDisabled: '!(credential_obj.summary_fields.user_capabilities.edit || canAdd)' }, description: { label: 'Description', type: 'text', addRequired: false, editRequired: false, - ngDisabled: '!credential_obj.summary_fields.user_capabilities.edit' + ngDisabled: '!(credential_obj.summary_fields.user_capabilities.edit || canAdd)' }, organization: { addRequired: false, @@ -55,7 +55,7 @@ export default dataTitle: 'Organization ', dataPlacement: 'bottom', dataContainer: "body", - ngDisabled: '!credential_obj.summary_fields.user_capabilities.edit' + ngDisabled: '!(credential_obj.summary_fields.user_capabilities.edit || canAdd)' }, kind: { label: 'Type', @@ -87,7 +87,7 @@ export default dataPlacement: 'right', dataContainer: "body", hasSubForm: true, - ngDisabled: '!credential_obj.summary_fields.user_capabilities.edit' + ngDisabled: '!(credential_obj.summary_fields.user_capabilities.edit || canAdd)' }, access_key: { label: 'Access Key', @@ -100,13 +100,13 @@ export default autocomplete: false, apiField: 'username', subForm: 'credentialSubForm', - ngDisabled: '!credential_obj.summary_fields.user_capabilities.edit' + ngDisabled: '!(credential_obj.summary_fields.user_capabilities.edit || canAdd)' }, secret_key: { label: 'Secret Key', type: 'sensitive', ngShow: "kind.value == 'aws'", - ngDisabled: "secret_key_ask || !credential_obj.summary_fields.user_capabilities.edit", + ngDisabled: "secret_key_ask || !(credential_obj.summary_fields.user_capabilities.edit || canAdd)", awRequiredWhen: { reqExpression: "aws_required", init: false @@ -129,7 +129,7 @@ export default dataPlacement: 'right', dataContainer: "body", subForm: 'credentialSubForm', - ngDisabled: '!credential_obj.summary_fields.user_capabilities.edit' + ngDisabled: '!(credential_obj.summary_fields.user_capabilities.edit || canAdd)' }, "host": { labelBind: 'hostLabel', @@ -146,7 +146,7 @@ export default init: false }, subForm: 'credentialSubForm', - ngDisabled: '!credential_obj.summary_fields.user_capabilities.edit' + ngDisabled: '!(credential_obj.summary_fields.user_capabilities.edit || canAdd)' }, "subscription": { label: "Subscription ID", @@ -164,7 +164,7 @@ export default dataPlacement: 'right', dataContainer: "body", subForm: 'credentialSubForm', - ngDisabled: '!credential_obj.summary_fields.user_capabilities.edit' + ngDisabled: '!(credential_obj.summary_fields.user_capabilities.edit || canAdd)' }, "username": { labelBind: 'usernameLabel', @@ -177,7 +177,7 @@ export default }, autocomplete: false, subForm: "credentialSubForm", - ngDisabled: '!credential_obj.summary_fields.user_capabilities.edit' + ngDisabled: '!(credential_obj.summary_fields.user_capabilities.edit || canAdd)' }, "email_address": { labelBind: 'usernameLabel', @@ -193,7 +193,7 @@ export default dataPlacement: 'right', dataContainer: "body", subForm: 'credentialSubForm', - ngDisabled: '!credential_obj.summary_fields.user_capabilities.edit' + ngDisabled: '!(credential_obj.summary_fields.user_capabilities.edit || canAdd)' }, "api_key": { label: 'API Key', @@ -207,7 +207,7 @@ export default hasShowInputButton: true, clear: false, subForm: 'credentialSubForm', - ngDisabled: '!credential_obj.summary_fields.user_capabilities.edit' + ngDisabled: '!(credential_obj.summary_fields.user_capabilities.edit || canAdd)' }, "password": { labelBind: 'passwordLabel', @@ -221,13 +221,13 @@ export default init: false }, subForm: "credentialSubForm", - ngDisabled: '!credential_obj.summary_fields.user_capabilities.edit' + ngDisabled: '!(credential_obj.summary_fields.user_capabilities.edit || canAdd)' }, "ssh_password": { label: 'Password', type: 'sensitive', ngShow: "kind.value == 'ssh'", - ngDisabled: "ssh_password_ask || !credential_obj.summary_fields.user_capabilities.edit", + ngDisabled: "ssh_password_ask || !(credential_obj.summary_fields.user_capabilities.edit || canAdd)", addRequired: false, editRequired: false, subCheckbox: { @@ -260,7 +260,7 @@ export default dataPlacement: 'right', dataContainer: "body", subForm: "credentialSubForm", - ngDisabled: '!credential_obj.summary_fields.user_capabilities.edit' + ngDisabled: '!(credential_obj.summary_fields.user_capabilities.edit || canAdd)' }, "ssh_key_unlock": { label: 'Private Key Passphrase', @@ -268,7 +268,7 @@ export default ngShow: "kind.value == 'ssh' || kind.value == 'scm'", addRequired: false, editRequired: false, - ngDisabled: "keyEntered === false || ssh_key_unlock_ask || !credential_obj.summary_fields.user_capabilities.edit", + ngDisabled: "keyEntered === false || ssh_key_unlock_ask || !(credential_obj.summary_fields.user_capabilities.edit || canAdd)", subCheckbox: { variable: 'ssh_key_unlock_ask', ngShow: "kind.value == 'ssh'", @@ -292,7 +292,7 @@ export default dataPlacement: 'right', dataContainer: "body", subForm: 'credentialSubForm', - ngDisabled: '!credential_obj.summary_fields.user_capabilities.edit' + ngDisabled: '!(credential_obj.summary_fields.user_capabilities.edit || canAdd)' }, "become_username": { labelBind: 'becomeUsernameLabel', @@ -302,13 +302,13 @@ export default editRequired: false, autocomplete: false, subForm: 'credentialSubForm', - ngDisabled: '!credential_obj.summary_fields.user_capabilities.edit' + ngDisabled: '!(credential_obj.summary_fields.user_capabilities.edit || canAdd)' }, "become_password": { labelBind: 'becomePasswordLabel', type: 'sensitive', ngShow: "(kind.value == 'ssh' && (become_method && become_method.value)) ", - ngDisabled: "become_password_ask || !credential_obj.summary_fields.user_capabilities.edit", + ngDisabled: "become_password_ask || !(credential_obj.summary_fields.user_capabilities.edit || canAdd)", addRequired: false, editRequired: false, subCheckbox: { @@ -325,7 +325,7 @@ export default label: 'Client ID', subForm: 'credentialSubForm', ngShow: "kind.value === 'azure_rm'", - ngDisabled: '!credential_obj.summary_fields.user_capabilities.edit' + ngDisabled: '!(credential_obj.summary_fields.user_capabilities.edit || canAdd)' }, secret:{ type: 'sensitive', @@ -334,14 +334,14 @@ export default label: 'Client Secret', subForm: 'credentialSubForm', ngShow: "kind.value === 'azure_rm'", - ngDisabled: '!credential_obj.summary_fields.user_capabilities.edit' + ngDisabled: '!(credential_obj.summary_fields.user_capabilities.edit || canAdd)' }, tenant: { type: 'text', label: 'Tenant ID', subForm: 'credentialSubForm', ngShow: "kind.value === 'azure_rm'", - ngDisabled: '!credential_obj.summary_fields.user_capabilities.edit' + ngDisabled: '!(credential_obj.summary_fields.user_capabilities.edit || canAdd)' }, authorize: { label: 'Authorize', @@ -349,7 +349,7 @@ export default ngChange: "toggleCallback('host_config_key')", subForm: 'credentialSubForm', ngShow: "kind.value === 'net'", - ngDisabled: '!credential_obj.summary_fields.user_capabilities.edit' + ngDisabled: '!(credential_obj.summary_fields.user_capabilities.edit || canAdd)' }, authorize_password: { label: 'Authorize Password', @@ -358,7 +358,7 @@ export default autocomplete: false, subForm: 'credentialSubForm', ngShow: "authorize && authorize !== 'false'", - ngDisabled: '!credential_obj.summary_fields.user_capabilities.edit' + ngDisabled: '!(credential_obj.summary_fields.user_capabilities.edit || canAdd)' }, "project": { labelBind: 'projectLabel', @@ -376,7 +376,7 @@ export default init: false }, subForm: 'credentialSubForm', - ngDisabled: '!credential_obj.summary_fields.user_capabilities.edit' + ngDisabled: '!(credential_obj.summary_fields.user_capabilities.edit || canAdd)' }, "domain": { labelBind: 'domainLabel', @@ -393,13 +393,13 @@ export default addRequired: false, editRequired: false, subForm: 'credentialSubForm', - ngDisabled: '!credential_obj.summary_fields.user_capabilities.edit' + ngDisabled: '!(credential_obj.summary_fields.user_capabilities.edit || canAdd)' }, "vault_password": { label: "Vault Password", type: 'sensitive', ngShow: "kind.value == 'ssh'", - ngDisabled: "vault_password_ask || !credential_obj.summary_fields.user_capabilities.edit", + ngDisabled: "vault_password_ask || !(credential_obj.summary_fields.user_capabilities.edit || canAdd)", addRequired: false, editRequired: false, subCheckbox: { @@ -416,17 +416,17 @@ export default buttons: { cancel: { ngClick: 'formCancel()', - ngShow: 'credential_obj.summary_fields.user_capabilities.edit' + ngShow: '(credential_obj.summary_fields.user_capabilities.edit || canAdd)' }, close: { ngClick: 'formCancel()', - ngShow: '!credential_obj.summary_fields.user_capabilities.edit' + ngShow: '!(credential_obj.summary_fields.user_capabilities.edit || canAdd)' }, save: { label: 'Save', ngClick: 'formSave()', //$scope.function to call on click, optional ngDisabled: true, - ngShow: 'credential_obj.summary_fields.user_capabilities.edit' //Disable when $pristine or $invalid, optional + ngShow: '(credential_obj.summary_fields.user_capabilities.edit || canAdd)' //Disable when $pristine or $invalid, optional } }, @@ -450,7 +450,7 @@ export default awToolTip: 'Add a permission', actionClass: 'btn List-buttonSubmit', buttonContent: '+ ADD', - ngShow: 'credential_obj.summary_fields.user_capabilities.edit' + ngShow: '(credential_obj.summary_fields.user_capabilities.edit || canAdd)' } }, diff --git a/awx/ui/client/src/forms/Groups.js b/awx/ui/client/src/forms/Groups.js index 40a5cd3301..32723ff5f2 100644 --- a/awx/ui/client/src/forms/Groups.js +++ b/awx/ui/client/src/forms/Groups.js @@ -27,7 +27,7 @@ export default addRequired: true, editRequired: true, tab: 'properties', - ngDisabled: '!group_obj.summary_fields.user_capabilities.edit' + ngDisabled: '!(group_obj.summary_fields.user_capabilities.edit || canAdd)' }, description: { label: 'Description', @@ -35,7 +35,7 @@ export default addRequired: false, editRequired: false, tab: 'properties', - ngDisabled: '!group_obj.summary_fields.user_capabilities.edit' + ngDisabled: '!(group_obj.summary_fields.user_capabilities.edit || canAdd)' }, variables: { label: 'Variables', @@ -68,7 +68,7 @@ export default addRequired: false, editRequired: false, ngModel: 'source', - ngDisabled: '!group_obj.summary_fields.user_capabilities.edit' + ngDisabled: '!(group_obj.summary_fields.user_capabilities.edit || canAdd)' }, credential: { label: 'Cloud Credential', @@ -81,7 +81,7 @@ export default reqExpression: "cloudCredentialRequired", init: "false" }, - ngDisabled: '!group_obj.summary_fields.user_capabilities.edit' + ngDisabled: '!(group_obj.summary_fields.user_capabilities.edit || canAdd)' }, source_regions: { label: 'Regions', @@ -97,7 +97,7 @@ export default "or choose All to include all regions. Tower will only be updated with Hosts associated with the selected regions." + "

    ", dataContainer: 'body', - ngDisabled: '!group_obj.summary_fields.user_capabilities.edit' + ngDisabled: '!(group_obj.summary_fields.user_capabilities.edit || canAdd)' }, instance_filters: { label: 'Instance Filters', @@ -118,7 +118,7 @@ export default "

    View the Describe Instances documentation " + "for a complete list of supported filters.

    ", dataContainer: 'body', - ngDisabled: '!group_obj.summary_fields.user_capabilities.edit' + ngDisabled: '!(group_obj.summary_fields.user_capabilities.edit || canAdd)' }, group_by: { label: 'Only Group By', @@ -144,7 +144,7 @@ export default "
  • Tag None: tags » tag_none
  • " + "

    If blank, all groups above are created except Instance ID.

    ", dataContainer: 'body', - ngDisabled: '!group_obj.summary_fields.user_capabilities.edit' + ngDisabled: '!(group_obj.summary_fields.user_capabilities.edit || canAdd)' }, inventory_script: { label : "Custom Inventory Script", @@ -156,7 +156,7 @@ export default addRequired: true, editRequired: true, ngRequired: "source && source.value === 'custom'", - ngDisabled: '!group_obj.summary_fields.user_capabilities.edit', + ngDisabled: '!(group_obj.summary_fields.user_capabilities.edit || canAdd)', }, custom_variables: { id: 'custom_variables', @@ -278,7 +278,7 @@ export default dataContainer: 'body', dataPlacement: 'right', labelClass: 'checkbox-options', - ngDisabled: '!group_obj.summary_fields.user_capabilities.edit' + ngDisabled: '!(group_obj.summary_fields.user_capabilities.edit || canAdd)' }, { name: 'overwrite_vars', label: 'Overwrite Variables', @@ -293,7 +293,7 @@ export default dataContainer: 'body', dataPlacement: 'right', labelClass: 'checkbox-options', - ngDisabled: '!group_obj.summary_fields.user_capabilities.edit' + ngDisabled: '!(group_obj.summary_fields.user_capabilities.edit || canAdd)' }, { name: 'update_on_launch', label: 'Update on Launch', @@ -307,7 +307,7 @@ export default dataContainer: 'body', dataPlacement: 'right', labelClass: 'checkbox-options', - ngDisabled: '!group_obj.summary_fields.user_capabilities.edit' + ngDisabled: '!(group_obj.summary_fields.user_capabilities.edit || canAdd)' }] }, update_cache_timeout: { @@ -333,16 +333,16 @@ export default buttons: { cancel: { ngClick: 'formCancel()', - ngShow: 'group_obj.summary_fields.user_capabilities.edit' + ngShow: '(group_obj.summary_fields.user_capabilities.edit || canAdd)' }, close: { ngClick: 'formCancel()', - ngShow: '!group_obj.summary_fields.user_capabilities.edit' + ngShow: '!(group_obj.summary_fields.user_capabilities.edit || canAdd)' }, save: { ngClick: 'formSave()', ngDisabled: true, - ngShow: 'group_obj.summary_fields.user_capabilities.edit' + ngShow: '(group_obj.summary_fields.user_capabilities.edit || canAdd)' } }, diff --git a/awx/ui/client/src/forms/Hosts.js b/awx/ui/client/src/forms/Hosts.js index 3f01a6cf5c..a1c0e69537 100644 --- a/awx/ui/client/src/forms/Hosts.js +++ b/awx/ui/client/src/forms/Hosts.js @@ -47,14 +47,14 @@ export default dataTitle: 'Host Name', dataPlacement: 'right', dataContainer: 'body', - ngDisabled: '!host.summary_fields.user_capabilities.edit' + ngDisabled: '!(host.summary_fields.user_capabilities.edit || canAdd)' }, description: { label: 'Description', type: 'text', addRequired: false, editRequired: false, - ngDisabled: '!host.summary_fields.user_capabilities.edit' + ngDisabled: '!(host.summary_fields.user_capabilities.edit || canAdd)' }, variables: { label: 'Variables', @@ -85,16 +85,16 @@ export default buttons: { cancel: { ngClick: 'formCancel()', - ngShow: 'host.summary_fields.user_capabilities.edit' + ngShow: '(host.summary_fields.user_capabilities.edit || canAdd)' }, close: { ngClick: 'formCancel()', - ngShow: '!host.summary_fields.user_capabilities.edit' + ngShow: '!(host.summary_fields.user_capabilities.edit || canAdd)' }, save: { ngClick: 'formSave()', ngDisabled: true, - ngShow: 'host.summary_fields.user_capabilities.edit' + ngShow: '(host.summary_fields.user_capabilities.edit || canAdd)' } }, diff --git a/awx/ui/client/src/forms/Inventories.js b/awx/ui/client/src/forms/Inventories.js index 425c4d342d..72852079c3 100644 --- a/awx/ui/client/src/forms/Inventories.js +++ b/awx/ui/client/src/forms/Inventories.js @@ -27,7 +27,7 @@ export default addRequired: true, editRequired: true, capitalize: false, - ngDisabled: '!inventory_obj.summary_fields.user_capabilities.edit' + ngDisabled: '!(inventory_obj.summary_fields.user_capabilities.edit || canAdd)' }, inventory_description: { realName: 'description', @@ -35,7 +35,7 @@ export default type: 'text', addRequired: false, editRequired: false, - ngDisabled: '!inventory_obj.summary_fields.user_capabilities.edit' + ngDisabled: '!(inventory_obj.summary_fields.user_capabilities.edit || canAdd)' }, organization: { label: 'Organization', @@ -47,7 +47,7 @@ export default reqExpression: "organizationrequired", init: "true" }, - ngDisabled: '!inventory_obj.summary_fields.user_capabilities.edit' + ngDisabled: '!(inventory_obj.summary_fields.user_capabilities.edit || canAdd)' }, variables: { label: 'Variables', @@ -67,23 +67,23 @@ export default dataTitle: 'Inventory Variables', dataPlacement: 'right', dataContainer: 'body', - ngDisabled: '!inventory_obj.summary_fields.user_capabilities.edit' // TODO: get working + ngDisabled: '!(inventory_obj.summary_fields.user_capabilities.edit || canAdd)' // TODO: get working } }, buttons: { cancel: { ngClick: 'formCancel()', - ngShow: 'inventory_obj.summary_fields.user_capabilities.edit' + ngShow: '(inventory_obj.summary_fields.user_capabilities.edit || canAdd)' }, close: { ngClick: 'formCancel()', - ngHide: 'inventory_obj.summary_fields.user_capabilities.edit' + ngHide: '(inventory_obj.summary_fields.user_capabilities.edit || canAdd)' }, save: { ngClick: 'formSave()', ngDisabled: true, - ngShow: 'inventory_obj.summary_fields.user_capabilities.edit' + ngShow: '(inventory_obj.summary_fields.user_capabilities.edit || canAdd)' } }, @@ -105,7 +105,7 @@ export default awToolTip: 'Add a permission', actionClass: 'btn List-buttonSubmit', buttonContent: '+ ADD', - ngShow: 'inventory_obj.summary_fields.user_capabilities.edit' + ngShow: '(inventory_obj.summary_fields.user_capabilities.edit || canAdd)' } }, diff --git a/awx/ui/client/src/forms/JobTemplates.js b/awx/ui/client/src/forms/JobTemplates.js index 13e826492d..e9fe0553b8 100644 --- a/awx/ui/client/src/forms/JobTemplates.js +++ b/awx/ui/client/src/forms/JobTemplates.js @@ -28,7 +28,7 @@ export default addRequired: true, editRequired: true, column: 1, - ngDisabled: '!job_template_obj.summary_fields.user_capabilities.edit' + ngDisabled: '!(job_template_obj.summary_fields.user_capabilities.edit || canAdd)' }, description: { label: 'Description', @@ -36,7 +36,7 @@ export default addRequired: false, editRequired: false, column: 1, - ngDisabled: '!job_template_obj.summary_fields.user_capabilities.edit' + ngDisabled: '!(job_template_obj.summary_fields.user_capabilities.edit || canAdd)' }, job_type: { label: 'Job Type', @@ -59,7 +59,7 @@ export default ngShow: "!job_type.value || job_type.value !== 'scan'", text: 'Prompt on launch' }, - ngDisabled: '!job_template_obj.summary_fields.user_capabilities.edit' + ngDisabled: '!(job_template_obj.summary_fields.user_capabilities.edit || canAdd)' }, inventory: { label: 'Inventory', @@ -82,7 +82,7 @@ export default ngShow: "!job_type.value || job_type.value !== 'scan'", text: 'Prompt on launch' }, - ngDisabled: '!job_template_obj.summary_fields.user_capabilities.edit' + ngDisabled: '!(job_template_obj.summary_fields.user_capabilities.edit || canAdd)' }, project: { label: 'Project', @@ -104,13 +104,13 @@ export default dataTitle: 'Project', dataPlacement: 'right', dataContainer: "body", - ngDisabled: '!job_template_obj.summary_fields.user_capabilities.edit' + ngDisabled: '!(job_template_obj.summary_fields.user_capabilities.edit || canAdd)' }, playbook: { label: 'Playbook', type:'select', ngOptions: 'book for book in playbook_options track by book', - ngDisabled: "(job_type.value === 'scan' && project_name === 'Default') || !job_template_obj.summary_fields.user_capabilities.edit", + ngDisabled: "(job_type.value === 'scan' && project_name === 'Default') || !(job_template_obj.summary_fields.user_capabilities.edit || canAdd)", id: 'playbook-select', awRequiredWhen: { reqExpression: "playbookrequired", @@ -144,7 +144,7 @@ export default variable: 'ask_credential_on_launch', text: 'Prompt on launch' }, - ngDisabled: '!job_template_obj.summary_fields.user_capabilities.edit' + ngDisabled: '!(job_template_obj.summary_fields.user_capabilities.edit || canAdd)' }, cloud_credential: { label: 'Cloud Credential', @@ -160,7 +160,7 @@ export default dataTitle: 'Cloud Credential', dataPlacement: 'right', dataContainer: "body", - ngDisabled: '!job_template_obj.summary_fields.user_capabilities.edit' + ngDisabled: '!(job_template_obj.summary_fields.user_capabilities.edit || canAdd)' }, network_credential: { label: 'Network Credential', @@ -175,7 +175,7 @@ export default dataTitle: 'Network Credential', dataPlacement: 'right', dataContainer: "body", - ngDisabled: '!job_template_obj.summary_fields.user_capabilities.edit' + ngDisabled: '!(job_template_obj.summary_fields.user_capabilities.edit || canAdd)' }, forks: { label: 'Forks', @@ -195,7 +195,7 @@ export default dataTitle: 'Forks', dataPlacement: 'right', dataContainer: "body", - ngDisabled: '!job_template_obj.summary_fields.user_capabilities.edit' // TODO: get working + ngDisabled: '!(job_template_obj.summary_fields.user_capabilities.edit || canAdd)' // TODO: get working }, limit: { label: 'Limit', @@ -213,7 +213,7 @@ export default variable: 'ask_limit_on_launch', text: 'Prompt on launch' }, - ngDisabled: '!job_template_obj.summary_fields.user_capabilities.edit' + ngDisabled: '!(job_template_obj.summary_fields.user_capabilities.edit || canAdd)' }, verbosity: { label: 'Verbosity', @@ -227,7 +227,7 @@ export default dataTitle: 'Verbosity', dataPlacement: 'right', dataContainer: "body", - ngDisabled: '!job_template_obj.summary_fields.user_capabilities.edit' + ngDisabled: '!(job_template_obj.summary_fields.user_capabilities.edit || canAdd)' }, job_tags: { label: 'Job Tags', @@ -247,7 +247,7 @@ export default variable: 'ask_tags_on_launch', text: 'Prompt on launch' }, - ngDisabled: '!job_template_obj.summary_fields.user_capabilities.edit' + ngDisabled: '!(job_template_obj.summary_fields.user_capabilities.edit || canAdd)' }, skip_tags: { label: 'Skip Tags', @@ -267,7 +267,7 @@ export default variable: 'ask_skip_tags_on_launch', text: 'Prompt on launch' }, - ngDisabled: '!job_template_obj.summary_fields.user_capabilities.edit' + ngDisabled: '!(job_template_obj.summary_fields.user_capabilities.edit || canAdd)' }, checkbox_group: { label: 'Options', @@ -284,7 +284,7 @@ export default dataTitle: 'Become Privilege Escalation', dataContainer: "body", labelClass: 'stack-inline', - ngDisabled: '!job_template_obj.summary_fields.user_capabilities.edit' + ngDisabled: '!(job_template_obj.summary_fields.user_capabilities.edit || canAdd)' }, { name: 'allow_callbacks', label: 'Allow Provisioning Callbacks', @@ -299,7 +299,7 @@ export default dataTitle: 'Allow Provisioning Callbacks', dataContainer: "body", labelClass: 'stack-inline', - ngDisabled: '!job_template_obj.summary_fields.user_capabilities.edit' + ngDisabled: '!(job_template_obj.summary_fields.user_capabilities.edit || canAdd)' }] }, callback_url: { @@ -315,7 +315,7 @@ export default dataPlacement: 'top', dataTitle: 'Provisioning Callback URL', dataContainer: "body", - ngDisabled: '!job_template_obj.summary_fields.user_capabilities.edit' + ngDisabled: '!(job_template_obj.summary_fields.user_capabilities.edit || canAdd)' }, host_config_key: { label: 'Host Config Key', @@ -329,7 +329,7 @@ export default dataPlacement: 'right', dataTitle: "Host Config Key", dataContainer: "body", - ngDisabled: '!job_template_obj.summary_fields.user_capabilities.edit' + ngDisabled: '!(job_template_obj.summary_fields.user_capabilities.edit || canAdd)' }, labels: { label: 'Labels', @@ -343,7 +343,7 @@ export default dataPlacement: 'right', awPopOver: "

    Optional labels that describe this job template, such as 'dev' or 'test'. Labels can be used to group and filter job templates and completed jobs in the Tower display.

    ", dataContainer: 'body', - ngDisabled: '!job_template_obj.summary_fields.user_capabilities.edit' + ngDisabled: '!(job_template_obj.summary_fields.user_capabilities.edit || canAdd)' }, variables: { label: 'Extra Variables', @@ -367,14 +367,14 @@ export default variable: 'ask_variables_on_launch', text: 'Prompt on launch' }, - ngDisabled: '!job_template_obj.summary_fields.user_capabilities.edit' // TODO: get working + ngDisabled: '!(job_template_obj.summary_fields.user_capabilities.edit || canAdd)' // TODO: get working } }, buttons: { //for now always generates
    -
    +
    -
    +
    -
    +
    PREVIEW
    @@ -56,13 +56,13 @@ {{question.question_description}}
    - +   -
    +
    @@ -80,10 +80,10 @@
    - - - - + + + +
    From 44ea28feb69400eead4ac789199929fb349bac8a Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Fri, 16 Sep 2016 13:02:35 -0400 Subject: [PATCH 32/51] fix dangling rbac hide issues --- awx/ui/client/src/forms/JobTemplates.js | 2 +- .../inventories/manage/hosts/hosts-add.controller.js | 12 ++++++++++-- .../src/inventory-scripts/inventory-scripts.form.js | 2 +- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/awx/ui/client/src/forms/JobTemplates.js b/awx/ui/client/src/forms/JobTemplates.js index e9fe0553b8..3ec35b2288 100644 --- a/awx/ui/client/src/forms/JobTemplates.js +++ b/awx/ui/client/src/forms/JobTemplates.js @@ -399,7 +399,7 @@ export default }, save: { ngClick: 'formSave()', //$scope.function to call on click, optional - ngDisabled: "job_templates_form.$invalid || can_edit!==true",//true //Disable when $pristine or $invalid, optional and when can_edit = false, for permission reasons + ngDisabled: "job_templates_form.$invalid",//true //Disable when $pristine or $invalid, optional and when can_edit = false, for permission reasons ngShow: '(job_template_obj.summary_fields.user_capabilities.edit || canAdd)' } }, diff --git a/awx/ui/client/src/inventories/manage/hosts/hosts-add.controller.js b/awx/ui/client/src/inventories/manage/hosts/hosts-add.controller.js index d0dd6c3c86..8dd3386871 100644 --- a/awx/ui/client/src/inventories/manage/hosts/hosts-add.controller.js +++ b/awx/ui/client/src/inventories/manage/hosts/hosts-add.controller.js @@ -5,10 +5,18 @@ *************************************************/ export default - ['$state', '$stateParams', '$scope', 'HostForm', 'ParseTypeChange', 'GenerateForm', 'HostManageService', - function($state, $stateParams, $scope, HostForm, ParseTypeChange, GenerateForm, HostManageService){ + ['$state', '$stateParams', '$scope', 'HostForm', 'ParseTypeChange', 'GenerateForm', 'HostManageService', 'rbacUiControlService', 'GetBasePath', + function($state, $stateParams, $scope, HostForm, ParseTypeChange, GenerateForm, HostManageService, rbacUiControlService, GetBasePath){ var generator = GenerateForm, form = HostForm; + + $scope.canAdd = false; + + rbacUiControlService.canAdd(GetBasePath('inventory') + $stateParams.inventory_id + "/hosts") + .then(function(canAdd) { + $scope.canAdd = canAdd; + }); + $scope.parseType = 'yaml'; $scope.formCancel = function(){ $state.go('^'); diff --git a/awx/ui/client/src/inventory-scripts/inventory-scripts.form.js b/awx/ui/client/src/inventory-scripts/inventory-scripts.form.js index c2a9e7ca3b..95ebed3458 100644 --- a/awx/ui/client/src/inventory-scripts/inventory-scripts.form.js +++ b/awx/ui/client/src/inventory-scripts/inventory-scripts.form.js @@ -75,7 +75,7 @@ export default function() { }, save: { ngClick: 'formSave()', //$scope.function to call on click, optional - ngDisabled: 'custom_inventory_form.$pristine || custom_inventory_form.$invalid || !canEdit', //Disable when $pristine or $invalid, optional + ngDisabled: 'custom_inventory_form.$pristine || custom_inventory_form.$invalid', //Disable when $pristine or $invalid, optional ngShow: '(inventory_script_obj.summary_fields.user_capabilities.edit || canAdd)' } } From 4c7e6835ad29b8bec0ffa00728ac13b43b8d17f9 Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Fri, 16 Sep 2016 13:15:01 -0400 Subject: [PATCH 33/51] one more rbac disabled fix for inv groups --- .../inventories/manage/groups/groups-add.controller.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/awx/ui/client/src/inventories/manage/groups/groups-add.controller.js b/awx/ui/client/src/inventories/manage/groups/groups-add.controller.js index d1272f508b..c8662bd55c 100644 --- a/awx/ui/client/src/inventories/manage/groups/groups-add.controller.js +++ b/awx/ui/client/src/inventories/manage/groups/groups-add.controller.js @@ -6,9 +6,9 @@ export default ['$state', '$stateParams', '$scope', 'GroupForm', 'CredentialList', 'inventoryScriptsListObject', 'ParseTypeChange', 'GenerateForm', 'inventoryData', 'LookUpInit', - 'GroupManageService', 'GetChoices', 'GetBasePath', 'CreateSelect2', 'GetSourceTypeOptions', + 'GroupManageService', 'GetChoices', 'GetBasePath', 'CreateSelect2', 'GetSourceTypeOptions', 'rbacUiControlService', function($state, $stateParams, $scope, GroupForm, CredentialList, InventoryScriptsList, ParseTypeChange, GenerateForm, inventoryData, LookUpInit, - GroupManageService, GetChoices, GetBasePath, CreateSelect2, GetSourceTypeOptions){ + GroupManageService, GetChoices, GetBasePath, CreateSelect2, GetSourceTypeOptions, rbacUiControlService){ var generator = GenerateForm, form = GroupForm(); @@ -16,6 +16,11 @@ CredentialList = _.cloneDeep(CredentialList); CredentialList.fields.kind.noSearch = true; + rbacUiControlService.canAdd(GetBasePath('inventory') + $stateParams.inventory_id + "/groups") + .then(function(canAdd) { + $scope.canAdd = canAdd; + }); + $scope.formCancel = function(){ $state.go('^'); }; From 94b440e7f5e6f496a32f6e18752b971ac3120753 Mon Sep 17 00:00:00 2001 From: James Laska Date: Mon, 19 Sep 2016 10:54:39 -0400 Subject: [PATCH 34/51] Allow unittests from containers to work Don't `sudo` for me, I'll `sudo` when I need it, yo. This appears to only affect the non-container based development workflow. For folks that still need to `make develop` outside of a container, please use `sudo make develop` --- Makefile | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index c1b7d582e1..23d03407d7 100644 --- a/Makefile +++ b/Makefile @@ -317,8 +317,8 @@ develop: pip uninstall -y awx; \ $(PYTHON) setup.py develop; \ else \ - sudo pip uninstall -y awx; \ - sudo $(PYTHON) setup.py develop; \ + pip uninstall -y awx; \ + $(PYTHON) setup.py develop; \ fi version_file: @@ -448,7 +448,7 @@ pylint: reports check: flake8 pep8 # pyflakes pylint -TEST_DIRS=awx/main/tests +TEST_DIRS ?= awx/main/tests # Run all API unit tests. test: @if [ "$(VENV_BASE)" ]; then \ From 83e23b7ac45b87e3f84c4ddfc93073f0587186fc Mon Sep 17 00:00:00 2001 From: James Laska Date: Mon, 19 Sep 2016 11:08:58 -0400 Subject: [PATCH 35/51] Only run the unit tests from the docker container --- tools/docker-compose/unit-tests/docker-compose.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/tools/docker-compose/unit-tests/docker-compose.yml b/tools/docker-compose/unit-tests/docker-compose.yml index 77bf989475..4293aac28a 100644 --- a/tools/docker-compose/unit-tests/docker-compose.yml +++ b/tools/docker-compose/unit-tests/docker-compose.yml @@ -7,6 +7,7 @@ services: dockerfile: tools/docker-compose/unit-tests/Dockerfile environment: SWIG_FEATURES: "-cpperraswarn -includeall -I/usr/include/openssl" + TEST_DIRS: "awx/main/tests/unit" command: ["make requirements_test test"] volumes: From 23d7e70204a7372be494cd355b8e0b6fcfcd0235 Mon Sep 17 00:00:00 2001 From: Wayne Witzel III Date: Mon, 19 Sep 2016 14:24:01 -0400 Subject: [PATCH 36/51] update network cred ssh agent unit test with post_run_hook mock --- awx/main/tests/unit/test_network_credential.py | 1 + 1 file changed, 1 insertion(+) diff --git a/awx/main/tests/unit/test_network_credential.py b/awx/main/tests/unit/test_network_credential.py index 8ef5c4cb5e..6517c14a89 100644 --- a/awx/main/tests/unit/test_network_credential.py +++ b/awx/main/tests/unit/test_network_credential.py @@ -74,6 +74,7 @@ def test_net_cred_ssh_agent(mocker, get_ssh_version): mocker.patch.object(run_job, 'should_use_proot', return_value=False) mocker.patch.object(run_job, 'run_pexpect', return_value=('successful', 0)) mocker.patch.object(run_job, 'open_fifo_write', return_value=None) + mocker.patch.object(run_job, 'post_run_hook', return_value=None) run_job.run(mock_job.id) assert run_job.update_model.call_count == 3 From 9fe408c6f7d77c878c474e37733e243975cfd625 Mon Sep 17 00:00:00 2001 From: Shane McDonald Date: Tue, 20 Sep 2016 09:56:19 -0400 Subject: [PATCH 37/51] Add instructions to rebuild unit test container --- tools/docker-compose/unit-tests/README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tools/docker-compose/unit-tests/README.md b/tools/docker-compose/unit-tests/README.md index c3fa200d38..49243893fe 100644 --- a/tools/docker-compose/unit-tests/README.md +++ b/tools/docker-compose/unit-tests/README.md @@ -6,6 +6,13 @@ $ docker-compose -f tools/docker-compose/unit-tests/docker-compose.yml run unit- This will start the container, install the dependencies, and run the unit tests. +To rebuild: + + +```shell +$ docker-compose -f tools/docker-compose/unit-tests/docker-compose.yml build +``` + If you just want to pop into a shell and poke around, run: ```shell From ecf8dd0d6d057ecf73e60572710e63b96205da49 Mon Sep 17 00:00:00 2001 From: Jim Ladd Date: Tue, 20 Sep 2016 11:35:51 -0400 Subject: [PATCH 38/51] Update output for tower-manage create_preload_data --- awx/main/management/commands/create_preload_data.py | 1 + 1 file changed, 1 insertion(+) diff --git a/awx/main/management/commands/create_preload_data.py b/awx/main/management/commands/create_preload_data.py index a6b1e41f0d..caeba2c0a3 100644 --- a/awx/main/management/commands/create_preload_data.py +++ b/awx/main/management/commands/create_preload_data.py @@ -47,3 +47,4 @@ class Command(BaseCommand): inventory=i, credential=c) print('Default organization added.') + print('Demo Credential, Inventory, and Job Template added.') From f68baadc39eaad2052de86aa549ffae416b5b926 Mon Sep 17 00:00:00 2001 From: Aaron Tan Date: Tue, 20 Sep 2016 23:38:05 -0400 Subject: [PATCH 39/51] Add can_start to notification template access --- awx/api/views.py | 1 + awx/main/access.py | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/awx/api/views.py b/awx/api/views.py index efbbecf10e..4a7e29da16 100644 --- a/awx/api/views.py +++ b/awx/api/views.py @@ -3677,6 +3677,7 @@ class NotificationTemplateTest(GenericAPIView): model = NotificationTemplate serializer_class = EmptySerializer new_in_300 = True + is_job_start = True def post(self, request, *args, **kwargs): obj = self.get_object() diff --git a/awx/main/access.py b/awx/main/access.py index c7eb368cad..d4b65113aa 100644 --- a/awx/main/access.py +++ b/awx/main/access.py @@ -1641,6 +1641,12 @@ class NotificationTemplateAccess(BaseAccess): def can_delete(self, obj): return self.can_change(obj, None) + @check_superuser + def can_start(self, obj): + if obj.organization is None: + return False + return self.user in obj.organization.admin_role + class NotificationAccess(BaseAccess): ''' I can see/use a notification if I have permission to From e958a12dabcf3530163e2e67c71ad9c1326d0aab Mon Sep 17 00:00:00 2001 From: Shane McDonald Date: Wed, 21 Sep 2016 11:45:54 -0400 Subject: [PATCH 40/51] Remove --unsafe-perm MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This breaks RPM builds… Will specify in DEB jobs only. --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 23d03407d7..2fcd0d1b75 100644 --- a/Makefile +++ b/Makefile @@ -485,7 +485,7 @@ test_jenkins : test_coverage # -------------------------------------- ui-deps-built: awx/ui/package.json - $(NPM_BIN) --unsafe-perm --prefix awx/ui install awx/ui + $(NPM_BIN) --prefix awx/ui install awx/ui touch awx/ui/.deps_built ui-docker-machine: ui-deps-built From dcf127ac6d5cb4f257f612d2a27f5a29e3e4d344 Mon Sep 17 00:00:00 2001 From: James Laska Date: Tue, 20 Sep 2016 16:24:09 -0400 Subject: [PATCH 41/51] Don't install csslint during requirements_jenkins --- Makefile | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Makefile b/Makefile index 2fcd0d1b75..fd0ef0228b 100644 --- a/Makefile +++ b/Makefile @@ -298,12 +298,10 @@ requirements_tower_dev: # Install third-party requirements needed for running unittests in jenkins requirements_jenkins: if [ "$(VENV_BASE)" ]; then \ - . $(VENV_BASE)/tower/bin/activate; \ - $(VENV_BASE)/tower/bin/pip install -Ir requirements/requirements_jenkins.txt; \ + . $(VENV_BASE)/tower/bin/activate && pip install -Ir requirements/requirements_jenkins.txt; \ else \ pip install -Ir requirements/requirements_jenkins.txt; \ - fi && \ - $(NPM_BIN) install csslint + fi requirements: requirements_ansible requirements_tower From 5b6f61097016ac74898b5bc165a77bae774f1ad1 Mon Sep 17 00:00:00 2001 From: James Laska Date: Tue, 20 Sep 2016 16:24:29 -0400 Subject: [PATCH 42/51] Move requirements build to unittest Dockerfile Previously, the requirements were built during compose runtime. --- tools/docker-compose/unit-tests/Dockerfile | 28 +++++++++++++++++-- .../unit-tests/docker-compose.yml | 3 +- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/tools/docker-compose/unit-tests/Dockerfile b/tools/docker-compose/unit-tests/Dockerfile index 66f2c54558..decbf01374 100644 --- a/tools/docker-compose/unit-tests/Dockerfile +++ b/tools/docker-compose/unit-tests/Dockerfile @@ -29,12 +29,34 @@ RUN yum install -y \ # Remove the 2 lines below and uncomment the 3 lines above to build # RPMs with the old JS build system. RUN curl --silent --location https://rpm.nodesource.com/setup_6.x | bash - -RUN yum install -y nodejs -RUN npm set progress=false +RUN yum install -y nodejs WORKDIR "/ansible-tower" -ENV VENV_BASE="/venv" +# Copy requirements files +COPY requirements/*.txt requirements/ + +# Copy __init__.py so the Makefile can retrieve `awx.__version__` +COPY awx/__init__.py awx/ + +# Copy Makefile +COPY Makefile . + +# Make tower runtime virtualenvs +ENV SWIG_FEATURES="-cpperraswarn -includeall -I/usr/include/openssl" +RUN make requirements + +RUN pip install -r requirements/requirements_jenkins.txt +# ENV VENV_BASE="" +# RUN make requirements_jenkins + +# Build front-end deps +COPY awx/ui/package.json awx/ui/ + +RUN npm set progress=false + +RUN make ui-deps-built + ENTRYPOINT ["/bin/bash", "-c"] CMD ["bash"] diff --git a/tools/docker-compose/unit-tests/docker-compose.yml b/tools/docker-compose/unit-tests/docker-compose.yml index 4293aac28a..125f93c6d4 100644 --- a/tools/docker-compose/unit-tests/docker-compose.yml +++ b/tools/docker-compose/unit-tests/docker-compose.yml @@ -8,7 +8,6 @@ services: environment: SWIG_FEATURES: "-cpperraswarn -includeall -I/usr/include/openssl" TEST_DIRS: "awx/main/tests/unit" - command: ["make requirements_test test"] - + command: ["make test"] volumes: - ../../../:/ansible-tower From e9570c9d52f075b57c9f5d86cc5db32f33086060 Mon Sep 17 00:00:00 2001 From: Leigh Johnson Date: Wed, 21 Sep 2016 16:18:10 -0400 Subject: [PATCH 43/51] Update package.json deps to include PhantomJS / karma launcher, update output location, freeze deps --- awx/ui/karma.conf.js | 8 +- awx/ui/npm-shrinkwrap.json | 613 ++++++++++++++++++++++--------------- awx/ui/package.json | 3 + 3 files changed, 372 insertions(+), 252 deletions(-) diff --git a/awx/ui/karma.conf.js b/awx/ui/karma.conf.js index 91e07bf67c..dbeab4a306 100644 --- a/awx/ui/karma.conf.js +++ b/awx/ui/karma.conf.js @@ -9,13 +9,13 @@ module.exports = function(config) { browsers: ['Chrome', 'Firefox'], coverageReporter: { reporters: [ - { type: 'html', subdir: 'html' } + { type: 'html', subdir: 'html' }, ] }, frameworks: [ 'jasmine', ], - reporters: ['progress', 'coverage'], + reporters: ['progress', 'coverage', 'junit'], files: [ './client/src/app.js', './node_modules/angular-mocks/angular-mocks.js', @@ -86,7 +86,9 @@ module.exports = function(config) { } }, junitReporter: { - outputFile: 'coverage/test-results.xml' + outputDir: 'coverage', + outputFile: 'ui-unit-test-results.xml', + useBrowserName: false } }); }; diff --git a/awx/ui/npm-shrinkwrap.json b/awx/ui/npm-shrinkwrap.json index 483276ace8..0e0666aaa2 100644 --- a/awx/ui/npm-shrinkwrap.json +++ b/awx/ui/npm-shrinkwrap.json @@ -8,9 +8,9 @@ "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.0.9.tgz" }, "accepts": { - "version": "1.2.13", - "from": "accepts@>=1.2.13 <1.3.0", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.2.13.tgz" + "version": "1.3.3", + "from": "accepts@>=1.3.3 <1.4.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.3.tgz" }, "acorn": { "version": "3.3.0", @@ -50,9 +50,9 @@ "resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz" }, "almond": { - "version": "0.3.2", + "version": "0.3.3", "from": "almond@>=0.3.1 <0.4.0", - "resolved": "https://registry.npmjs.org/almond/-/almond-0.3.2.tgz" + "resolved": "https://registry.npmjs.org/almond/-/almond-0.3.3.tgz" }, "amdefine": { "version": "1.0.0", @@ -149,11 +149,6 @@ "version": "3.8.0", "from": "lodash@>=3.8.0 <3.9.0", "resolved": "https://registry.npmjs.org/lodash/-/lodash-3.8.0.tgz" - }, - "rrule": { - "version": "2.2.0-dev", - "from": "jkbrzt/rrule#4ff63b2f8524fd6d5ba6e80db770953b5cd08a0c", - "resolved": "git://github.com/jkbrzt/rrule.git#4ff63b2f8524fd6d5ba6e80db770953b5cd08a0c" } } }, @@ -312,16 +307,9 @@ "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.2.0.tgz" }, "async": { - "version": "2.0.1", - "from": "async@>=2.0.1 <3.0.0", - "resolved": "https://registry.npmjs.org/async/-/async-2.0.1.tgz", - "dependencies": { - "lodash": { - "version": "4.15.0", - "from": "lodash@>=4.8.0 <5.0.0", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.15.0.tgz" - } - } + "version": "1.5.2", + "from": "async@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz" }, "async-each": { "version": "1.0.1", @@ -333,10 +321,15 @@ "from": "async-each-series@0.1.1", "resolved": "https://registry.npmjs.org/async-each-series/-/async-each-series-0.1.1.tgz" }, + "asynckit": { + "version": "0.4.0", + "from": "asynckit@>=0.4.0 <0.5.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz" + }, "autoprefixer": { - "version": "6.4.0", + "version": "6.4.1", "from": "autoprefixer@>=6.0.0 <7.0.0", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-6.4.0.tgz" + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-6.4.1.tgz" }, "aws-sign2": { "version": "0.6.0", @@ -364,9 +357,9 @@ "resolved": "https://registry.npmjs.org/json5/-/json5-0.4.0.tgz" }, "lodash": { - "version": "4.15.0", + "version": "4.16.1", "from": "lodash@>=4.2.0 <5.0.0", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.15.0.tgz" + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.16.1.tgz" }, "source-map": { "version": "0.5.6", @@ -381,9 +374,9 @@ "resolved": "https://registry.npmjs.org/babel-generator/-/babel-generator-6.14.0.tgz", "dependencies": { "lodash": { - "version": "4.15.0", + "version": "4.16.1", "from": "lodash@>=4.2.0 <5.0.0", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.15.0.tgz" + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.16.1.tgz" }, "source-map": { "version": "0.5.6", @@ -403,9 +396,9 @@ "resolved": "https://registry.npmjs.org/babel-helper-define-map/-/babel-helper-define-map-6.9.0.tgz", "dependencies": { "lodash": { - "version": "4.15.0", + "version": "4.16.1", "from": "lodash@>=4.2.0 <5.0.0", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.15.0.tgz" + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.16.1.tgz" } } }, @@ -435,9 +428,9 @@ "resolved": "https://registry.npmjs.org/babel-helper-regex/-/babel-helper-regex-6.9.0.tgz", "dependencies": { "lodash": { - "version": "4.15.0", + "version": "4.16.1", "from": "lodash@>=4.2.0 <5.0.0", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.15.0.tgz" + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.16.1.tgz" } } }, @@ -477,14 +470,14 @@ "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-block-scoped-functions/-/babel-plugin-transform-es2015-block-scoped-functions-6.8.0.tgz" }, "babel-plugin-transform-es2015-block-scoping": { - "version": "6.14.0", + "version": "6.15.0", "from": "babel-plugin-transform-es2015-block-scoping@>=6.14.0 <7.0.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-block-scoping/-/babel-plugin-transform-es2015-block-scoping-6.14.0.tgz", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-block-scoping/-/babel-plugin-transform-es2015-block-scoping-6.15.0.tgz", "dependencies": { "lodash": { - "version": "4.15.0", + "version": "4.16.1", "from": "lodash@>=4.2.0 <5.0.0", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.15.0.tgz" + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.16.1.tgz" } } }, @@ -599,9 +592,9 @@ "resolved": "https://registry.npmjs.org/babel-register/-/babel-register-6.14.0.tgz", "dependencies": { "lodash": { - "version": "4.15.0", + "version": "4.16.1", "from": "lodash@>=4.2.0 <5.0.0", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.15.0.tgz" + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.16.1.tgz" } } }, @@ -611,45 +604,45 @@ "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.11.6.tgz" }, "babel-template": { - "version": "6.14.0", + "version": "6.15.0", "from": "babel-template@>=6.14.0 <7.0.0", - "resolved": "https://registry.npmjs.org/babel-template/-/babel-template-6.14.0.tgz", + "resolved": "https://registry.npmjs.org/babel-template/-/babel-template-6.15.0.tgz", "dependencies": { "lodash": { - "version": "4.15.0", + "version": "4.16.1", "from": "lodash@>=4.2.0 <5.0.0", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.15.0.tgz" + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.16.1.tgz" } } }, "babel-traverse": { - "version": "6.14.0", + "version": "6.15.0", "from": "babel-traverse@>=6.14.0 <7.0.0", - "resolved": "https://registry.npmjs.org/babel-traverse/-/babel-traverse-6.14.0.tgz", + "resolved": "https://registry.npmjs.org/babel-traverse/-/babel-traverse-6.15.0.tgz", "dependencies": { "lodash": { - "version": "4.15.0", + "version": "4.16.1", "from": "lodash@>=4.2.0 <5.0.0", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.15.0.tgz" + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.16.1.tgz" } } }, "babel-types": { - "version": "6.14.0", + "version": "6.15.0", "from": "babel-types@>=6.14.0 <7.0.0", - "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.14.0.tgz", + "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.15.0.tgz", "dependencies": { "lodash": { - "version": "4.15.0", + "version": "4.16.1", "from": "lodash@>=4.2.0 <5.0.0", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.15.0.tgz" + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.16.1.tgz" } } }, "babylon": { - "version": "6.9.1", + "version": "6.10.0", "from": "babylon@>=6.9.0 <7.0.0", - "resolved": "https://registry.npmjs.org/babylon/-/babylon-6.9.1.tgz" + "resolved": "https://registry.npmjs.org/babylon/-/babylon-6.10.0.tgz" }, "backo2": { "version": "1.0.2", @@ -719,9 +712,9 @@ "resolved": "https://registry.npmjs.org/big.js/-/big.js-3.1.3.tgz" }, "binary-extensions": { - "version": "1.5.0", + "version": "1.6.0", "from": "binary-extensions@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.5.0.tgz" + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.6.0.tgz" }, "bindings": { "version": "1.2.1", @@ -739,15 +732,20 @@ "resolved": "https://registry.npmjs.org/blob/-/blob-0.0.4.tgz" }, "bluebird": { - "version": "3.4.3", + "version": "3.4.6", "from": "bluebird@>=3.3.0 <4.0.0", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.3.tgz" + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.6.tgz" }, "body-parser": { "version": "1.14.2", "from": "body-parser@>=1.14.0 <1.15.0", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.14.2.tgz", "dependencies": { + "http-errors": { + "version": "1.3.1", + "from": "http-errors@>=1.3.1 <1.4.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.3.1.tgz" + }, "qs": { "version": "5.2.0", "from": "qs@5.2.0", @@ -781,39 +779,24 @@ "resolved": "https://registry.npmjs.org/braces/-/braces-1.8.5.tgz" }, "browser-sync": { - "version": "2.14.0", + "version": "2.16.0", "from": "browser-sync@>=2.14.0 <3.0.0", - "resolved": "https://registry.npmjs.org/browser-sync/-/browser-sync-2.14.0.tgz", + "resolved": "https://registry.npmjs.org/browser-sync/-/browser-sync-2.16.0.tgz", "dependencies": { - "camelcase": { - "version": "3.0.0", - "from": "camelcase@>=3.0.0 <4.0.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz" - }, "cliui": { "version": "3.2.0", "from": "cliui@>=3.2.0 <4.0.0", "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz" }, - "micromatch": { - "version": "2.3.8", - "from": "micromatch@2.3.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-2.3.8.tgz" - }, - "qs": { - "version": "6.2.0", - "from": "qs@6.2.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.2.0.tgz" - }, "window-size": { "version": "0.2.0", "from": "window-size@>=0.2.0 <0.3.0", "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.2.0.tgz" }, "yargs": { - "version": "4.7.1", - "from": "yargs@4.7.1", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-4.7.1.tgz" + "version": "5.0.0", + "from": "yargs@5.0.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-5.0.0.tgz" } } }, @@ -823,9 +806,9 @@ "resolved": "https://registry.npmjs.org/browser-sync-client/-/browser-sync-client-2.4.2.tgz" }, "browser-sync-ui": { - "version": "0.6.0", - "from": "browser-sync-ui@0.6.0", - "resolved": "https://registry.npmjs.org/browser-sync-ui/-/browser-sync-ui-0.6.0.tgz" + "version": "0.6.1", + "from": "browser-sync-ui@0.6.1", + "resolved": "https://registry.npmjs.org/browser-sync-ui/-/browser-sync-ui-0.6.1.tgz" }, "browserify-zlib": { "version": "0.1.4", @@ -834,13 +817,13 @@ }, "browserslist": { "version": "1.3.6", - "from": "browserslist@>=1.3.5 <1.4.0", + "from": "browserslist@>=1.3.6 <1.4.0", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-1.3.6.tgz" }, "bs-recipes": { - "version": "1.2.2", - "from": "bs-recipes@1.2.2", - "resolved": "https://registry.npmjs.org/bs-recipes/-/bs-recipes-1.2.2.tgz" + "version": "1.2.3", + "from": "bs-recipes@1.2.3", + "resolved": "https://registry.npmjs.org/bs-recipes/-/bs-recipes-1.2.3.tgz" }, "buffer": { "version": "4.9.1", @@ -885,9 +868,9 @@ } }, "caniuse-db": { - "version": "1.0.30000526", - "from": "caniuse-db@>=1.0.30000515 <2.0.0", - "resolved": "https://registry.npmjs.org/caniuse-db/-/caniuse-db-1.0.30000526.tgz" + "version": "1.0.30000538", + "from": "caniuse-db@>=1.0.30000527 <2.0.0", + "resolved": "https://registry.npmjs.org/caniuse-db/-/caniuse-db-1.0.30000538.tgz" }, "caseless": { "version": "0.11.0", @@ -905,9 +888,9 @@ "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz" }, "chokidar": { - "version": "1.5.1", - "from": "chokidar@1.5.1", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-1.5.1.tgz" + "version": "1.6.0", + "from": "chokidar@1.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-1.6.0.tgz" }, "cli": { "version": "1.0.0", @@ -915,9 +898,9 @@ "resolved": "https://registry.npmjs.org/cli/-/cli-1.0.0.tgz", "dependencies": { "glob": { - "version": "7.0.6", + "version": "7.1.0", "from": "glob@>=7.0.5 <8.0.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.0.6.tgz" + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.0.tgz" } } }, @@ -949,9 +932,9 @@ "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.0.0.tgz" }, "codemirror": { - "version": "5.18.2", + "version": "5.19.0", "from": "codemirror@>=5.17.0 <6.0.0", - "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.18.2.tgz" + "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.19.0.tgz" }, "coffee-script": { "version": "1.10.0", @@ -964,14 +947,14 @@ "resolved": "https://registry.npmjs.org/colors/-/colors-1.1.2.tgz" }, "combine-lists": { - "version": "1.0.0", + "version": "1.0.1", "from": "combine-lists@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/combine-lists/-/combine-lists-1.0.0.tgz", + "resolved": "https://registry.npmjs.org/combine-lists/-/combine-lists-1.0.1.tgz", "dependencies": { "lodash": { - "version": "4.15.0", + "version": "4.16.1", "from": "lodash@>=4.5.0 <5.0.0", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.15.0.tgz" + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.16.1.tgz" } } }, @@ -1032,20 +1015,10 @@ "from": "compression@>=1.5.2 <2.0.0", "resolved": "https://registry.npmjs.org/compression/-/compression-1.6.2.tgz", "dependencies": { - "accepts": { - "version": "1.3.3", - "from": "accepts@>=1.3.3 <1.4.0", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.3.tgz" - }, "bytes": { "version": "2.3.0", "from": "bytes@2.3.0", "resolved": "https://registry.npmjs.org/bytes/-/bytes-2.3.0.tgz" - }, - "negotiator": { - "version": "0.6.1", - "from": "negotiator@0.6.1", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz" } } }, @@ -1054,6 +1027,11 @@ "from": "concat-map@0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz" }, + "concat-stream": { + "version": "1.5.0", + "from": "concat-stream@1.5.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.5.0.tgz" + }, "connect": { "version": "3.4.1", "from": "connect@3.4.1", @@ -1340,12 +1318,19 @@ "end-of-stream": { "version": "1.0.0", "from": "end-of-stream@1.0.0", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.0.0.tgz" + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.0.0.tgz", + "dependencies": { + "once": { + "version": "1.3.3", + "from": "once@>=1.3.0 <1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.3.3.tgz" + } + } }, "engine.io": { - "version": "1.6.9", - "from": "engine.io@1.6.9", - "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-1.6.9.tgz", + "version": "1.6.11", + "from": "engine.io@1.6.11", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-1.6.11.tgz", "dependencies": { "accepts": { "version": "1.1.4", @@ -1368,16 +1353,16 @@ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.4.9.tgz" }, "ws": { - "version": "1.0.1", - "from": "ws@1.0.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-1.0.1.tgz" + "version": "1.1.0", + "from": "ws@1.1.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-1.1.0.tgz" } } }, "engine.io-client": { - "version": "1.6.9", - "from": "engine.io-client@1.6.9", - "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-1.6.9.tgz", + "version": "1.6.11", + "from": "engine.io-client@1.6.11", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-1.6.11.tgz", "dependencies": { "ws": { "version": "1.0.1", @@ -1435,6 +1420,11 @@ "from": "error-ex@>=1.2.0 <2.0.0", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.0.tgz" }, + "es6-promise": { + "version": "3.2.1", + "from": "es6-promise@>=3.2.1 <3.3.0", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.2.1.tgz" + }, "escape-html": { "version": "1.0.3", "from": "escape-html@>=1.0.3 <1.1.0", @@ -1571,21 +1561,48 @@ "from": "extglob@>=0.3.1 <0.4.0", "resolved": "https://registry.npmjs.org/extglob/-/extglob-0.3.2.tgz" }, + "extract-zip": { + "version": "1.5.0", + "from": "extract-zip@>=1.5.0 <1.6.0", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-1.5.0.tgz", + "dependencies": { + "debug": { + "version": "0.7.4", + "from": "debug@0.7.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-0.7.4.tgz" + }, + "minimist": { + "version": "0.0.8", + "from": "minimist@0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz" + }, + "mkdirp": { + "version": "0.5.0", + "from": "mkdirp@0.5.0", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.0.tgz" + } + } + }, "extsprintf": { "version": "1.0.2", "from": "extsprintf@1.0.2", "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.0.2.tgz" }, "fast-levenshtein": { - "version": "1.1.4", - "from": "fast-levenshtein@>=1.1.0 <2.0.0", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-1.1.4.tgz" + "version": "2.0.4", + "from": "fast-levenshtein@>=2.0.4 <2.1.0", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.4.tgz" }, "faye-websocket": { "version": "0.10.0", "from": "faye-websocket@>=0.10.0 <0.11.0", "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.10.0.tgz" }, + "fd-slicer": { + "version": "1.0.1", + "from": "fd-slicer@>=1.0.1 <1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.0.1.tgz" + }, "figures": { "version": "1.7.0", "from": "figures@>=1.3.5 <2.0.0", @@ -1641,9 +1658,9 @@ "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-0.3.0.tgz" }, "for-in": { - "version": "0.1.5", + "version": "0.1.6", "from": "for-in@>=0.1.5 <0.2.0", - "resolved": "https://registry.npmjs.org/for-in/-/for-in-0.1.5.tgz" + "resolved": "https://registry.npmjs.org/for-in/-/for-in-0.1.6.tgz" }, "for-own": { "version": "0.1.4", @@ -1656,9 +1673,9 @@ "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz" }, "form-data": { - "version": "1.0.1", - "from": "form-data@>=1.0.0-rc4 <1.1.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-1.0.1.tgz" + "version": "2.0.0", + "from": "form-data@>=2.0.0 <2.1.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.0.0.tgz" }, "formidable": { "version": "1.0.17", @@ -1686,9 +1703,9 @@ "resolved": "https://registry.npmjs.org/fs-access/-/fs-access-1.0.0.tgz" }, "fs-extra": { - "version": "0.26.7", - "from": "fs-extra@0.26.7", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-0.26.7.tgz" + "version": "0.30.0", + "from": "fs-extra@0.30.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-0.30.0.tgz" }, "fs.realpath": { "version": "1.0.0", @@ -2317,6 +2334,11 @@ "from": "generate-object-property@>=1.1.0 <2.0.0", "resolved": "https://registry.npmjs.org/generate-object-property/-/generate-object-property-1.2.0.tgz" }, + "get-caller-file": { + "version": "1.0.2", + "from": "get-caller-file@>=1.0.1 <2.0.0", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.2.tgz" + }, "get-stdin": { "version": "4.0.1", "from": "get-stdin@>=4.0.1 <5.0.0", @@ -2413,11 +2435,6 @@ "from": "grunt-legacy-util@>=1.0.0 <1.1.0", "resolved": "https://registry.npmjs.org/grunt-legacy-util/-/grunt-legacy-util-1.0.0.tgz", "dependencies": { - "async": { - "version": "1.5.2", - "from": "async@>=1.5.2 <1.6.0", - "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz" - }, "lodash": { "version": "4.3.0", "from": "lodash@>=4.3.0 <4.4.0", @@ -2430,11 +2447,6 @@ "from": "handlebars@>=4.0.0 <4.1.0", "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.0.5.tgz", "dependencies": { - "async": { - "version": "1.5.2", - "from": "async@>=1.4.0 <2.0.0", - "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz" - }, "source-map": { "version": "0.4.4", "from": "source-map@>=0.4.4 <0.5.0", @@ -2491,6 +2503,11 @@ "from": "has-flag@>=1.0.0 <2.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz" }, + "hasha": { + "version": "2.2.0", + "from": "hasha@>=2.2.0 <2.3.0", + "resolved": "https://registry.npmjs.org/hasha/-/hasha-2.2.0.tgz" + }, "hawk": { "version": "3.1.3", "from": "hawk@>=3.1.3 <3.2.0", @@ -2544,29 +2561,31 @@ "resolved": "https://registry.npmjs.org/http-browserify/-/http-browserify-1.7.0.tgz" }, "http-errors": { - "version": "1.3.1", - "from": "http-errors@>=1.3.1 <1.4.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.3.1.tgz" + "version": "1.5.0", + "from": "http-errors@>=1.5.0 <1.6.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.5.0.tgz", + "dependencies": { + "inherits": { + "version": "2.0.1", + "from": "inherits@2.0.1", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz" + } + } }, "http-proxy": { - "version": "1.13.3", - "from": "http-proxy@1.13.3", - "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.13.3.tgz" + "version": "1.14.0", + "from": "http-proxy@1.14.0", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.14.0.tgz" }, "http-proxy-middleware": { "version": "0.17.1", "from": "http-proxy-middleware@>=0.17.1 <0.18.0", "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-0.17.1.tgz", "dependencies": { - "http-proxy": { - "version": "1.14.0", - "from": "http-proxy@>=1.14.0 <2.0.0", - "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.14.0.tgz" - }, "lodash": { - "version": "4.15.0", + "version": "4.16.1", "from": "lodash@>=4.14.2 <5.0.0", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.15.0.tgz" + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.16.1.tgz" } } }, @@ -2628,9 +2647,9 @@ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.5.tgz" }, "inherits": { - "version": "2.0.1", + "version": "2.0.3", "from": "inherits@>=2.0.1 <2.1.0", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz" + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz" }, "inquirer": { "version": "0.8.5", @@ -2730,9 +2749,9 @@ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz" }, "is-my-json-valid": { - "version": "2.13.1", + "version": "2.14.0", "from": "is-my-json-valid@>=2.12.4 <3.0.0", - "resolved": "https://registry.npmjs.org/is-my-json-valid/-/is-my-json-valid-2.13.1.tgz" + "resolved": "https://registry.npmjs.org/is-my-json-valid/-/is-my-json-valid-2.14.0.tgz" }, "is-number": { "version": "2.1.0", @@ -2754,6 +2773,11 @@ "from": "is-property@>=1.0.0 <2.0.0", "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz" }, + "is-stream": { + "version": "1.1.0", + "from": "is-stream@>=1.0.1 <2.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz" + }, "is-typedarray": { "version": "1.0.0", "from": "is-typedarray@>=1.0.0 <1.1.0", @@ -2794,11 +2818,6 @@ "from": "istanbul@>=0.4.0 <0.5.0", "resolved": "https://registry.npmjs.org/istanbul/-/istanbul-0.4.5.tgz", "dependencies": { - "async": { - "version": "1.5.2", - "from": "async@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz" - }, "supports-color": { "version": "3.1.2", "from": "supports-color@>=3.1.0 <4.0.0", @@ -2808,13 +2827,13 @@ }, "istanbul-lib-coverage": { "version": "1.0.0", - "from": "istanbul-lib-coverage@>=1.0.0-alpha.4 <2.0.0", + "from": "istanbul-lib-coverage@>=1.0.0 <2.0.0", "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-1.0.0.tgz" }, "istanbul-lib-instrument": { - "version": "1.1.1", + "version": "1.1.3", "from": "istanbul-lib-instrument@>=1.1.1 <2.0.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-1.1.1.tgz" + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-1.1.3.tgz" }, "javascript-detect-element-resize": { "version": "0.5.3", @@ -2872,9 +2891,9 @@ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz" }, "json-schema": { - "version": "0.2.2", - "from": "json-schema@0.2.2", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.2.tgz" + "version": "0.2.3", + "from": "json-schema@0.2.3", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz" }, "json-stringify-safe": { "version": "5.0.1", @@ -2892,9 +2911,9 @@ "resolved": "https://registry.npmjs.org/json5/-/json5-0.5.0.tgz" }, "jsonfile": { - "version": "2.3.1", + "version": "2.4.0", "from": "jsonfile@>=2.1.0 <3.0.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-2.3.1.tgz" + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-2.4.0.tgz" }, "jsonpointer": { "version": "2.0.0", @@ -2902,15 +2921,20 @@ "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-2.0.0.tgz" }, "jsprim": { - "version": "1.3.0", + "version": "1.3.1", "from": "jsprim@>=1.2.2 <2.0.0", - "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.3.0.tgz" + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.3.1.tgz" }, "jstimezonedetect": { "version": "1.0.5", "from": "jstimezonedetect@1.0.5", "resolved": "https://registry.npmjs.org/jstimezonedetect/-/jstimezonedetect-1.0.5.tgz" }, + "kew": { + "version": "0.7.0", + "from": "kew@>=0.7.0 <0.8.0", + "resolved": "https://registry.npmjs.org/kew/-/kew-0.7.0.tgz" + }, "kind-of": { "version": "3.0.4", "from": "kind-of@>=3.0.2 <4.0.0", @@ -2996,9 +3020,9 @@ "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz" }, "loader-utils": { - "version": "0.2.15", + "version": "0.2.16", "from": "loader-utils@>=0.2.6 <0.3.0", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-0.2.15.tgz" + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-0.2.16.tgz" }, "localtunnel": { "version": "1.8.1", @@ -3015,6 +3039,11 @@ "from": "assert-plus@>=0.1.5 <0.2.0", "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.1.5.tgz" }, + "async": { + "version": "2.0.1", + "from": "async@>=2.0.1 <3.0.0", + "resolved": "https://registry.npmjs.org/async/-/async-2.0.1.tgz" + }, "bl": { "version": "1.0.3", "from": "bl@>=1.0.0 <1.1.0", @@ -3025,11 +3054,21 @@ "from": "cliui@>=3.0.3 <4.0.0", "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz" }, + "form-data": { + "version": "1.0.1", + "from": "form-data@>=1.0.0-rc3 <1.1.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-1.0.1.tgz" + }, "http-signature": { "version": "0.11.0", "from": "http-signature@>=0.11.0 <0.12.0", "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-0.11.0.tgz" }, + "lodash": { + "version": "4.16.1", + "from": "lodash@>=4.8.0 <5.0.0", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.16.1.tgz" + }, "qs": { "version": "5.2.1", "from": "qs@>=5.2.0 <5.3.0", @@ -3172,14 +3211,14 @@ "resolved": "https://registry.npmjs.org/mime/-/mime-1.2.4.tgz" }, "mime-db": { - "version": "1.23.0", - "from": "mime-db@>=1.23.0 <1.24.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.23.0.tgz" + "version": "1.24.0", + "from": "mime-db@>=1.24.0 <1.25.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.24.0.tgz" }, "mime-types": { - "version": "2.1.11", + "version": "2.1.12", "from": "mime-types@>=2.1.7 <2.2.0", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.11.tgz" + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.12.tgz" }, "minimatch": { "version": "3.0.3", @@ -3204,9 +3243,9 @@ } }, "moment": { - "version": "2.14.1", + "version": "2.15.1", "from": "moment@>=2.10.2 <3.0.0", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.14.1.tgz" + "resolved": "https://registry.npmjs.org/moment/-/moment-2.15.1.tgz" }, "ms": { "version": "0.7.1", @@ -3229,9 +3268,9 @@ "resolved": "https://registry.npmjs.org/nan/-/nan-2.4.0.tgz" }, "negotiator": { - "version": "0.5.3", - "from": "negotiator@0.5.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.5.3.tgz" + "version": "0.6.1", + "from": "negotiator@0.6.1", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz" }, "ng-toast": { "version": "2.0.0", @@ -3353,9 +3392,9 @@ "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.1.tgz" }, "once": { - "version": "1.3.3", + "version": "1.4.0", "from": "once@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.3.3.tgz" + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz" }, "open": { "version": "0.0.5", @@ -3368,9 +3407,9 @@ "resolved": "https://registry.npmjs.org/openurl/-/openurl-1.1.0.tgz" }, "opn": { - "version": "3.0.3", - "from": "opn@>=3.0.2 <4.0.0", - "resolved": "https://registry.npmjs.org/opn/-/opn-3.0.3.tgz" + "version": "4.0.2", + "from": "opn@4.0.2", + "resolved": "https://registry.npmjs.org/opn/-/opn-4.0.2.tgz" }, "optimist": { "version": "0.6.1", @@ -3390,9 +3429,9 @@ } }, "optionator": { - "version": "0.8.1", + "version": "0.8.2", "from": "optionator@>=0.8.1 <0.9.0", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.1.tgz" + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz" }, "options": { "version": "0.0.6", @@ -3508,6 +3547,38 @@ "from": "pbkdf2-compat@2.0.1", "resolved": "https://registry.npmjs.org/pbkdf2-compat/-/pbkdf2-compat-2.0.1.tgz" }, + "pend": { + "version": "1.2.0", + "from": "pend@>=1.2.0 <1.3.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz" + }, + "phantomjs-prebuilt": { + "version": "2.1.12", + "from": "phantomjs-prebuilt@>=2.1.12 <3.0.0", + "resolved": "https://registry.npmjs.org/phantomjs-prebuilt/-/phantomjs-prebuilt-2.1.12.tgz", + "dependencies": { + "async": { + "version": "2.0.1", + "from": "async@>=2.0.1 <3.0.0", + "resolved": "https://registry.npmjs.org/async/-/async-2.0.1.tgz" + }, + "form-data": { + "version": "1.0.1", + "from": "form-data@>=1.0.0-rc4 <1.1.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-1.0.1.tgz" + }, + "lodash": { + "version": "4.16.1", + "from": "lodash@>=4.8.0 <5.0.0", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.16.1.tgz" + }, + "request": { + "version": "2.74.0", + "from": "request@>=2.74.0 <2.75.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.74.0.tgz" + } + } + }, "pify": { "version": "2.3.0", "from": "pify@>=2.0.0 <3.0.0", @@ -3523,11 +3594,6 @@ "from": "pinkie-promise@>=2.0.0 <3.0.0", "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz" }, - "pkg-conf": { - "version": "1.1.3", - "from": "pkg-conf@>=1.1.2 <2.0.0", - "resolved": "https://registry.npmjs.org/pkg-conf/-/pkg-conf-1.1.3.tgz" - }, "pkg-up": { "version": "1.0.0", "from": "pkg-up@>=1.0.0 <2.0.0", @@ -3551,9 +3617,9 @@ } }, "postcss": { - "version": "5.1.2", + "version": "5.2.0", "from": "postcss@>=5.0.0 <6.0.0", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.1.2.tgz", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.0.tgz", "dependencies": { "source-map": { "version": "0.5.6", @@ -3600,15 +3666,20 @@ "resolved": "https://registry.npmjs.org/private/-/private-0.1.6.tgz" }, "process": { - "version": "0.11.8", + "version": "0.11.9", "from": "process@>=0.11.0 <0.12.0", - "resolved": "https://registry.npmjs.org/process/-/process-0.11.8.tgz" + "resolved": "https://registry.npmjs.org/process/-/process-0.11.9.tgz" }, "process-nextick-args": { "version": "1.0.7", "from": "process-nextick-args@>=1.0.6 <1.1.0", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz" }, + "progress": { + "version": "1.1.8", + "from": "progress@>=1.1.8 <1.2.0", + "resolved": "https://registry.npmjs.org/progress/-/progress-1.1.8.tgz" + }, "promise": { "version": "7.1.1", "from": "promise@>=7.1.1 <8.0.0", @@ -3632,7 +3703,14 @@ "end-of-stream": { "version": "1.1.0", "from": "end-of-stream@>=1.1.0 <2.0.0", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.1.0.tgz" + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.1.0.tgz", + "dependencies": { + "once": { + "version": "1.3.3", + "from": "once@>=1.3.0 <1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.3.3.tgz" + } + } } } }, @@ -3682,9 +3760,9 @@ "resolved": "https://registry.npmjs.org/randomatic/-/randomatic-1.1.5.tgz" }, "range-parser": { - "version": "1.0.3", - "from": "range-parser@>=1.0.3 <1.1.0", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.0.3.tgz" + "version": "1.2.0", + "from": "range-parser@>=1.2.0 <1.3.0", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz" }, "raw-body": { "version": "2.1.7", @@ -3803,9 +3881,19 @@ "resolved": "https://registry.npmjs.org/repeating/-/repeating-1.1.3.tgz" }, "request": { - "version": "2.74.0", + "version": "2.75.0", "from": "request@>=2.0.0 <3.0.0", - "resolved": "https://registry.npmjs.org/request/-/request-2.74.0.tgz" + "resolved": "https://registry.npmjs.org/request/-/request-2.75.0.tgz" + }, + "request-progress": { + "version": "2.0.1", + "from": "request-progress@>=2.0.1 <2.1.0", + "resolved": "https://registry.npmjs.org/request-progress/-/request-progress-2.0.1.tgz" + }, + "require-directory": { + "version": "2.1.1", + "from": "require-directory@>=2.1.1 <3.0.0", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz" }, "require-main-filename": { "version": "1.0.1", @@ -3848,9 +3936,9 @@ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.5.4.tgz", "dependencies": { "glob": { - "version": "7.0.6", + "version": "7.1.0", "from": "glob@>=7.0.5 <8.0.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.0.6.tgz" + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.0.tgz" } } }, @@ -3859,6 +3947,11 @@ "from": "ripemd160@0.2.0", "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-0.2.0.tgz" }, + "rrule": { + "version": "2.2.0-dev", + "from": "jkbrzt/rrule#4ff63b2f8524fd6d5ba6e80db770953b5cd08a0c", + "resolved": "git://github.com/jkbrzt/rrule.git#4ff63b2f8524fd6d5ba6e80db770953b5cd08a0c" + }, "rx": { "version": "4.1.0", "from": "rx@4.1.0", @@ -3897,31 +3990,26 @@ "resolved": "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz" }, "send": { - "version": "0.13.2", - "from": "send@0.13.2", - "resolved": "https://registry.npmjs.org/send/-/send-0.13.2.tgz", + "version": "0.14.1", + "from": "send@0.14.1", + "resolved": "https://registry.npmjs.org/send/-/send-0.14.1.tgz", "dependencies": { "mime": { "version": "1.3.4", "from": "mime@1.3.4", "resolved": "https://registry.npmjs.org/mime/-/mime-1.3.4.tgz" - }, - "statuses": { - "version": "1.2.1", - "from": "statuses@>=1.2.1 <1.3.0", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.2.1.tgz" } } }, "serve-index": { - "version": "1.7.3", - "from": "serve-index@1.7.3", - "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.7.3.tgz" + "version": "1.8.0", + "from": "serve-index@1.8.0", + "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.8.0.tgz" }, "serve-static": { - "version": "1.10.3", - "from": "serve-static@1.10.3", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.10.3.tgz" + "version": "1.11.1", + "from": "serve-static@1.11.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.11.1.tgz" }, "server-destroy": { "version": "1.0.1", @@ -3929,9 +4017,9 @@ "resolved": "https://registry.npmjs.org/server-destroy/-/server-destroy-1.0.1.tgz" }, "set-blocking": { - "version": "1.0.0", - "from": "set-blocking@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-1.0.0.tgz" + "version": "2.0.0", + "from": "set-blocking@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz" }, "set-immediate-shim": { "version": "1.0.1", @@ -3959,9 +4047,9 @@ "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.3.0.tgz" }, "signal-exit": { - "version": "3.0.0", + "version": "3.0.1", "from": "signal-exit@>=3.0.0 <4.0.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.0.tgz" + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.1.tgz" }, "slash": { "version": "1.0.0", @@ -3974,9 +4062,9 @@ "resolved": "https://registry.npmjs.org/sntp/-/sntp-1.0.9.tgz" }, "socket.io": { - "version": "1.4.6", - "from": "socket.io@1.4.6", - "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-1.4.6.tgz", + "version": "1.4.8", + "from": "socket.io@1.4.8", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-1.4.8.tgz", "dependencies": { "component-emitter": { "version": "1.2.0", @@ -3984,9 +4072,9 @@ "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.0.tgz" }, "socket.io-client": { - "version": "1.4.6", - "from": "socket.io-client@1.4.6", - "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-1.4.6.tgz" + "version": "1.4.8", + "from": "socket.io-client@1.4.8", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-1.4.8.tgz" } } }, @@ -4119,7 +4207,7 @@ }, "statuses": { "version": "1.3.0", - "from": "statuses@>=1.0.0 <2.0.0", + "from": "statuses@>=1.3.0 <2.0.0", "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.3.0.tgz" }, "stream-browserify": { @@ -4199,11 +4287,6 @@ "from": "supports-color@>=2.0.0 <3.0.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz" }, - "symbol": { - "version": "0.2.3", - "from": "symbol@>=0.2.1 <0.3.0", - "resolved": "https://registry.npmjs.org/symbol/-/symbol-0.2.3.tgz" - }, "tapable": { "version": "0.1.10", "from": "tapable@>=0.1.8 <0.2.0", @@ -4232,9 +4315,9 @@ } }, "test-exclude": { - "version": "2.1.1", + "version": "2.1.2", "from": "test-exclude@>=2.1.1 <3.0.0", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-2.1.1.tgz" + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-2.1.2.tgz" }, "text-table": { "version": "0.2.0", @@ -4246,6 +4329,11 @@ "from": "tfunk@>=3.0.1 <4.0.0", "resolved": "https://registry.npmjs.org/tfunk/-/tfunk-3.0.2.tgz" }, + "throttleit": { + "version": "1.0.0", + "from": "throttleit@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-1.0.0.tgz" + }, "through": { "version": "2.3.8", "from": "through@>=2.3.6 <3.0.0", @@ -4338,6 +4426,11 @@ "from": "type-is@>=1.6.10 <1.7.0", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.13.tgz" }, + "typedarray": { + "version": "0.0.6", + "from": "typedarray@>=0.0.5 <0.1.0", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz" + }, "ua-parser-js": { "version": "0.7.10", "from": "ua-parser-js@0.7.10", @@ -4408,7 +4501,14 @@ "util": { "version": "0.10.3", "from": "util@>=0.10.3 <0.11.0", - "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz" + "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz", + "dependencies": { + "inherits": { + "version": "2.0.1", + "from": "inherits@2.0.1", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz" + } + } }, "util-deprecate": { "version": "1.0.2", @@ -4421,9 +4521,9 @@ "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.0.tgz" }, "uuid": { - "version": "2.0.2", + "version": "2.0.3", "from": "uuid@>=2.0.2 <3.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-2.0.2.tgz" + "resolved": "https://registry.npmjs.org/uuid/-/uuid-2.0.3.tgz" }, "validate-npm-package-license": { "version": "3.0.1", @@ -4498,9 +4598,9 @@ "resolved": "https://registry.npmjs.org/bl/-/bl-0.9.5.tgz" }, "bluebird": { - "version": "2.10.2", + "version": "2.11.0", "from": "bluebird@>=2.9.30 <3.0.0", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-2.10.2.tgz" + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-2.11.0.tgz" }, "caseless": { "version": "0.9.0", @@ -4604,9 +4704,9 @@ } }, "webpack-dev-middleware": { - "version": "1.6.1", + "version": "1.8.1", "from": "webpack-dev-middleware@>=1.0.11 <2.0.0", - "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-1.6.1.tgz", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-1.8.1.tgz", "dependencies": { "mime": { "version": "1.3.4", @@ -4631,9 +4731,14 @@ "resolved": "https://registry.npmjs.org/weinre/-/weinre-2.0.0-pre-I0Z7U9OV.tgz" }, "which": { - "version": "1.2.10", + "version": "1.2.11", "from": "which@>=1.2.0 <1.3.0", - "resolved": "https://registry.npmjs.org/which/-/which-1.2.10.tgz" + "resolved": "https://registry.npmjs.org/which/-/which-1.2.11.tgz" + }, + "which-module": { + "version": "1.0.0", + "from": "which-module@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-1.0.0.tgz" }, "window-size": { "version": "0.1.0", @@ -4672,6 +4777,11 @@ } } }, + "xmlbuilder": { + "version": "8.2.2", + "from": "xmlbuilder@8.2.2", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-8.2.2.tgz" + }, "xmlhttprequest": { "version": "1.4.2", "from": "xmlhttprequest@1.4.2", @@ -4698,9 +4808,9 @@ "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.10.0.tgz" }, "yargs-parser": { - "version": "2.4.1", - "from": "yargs-parser@>=2.4.0 <3.0.0", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-2.4.1.tgz", + "version": "3.2.0", + "from": "yargs-parser@>=3.2.0 <4.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-3.2.0.tgz", "dependencies": { "camelcase": { "version": "3.0.0", @@ -4709,6 +4819,11 @@ } } }, + "yauzl": { + "version": "2.4.1", + "from": "yauzl@2.4.1", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.4.1.tgz" + }, "yeast": { "version": "0.1.2", "from": "yeast@0.1.2", diff --git a/awx/ui/package.json b/awx/ui/package.json index 60d5ce4dcb..6d826a459e 100644 --- a/awx/ui/package.json +++ b/awx/ui/package.json @@ -56,12 +56,15 @@ "karma-firefox-launcher": "^1.0.0", "karma-html2js-preprocessor": "^1.0.0", "karma-jasmine": "^1.0.2", + "karma-junit-reporter": "^1.1.0", + "karma-phantomjs-launcher": "^1.0.2", "karma-sauce-launcher": "^1.0.0", "karma-sourcemap-loader": "^0.3.7", "karma-webpack": "^1.8.0", "less-plugin-autoprefix": "^1.4.2", "load-grunt-configs": "^1.0.0", "load-grunt-tasks": "^3.5.0", + "phantomjs-prebuilt": "^2.1.12", "time-grunt": "^1.4.0", "webpack": "^1.13.1", "webpack-dev-server": "^1.14.1" From 8128bda8cc0bb110d3e2c0568342c1407df59332 Mon Sep 17 00:00:00 2001 From: Leigh Johnson Date: Wed, 21 Sep 2016 16:28:43 -0400 Subject: [PATCH 44/51] =?UTF-8?q?Turn=20off=20npm3=20progress=20reporter,?= =?UTF-8?q?=20remove=20a=20stray=20ui=20test=20that=20ended=20up=20in=20th?= =?UTF-8?q?e=20repo=20root=20=C2=AF\=5F(=E3=83=84)=5F/=C2=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- adhoc.controller-test.js | 199 --------------------------------------- awx/ui/.npmrc | 1 + 2 files changed, 1 insertion(+), 199 deletions(-) delete mode 100644 adhoc.controller-test.js create mode 100644 awx/ui/.npmrc diff --git a/adhoc.controller-test.js b/adhoc.controller-test.js deleted file mode 100644 index d49c699994..0000000000 --- a/adhoc.controller-test.js +++ /dev/null @@ -1,199 +0,0 @@ -import '../support/node'; - -import adhocModule from 'inventories/manage/adhoc/main'; -import RestStub from '../support/rest-stub'; - -describe("adhoc.controller", function() { - var $scope, $rootScope, $location, $stateParams, $stateExtender, - CheckPasswords, PromptForPasswords, CreateLaunchDialog, AdhocForm, - GenerateForm, Rest, ProcessErrors, ClearScope, GetBasePath, GetChoices, - KindChange, LookUpInit, CredentialList, Empty, Wait; - - var $controller, ctrl, generateFormCallback, waitCallback, locationCallback, - getBasePath, processErrorsCallback, restCallback, stateExtenderCallback; - - beforeEach("instantiate the adhoc module", function() { - angular.mock.module(adhocModule.name); - }); - - before("create spies", function() { - getBasePath = function(path) { - return '/' + path + '/'; - }; - generateFormCallback = { - inject: angular.noop - }; - waitCallback = sinon.spy(); - locationCallback = { - path: sinon.spy() - }; - processErrorsCallback = sinon.spy(); - restCallback = new RestStub(); - stateExtenderCallback = { - addState: angular.noop - } - }); - - - beforeEach("mock dependencies", angular.mock.module(['$provide', function(_provide_) { - var $provide = _provide_; - - $provide.value('$location', locationCallback); - $provide.value('CheckPasswords', angular.noop); - $provide.value('PromptForPasswords', angular.noop); - $provide.value('CreateLaunchDialog', angular.noop); - $provide.value('AdhocForm', angular.noop); - $provide.value('GenerateForm', generateFormCallback); - $provide.value('Rest', restCallback); - $provide.value('ProcessErrors', processErrorsCallback); - $provide.value('ClearScope', angular.noop); - $provide.value('GetBasePath', getBasePath); - $provide.value('GetChoices', angular.noop); - $provide.value('KindChange', angular.noop); - $provide.value('LookUpInit', angular.noop); - $provide.value('CredentialList', angular.noop); - $provide.value('Empty', angular.noop); - $provide.value('Wait', waitCallback); - $provide.value('$stateExtender', stateExtenderCallback); - $provide.value('$stateParams', angular.noop); - $provide.value('$state', angular.noop); - }])); - - beforeEach("put the controller in scope", inject(function($rootScope, $controller) { - var scope = $rootScope.$new(); - ctrl = $controller('adhocController', {$scope: scope}); - })); - - beforeEach("put $q in scope", window.inject(['$q', function($q) { - restCallback.$q = $q; - }])); - /* - describe("setAvailableUrls", function() { - it('should only have the specified urls ' + - 'available for adhoc commands', function() { - var urls = ctrl.privateFn.setAvailableUrls(); - expect(urls).to.have.keys('adhocUrl', 'inventoryUrl', - 'machineCredentialUrl'); - - var count = 0; - var i; - - for (i in urls) { - if (urls.hasOwnProperty(i)) { - count++; - } - } - expect(count).to.equal(3); - }); - - }); - - describe("setFieldDefaults", function() { - it('should set the select form field defaults' + - 'based on user settings', function() { - var verbosity_options = [ - {label: "0 (Foo)", value: 0, name: "0 (Foo)", - isDefault: false}, - {label: "1 (Bar)", value: 1, name: "1 (Bar)", - isDefault: true}, - ], - forks_field = {}; - - forks_field.default = 3; - - $scope.$apply(function() { - ctrl.privateFn.setFieldDefaults(verbosity_options, - forks_field.default); - }); - - expect($scope.forks).to.equal(forks_field.default); - expect($scope.verbosity.value).to.equal(1); - }); - }); - - describe("setLoadingStartStop", function() { - it('should start the controller working state when the form is ' + - 'loading', function() { - waitCallback.reset(); - ctrl.privateFn.setLoadingStartStop(); - expect(waitCallback).to.have.been.calledWith("start"); - }); - it('should stop the indicator after all REST calls in the form load have ' + - 'completed', function() { - var forks_field = {}, - adhoc_verbosity_options = {}; - forks_field.default = "1"; - $scope.$apply(function() { - $scope.forks_field = forks_field; - $scope.adhoc_verbosity_options = adhoc_verbosity_options; - }); - waitCallback.reset(); - $scope.$emit('adhocFormReady'); - $scope.$emit('adhocFormReady'); - expect(waitCallback).to.have.been.calledWith("stop"); - }); - }); - - describe("instantiateArgumentHelp", function() { - it("should initially provide a canned argument help response", function() { - expect($scope.argsPopOver).to.equal('

    These arguments are used ' + - 'with the specified module.

    '); - }); - - it("should change the help response when the module changes", function() { - $scope.$apply(function () { - $scope.module_name = {value: 'foo'}; - }); - expect($scope.argsPopOver).to.equal('

    These arguments are used ' + - 'with the specified module. You can find information about ' + - 'the foo module here.

    '); - }); - - it("should change the help response when the module changes again", function() { - $scope.$apply(function () { - $scope.module_name = {value: 'bar'}; - }); - expect($scope.argsPopOver).to.equal('

    These arguments are used ' + - 'with the specified module. You can find information about ' + - 'the bar module here.

    '); - }); - - it("should change the help response back to the canned response " + - "when no module is selected", function() { - $scope.$apply(function () { - $scope.module_name = null; - }); - expect($scope.argsPopOver).to.equal('

    These arguments are used ' + - 'with the specified module.

    '); - }); - }); - - describe("instantiateHostPatterns", function() { - it("should initialize the limit object based on the provided host " + - "pattern", function() { - ctrl.privateFn.instantiateHostPatterns("foo:bar"); - expect($scope.limit).to.equal("foo:bar"); - }); - - it("should set the providedHostPatterns variable to the provided host " + - "pattern so it is accesible on form reset", function() { - ctrl.privateFn.instantiateHostPatterns("foo:bar"); - expect($scope.providedHostPatterns).to.equal("foo:bar"); - }); - - it("should remove the hostPattern from rootScope after it has been " + - "utilized", function() { - $rootScope.hostPatterns = "foo"; - expect($rootScope.hostPatterns).to.exist; - ctrl.privateFn.instantiateHostPatterns("foo"); - expect($rootScope.hostPatterns).to.not.exist; - }); - }); - */ -}); diff --git a/awx/ui/.npmrc b/awx/ui/.npmrc new file mode 100644 index 0000000000..d883e4fa13 --- /dev/null +++ b/awx/ui/.npmrc @@ -0,0 +1 @@ +progress=false From a4cd5a0909c6f7cf2f982772d4c63d6ad2c0b255 Mon Sep 17 00:00:00 2001 From: James Laska Date: Wed, 21 Sep 2016 16:34:30 -0400 Subject: [PATCH 45/51] Use `make` when installing test requirements --- tools/docker-compose/unit-tests/Dockerfile | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tools/docker-compose/unit-tests/Dockerfile b/tools/docker-compose/unit-tests/Dockerfile index decbf01374..8d51955273 100644 --- a/tools/docker-compose/unit-tests/Dockerfile +++ b/tools/docker-compose/unit-tests/Dockerfile @@ -43,13 +43,13 @@ COPY awx/__init__.py awx/ # Copy Makefile COPY Makefile . -# Make tower runtime virtualenvs +# Install tower runtime virtualenvs ENV SWIG_FEATURES="-cpperraswarn -includeall -I/usr/include/openssl" RUN make requirements -RUN pip install -r requirements/requirements_jenkins.txt -# ENV VENV_BASE="" -# RUN make requirements_jenkins +# Install tower test requirements +ENV VENV_BASE="" +RUN make requirements_jenkins # Build front-end deps COPY awx/ui/package.json awx/ui/ From 51cd9e99e67d7a6ac07850096adf85fbf29d7e54 Mon Sep 17 00:00:00 2001 From: Aaron Tan Date: Wed, 21 Sep 2016 20:44:52 -0400 Subject: [PATCH 46/51] Refactor and patch extra vars verification. --- awx/main/models/jobs.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/awx/main/models/jobs.py b/awx/main/models/jobs.py index 9356884a19..34adcb73a4 100644 --- a/awx/main/models/jobs.py +++ b/awx/main/models/jobs.py @@ -340,6 +340,10 @@ class JobTemplate(UnifiedJobTemplate, JobOptions, ResourceMixin): errors.append("'%s' value missing" % survey_element['variable']) elif survey_element['type'] in ["textarea", "text", "password"]: if survey_element['variable'] in data: + if type(data[survey_element['variable']]) not in (str, unicode): + errors.append("Value %s for '%s' expected to be a string." % (data[survey_element['variable']], + survey_element['variable'])) + continue if 'min' in survey_element and survey_element['min'] not in ["", None] and len(data[survey_element['variable']]) < int(survey_element['min']): errors.append("'%s' value %s is too small (length is %s must be at least %s)." % (survey_element['variable'], data[survey_element['variable']], len(data[survey_element['variable']]), survey_element['min'])) @@ -348,6 +352,10 @@ class JobTemplate(UnifiedJobTemplate, JobOptions, ResourceMixin): (survey_element['variable'], data[survey_element['variable']], survey_element['max'])) elif survey_element['type'] == 'integer': if survey_element['variable'] in data: + if type(data[survey_element['variable']]) != int: + errors.append("Value %s for '%s' expected to be an integer." % (data[survey_element['variable']], + survey_element['variable'])) + continue if 'min' in survey_element and survey_element['min'] not in ["", None] and survey_element['variable'] in data and \ data[survey_element['variable']] < int(survey_element['min']): errors.append("'%s' value %s is too small (must be at least %s)." % @@ -356,20 +364,18 @@ class JobTemplate(UnifiedJobTemplate, JobOptions, ResourceMixin): data[survey_element['variable']] > int(survey_element['max']): errors.append("'%s' value %s is too large (must be no more than %s)." % (survey_element['variable'], data[survey_element['variable']], survey_element['max'])) - if type(data[survey_element['variable']]) != int: - errors.append("Value %s for '%s' expected to be an integer." % (data[survey_element['variable']], - survey_element['variable'])) elif survey_element['type'] == 'float': if survey_element['variable'] in data: + if type(data[survey_element['variable']]) not in (float, int): + errors.append("Value %s for '%s' expected to be a numeric type." % (data[survey_element['variable']], + survey_element['variable'])) + continue if 'min' in survey_element and survey_element['min'] not in ["", None] and data[survey_element['variable']] < float(survey_element['min']): errors.append("'%s' value %s is too small (must be at least %s)." % (survey_element['variable'], data[survey_element['variable']], survey_element['min'])) if 'max' in survey_element and survey_element['max'] not in ["", None] and data[survey_element['variable']] > float(survey_element['max']): errors.append("'%s' value %s is too large (must be no more than %s)." % (survey_element['variable'], data[survey_element['variable']], survey_element['max'])) - if type(data[survey_element['variable']]) not in (float, int): - errors.append("Value %s for '%s' expected to be a numeric type." % (data[survey_element['variable']], - survey_element['variable'])) elif survey_element['type'] == 'multiselect': if survey_element['variable'] in data: if type(data[survey_element['variable']]) != list: From c08a9f8bbcf41b2209f576c6e2aaa985fcbed898 Mon Sep 17 00:00:00 2001 From: Leigh Johnson Date: Thu, 22 Sep 2016 10:15:31 -0400 Subject: [PATCH 47/51] Use a local installation of grunt-cli, freeze deps --- awx/ui/npm-shrinkwrap.json | 9 +++++++-- awx/ui/package.json | 1 + 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/awx/ui/npm-shrinkwrap.json b/awx/ui/npm-shrinkwrap.json index 0e0666aaa2..376a395a52 100644 --- a/awx/ui/npm-shrinkwrap.json +++ b/awx/ui/npm-shrinkwrap.json @@ -868,9 +868,9 @@ } }, "caniuse-db": { - "version": "1.0.30000538", + "version": "1.0.30000539", "from": "caniuse-db@>=1.0.30000527 <2.0.0", - "resolved": "https://registry.npmjs.org/caniuse-db/-/caniuse-db-1.0.30000538.tgz" + "resolved": "https://registry.npmjs.org/caniuse-db/-/caniuse-db-1.0.30000539.tgz" }, "caseless": { "version": "0.11.0", @@ -2408,6 +2408,11 @@ "from": "graceful-readlink@>=1.0.0", "resolved": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz" }, + "grunt-cli": { + "version": "1.2.0", + "from": "grunt-cli@>=1.2.0 <2.0.0", + "resolved": "https://registry.npmjs.org/grunt-cli/-/grunt-cli-1.2.0.tgz" + }, "grunt-known-options": { "version": "1.1.0", "from": "grunt-known-options@>=1.1.0 <1.2.0", diff --git a/awx/ui/package.json b/awx/ui/package.json index 6d826a459e..414150fa49 100644 --- a/awx/ui/package.json +++ b/awx/ui/package.json @@ -36,6 +36,7 @@ "expose-loader": "^0.7.1", "grunt": "^1.0.1", "grunt-browser-sync": "^2.2.0", + "grunt-cli": "^1.2.0", "grunt-concurrent": "^2.3.0", "grunt-contrib-clean": "^1.0.0", "grunt-contrib-concat": "^1.0.1", From 57abd438c87d12aec055c8ea458f3b87b33497bf Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Fri, 16 Sep 2016 13:36:00 -0400 Subject: [PATCH 48/51] make user_capabilities validation front-loaded, try to run tests --- awx/api/permissions.py | 4 ++- awx/api/serializers.py | 2 +- awx/main/access.py | 30 +++++++++++++------ awx/main/tests/factories/fixtures.py | 2 +- .../functional/api/test_rbac_displays.py | 23 +++++++++++++- awx/main/tests/functional/conftest.py | 2 +- 6 files changed, 49 insertions(+), 14 deletions(-) diff --git a/awx/api/permissions.py b/awx/api/permissions.py index 660c105200..ecd725bc6e 100644 --- a/awx/api/permissions.py +++ b/awx/api/permissions.py @@ -209,6 +209,8 @@ class ProjectUpdatePermission(ModelAccessPermission): class UserPermission(ModelAccessPermission): def check_post_permissions(self, request, view, obj=None): - if request.user.is_superuser: + if not request.data: + return request.user.admin_of_organizations.exists() + elif request.user.is_superuser: return True raise PermissionDenied() diff --git a/awx/api/serializers.py b/awx/api/serializers.py index 63533eb141..5b45c9de5b 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -337,7 +337,7 @@ class BaseSerializer(serializers.ModelSerializer): if hasattr(self, 'show_capabilities'): view = self.context.get('view', None) parent_obj = None - if hasattr(view, 'parent_model'): + if view and hasattr(view, 'parent_model'): parent_obj = view.get_parent_object() if view and view.request and view.request.user: user_capabilities = get_user_capabilities( diff --git a/awx/main/access.py b/awx/main/access.py index 1105ed0999..7eb8dcecbc 100644 --- a/awx/main/access.py +++ b/awx/main/access.py @@ -220,6 +220,8 @@ class BaseAccess(object): raise LicenseForbids("Features not found in active license.") def get_user_capabilities(self, obj, method_list=[], parent_obj=None): + if obj is None: + return {} user_capabilities = {} # Custom ordering to loop through methods so we can reuse earlier calcs @@ -227,6 +229,17 @@ class BaseAccess(object): if display_method not in method_list: continue + # Validation consistency checks + if display_method == 'copy' and isinstance(obj, JobTemplate): + validation_errors, resources_needed_to_start = obj.resource_validation_data() + if validation_errors: + user_capabilities[display_method] = False + continue + elif display_method == 'start' and isinstance(obj, Group): + if obj.inventory_source and not obj.inventory_source._can_update(): + user_capabilities[display_method] = False + continue + # Grab the answer from the cache, if available if hasattr(obj, 'capabilities_cache') and display_method in obj.capabilities_cache: user_capabilities[display_method] = obj.capabilities_cache[display_method] @@ -243,22 +256,21 @@ class BaseAccess(object): method = display_method # Shortcuts in certain cases by deferring to earlier property - if display_method == 'schedule' and 'edit' in user_capabilities: + if display_method == 'schedule': user_capabilities['schedule'] = user_capabilities['edit'] continue elif display_method == 'delete' and not isinstance(obj, (User, UnifiedJob)): user_capabilities['delete'] = user_capabilities['edit'] continue - if display_method == 'copy' and isinstance(obj, JobTemplate): - validation_errors, resources_needed_to_start = obj.resource_validation_data() - if validation_errors: - user_capabilities['copy'] = False - continue + elif display_method == 'copy' and isinstance(obj, (Group, Host)): + user_capabilities['copy'] = user_capabilities['edit'] + continue # Preprocessing before the access method is called data = {} - if method == 'add' and isinstance(obj, JobTemplate): - data['reference_obj'] = obj + if method == 'add': + if isinstance(obj, JobTemplate): + data['reference_obj'] = obj # Compute permission access_method = getattr(self, "can_%s" % method) @@ -599,7 +611,7 @@ class GroupAccess(BaseAccess): return True def can_start(self, obj): - # Used as another alias to inventory_source start access + # Used as another alias to inventory_source start access for user_capabilities if obj and obj.inventory_source: return self.user.can_access(InventorySource, 'start', obj.inventory_source) return False diff --git a/awx/main/tests/factories/fixtures.py b/awx/main/tests/factories/fixtures.py index c51c29e83c..e52b627076 100644 --- a/awx/main/tests/factories/fixtures.py +++ b/awx/main/tests/factories/fixtures.py @@ -29,7 +29,7 @@ def mk_instance(persisted=True): if not persisted: raise RuntimeError('creating an Instance requires persisted=True') from django.conf import settings - return Instance.objects.get_or_create(uuid=settings.SYSTEM_UUID, primary=True, hostname="instance.example.org") + return Instance.objects.get_or_create(uuid=settings.SYSTEM_UUID, hostname="instance.example.org") def mk_organization(name, description=None, persisted=True): diff --git a/awx/main/tests/functional/api/test_rbac_displays.py b/awx/main/tests/functional/api/test_rbac_displays.py index d207081929..eb94e01dff 100644 --- a/awx/main/tests/functional/api/test_rbac_displays.py +++ b/awx/main/tests/functional/api/test_rbac_displays.py @@ -5,7 +5,10 @@ from django.test.client import RequestFactory from awx.main.models.jobs import JobTemplate from awx.main.models import Role, Group -from awx.main.access import access_registry +from awx.main.access import ( + access_registry, + get_user_capabilities +) from awx.main.utils import cache_list_capabilities from awx.api.serializers import JobTemplateSerializer @@ -300,3 +303,21 @@ def test_prefetch_jt_copy_capability(job_template, project, inventory, machine_c ]}], JobTemplate, rando) assert qs[0].capabilities_cache == {'copy': True} +@pytest.mark.django_db +def test_group_update_capabilities_possible(group, inventory_source, admin_user): + group.inventory_source = inventory_source + group.save() + + capabilities = get_user_capabilities(admin_user, group, method_list=['start']) + assert capabilities['start'] + +@pytest.mark.django_db +def test_group_update_capabilities_impossible(group, inventory_source, admin_user): + inventory_source.source = "" + inventory_source.save() + group.inventory_source = inventory_source + group.save() + + capabilities = get_user_capabilities(admin_user, group, method_list=['start']) + assert not capabilities['start'] + diff --git a/awx/main/tests/functional/conftest.py b/awx/main/tests/functional/conftest.py index e5e1222a39..0c620feb7e 100644 --- a/awx/main/tests/functional/conftest.py +++ b/awx/main/tests/functional/conftest.py @@ -152,7 +152,7 @@ def user_project(user): @pytest.fixture def instance(settings): - return Instance.objects.create(uuid=settings.SYSTEM_UUID, primary=True, hostname="instance.example.org") + return Instance.objects.create(uuid=settings.SYSTEM_UUID, hostname="instance.example.org") @pytest.fixture def organization(instance): From 049d8a7bd6c7958c62fbb95995164b0be1814c09 Mon Sep 17 00:00:00 2001 From: Shane McDonald Date: Thu, 22 Sep 2016 14:19:28 -0400 Subject: [PATCH 49/51] Support GPG signing in the containerized RPM jobs. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Added a script that allows for piping signing keys into the container’s stdin stream. - Got sign.exp working - I ended up getting mock to run as root, which means both RPM and DEB jobs both use —unsafe-perm now, so I added that back to the Makefile. --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index fd0ef0228b..fd9d87cd2e 100644 --- a/Makefile +++ b/Makefile @@ -483,7 +483,7 @@ test_jenkins : test_coverage # -------------------------------------- ui-deps-built: awx/ui/package.json - $(NPM_BIN) --prefix awx/ui install awx/ui + $(NPM_BIN) --unsafe-perm --prefix awx/ui install awx/ui touch awx/ui/.deps_built ui-docker-machine: ui-deps-built From 535f979fd7aca2e293cfb672b36cb32aff4443dd Mon Sep 17 00:00:00 2001 From: Aaron Tan Date: Thu, 22 Sep 2016 22:34:40 -0400 Subject: [PATCH 50/51] Unit test added. --- .../unit/models/test_job_template_unit.py | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/awx/main/tests/unit/models/test_job_template_unit.py b/awx/main/tests/unit/models/test_job_template_unit.py index a156d1e920..9720692f1f 100644 --- a/awx/main/tests/unit/models/test_job_template_unit.py +++ b/awx/main/tests/unit/models/test_job_template_unit.py @@ -50,3 +50,32 @@ def test_job_template_survey_password_redaction(job_template_with_survey_passwor """Tests the JobTemplate model's funciton to redact passwords from extra_vars - used when creating a new job""" assert job_template_with_survey_passwords_unit.survey_password_variables() == ['secret_key', 'SSN'] + +def test_job_template_survey_variable_validation(job_template_factory): + objects = job_template_factory( + 'survey_variable_validation', + organization='org1', + inventory='inventory1', + credential='cred1', + persisted=False, + ) + obj = objects.job_template + obj.survey_spec = { + "description": "", + "spec": [ + { + "required": True, + "min": 0, + "default": "5", + "max": 1024, + "question_description": "", + "choices": "", + "variable": "a", + "question_name": "Whosyourdaddy", + "type": "text" + } + ], + "name": "" + } + obj.survey_enabled = True + assert obj.survey_variable_validation({"a": 5}) == ["Value 5 for 'a' expected to be a string."] From 33e3263310a728038312db2ea24bce74a5fe2934 Mon Sep 17 00:00:00 2001 From: Aaron Tan Date: Fri, 23 Sep 2016 09:27:18 -0400 Subject: [PATCH 51/51] Flake8 fixes. --- .../unit/models/test_job_template_unit.py | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/awx/main/tests/unit/models/test_job_template_unit.py b/awx/main/tests/unit/models/test_job_template_unit.py index 9720692f1f..088b4d3d7c 100644 --- a/awx/main/tests/unit/models/test_job_template_unit.py +++ b/awx/main/tests/unit/models/test_job_template_unit.py @@ -61,21 +61,21 @@ def test_job_template_survey_variable_validation(job_template_factory): ) obj = objects.job_template obj.survey_spec = { - "description": "", - "spec": [ - { - "required": True, - "min": 0, - "default": "5", - "max": 1024, - "question_description": "", - "choices": "", - "variable": "a", - "question_name": "Whosyourdaddy", - "type": "text" - } - ], - "name": "" + "description": "", + "spec": [ + { + "required": True, + "min": 0, + "default": "5", + "max": 1024, + "question_description": "", + "choices": "", + "variable": "a", + "question_name": "Whosyourdaddy", + "type": "text" + } + ], + "name": "" } obj.survey_enabled = True assert obj.survey_variable_validation({"a": 5}) == ["Value 5 for 'a' expected to be a string."]