From dc3dd0db26497abc5668756568b20096621da279 Mon Sep 17 00:00:00 2001 From: Jared Tabor Date: Tue, 4 Nov 2014 09:51:44 -0500 Subject: [PATCH 01/15] JobSubmission - remove ng-keydown removed ng-keydown that would submit the prompt for passwords modal that no longer exists --- awx/ui/static/js/helpers/JobSubmission.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/awx/ui/static/js/helpers/JobSubmission.js b/awx/ui/static/js/helpers/JobSubmission.js index 4422cad7fc..67097137a5 100644 --- a/awx/ui/static/js/helpers/JobSubmission.js +++ b/awx/ui/static/js/helpers/JobSubmission.js @@ -241,7 +241,7 @@ function($location, Wait, GetBasePath, LookUpInit, JobTemplateForm, CredentialLi html += "\n"; html += " " + field.label + "\n"; html += " Date: Tue, 4 Nov 2014 11:27:20 -0500 Subject: [PATCH 02/15] Added /var/log/supervisor to paths hidden by proot. --- awx/main/tasks.py | 5 +++-- awx/main/tests/tasks.py | 17 +++++++++++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/awx/main/tasks.py b/awx/main/tasks.py index d6470fb908..04718c2af7 100644 --- a/awx/main/tasks.py +++ b/awx/main/tasks.py @@ -338,12 +338,13 @@ class BaseTask(Task): - /etc/tower (to prevent obtaining db info or secret key) - /var/lib/awx (except for current project) - /var/log/tower + - /var/log/supervisor - /tmp (except for own tmp files) ''' new_args = [getattr(settings, 'AWX_PROOT_CMD', 'proot'), '-r', '/'] hide_paths = ['/etc/tower', '/var/lib/awx', '/var/log/tower', - tempfile.gettempdir(), settings.PROJECTS_ROOT, - settings.JOBOUTPUT_ROOT] + '/var/log/supervisor', tempfile.gettempdir(), + settings.PROJECTS_ROOT, settings.JOBOUTPUT_ROOT] hide_paths.extend(getattr(settings, 'AWX_PROOT_HIDE_PATHS', None) or []) for path in sorted(set(hide_paths)): if not os.path.exists(path): diff --git a/awx/main/tests/tasks.py b/awx/main/tests/tasks.py index d96e63ec21..0ed78b5d4f 100644 --- a/awx/main/tests/tasks.py +++ b/awx/main/tests/tasks.py @@ -3,6 +3,7 @@ # Python from distutils.version import StrictVersion as Version +import glob import json import os import shutil @@ -176,6 +177,15 @@ TEST_PROOT_PLAYBOOK = ''' assert: that: - "not temp_stat.stat.exists" + - name: check for supervisor log path + stat: path={{ supervisor_log_path }} + register: supervisor_log_stat + when: supervisor_log_path is defined + - name: check that supervisor log path was not found + assert: + that: + - "not supervisor_log_stat.stat.exists" + when: supervisor_log_path is defined - name: try to run a tower-manage command command: tower-manage validate ignore_errors: true @@ -1354,6 +1364,11 @@ class RunJobTest(BaseCeleryTest): # Create a temp directory that should not be visible to the playbook. temp_path = tempfile.mkdtemp() self._temp_paths.append(temp_path) + # Find a file in supervisor logs that should not be visible. + try: + supervisor_log_path = glob.glob('/var/log/supervisor/*')[0] + except IndexError: + supervisor_log_path = None # Create our test project and job template. self.create_test_project(TEST_PROOT_PLAYBOOK) project_path = self.project.local_path @@ -1365,6 +1380,8 @@ class RunJobTest(BaseCeleryTest): 'other_project_path': other_project_path, 'temp_path': temp_path, } + if supervisor_log_path: + extra_vars['supervisor_log_path'] = supervisor_log_path job = self.create_test_job(job_template=job_template, verbosity=3, extra_vars=json.dumps(extra_vars)) self.assertEqual(job.status, 'new') From 43f46f830f11cb0cd7ac5ec6925b232244de846b Mon Sep 17 00:00:00 2001 From: Chris Church Date: Tue, 4 Nov 2014 12:06:09 -0500 Subject: [PATCH 03/15] Update VMware inventory to better handle AttributeErrors. --- awx/plugins/inventory/vmware.py | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/awx/plugins/inventory/vmware.py b/awx/plugins/inventory/vmware.py index 85b38739b4..d72bed09b6 100755 --- a/awx/plugins/inventory/vmware.py +++ b/awx/plugins/inventory/vmware.py @@ -193,10 +193,13 @@ class VMwareInventory(object): host_info = { 'name': host.name, 'tag': host.tag, - 'datastores': self._get_obj_info(host.datastore, depth=0), - 'networks': self._get_obj_info(host.network, depth=0), - 'vms': self._get_obj_info(host.vm, depth=0), } + for attr in ('datastore', 'network', 'vm'): + try: + value = getattr(host, attr) + host_info['%ss' % attr] = self._get_obj_info(value, depth=0) + except AttributeError: + host_info['%ss' % attr] = [] for k, v in self._get_obj_info(host.summary, depth=0).items(): if isinstance(v, collections.MutableMapping): for k2, v2 in v.items(): @@ -219,11 +222,21 @@ class VMwareInventory(object): vm_info = { 'name': vm.name, 'tag': vm.tag, - 'datastores': self._get_obj_info(vm.datastore, depth=0), - 'networks': self._get_obj_info(vm.network, depth=0), - 'resourcePool': self._get_obj_info(vm.resourcePool, depth=0), - 'guestState': vm.guest.guestState, } + for attr in ('datastore', 'network'): + try: + value = getattr(vm, attr) + vm_info['%ss' % attr] = self._get_obj_info(value, depth=0) + except AttributeError: + vm_info['%ss' % attr] = [] + try: + vm_info['resourcePool'] = self._get_obj_info(vm.resourcePool, depth=0) + except AttributeError: + vm_info['resourcePool'] = '' + try: + vm_info['guestState'] = vm.guest.guestState + except AttributeError: + vm_info['guestState'] = '' for k, v in self._get_obj_info(vm.summary, depth=0).items(): if isinstance(v, collections.MutableMapping): for k2, v2 in v.items(): From ae55f1c0d38ba90ed178c0e1514a34152a0d421c Mon Sep 17 00:00:00 2001 From: Jared Tabor Date: Wed, 5 Nov 2014 13:07:23 -0500 Subject: [PATCH 04/15] Websockets live updating portal mode fixed issue with websockets not updating view in portal mode --- awx/ui/static/js/config.js | 2 +- awx/ui/static/js/controllers/Jobs.js | 3 +- awx/ui/static/js/controllers/Portal.js | 69 ++++++++++------------- awx/ui/static/js/helpers/JobSubmission.js | 4 +- awx/ui/static/js/helpers/Jobs.js | 3 + awx/ui/static/js/lists/PortalJobs.js | 10 ++-- awx/ui/static/js/widgets/PortalJobs.js | 30 +--------- awx/ui/static/lib/ansible/Socket.js | 1 - 8 files changed, 43 insertions(+), 79 deletions(-) diff --git a/awx/ui/static/js/config.js b/awx/ui/static/js/config.js index ce064c6eba..abed710242 100644 --- a/awx/ui/static/js/config.js +++ b/awx/ui/static/js/config.js @@ -19,7 +19,7 @@ tooltip_delay: {show: 500, hide: 100}, // Default number of milliseconds to delay displaying/hiding tooltips - debug_mode: true, // Enable console logging messages + debug_mode: false, // Enable console logging messages password_strength: 45, // User password strength. Integer between 0 and 100, 100 being impossibly strong. // This value controls progress bar colors: diff --git a/awx/ui/static/js/controllers/Jobs.js b/awx/ui/static/js/controllers/Jobs.js index 7f95a68d0b..cbafb57b89 100644 --- a/awx/ui/static/js/controllers/Jobs.js +++ b/awx/ui/static/js/controllers/Jobs.js @@ -77,10 +77,9 @@ function JobsListController ($rootScope, $log, $scope, $compile, $routeParams, C case 'pending': case 'waiting': queued_scope.search('queued_job'); - break; - case 'successful': completed_scope.search('completed_job'); break; + case 'successful': case 'failed': case 'error': case 'canceled': diff --git a/awx/ui/static/js/controllers/Portal.js b/awx/ui/static/js/controllers/Portal.js index c936f7be4a..0956cb09ed 100644 --- a/awx/ui/static/js/controllers/Portal.js +++ b/awx/ui/static/js/controllers/Portal.js @@ -79,7 +79,6 @@ function PortalController($scope, $compile, $routeParams, $rootScope, $location, searchSize: 'col-lg-6 col-md-6' }); - $rootScope.flashMessage = null; SearchInit({ scope: $scope, @@ -120,48 +119,40 @@ function PortalController($scope, $compile, $routeParams, $rootScope, $location, $('.list-well:eq(1)').css('margin-top' , '0px'); }); - // function processEvent(event) { - // switch(event.status) { - // case 'running': - // jobs_scope.search('running_job'); - // jobs_scope.search('queued_job'); - - // break; - // case 'new': - // case 'pending': - // case 'waiting': - // jobs_scope.search('queued_job'); - - // break; - // case 'successful': - // jobs_scope.search('completed_job'); - // case 'failed': - // case 'error': - // case 'canceled': - // jobs_scope.search('completed_job'); - // jobs_scope.search('running_job'); - // jobs_scope.search('queued_job'); - // } - // } + function processEvent(event) { + switch(event.status) { + case 'running': + jobs_scope.search('portal_job'); + // queued_scope.search('queued_job'); + break; + case 'new': + case 'pending': + jobs_scope.search('portal_job'); + break; + case 'waiting': + jobs_scope.search('portal_job'); + // completed_scope.search('completed_job'); + break; + case 'successful': + // // console.log('successful'); + // running_scope.search('running_job'); + // completed_scope.search('completed_job'); + // break; + case 'failed': + case 'error': + case 'canceled': + jobs_scope.search('portal_job'); + // running_scope.search('running_job'); + // queued_scope.search('queued_job'); + } + } if ($rootScope.removeJobStatusChange) { $rootScope.removeJobStatusChange(); } - $rootScope.removeJobStatusChange = $rootScope.$on('JobStatusChange', function() { - jobs_scope.refreshJobs(); - // if(data.status==='pending'){ - // // $scope.refresh(); - // $('#portal-jobs').empty(); - // // $rootScope.flashMessage = null; - // PortalJobsWidget({ - // scope: $scope, - // target: 'portal-jobs', - // searchSize: 'col-lg-6 col-md-6' - // }); - // } - - - //x`processEvent(data); + $rootScope.removeJobStatusChange = $rootScope.$on('JobStatusChange', function(e, event) { + // jobs_scope.search('portal_job'); + processEvent(event); }); diff --git a/awx/ui/static/js/helpers/JobSubmission.js b/awx/ui/static/js/helpers/JobSubmission.js index 67097137a5..feb860a0b9 100644 --- a/awx/ui/static/js/helpers/JobSubmission.js +++ b/awx/ui/static/js/helpers/JobSubmission.js @@ -53,7 +53,7 @@ angular.module('JobSubmissionHelper', [ 'RestServices', 'Utilities', 'Credential $('#password-modal').dialog('close'); } scope.$emit(callback, data); - scope.$destroy(); + // scope.$destroy(); }) .error(function(data, status) { ProcessErrors(scope, data, status, null, { hdr: 'Error!', @@ -764,7 +764,7 @@ function($location, Wait, GetBasePath, LookUpInit, JobTemplateForm, CredentialLi } scope.removePlaybookLaunchFinished = scope.$on('PlaybookLaunchFinished', function(e, data) { //var base = $location.path().replace(/^\//, '').split('/')[0]; - if(scope.portalMode===false ){ + if(scope.portalMode===false || scope.$parent.portalMode===false ){ $location.path('/jobs/' + data.job); } diff --git a/awx/ui/static/js/helpers/Jobs.js b/awx/ui/static/js/helpers/Jobs.js index 32411cade4..a227faf2cd 100644 --- a/awx/ui/static/js/helpers/Jobs.js +++ b/awx/ui/static/js/helpers/Jobs.js @@ -405,6 +405,9 @@ angular.module('JobsHelper', ['Utilities', 'RestServices', 'FormGenerator', 'Job JobsControllerInit({ scope: scope, parent_scope: parent_scope }); JobsListUpdate({ scope: scope, parent_scope: parent_scope, list: list }); parent_scope.$emit('listLoaded'); + // setTimeout(function(){ + // scope.$apply(); + // }, 300); }); if (base === 'jobs' && list.name === 'completed_jobs') { diff --git a/awx/ui/static/js/lists/PortalJobs.js b/awx/ui/static/js/lists/PortalJobs.js index 733dd78b90..22dd48031e 100644 --- a/awx/ui/static/js/lists/PortalJobs.js +++ b/awx/ui/static/js/lists/PortalJobs.js @@ -14,8 +14,8 @@ angular.module('PortalJobsListDefinition', []) .value( 'PortalJobsList', { - name: 'jobs', - iterator: 'job', + name: 'portal_jobs', + iterator: 'portal_job', editTitle: 'Jobs', 'class': 'table-condensed', index: false, @@ -39,8 +39,8 @@ angular.module('PortalJobsListDefinition', []) columnClass: 'col-lg-1 col-md-2 col-sm-2 col-xs-2', // awToolTip: "{{ job.status_tip }}", // awTipPlacement: "top", - dataTitle: "{{ job.status_popover_title }}", - icon: 'icon-job-{{ job.status }}', + dataTitle: "{{ portal_job.status_popover_title }}", + icon: 'icon-job-{{ portal_job.status }}', iconOnly: true, // ngClick:"viewJobLog(job.id)", searchable: true, @@ -73,7 +73,7 @@ angular.module('PortalJobsListDefinition', []) fieldActions: { job_details: { mode: 'all', - ngClick: "viewJobLog(job.id)", + ngClick: "viewJobLog(portal_job.id)", awToolTip: 'View job details', dataPlacement: 'top' } diff --git a/awx/ui/static/js/widgets/PortalJobs.js b/awx/ui/static/js/widgets/PortalJobs.js index 3b2a65c7dd..b70455b489 100644 --- a/awx/ui/static/js/widgets/PortalJobs.js +++ b/awx/ui/static/js/widgets/PortalJobs.js @@ -76,39 +76,11 @@ angular.module('PortalJobsWidget', ['RestServices', 'Utilities']) scope: jobs_scope, list: PortalJobsList, id: 'active-jobs', - url: GetBasePath('unified_jobs') + '?status__in=running,completed,failed,successful,error,canceled', + url: GetBasePath('jobs'), //+ '?type__in=job' , //&status__in=running,completed,failed,successful,error,canceled', pageSize: max_rows, spinner: true }); - // completed_scope.showJobType = true; - // LoadJobsScope({ - // parent_scope: scope, - // scope: completed_scope, - // list: PortalJobsList, - // id: 'active-jobs', - // url: GetBasePath('unified_jobs') + '?or__status=successful&or__status=failed&or__status=error&or__status=canceled', - // // searchParams: search_params, - // pageSize: max_rows - // }); - - // LoadJobsScope({ - // parent_scope: scope, - // scope: running_scope, - // list: PortalJobsList, - // id: 'active-jobs', - // url: GetBasePath('unified_jobs') + '?status=running', - // pageSize: max_rows - // }); - - // LoadJobsScope({ - // parent_scope: scope, - // scope: queued_scope, - // list: PortalJobsList, - // id: 'active-jobs', - // url: GetBasePath('unified_jobs') + '?or__status=pending&or__status=waiting&or__status=new', - // pageSize: max_rows - // }); $(window).resize(_.debounce(function() { resizePortalJobsWidget(); diff --git a/awx/ui/static/lib/ansible/Socket.js b/awx/ui/static/lib/ansible/Socket.js index 9dc7c9cd92..ccbd4ec150 100644 --- a/awx/ui/static/lib/ansible/Socket.js +++ b/awx/ui/static/lib/ansible/Socket.js @@ -28,7 +28,6 @@ angular.module('SocketIO', ['AuthService', 'Utilities']) endpoint = params.endpoint, protocol = $location.protocol(), config, socketPort, - // handshakeData, url; // Since some pages are opened in a new tab, we might get here before AnsibleConfig is available. From 27fd173e7200cd5ffde5d3d0367d8852747ecc3f Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Thu, 6 Nov 2014 11:17:22 -0500 Subject: [PATCH 05/15] Custom inventory script environment variable blacklist --- awx/main/tasks.py | 2 +- awx/settings/defaults.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/awx/main/tasks.py b/awx/main/tasks.py index 04718c2af7..64cd1fcd65 100644 --- a/awx/main/tasks.py +++ b/awx/main/tasks.py @@ -1052,7 +1052,7 @@ class RunInventoryUpdate(BaseTask): pass elif inventory_update.source == 'custom': for env_k in inventory_update.source_vars_dict: - if str(env_k) not in os.environ: + if str(env_k) not in os.environ and str(env_k) not in settings.INV_ENV_VARIABLE_BLACKLIST: env[str(env_k)] = unicode(inventory_update.source_vars_dict[env_k]) return env diff --git a/awx/settings/defaults.py b/awx/settings/defaults.py index 7e5cf1b64a..1fbd7c27f3 100644 --- a/awx/settings/defaults.py +++ b/awx/settings/defaults.py @@ -351,6 +351,8 @@ RAX_GROUP_FILTER = r'^(?!instance-.+).+$' RAX_HOST_FILTER = r'^.+$' RAX_EXCLUDE_EMPTY_GROUPS = True +INV_ENV_VARIABLE_BLACKLIST = ("HOME", "_") + # ---------------- # -- Amazon EC2 -- # ---------------- From cacdefafbf8656b9d6493dfbc86d22209f9eecf2 Mon Sep 17 00:00:00 2001 From: Jared Tabor Date: Thu, 6 Nov 2014 14:25:48 -0500 Subject: [PATCH 06/15] Portal mode authorization Had to override the authorization controller which automatically redirects the user to hte home page if it is their first time visiting tower. I perform a check to see if the url is to /portal, and then set lastPath to portal to force it to portal mode on the first visit --- awx/ui/static/js/app.js | 4 ++++ awx/ui/static/lib/ansible/AuthService.js | 13 +++++++++++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/awx/ui/static/js/app.js b/awx/ui/static/js/app.js index 1d6390c11b..ec889c0302 100644 --- a/awx/ui/static/js/app.js +++ b/awx/ui/static/js/app.js @@ -649,6 +649,10 @@ angular.module('Tower', [ $AnsibleConfig = Store('AnsibleConfig'); } + //the authorization controller redirects to the home page automatcially if there is no last path defined. in order to override + // this, set the last path to /portal for instances where portal is visited for the first time. + $rootScope.lastPath = ($location.path() === "/portal") ? 'portal' : undefined; + LoadConfig(); } ]); diff --git a/awx/ui/static/lib/ansible/AuthService.js b/awx/ui/static/lib/ansible/AuthService.js index c782292e8a..2094df56fd 100644 --- a/awx/ui/static/lib/ansible/AuthService.js +++ b/awx/ui/static/lib/ansible/AuthService.js @@ -64,7 +64,16 @@ angular.module('AuthService', ['ngCookies', 'Utilities']) //$rootScope.$destroy(); $cookieStore.remove('token_expires'); $cookieStore.remove('current_user'); - $cookieStore.remove('lastPath'); + + if($cookieStore.get('lastPath')==='/portal'){ + $cookieStore.put( 'lastPath', '/portal'); + $rootScope.lastPath = '/portal'; + } + else { + $cookieStore.remove('lastPath'); + $rootScope.lastPath = '/home'; + } + $cookieStore.remove('token'); $cookieStore.put('userLoggedIn', false); $cookieStore.put('sessionExpired', false); @@ -75,7 +84,7 @@ angular.module('AuthService', ['ngCookies', 'Utilities']) $rootScope.sessionExpired = false; $rootScope.token = null; $rootScope.token_expires = null; - $rootScope.lastPath = '/home'; + $rootScope.login_username = null; $rootScope.login_password = null; }, From fd8908f7dc7246c88ce0e2f9d444295a76c85263 Mon Sep 17 00:00:00 2001 From: Jared Tabor Date: Thu, 6 Nov 2014 14:28:18 -0500 Subject: [PATCH 07/15] Job Submission cleanup removed some commented code from the fix for the job submission from yesterday --- awx/ui/static/js/controllers/Portal.js | 34 ++--------------------- awx/ui/static/js/helpers/JobSubmission.js | 1 - awx/ui/static/js/widgets/PortalJobs.js | 14 ---------- awx/ui/static/lib/ansible/AuthService.js | 1 - 4 files changed, 2 insertions(+), 48 deletions(-) diff --git a/awx/ui/static/js/controllers/Portal.js b/awx/ui/static/js/controllers/Portal.js index 0956cb09ed..d12f9e1c8f 100644 --- a/awx/ui/static/js/controllers/Portal.js +++ b/awx/ui/static/js/controllers/Portal.js @@ -119,41 +119,11 @@ function PortalController($scope, $compile, $routeParams, $rootScope, $location, $('.list-well:eq(1)').css('margin-top' , '0px'); }); - function processEvent(event) { - switch(event.status) { - case 'running': - jobs_scope.search('portal_job'); - // queued_scope.search('queued_job'); - break; - case 'new': - case 'pending': - jobs_scope.search('portal_job'); - break; - case 'waiting': - jobs_scope.search('portal_job'); - // completed_scope.search('completed_job'); - break; - case 'successful': - // // console.log('successful'); - // running_scope.search('running_job'); - // completed_scope.search('completed_job'); - // break; - case 'failed': - case 'error': - case 'canceled': - jobs_scope.search('portal_job'); - // running_scope.search('running_job'); - // queued_scope.search('queued_job'); - } - } - if ($rootScope.removeJobStatusChange) { $rootScope.removeJobStatusChange(); } - $rootScope.removeJobStatusChange = $rootScope.$on('JobStatusChange', function(e, event) { - // jobs_scope.search('portal_job'); - processEvent(event); - + $rootScope.removeJobStatusChange = $rootScope.$on('JobStatusChange', function() { + jobs_scope.search('portal_job'); //processEvent(event); }); $scope.submitJob = function (id) { diff --git a/awx/ui/static/js/helpers/JobSubmission.js b/awx/ui/static/js/helpers/JobSubmission.js index feb860a0b9..f2817a833e 100644 --- a/awx/ui/static/js/helpers/JobSubmission.js +++ b/awx/ui/static/js/helpers/JobSubmission.js @@ -53,7 +53,6 @@ angular.module('JobSubmissionHelper', [ 'RestServices', 'Utilities', 'Credential $('#password-modal').dialog('close'); } scope.$emit(callback, data); - // scope.$destroy(); }) .error(function(data, status) { ProcessErrors(scope, data, status, null, { hdr: 'Error!', diff --git a/awx/ui/static/js/widgets/PortalJobs.js b/awx/ui/static/js/widgets/PortalJobs.js index b70455b489..bc2c72e9f3 100644 --- a/awx/ui/static/js/widgets/PortalJobs.js +++ b/awx/ui/static/js/widgets/PortalJobs.js @@ -19,23 +19,11 @@ angular.module('PortalJobsWidget', ['RestServices', 'Utilities']) choicesCount = 0, listCount = 0, jobs_scope = scope.$new(true), - // completed_scope = scope.$new(true), - // running_scope = scope.$new(true), - // queued_scope = scope.$new(true), - // scheduled_scope = scope.$new(true), max_rows, html, e; html = ''; html += "
\n"; - // html+= "Job Templates "; - // html += "
    \n"; - // html += "
  • Jobs
  • \n"; - // html += "
  • Schedule
  • \n"; - // html += "
\n"; - // html += "
\n"; html += "
\n"; html += "\n"; //list html += "
\n"; //active-jobs-tab - // html += "
\n"; - // html += "
\n"; // jobs-list-container html += "
\n"; e = angular.element(document.getElementById(target)); diff --git a/awx/ui/static/lib/ansible/AuthService.js b/awx/ui/static/lib/ansible/AuthService.js index 2094df56fd..6da3ec3297 100644 --- a/awx/ui/static/lib/ansible/AuthService.js +++ b/awx/ui/static/lib/ansible/AuthService.js @@ -84,7 +84,6 @@ angular.module('AuthService', ['ngCookies', 'Utilities']) $rootScope.sessionExpired = false; $rootScope.token = null; $rootScope.token_expires = null; - $rootScope.login_username = null; $rootScope.login_password = null; }, From 9873bd2bc2c76ff1fbc7a1cb6577d7bd995242be Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Fri, 7 Nov 2014 10:45:19 -0500 Subject: [PATCH 08/15] Add created_by and modified_by to some serializers --- awx/api/serializers.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/awx/api/serializers.py b/awx/api/serializers.py index c5cb4f7f3a..3d6ba3f52e 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -41,7 +41,7 @@ from awx.main.utils import update_scm_url, get_type_for_model, get_model_for_typ logger = logging.getLogger('awx.api.serializers') # Fields that should be summarized regardless of object type. -DEFAULT_SUMMARY_FIELDS = ('name', 'description')#, 'type') +DEFAULT_SUMMARY_FIELDS = ('name', 'description')#, 'created_by', 'modified_by')#, 'type') # Keys are fields (foreign keys) where, if found on an instance, summary info # should be added to the serialized data. Values are a tuple of field names on @@ -49,7 +49,7 @@ DEFAULT_SUMMARY_FIELDS = ('name', 'description')#, 'type') # the related object). SUMMARIZABLE_FK_FIELDS = { 'organization': DEFAULT_SUMMARY_FIELDS, - 'user': ('username', 'first_name', 'last_name'), + 'user': ('id', 'username', 'first_name', 'last_name'), 'team': DEFAULT_SUMMARY_FIELDS, 'inventory': DEFAULT_SUMMARY_FIELDS + ('has_active_failures', 'total_hosts', @@ -289,6 +289,15 @@ class BaseSerializer(serializers.ModelSerializer): # Can be raised by the reverse accessor for a OneToOneField. except ObjectDoesNotExist: pass + if getattr(obj, 'created_by', None) and obj.created_by.is_active: + summary_fields['created_by'] = SortedDict() + for field in SUMMARIZABLE_FK_FIELDS['user']: + summary_fields['created_by'][field] = getattr(obj.created_by, field) + if getattr(obj, 'modified_by', None) and obj.modified_by.is_active: + summary_fields['modified_by'] = SortedDict() + for field in SUMMARIZABLE_FK_FIELDS['user']: + summary_fields['modified_by'][field] = getattr(obj.modified_by, field) + print summary_fields return summary_fields def get_created(self, obj): From 8fb6e50e708982be3d1f3183c9abc93a20a263f0 Mon Sep 17 00:00:00 2001 From: Jared Tabor Date: Fri, 7 Nov 2014 10:52:53 -0500 Subject: [PATCH 09/15] Portal Jobs adding correct query for a list of jobs that the current user ran. --- awx/ui/static/js/widgets/PortalJobs.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/awx/ui/static/js/widgets/PortalJobs.js b/awx/ui/static/js/widgets/PortalJobs.js index bc2c72e9f3..84d17a51d9 100644 --- a/awx/ui/static/js/widgets/PortalJobs.js +++ b/awx/ui/static/js/widgets/PortalJobs.js @@ -20,6 +20,7 @@ angular.module('PortalJobsWidget', ['RestServices', 'Utilities']) listCount = 0, jobs_scope = scope.$new(true), max_rows, + user, html, e; html = ''; @@ -57,12 +58,13 @@ angular.module('PortalJobsWidget', ['RestServices', 'Utilities']) if (PortalJobsList.fields.type) { PortalJobsList.fields.type.searchOptions = scope.type_choices; } + user = scope.$parent.current_user.id; LoadJobsScope({ parent_scope: scope, scope: jobs_scope, list: PortalJobsList, id: 'active-jobs', - url: GetBasePath('jobs'), //+ '?type__in=job' , //&status__in=running,completed,failed,successful,error,canceled', + url: GetBasePath('jobs')+'?created_by='+user, pageSize: max_rows, spinner: true }); From 22584257109b350720c1845f14c40a6ffc4899ab Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Fri, 7 Nov 2014 11:37:08 -0500 Subject: [PATCH 10/15] Denote whether a credential is required on the launch endpoint if it was not provided on the job template --- awx/api/views.py | 1 + 1 file changed, 1 insertion(+) diff --git a/awx/api/views.py b/awx/api/views.py index 896260c75b..21a3a05251 100644 --- a/awx/api/views.py +++ b/awx/api/views.py @@ -1465,6 +1465,7 @@ class JobTemplateLaunch(GenericAPIView): data['passwords_needed_to_start'] = obj.passwords_needed_to_start data['ask_variables_on_launch'] = obj.ask_variables_on_launch data['variables_needed_to_start'] = obj.variables_needed_to_start + data['credential_required'] = obj.credential is None data['survey_enabled'] = obj.survey_enabled return Response(data) From 902ef1a7ab423f49c5c6ab6ee2301fc31085f6b9 Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Fri, 7 Nov 2014 13:18:01 -0500 Subject: [PATCH 11/15] Read and run permission tweaks for new deployment permission type --- awx/main/access.py | 103 ++++++++++++++++++++++++++++++++------------- 1 file changed, 74 insertions(+), 29 deletions(-) diff --git a/awx/main/access.py b/awx/main/access.py index 4eaa4ad7f5..0c867426f7 100644 --- a/awx/main/access.py +++ b/awx/main/access.py @@ -826,41 +826,56 @@ class JobTemplateAccess(BaseAccess): org_admin_qs = base_qs.filter( project__organizations__admins__in=[self.user] ) - allowed = [PERM_JOBTEMPLATE_CREATE, PERM_INVENTORY_CHECK, PERM_INVENTORY_DEPLOY] - perm_qs = base_qs.filter( + allowed = [PERM_INVENTORY_CHECK, PERM_INVENTORY_DEPLOY] + allowed_deploy = [PERM_JOBTEMPLATE_CREATE, PERM_INVENTORY_DEPLOY] + allowed_check = [PERM_JOBTEMPLATE_CREATE, PERM_INVENTORY_DEPLOY, PERM_INVENTORY_CHECK] + + # perm_qs = base_qs.filter( + # Q(inventory__permissions__user=self.user) | Q(inventory__permissions__team__users__in=[self.user]), + # Q(project__permissions__user=self.user) | Q(project__permissions__team__users__in=[self.user]), + # inventory__permissions__permission_type__in=allowed, + # project__permissions__permission_type__in=allowed, + # inventory__permissions__active=True, + # project__permissions__active=True, + # inventory__permissions__pk=F('project__permissions__pk'), + # ) + + perm_deploy_qs = base_qs.filter( Q(inventory__permissions__user=self.user) | Q(inventory__permissions__team__users__in=[self.user]), Q(project__permissions__user=self.user) | Q(project__permissions__team__users__in=[self.user]), - inventory__permissions__permission_type__in=allowed, - project__permissions__permission_type__in=allowed, + job_type=PERM_INVENTORY_DEPLOY, + inventory__permissions__permission_type__in=allowed_deploy, + project__permissions__permission_type__in=allowed_deploy, inventory__permissions__active=True, project__permissions__active=True, inventory__permissions__pk=F('project__permissions__pk'), ) + + perm_check_qs = base_qs.filter( + Q(inventory__permissions__user=self.user) | Q(inventory__permissions__team__users__in=[self.user]), + Q(project__permissions__user=self.user) | Q(project__permissions__team__users__in=[self.user]), + job_type=PERM_INVENTORY_CHECK, + inventory__permissions__permission_type__in=allowed_check, + project__permissions__permission_type__in=allowed_check, + inventory__permissions__active=True, + project__permissions__active=True, + inventory__permissions__pk=F('project__permissions__pk'), + ) + # FIXME: I *think* this should work... needs more testing. - return org_admin_qs | perm_qs + return org_admin_qs | perm_deploy_qs | perm_check_qs def can_read(self, obj): # you can only see the job templates that you have permission to launch. - data = { - 'job_type': obj.job_type, - } - if obj.inventory and obj.inventory.pk: - data['inventory'] = obj.inventory.pk - if obj.project and obj.project.pk: - data['project'] = obj.project.pk - if obj.credential: - data['credential'] = obj.credential.pk - if obj.cloud_credential: - data['cloud_credential'] = obj.cloud_credential.pk - return self.can_add(data) + return self.can_start(obj) def can_add(self, data): ''' a user can create a job template if they are a superuser, an org admin of any org that the project is a member, or if they have user or team based permissions tying the project to the inventory source for the - given action. users who are able to create deploy jobs can also make - check (dry run) jobs. + given action as well as the 'create' deploy permission. + Users who are able to create deploy jobs can also run normal and check (dry run) jobs. ''' if not data or '_method' in data: # So the browseable API will work? return True @@ -950,29 +965,32 @@ class JobTemplateAccess(BaseAccess): if obj.inventory is None or obj.project is None: return False # If the user has admin access to the project they can start a job - if self.user.can_access(Project, 'admin', obj.project): + if self.user.can_access(Project, 'admin', obj.project, None): return True # Otherwise check for explicitly granted permissions permission_qs = Permission.objects.filter( Q(user=self.user) | Q(team__users__in=[self.user]), - inventory=inventory, - project=project, + inventory=obj.inventory, + project=obj.project, permission_type__in=[PERM_JOBTEMPLATE_CREATE, PERM_INVENTORY_CHECK, PERM_INVENTORY_DEPLOY], ) + has_perm = False for perm in permission_qs: # If you have job template create permission that implies both CHECK and DEPLOY # If you have DEPLOY permissions you can run both CHECK and DEPLOY - if perm.permission_type in [PERM_JOBTEMPLATE_CREATE, PERM_INVENTORY_DEPLOY]: + if perm.permission_type in [PERM_JOBTEMPLATE_CREATE, PERM_INVENTORY_DEPLOY] and \ + obj.job_type == PERM_INVENTORY_DEPLOY: has_perm = True # If you only have CHECK permission then you can only run CHECK - if perm.permission_type == PERM_INVENTORY_CHECK and perm.permission_type == PERM_INVENTORY_CHECK: + if perm.permission_type in [PERM_JOBTEMPLATE_CREATE, PERM_INVENTORY_DEPLOY, PERM_INVENTORY_CHECK] and \ + obj.job_type == PERM_INVENTORY_CHECK: has_perm = True dep_access = self.user.can_access(Inventory, 'read', obj.inventory) and \ self.user.can_access(Project, 'read', obj.project) - return self.can_read(obj) and dep_access and has_perm + return dep_access and has_perm def can_change(self, obj, data): return self.can_read(obj) and self.can_add(data) @@ -998,17 +1016,44 @@ class JobAccess(BaseAccess): project__organizations__admins__in=[self.user] ) allowed = [PERM_INVENTORY_CHECK, PERM_INVENTORY_DEPLOY] - perm_qs = base_qs.filter( + + allowed_deploy = [PERM_JOBTEMPLATE_CREATE, PERM_INVENTORY_DEPLOY] + allowed_check = [PERM_JOBTEMPLATE_CREATE, PERM_INVENTORY_DEPLOY, PERM_INVENTORY_CHECK] + + # perm_qs = base_qs.filter( + # Q(inventory__permissions__user=self.user) | Q(inventory__permissions__team__users__in=[self.user]), + # Q(project__permissions__user=self.user) | Q(project__permissions__team__users__in=[self.user]), + # inventory__permissions__permission_type__in=allowed, + # project__permissions__permission_type__in=allowed, + # inventory__permissions__active=True, + # project__permissions__active=True, + # inventory__permissions__pk=F('project__permissions__pk'), + # ) + + perm_deploy_qs = base_qs.filter( Q(inventory__permissions__user=self.user) | Q(inventory__permissions__team__users__in=[self.user]), Q(project__permissions__user=self.user) | Q(project__permissions__team__users__in=[self.user]), - inventory__permissions__permission_type__in=allowed, - project__permissions__permission_type__in=allowed, + job_type=PERM_INVENTORY_DEPLOY, + inventory__permissions__permission_type__in=allowed_deploy, + project__permissions__permission_type__in=allowed_deploy, inventory__permissions__active=True, project__permissions__active=True, inventory__permissions__pk=F('project__permissions__pk'), ) + + perm_check_qs = base_qs.filter( + Q(inventory__permissions__user=self.user) | Q(inventory__permissions__team__users__in=[self.user]), + Q(project__permissions__user=self.user) | Q(project__permissions__team__users__in=[self.user]), + job_type=PERM_INVENTORY_CHECK, + inventory__permissions__permission_type__in=allowed_check, + project__permissions__permission_type__in=allowed_check, + inventory__permissions__active=True, + project__permissions__active=True, + inventory__permissions__pk=F('project__permissions__pk'), + ) + # FIXME: I *think* this should work... needs more testing. - return org_admin_qs | perm_qs + return org_admin_qs | perm_deploy_qs | perm_check_qs def can_add(self, data): if not data or '_method' in data: # So the browseable API will work? From ea1b70273de475b6d540e937c0ccc950afe8b5e4 Mon Sep 17 00:00:00 2001 From: Jared Tabor Date: Fri, 7 Nov 2014 13:40:19 -0500 Subject: [PATCH 12/15] Jobs helper added a check if the job run was in the portal job list --- awx/ui/static/js/helpers/Jobs.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/awx/ui/static/js/helpers/Jobs.js b/awx/ui/static/js/helpers/Jobs.js index a227faf2cd..3ce544baa1 100644 --- a/awx/ui/static/js/helpers/Jobs.js +++ b/awx/ui/static/js/helpers/Jobs.js @@ -87,6 +87,9 @@ angular.module('JobsHelper', ['Utilities', 'RestServices', 'FormGenerator', 'Job else if (scope.jobs) { list = scope.jobs; } + else if(scope.portal_jobs){ + list=scope.portal_jobs; + } job = Find({ list: list, key: 'id', val: id }); if (job.type === 'job') { if(scope.$parent.portalMode===true){ From 2925498c4e488a138f5d54ac885ceae619afee07 Mon Sep 17 00:00:00 2001 From: Luke Sneeringer Date: Fri, 7 Nov 2014 16:23:42 -0600 Subject: [PATCH 13/15] More complete superuser fix. --- awx/api/views.py | 1 + 1 file changed, 1 insertion(+) diff --git a/awx/api/views.py b/awx/api/views.py index 21a3a05251..f889c89ac9 100644 --- a/awx/api/views.py +++ b/awx/api/views.py @@ -839,6 +839,7 @@ class UserActivityStreamList(SubListAPIView): return qs.filter(Q(actor=parent) | Q(user__in=[parent])) + class UserDetail(RetrieveUpdateDestroyAPIView): model = User From db6a068c4a0bd2a2a775d2709c497ed7d4492f9f Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Sun, 9 Nov 2014 19:42:02 -0500 Subject: [PATCH 14/15] Make the credential requirement information more consistent --- awx/api/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/api/views.py b/awx/api/views.py index f889c89ac9..b22946a77c 100644 --- a/awx/api/views.py +++ b/awx/api/views.py @@ -1466,7 +1466,7 @@ class JobTemplateLaunch(GenericAPIView): data['passwords_needed_to_start'] = obj.passwords_needed_to_start data['ask_variables_on_launch'] = obj.ask_variables_on_launch data['variables_needed_to_start'] = obj.variables_needed_to_start - data['credential_required'] = obj.credential is None + data['credential_needed_to_start'] = obj.credential is None data['survey_enabled'] = obj.survey_enabled return Response(data) From 71946d3802a3abe900eaceaea7017c2aade87fb8 Mon Sep 17 00:00:00 2001 From: Luke Sneeringer Date: Mon, 10 Nov 2014 08:43:16 -0600 Subject: [PATCH 15/15] Superuser fix redux. --- awx/api/views.py | 58 +++++++++++++++++++++++++++--------------------- 1 file changed, 33 insertions(+), 25 deletions(-) diff --git a/awx/api/views.py b/awx/api/views.py index b22946a77c..08015cdfc6 100644 --- a/awx/api/views.py +++ b/awx/api/views.py @@ -5,6 +5,7 @@ # Python import datetime import dateutil +import functools import time import re import socket @@ -431,30 +432,32 @@ class DashboardInventoryGraphView(APIView): return Response(dashboard_data) -class UserCreateAPIMixin(object): - """A mixin subclass that ensures that only a superuser is able to create - another superuser. +def disallow_superuser_escalation(cls): + """Decorator that ensures that the post, put, and patch methods on the + class, if they exist, perform a sanity check and disallow superuser + escalation by non-superusers. """ - def post(self, request, pk=None): - self._superuser_sanity_check(request) - return super(UserCreateAPIMixin, self).post(request, pk=pk) + # Create a method decorator that ensures superuser escalation by + # non-superusers is disallowed. + def superuser_lockdown(method): + @functools.wraps(method) + def fx(self, request, *a, **kw): + if not request.user.is_superuser: + if request.DATA.get('is_superuser', False): + raise PermissionDenied('Only superusers may create ' + 'other superusers.') + return method(self, request, *a, **kw) + return fx - # def put(self, request, pk=None): - # self._superuser_sanity_check(request) - # return super(UserCreateAPIMixin, self).put(request, pk=pk) + # Ensure that if post, put, or patch methods exist, that they are decorated + # with the sanity check decorator. + for vuln_method in ('post', 'put', 'patch'): + original_method = getattr(cls, vuln_method, None) + if original_method is not None: + setattr(cls, vuln_method, superuser_lockdown(original_method)) - # def patch(self, request, pk=None): - # self._superuser_sanity_check(request) - # return super(UserCreateAPIMixin, self).patch(request, pk=pk) - - def _superuser_sanity_check(self, request): - """Ensure that if a non-superuser tries to create a superuser, - that the request is rejected. - """ - if not request.user.is_superuser: - if request.DATA.get('is_superuser', False): - raise PermissionDenied('Only superusers may create ' - 'other superusers.') + # Return the class object. + return cls class ScheduleList(ListAPIView): @@ -518,14 +521,16 @@ class OrganizationInventoriesList(SubListAPIView): parent_model = Organization relationship = 'inventories' -class OrganizationUsersList(UserCreateAPIMixin, SubListCreateAPIView): +@disallow_superuser_escalation +class OrganizationUsersList(SubListCreateAPIView): model = User serializer_class = UserSerializer parent_model = Organization relationship = 'users' -class OrganizationAdminsList(UserCreateAPIMixin, SubListCreateAPIView): +@disallow_superuser_escalation +class OrganizationAdminsList(SubListCreateAPIView): model = User serializer_class = UserSerializer @@ -565,7 +570,8 @@ class TeamDetail(RetrieveUpdateDestroyAPIView): model = Team serializer_class = TeamSerializer -class TeamUsersList(UserCreateAPIMixin, SubListCreateAPIView): +@disallow_superuser_escalation +class TeamUsersList(SubListCreateAPIView): model = User serializer_class = UserSerializer @@ -760,7 +766,9 @@ class ProjectUpdateCancel(GenericAPIView): else: return self.http_method_not_allowed(request, *args, **kwargs) -class UserList(UserCreateAPIMixin, ListCreateAPIView): + +@disallow_superuser_escalation +class UserList(ListCreateAPIView): model = User serializer_class = UserSerializer