From 13913019dd5026ea5cbcf6101a40177b100a13ff Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Mon, 26 Jan 2015 13:35:31 -0500 Subject: [PATCH 01/10] Making the index.html file serving conditional more obvious --- awx/ui/templates/ui/index.html | 270 ++++++++++++++++----------------- 1 file changed, 135 insertions(+), 135 deletions(-) diff --git a/awx/ui/templates/ui/index.html b/awx/ui/templates/ui/index.html index d0c0e8e45f..6c5e1221ee 100644 --- a/awx/ui/templates/ui/index.html +++ b/awx/ui/templates/ui/index.html @@ -44,142 +44,142 @@ {% if settings.USE_MINIFIED_JS %} - + {% else %} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {% endif %} From 18de663c0275216312c6dd26dc390106cb9f62d9 Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Mon, 26 Jan 2015 19:09:48 -0500 Subject: [PATCH 02/10] Initial fix of displaying shell commands and warnings in event detail modals --- awx/ui/static/js/helpers/EventViewer.js | 30 ++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/awx/ui/static/js/helpers/EventViewer.js b/awx/ui/static/js/helpers/EventViewer.js index e316bcd6b3..c980d39b0f 100644 --- a/awx/ui/static/js/helpers/EventViewer.js +++ b/awx/ui/static/js/helpers/EventViewer.js @@ -486,7 +486,7 @@ angular.module('EventViewerHelper', ['ModalDialog', 'Utilities', 'EventsViewerFo h = ''; if (key !== 'host_id' && key !== 'parent' && key !== 'event' && key !== 'src' && key !== 'md5sum' && key !== 'stdout' && key !== 'traceback' && key !== 'stderr' && key !== 'cmd' && key !=='changed' && key !== "verbose_override" && - key !== 'feature_result') { + key !== 'feature_result' && key !== 'warnings') { if (!EventsViewerForm.fields[key]) { h = parseItem(obj[key], key, key); if (h) { @@ -494,6 +494,34 @@ angular.module('EventViewerHelper', ['ModalDialog', 'Utilities', 'EventsViewerFo found = true; } } + } else if (key === 'cmd') { + // only show cmd if it's a cmd that was run + if (!EventsViewerForm.fields[key] && obj[key].length > 0) { + // include the label head Shell Command instead of CMD in the modal + var i = 0, + string_cmd = ""; + for (i; i < obj[key].length; i++) { + string_cmd += obj[key][i] + " "; + } + h = parseItem(string_cmd, key, "Shell Command"); + if (h) { + html += h; + found = true; + } + } + } else if (key === 'warnings') { + if (!EventsViewerForm.fields[key] && obj[key].length > 0) { + var i = 0, + string_warnings = ""; + for (i; i < obj[key].length; i++) { + string_warnings += obj[key][i] + " "; + } + h = parseItem(string_warnings, key, "Warnings"); + if (h) { + html += h; + found = true; + } + } } } } From 7fbdc1d0dbd39268e04aedc18ae59a63a0cc1884 Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Mon, 26 Jan 2015 19:21:51 -0500 Subject: [PATCH 03/10] Fixing jshint errors --- awx/ui/static/js/helpers/EventViewer.js | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/awx/ui/static/js/helpers/EventViewer.js b/awx/ui/static/js/helpers/EventViewer.js index c980d39b0f..409f0e0266 100644 --- a/awx/ui/static/js/helpers/EventViewer.js +++ b/awx/ui/static/js/helpers/EventViewer.js @@ -461,7 +461,7 @@ angular.module('EventViewerHelper', ['ModalDialog', 'Utilities', 'EventsViewerFo } function parseJSON(obj) { - var h, html = '', key, keys, found = false; + var h, html = '', key, keys, found = false, i = 0, string_warnings = "", string_cmd = ""; if (typeof obj === "object") { html += "\n"; html += "\n"; @@ -498,9 +498,7 @@ angular.module('EventViewerHelper', ['ModalDialog', 'Utilities', 'EventsViewerFo // only show cmd if it's a cmd that was run if (!EventsViewerForm.fields[key] && obj[key].length > 0) { // include the label head Shell Command instead of CMD in the modal - var i = 0, - string_cmd = ""; - for (i; i < obj[key].length; i++) { + for (i = 0; i < obj[key].length; i++) { string_cmd += obj[key][i] + " "; } h = parseItem(string_cmd, key, "Shell Command"); @@ -511,9 +509,7 @@ angular.module('EventViewerHelper', ['ModalDialog', 'Utilities', 'EventsViewerFo } } else if (key === 'warnings') { if (!EventsViewerForm.fields[key] && obj[key].length > 0) { - var i = 0, - string_warnings = ""; - for (i; i < obj[key].length; i++) { + for (i = 0; i < obj[key].length; i++) { string_warnings += obj[key][i] + " "; } h = parseItem(string_warnings, key, "Warnings"); From 85743d225094c84b403df0500adb27998c5192d8 Mon Sep 17 00:00:00 2001 From: Chris Meyers Date: Tue, 27 Jan 2015 08:58:30 -0500 Subject: [PATCH 04/10] fixed inventory source update # license hosts check --- awx/main/models/inventory.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/main/models/inventory.py b/awx/main/models/inventory.py index 494dd11afb..60e19724a0 100644 --- a/awx/main/models/inventory.py +++ b/awx/main/models/inventory.py @@ -1219,7 +1219,7 @@ class InventoryUpdate(UnifiedJob, InventorySourceOptions): def save(self, *args, **kwargs): update_fields = kwargs.get('update_fields', []) - if bool('license' in self.result_stdout and + if bool(('license' in self.result_stdout or 'licensed' in self.result_stdout) and 'exceeded' in self.result_stdout and not self.license_error): self.license_error = True if 'license_error' not in update_fields: From 7728c720dbdafe4780d2af3b2a5e48591c5e6772 Mon Sep 17 00:00:00 2001 From: James Laska Date: Mon, 26 Jan 2015 09:21:23 -0500 Subject: [PATCH 05/10] Mark eula_accepted as new to 2.1.1 --- awx/api/templates/api/api_v1_config_view.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/awx/api/templates/api/api_v1_config_view.md b/awx/api/templates/api/api_v1_config_view.md index 21f8581bb5..d99b97d553 100644 --- a/awx/api/templates/api/api_v1_config_view.md +++ b/awx/api/templates/api/api_v1_config_view.md @@ -14,5 +14,7 @@ the following fields (some fields may not be visible to all users): (_New in Ansible Tower 2.0.0_) Make a POST request to this resource as a super user to install or update the existing license. The license data itself can -be POSTed as a normal json data structure. The POST must include a `eula_accepted` -boolean element indicating acceptance of the End-User License Agreement. +be POSTed as a normal json data structure. + +(_New in Ansible Tower 2.1.1_) The POST must include a `eula_accepted` boolean +element indicating acceptance of the End-User License Agreement. From 77c76bece19ee9412f9593cf16be5afa253d80f2 Mon Sep 17 00:00:00 2001 From: James Laska Date: Tue, 27 Jan 2015 13:18:29 -0500 Subject: [PATCH 06/10] Disable SSLv3 in apache config Fixes: https://trello.com/c/BGy5voWf --- config/awx-httpd-443.conf | 1 + 1 file changed, 1 insertion(+) diff --git a/config/awx-httpd-443.conf b/config/awx-httpd-443.conf index 6923a2bc0a..c74ec5697a 100644 --- a/config/awx-httpd-443.conf +++ b/config/awx-httpd-443.conf @@ -9,6 +9,7 @@ WSGISocketPrefix /var/run/wsgi SSLEngine on SSLCertificateFile /etc/tower/tower.cert SSLCertificateKeyFile /etc/tower/tower.key + SSLProtocol all -SSLv3 -SSLv2 WSGIScriptAlias / /var/lib/awx/wsgi.py WSGIPassAuthorization On From 280126310a15bdebff8402def34e6fb88e1d6dd9 Mon Sep 17 00:00:00 2001 From: Jared Tabor Date: Tue, 27 Jan 2015 14:30:42 -0500 Subject: [PATCH 07/10] Adding filter on portal jobs for user/team Feature request from Bill for the jobs view of portal mode to be filterable for users/teams --- awx/ui/static/js/widgets/PortalJobs.js | 70 ++++++++++++++++++-------- 1 file changed, 48 insertions(+), 22 deletions(-) diff --git a/awx/ui/static/js/widgets/PortalJobs.js b/awx/ui/static/js/widgets/PortalJobs.js index dddfde7a45..8fee74ff41 100644 --- a/awx/ui/static/js/widgets/PortalJobs.js +++ b/awx/ui/static/js/widgets/PortalJobs.js @@ -11,33 +11,19 @@ 'use strict'; angular.module('PortalJobsWidget', ['RestServices', 'Utilities']) -.factory('PortalJobsWidget', ['$rootScope', '$compile', 'LoadSchedulesScope', 'LoadJobsScope', 'PortalJobsList', 'ScheduledJobsList', 'GetChoices', 'GetBasePath', 'PortalJobTemplateList', - function ($rootScope, $compile, LoadSchedulesScope, LoadJobsScope, PortalJobsList, ScheduledJobsList, GetChoices, GetBasePath, PortalJobTemplateList) { +.factory('PortalJobsWidget', ['$rootScope', '$compile', 'LoadSchedulesScope', 'LoadJobsScope', 'PortalJobsList', 'ScheduledJobsList', 'GetChoices', 'GetBasePath', 'PortalJobTemplateList' , + function ($rootScope, $compile, LoadSchedulesScope, LoadJobsScope, PortalJobsList, ScheduledJobsList, GetChoices, GetBasePath, PortalJobTemplateList ) { return function (params) { var scope = params.scope, target = params.target, + filter = params.filter || "User" , choicesCount = 0, listCount = 0, jobs_scope = scope.$new(true), max_rows, user, - html, e; - - html = ''; - html += "
\n"; - html += "
\n"; - html += "\n"; //row - html += "
\n"; - html += "
\n"; - html += "
\n"; //list - html += "
\n"; //active-jobs-tab - html += "
\n"; - - e = angular.element(document.getElementById(target)); - e.html(html); - $compile(e)(scope); + html, e, + url; if (scope.removeListLoaded) { scope.removeListLoaded(); @@ -59,18 +45,17 @@ angular.module('PortalJobsWidget', ['RestServices', 'Utilities']) PortalJobsList.fields.type.searchOptions = scope.type_choices; } user = scope.$parent.current_user.id; + url = (filter === "Team" ) ? GetBasePath('jobs') : GetBasePath('jobs')+'?created_by='+user ; LoadJobsScope({ parent_scope: scope, scope: jobs_scope, list: PortalJobsList, id: 'active-jobs', - url: GetBasePath('jobs')+'?created_by='+user, + url: url , //GetBasePath('jobs')+'?created_by='+user, pageSize: max_rows, spinner: true }); - - $(window).resize(_.debounce(function() { resizePortalJobsWidget(); }, 500)); @@ -87,6 +72,47 @@ angular.module('PortalJobsWidget', ['RestServices', 'Utilities']) } }); + + scope.filterPortalJobs = function(filter) { + $("#active-jobs").empty(); + $("#active-jobs-search-container").empty(); + user = scope.$parent.current_user.id; + url = (filter === "Team" ) ? GetBasePath('jobs') : GetBasePath('jobs')+'?created_by='+user ; + LoadJobsScope({ + parent_scope: scope, + scope: jobs_scope, + list: PortalJobsList, + id: 'active-jobs', + url: url , //GetBasePath('jobs')+'?created_by='+user, + pageSize: max_rows, + spinner: true + }); + }; + + html = ''; + html += "
\n"; + html += "
\n"; + html += "\n"; //row + + html += "
\n"; + html += "
\n"; + html += "
\n"; //list + html += "
\n"; //active-jobs-tab + html += "
\n"; + + e = angular.element(document.getElementById(target)); + e.html(html); + $compile(e)(scope); + + GetChoices({ scope: scope, url: GetBasePath('unified_jobs'), From e09436ee0c2dcba6054ddaefeaa9a3712fcb0aa4 Mon Sep 17 00:00:00 2001 From: Chris Meyers Date: Tue, 27 Jan 2015 14:56:03 -0500 Subject: [PATCH 08/10] inventory_import behave more like ansible ``` mkdir inv echo foo > inv/hosts mkdir inv/host_vars touch inv/host_vars/foo touch inv/host_vars/foo.yml ``` ``` ansible -i ./inv -c local -m ping all ``` ``` tower-mange inventory_import --inventory-id=1 ./inv ``` The tower command above now throws an error similar to ansible: `Multiple variable files found. There should only be one.` --- awx/main/management/commands/inventory_import.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/awx/main/management/commands/inventory_import.py b/awx/main/management/commands/inventory_import.py index 8455f92bef..09c8f24c4b 100644 --- a/awx/main/management/commands/inventory_import.py +++ b/awx/main/management/commands/inventory_import.py @@ -55,12 +55,16 @@ class MemObject(object): def load_vars(self, base_path): all_vars = {} + files_found = 0 for suffix in ('', '.yml', '.yaml', '.json'): path = ''.join([base_path, suffix]) if not os.path.exists(path): continue if not os.path.isfile(path): continue + files_found += 1 + if files_found > 1: + raise RuntimeError('Multiple variable files found. There should only be one. %s ' % self.name) vars_name = os.path.basename(os.path.dirname(path)) logger.debug('Loading %s from %s', vars_name, path) try: From 02b31674c27937f77f9da487e42a05d4d58b8a94 Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Tue, 27 Jan 2015 15:11:54 -0500 Subject: [PATCH 09/10] Fix up some serious issues posting new surveys and deleting them by non-super users. Also fix up some issues checking can_change for job templates for operations like PATCH where not all of the data points will be submitted --- awx/api/views.py | 3 +++ awx/main/access.py | 9 ++++++++- awx/main/tests/jobs.py | 30 ++++++++++++++++++++++++++++++ 3 files changed, 41 insertions(+), 1 deletion(-) diff --git a/awx/api/views.py b/awx/api/views.py index 7f7f072d31..feb9184678 100644 --- a/awx/api/views.py +++ b/awx/api/views.py @@ -1493,6 +1493,7 @@ class JobTemplateSchedulesList(SubListCreateAPIView): class JobTemplateSurveySpec(GenericAPIView): model = JobTemplate + parent_model = JobTemplate # FIXME: Add serializer class to define fields in OPTIONS request! def get(self, request, *args, **kwargs): @@ -1537,6 +1538,8 @@ class JobTemplateSurveySpec(GenericAPIView): def delete(self, request, *args, **kwargs): obj = self.get_object() + if not request.user.can_access(self.model, 'delete', obj): + raise PermissionDenied() obj.survey_spec = {} obj.save() return Response() diff --git a/awx/main/access.py b/awx/main/access.py index 1363498a4f..b43c18dc1a 100644 --- a/awx/main/access.py +++ b/awx/main/access.py @@ -1005,7 +1005,14 @@ class JobTemplateAccess(BaseAccess): return dep_access and has_perm def can_change(self, obj, data): - return self.can_read(obj) and self.can_add(data) + data_for_change = data + if data is not None: + data_for_change = dict(data) + for required_field in ('credential', 'cloud_credential', 'inventory', 'project'): + required_obj = getattr(obj, required_field, None) + if required_field not in data_for_change and required_obj is not None: + data_for_change[required_field] = required_obj.pk + return self.can_read(obj) and self.can_add(data_for_change) def can_delete(self, obj): add_obj = dict(credential=obj.credential.id if obj.credential is not None else None, diff --git a/awx/main/tests/jobs.py b/awx/main/tests/jobs.py index a532d7f27c..c4ed43ac71 100644 --- a/awx/main/tests/jobs.py +++ b/awx/main/tests/jobs.py @@ -1021,6 +1021,36 @@ class JobTemplateTest(BaseJobTestMixin, django.test.TestCase): # Nested json self.post(launch_url, dict(extra_vars=dict(json_answer=dict(test="val", num=1), reqd_answer="foo")), expect=202) + # Bob can access and update the survey because he's an org-admin + with self.current_user(self.user_bob): + self.post(url, json.loads(TEST_SURVEY_REQUIREMENTS), expect=200) + + # Chuck is the lead engineer and has the right permissions to edit it also + with self.current_user(self.user_chuck): + self.post(url, json.loads(TEST_SURVEY_REQUIREMENTS), expect=200) + + # Doug shouldn't be able to access this playbook + with self.current_user(self.user_doug): + self.post(url, json.loads(TEST_SURVEY_REQUIREMENTS), expect=403) + + # Neither can juan because he doesn't have the job template create permission + with self.current_user(self.user_juan): + self.post(url, json.loads(TEST_SURVEY_REQUIREMENTS), expect=403) + + # Bob and chuck can read the template + with self.current_user(self.user_bob): + self.get(url, expect=200) + + with self.current_user(self.user_chuck): + self.get(url, expect=200) + + # Doug and Juan can't + with self.current_user(self.user_doug): + self.get(url, expect=403) + + with self.current_user(self.user_juan): + self.get(url, expect=403) + def test_launch_job_template(self): url = reverse('api:job_template_list') data = dict( From 0f24ed954c984a41d0becd9f2d28cf2c2df008a7 Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Tue, 27 Jan 2015 16:14:18 -0500 Subject: [PATCH 10/10] using join instead of the for loop --- awx/ui/static/js/helpers/EventViewer.js | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/awx/ui/static/js/helpers/EventViewer.js b/awx/ui/static/js/helpers/EventViewer.js index 409f0e0266..898c464429 100644 --- a/awx/ui/static/js/helpers/EventViewer.js +++ b/awx/ui/static/js/helpers/EventViewer.js @@ -461,7 +461,7 @@ angular.module('EventViewerHelper', ['ModalDialog', 'Utilities', 'EventsViewerFo } function parseJSON(obj) { - var h, html = '', key, keys, found = false, i = 0, string_warnings = "", string_cmd = ""; + var h, html = '', key, keys, found = false, string_warnings = "", string_cmd = ""; if (typeof obj === "object") { html += "
\n"; html += "\n"; @@ -498,9 +498,7 @@ angular.module('EventViewerHelper', ['ModalDialog', 'Utilities', 'EventsViewerFo // only show cmd if it's a cmd that was run if (!EventsViewerForm.fields[key] && obj[key].length > 0) { // include the label head Shell Command instead of CMD in the modal - for (i = 0; i < obj[key].length; i++) { - string_cmd += obj[key][i] + " "; - } + string_cmd += obj[key].join(" "); h = parseItem(string_cmd, key, "Shell Command"); if (h) { html += h; @@ -509,9 +507,7 @@ angular.module('EventViewerHelper', ['ModalDialog', 'Utilities', 'EventsViewerFo } } else if (key === 'warnings') { if (!EventsViewerForm.fields[key] && obj[key].length > 0) { - for (i = 0; i < obj[key].length; i++) { - string_warnings += obj[key][i] + " "; - } + string_warnings += obj[key].join(" "); h = parseItem(string_warnings, key, "Warnings"); if (h) { html += h;