From 45d037a2cc9bd2139c647f3222f4388d9d4ee96e Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Wed, 25 Apr 2018 21:12:13 -0400 Subject: [PATCH 001/169] don't prefer destructuring for arrays within es-lint --- awx/ui/.eslintrc.js | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/awx/ui/.eslintrc.js b/awx/ui/.eslintrc.js index 217601f5f9..9a42d34ab2 100644 --- a/awx/ui/.eslintrc.js +++ b/awx/ui/.eslintrc.js @@ -54,6 +54,18 @@ module.exports = { 'no-multiple-empty-lines': ['error', { max: 1 }], 'object-curly-newline': 'off', 'space-before-function-paren': ['error', 'always'], - 'no-trailing-spaces': ['error'] - } + 'no-trailing-spaces': ['error'], + 'prefer-destructuring': ['error', { + 'VariableDeclarator': { + 'array': false, + 'object': true + }, + 'AssignmentExpression': { + 'array': false, + 'object': true + } + }, { + 'enforceForRenamedProperties': false + }] + } }; From 6306ac2825828275a0a7e33892fd4f469fc330fc Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Tue, 10 Apr 2018 13:44:08 -0400 Subject: [PATCH 002/169] use field validation in both filter classes --- awx/api/filters.py | 145 ++++++++++-------- .../tests/functional/api/test_inventory.py | 5 +- awx/main/tests/unit/api/test_filters.py | 29 +++- 3 files changed, 113 insertions(+), 66 deletions(-) diff --git a/awx/api/filters.py b/awx/api/filters.py index 1c5a47f847..c7425f5e75 100644 --- a/awx/api/filters.py +++ b/awx/api/filters.py @@ -77,6 +77,65 @@ class TypeFilterBackend(BaseFilterBackend): raise ParseError(*e.args) +def get_field_from_path(model, path): + ''' + Given a Django ORM lookup path (possibly over multiple models) + Returns the last field in the line, and also the revised lookup path + ex., given + model=Organization + path='project__timeout' + returns tuple of field at the end of the line as well as a corrected + path, for special cases we do substitutions + (, 'project__timeout') + ''' + # Store of all the fields used to detect repeats + field_set = set([]) + new_parts = [] + for name in path.split('__'): + if model is None: + raise ParseError(_('No related model for field {}.').format(name)) + # HACK: Make project and inventory source filtering by old field names work for backwards compatibility. + if model._meta.object_name in ('Project', 'InventorySource'): + name = { + 'current_update': 'current_job', + 'last_update': 'last_job', + 'last_update_failed': 'last_job_failed', + 'last_updated': 'last_job_run', + }.get(name, name) + + if name == 'type' and 'polymorphic_ctype' in get_all_field_names(model): + name = 'polymorphic_ctype' + new_parts.append('polymorphic_ctype__model') + else: + new_parts.append(name) + + if name in getattr(model, 'PASSWORD_FIELDS', ()): + raise PermissionDenied(_('Filtering on password fields is not allowed.')) + elif name == 'pk': + field = model._meta.pk + else: + name_alt = name.replace("_", "") + if name_alt in model._meta.fields_map.keys(): + field = model._meta.fields_map[name_alt] + new_parts.pop() + new_parts.append(name_alt) + else: + field = model._meta.get_field(name) + if 'auth' in name or 'token' in name: + raise PermissionDenied(_('Filtering on %s is not allowed.' % name)) + if isinstance(field, ForeignObjectRel) and getattr(field.field, '__prevent_search__', False): + raise PermissionDenied(_('Filtering on %s is not allowed.' % name)) + elif getattr(field, '__prevent_search__', False): + raise PermissionDenied(_('Filtering on %s is not allowed.' % name)) + if field in field_set: + # Field traversed twice, could create infinite JOINs, DoSing Tower + raise ParseError(_('Loops not allowed in filters, detected on field {}.').format(field.name)) + field_set.add(field) + model = getattr(field, 'related_model', None) + + return field, '__'.join(new_parts) + + class FieldLookupBackend(BaseFilterBackend): ''' Filter using field lookups provided via query string parameters. @@ -91,61 +150,23 @@ class FieldLookupBackend(BaseFilterBackend): 'isnull', 'search') def get_field_from_lookup(self, model, lookup): - field = None - parts = lookup.split('__') - if parts and parts[-1] not in self.SUPPORTED_LOOKUPS: - parts.append('exact') + + if '__' in lookup and lookup.rsplit('__', 1)[-1] in self.SUPPORTED_LOOKUPS: + path, suffix = lookup.rsplit('__', 1) + else: + path = lookup + suffix = 'exact' + + if not path: + raise ParseError(_('Query string field name not provided.')) + # FIXME: Could build up a list of models used across relationships, use # those lookups combined with request.user.get_queryset(Model) to make # sure user cannot query using objects he could not view. - new_parts = [] + field, new_path = get_field_from_path(model, path) - # Store of all the fields used to detect repeats - field_set = set([]) - - for name in parts[:-1]: - # HACK: Make project and inventory source filtering by old field names work for backwards compatibility. - if model._meta.object_name in ('Project', 'InventorySource'): - name = { - 'current_update': 'current_job', - 'last_update': 'last_job', - 'last_update_failed': 'last_job_failed', - 'last_updated': 'last_job_run', - }.get(name, name) - - if name == 'type' and 'polymorphic_ctype' in get_all_field_names(model): - name = 'polymorphic_ctype' - new_parts.append('polymorphic_ctype__model') - else: - new_parts.append(name) - - if name in getattr(model, 'PASSWORD_FIELDS', ()): - raise PermissionDenied(_('Filtering on password fields is not allowed.')) - elif name == 'pk': - field = model._meta.pk - else: - name_alt = name.replace("_", "") - if name_alt in model._meta.fields_map.keys(): - field = model._meta.fields_map[name_alt] - new_parts.pop() - new_parts.append(name_alt) - else: - field = model._meta.get_field(name) - if 'auth' in name or 'token' in name: - raise PermissionDenied(_('Filtering on %s is not allowed.' % name)) - if isinstance(field, ForeignObjectRel) and getattr(field.field, '__prevent_search__', False): - raise PermissionDenied(_('Filtering on %s is not allowed.' % name)) - elif getattr(field, '__prevent_search__', False): - raise PermissionDenied(_('Filtering on %s is not allowed.' % name)) - if field in field_set: - # Field traversed twice, could create infinite JOINs, DoSing Tower - raise ParseError(_('Loops not allowed in filters, detected on field {}.').format(field.name)) - field_set.add(field) - model = getattr(field, 'related_model', None) or field.model - - if parts: - new_parts.append(parts[-1]) - new_lookup = '__'.join(new_parts) + new_lookup = new_path + new_lookup = '__'.join([new_path, suffix]) return field, new_lookup def to_python_related(self, value): @@ -371,7 +392,7 @@ class OrderByBackend(BaseFilterBackend): else: order_by = (value,) if order_by: - order_by = self._strip_sensitive_model_fields(queryset.model, order_by) + order_by = self._validate_ordering_fields(queryset.model, order_by) # Special handling of the type field for ordering. In this # case, we're not sorting exactly on the type field, but @@ -396,15 +417,17 @@ class OrderByBackend(BaseFilterBackend): # Return a 400 for invalid field names. raise ParseError(*e.args) - def _strip_sensitive_model_fields(self, model, order_by): + def _validate_ordering_fields(self, model, order_by): for field_name in order_by: # strip off the negation prefix `-` if it exists - _field_name = field_name.split('-')[-1] + prefix = '' + path = field_name + if field_name[0] == '-': + prefix = field_name[0] + path = field_name[1:] try: - # if the field name is encrypted/sensitive, don't sort on it - if _field_name in getattr(model, 'PASSWORD_FIELDS', ()) or \ - getattr(model._meta.get_field(_field_name), '__prevent_search__', False): - raise ParseError(_('cannot order by field %s') % _field_name) - except FieldDoesNotExist: - pass - yield field_name + field, new_path = get_field_from_path(model, path) + new_path = '{}{}'.format(prefix, new_path) + except (FieldError, FieldDoesNotExist) as e: + raise ParseError(e.args[0]) + yield new_path diff --git a/awx/main/tests/functional/api/test_inventory.py b/awx/main/tests/functional/api/test_inventory.py index c96bb8057c..2e4b7df63e 100644 --- a/awx/main/tests/functional/api/test_inventory.py +++ b/awx/main/tests/functional/api/test_inventory.py @@ -126,9 +126,8 @@ def test_list_cannot_order_by_unsearchable_field(get, organization, alice, order ) custom_script.admin_role.members.add(alice) - response = get(reverse('api:inventory_script_list'), alice, - QUERY_STRING='order_by=%s' % order_by, status=400) - assert response.status_code == 400 + get(reverse('api:inventory_script_list'), alice, + QUERY_STRING='order_by=%s' % order_by, expect=403) @pytest.mark.parametrize("role_field,expected_status_code", [ diff --git a/awx/main/tests/unit/api/test_filters.py b/awx/main/tests/unit/api/test_filters.py index 12ff3663a5..1a70b9716c 100644 --- a/awx/main/tests/unit/api/test_filters.py +++ b/awx/main/tests/unit/api/test_filters.py @@ -3,14 +3,18 @@ import pytest from rest_framework.exceptions import PermissionDenied, ParseError -from awx.api.filters import FieldLookupBackend +from awx.api.filters import FieldLookupBackend, OrderByBackend, get_field_from_path from awx.main.models import (AdHocCommand, ActivityStream, CustomInventoryScript, Credential, Job, JobTemplate, SystemJob, UnifiedJob, User, WorkflowJob, WorkflowJobTemplate, - WorkflowJobOptions, InventorySource) + WorkflowJobOptions, InventorySource, + JobEvent) from awx.main.models.jobs import JobOptions +# Django +from django.db.models.fields import FieldDoesNotExist + def test_related(): field_lookup = FieldLookupBackend() @@ -20,6 +24,27 @@ def test_related(): print(new_lookup) +def test_invalid_filter_key(): + field_lookup = FieldLookupBackend() + # FieldDoesNotExist is caught and converted to ParseError by filter_queryset + with pytest.raises(FieldDoesNotExist) as excinfo: + field_lookup.value_to_python(JobEvent, 'event_data.task_action', 'foo') + assert 'has no field named' in str(excinfo) + + +def test_invalid_field_hop(): + with pytest.raises(ParseError) as excinfo: + get_field_from_path(Credential, 'organization__description__user') + assert 'No related model for' in str(excinfo) + + +def test_invalid_order_by_key(): + field_order_by = OrderByBackend() + with pytest.raises(ParseError) as excinfo: + [f for f in field_order_by._validate_ordering_fields(JobEvent, ('event_data.task_action',))] + assert 'has no field named' in str(excinfo) + + @pytest.mark.parametrize(u"empty_value", [u'', '']) def test_empty_in(empty_value): field_lookup = FieldLookupBackend() From 9cc9bdc4b58b3c855cc268a7f7b1d0b2f7df8dc6 Mon Sep 17 00:00:00 2001 From: Jake McDermott Date: Thu, 26 Apr 2018 13:21:28 -0400 Subject: [PATCH 003/169] clickable stdout events and host details modal for projects --- .../output/host-event/host-event.service.js | 24 +++++++++++++++---- .../client/features/output/render.service.js | 16 ++++++++++++- 2 files changed, 34 insertions(+), 6 deletions(-) diff --git a/awx/ui/client/features/output/host-event/host-event.service.js b/awx/ui/client/features/output/host-event/host-event.service.js index 4454bde27b..1e0588b329 100644 --- a/awx/ui/client/features/output/host-event/host-event.service.js +++ b/awx/ui/client/features/output/host-event/host-event.service.js @@ -5,13 +5,27 @@ function HostEventService ( $rootScope ) { this.getUrl = (id, type, params) => { - let url; + const queryString = this.stringifyParams(params); + + let baseUrl; + let related; + if (type === 'playbook') { - url = `${GetBasePath('jobs')}${id}/job_events/?${this.stringifyParams(params)}`; - } else if (type === 'command') { - url = `${GetBasePath('ad_hoc_commands')}${id}/events/?${this.stringifyParams(params)}`; + baseUrl = GetBasePath('jobs'); + related = 'job_events'; } - return url; + + if (type === 'command') { + baseUrl = GetBasePath('ad_hoc_commands'); + related = 'events'; + } + + if (type === 'project') { + baseUrl = GetBasePath('project_updates'); + related = 'events'; + } + + return `${baseUrl}${id}/${related}/?${queryString}`; }; // GET events related to a job run diff --git a/awx/ui/client/features/output/render.service.js b/awx/ui/client/features/output/render.service.js index aa86913133..81728150fe 100644 --- a/awx/ui/client/features/output/render.service.js +++ b/awx/ui/client/features/output/render.service.js @@ -96,6 +96,20 @@ function JobRenderService ($q, $sce, $window) { return { html, count }; }; + this.isHostEvent = (event) => { + if (typeof event.host === 'number') { + return true; + } + + if (event.type === 'project_update_event' && + event.event !== 'runner_on_skipped' && + event.event_data.host) { + return true; + } + + return false; + }; + this.createRecord = (ln, lines, event) => { if (!event.uuid) { return null; @@ -109,7 +123,7 @@ function JobRenderService ($q, $sce, $window) { start: event.start_line, end: event.end_line, isTruncated: (event.end_line - event.start_line) > lines.length, - isHost: typeof event.host === 'number' + isHost: this.isHostEvent(event), }; if (event.parent_uuid) { From 72254758f652f9c67aef7de60a86e881b23d171a Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Thu, 26 Apr 2018 13:22:39 -0400 Subject: [PATCH 004/169] add support for additional selects --- .../add-applications.controller.js | 6 +--- .../add-edit-applications.view.html | 2 ++ .../edit-applications.controller.js | 6 +--- .../lib/components/form/form.directive.js | 4 ++- .../lib/components/input/base.controller.js | 2 +- .../lib/components/input/select.directive.js | 4 ++- awx/ui/client/lib/models/Application.js | 31 ++++++++++++++++++- 7 files changed, 41 insertions(+), 14 deletions(-) diff --git a/awx/ui/client/features/applications/add-applications.controller.js b/awx/ui/client/features/applications/add-applications.controller.js index 173460157e..23453734fe 100644 --- a/awx/ui/client/features/applications/add-applications.controller.js +++ b/awx/ui/client/features/applications/add-applications.controller.js @@ -3,10 +3,8 @@ function AddApplicationsController (models, $state, strings) { const { application, me, organization } = models; const omit = [ - 'authorization_grant_type', 'client_id', 'client_secret', - 'client_type', 'created', 'modified', 'related', @@ -54,9 +52,7 @@ function AddApplicationsController (models, $state, strings) { vm.form.save = data => { const hiddenData = { - authorization_grant_type: 'implicit', - user: me.get('id'), - client_type: 'public' + user: me.get('id') }; const payload = _.merge(data, hiddenData); diff --git a/awx/ui/client/features/applications/add-edit-applications.view.html b/awx/ui/client/features/applications/add-edit-applications.view.html index a8d8d68e6c..9f2bbb96d3 100644 --- a/awx/ui/client/features/applications/add-edit-applications.view.html +++ b/awx/ui/client/features/applications/add-edit-applications.view.html @@ -15,6 +15,8 @@ + + diff --git a/awx/ui/client/features/applications/edit-applications.controller.js b/awx/ui/client/features/applications/edit-applications.controller.js index 6279b642ee..6b7e21aed4 100644 --- a/awx/ui/client/features/applications/edit-applications.controller.js +++ b/awx/ui/client/features/applications/edit-applications.controller.js @@ -4,10 +4,8 @@ function EditApplicationsController (models, $state, strings, $scope) { const { me, application, organization } = models; const omit = [ - 'authorization_grant_type', 'client_id', 'client_secret', - 'client_type', 'created', 'modified', 'related', @@ -90,9 +88,7 @@ function EditApplicationsController (models, $state, strings, $scope) { vm.form.save = data => { const hiddenData = { - authorization_grant_type: 'implicit', - user: me.get('id'), - client_type: 'public' + user: me.get('id') }; const payload = _.merge(data, hiddenData); diff --git a/awx/ui/client/lib/components/form/form.directive.js b/awx/ui/client/lib/components/form/form.directive.js index dc19c61e2a..e0ac85c595 100644 --- a/awx/ui/client/lib/components/form/form.directive.js +++ b/awx/ui/client/lib/components/form/form.directive.js @@ -76,7 +76,9 @@ function AtFormController (eventService, strings) { return values; } - if (component.state._key && typeof component.state._value === 'object') { + if (component.state._format === 'selectFromOptions') { + values[component.state.id] = component.state._value[0]; + } else if (component.state._key && typeof component.state._value === 'object') { values[component.state.id] = component.state._value[component.state._key]; } else if (component.state._group) { values[component.state._key] = values[component.state._key] || {}; diff --git a/awx/ui/client/lib/components/input/base.controller.js b/awx/ui/client/lib/components/input/base.controller.js index 5c27d903f0..75caad2f07 100644 --- a/awx/ui/client/lib/components/input/base.controller.js +++ b/awx/ui/client/lib/components/input/base.controller.js @@ -14,7 +14,7 @@ function BaseInputController (strings) { scope.state._required = scope.state.required || false; scope.state._isValid = scope.state._isValid || false; scope.state._disabled = scope.state._disabled || false; - scope.state._activeModel = '_value'; + scope.state._activeModel = scope.state._activeModel || '_value'; if (scope.state.ask_at_runtime) { scope.state._displayPromptOnLaunch = true; diff --git a/awx/ui/client/lib/components/input/select.directive.js b/awx/ui/client/lib/components/input/select.directive.js index 8507573a61..ddc0e23766 100644 --- a/awx/ui/client/lib/components/input/select.directive.js +++ b/awx/ui/client/lib/components/input/select.directive.js @@ -59,7 +59,9 @@ function AtInputSelectController (baseInputController, eventService) { }; vm.updateDisplayModel = () => { - if (scope.state._format === 'array') { + if (scope.state._format === 'selectFromOptions') { + scope.displayModel = scope.state._value[1]; + } else if (scope.state._format === 'array') { scope.displayModel = scope.state._value; } else if (scope.state._format === 'objects') { scope.displayModel = scope.state._value[scope.state._display]; diff --git a/awx/ui/client/lib/models/Application.js b/awx/ui/client/lib/models/Application.js index 5c8cdbd066..1fa79a3661 100644 --- a/awx/ui/client/lib/models/Application.js +++ b/awx/ui/client/lib/models/Application.js @@ -1,6 +1,21 @@ let Base; function createFormSchema (method, config) { + function mungeSelectFromOptions (configObj, value) { + configObj.choices = [['', '']].concat(configObj.choices); + configObj._data = configObj.choices; + configObj._exp = 'choice[1] for choice in state._data'; + configObj._format = 'selectFromOptions'; + + configObj._data.forEach((val, i) => { + if (val[0] === value) { + configObj._value = configObj._data[i]; + } + }); + + return configObj; + } + if (!config) { config = method; method = 'GET'; @@ -15,11 +30,25 @@ function createFormSchema (method, config) { Object.keys(schema).forEach(key => { schema[key].id = key; - if (this.has(key)) { + if (this.has(key) && schema[key].type !== 'choice') { schema[key]._value = this.get(key); } + + if (schema[key].type === 'choice') { + schema[key] = mungeSelectFromOptions(schema[key], this.get(key)); + } }); + // necessary because authorization_grant_type is not changeable on update + if (method === 'put') { + schema.authorization_grant_type = mungeSelectFromOptions(Object.assign({}, this + .options('actions.GET.authorization_grant_type')), this + .get('authorization_grant_type')); + + schema.authorization_grant_type._required = false; + schema.authorization_grant_type._disabled = true; + } + return schema; } From 3bcc48402cc9d03721ce612030fe023d08c8338b Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Thu, 26 Apr 2018 13:23:03 -0400 Subject: [PATCH 005/169] add support for applications activity stream --- awx/ui/client/features/applications/index.js | 2 -- awx/ui/client/src/activity-stream/get-target-title.factory.js | 3 +++ .../src/activity-stream/model-to-base-path-key.factory.js | 3 +++ .../streamDropdownNav/stream-dropdown-nav.directive.js | 1 + 4 files changed, 7 insertions(+), 2 deletions(-) diff --git a/awx/ui/client/features/applications/index.js b/awx/ui/client/features/applications/index.js index ca70be6165..448ea12f88 100644 --- a/awx/ui/client/features/applications/index.js +++ b/awx/ui/client/features/applications/index.js @@ -62,7 +62,6 @@ function ApplicationsRun ($stateExtender, strings) { }, data: { activityStream: true, - // TODO: double-check activity stream works activityStreamTarget: 'application' }, views: { @@ -111,7 +110,6 @@ function ApplicationsRun ($stateExtender, strings) { }, data: { activityStream: true, - // TODO: double-check activity stream works activityStreamTarget: 'application' }, views: { diff --git a/awx/ui/client/src/activity-stream/get-target-title.factory.js b/awx/ui/client/src/activity-stream/get-target-title.factory.js index 3921f2d530..f782769c5c 100644 --- a/awx/ui/client/src/activity-stream/get-target-title.factory.js +++ b/awx/ui/client/src/activity-stream/get-target-title.factory.js @@ -43,6 +43,9 @@ export default function GetTargetTitle(i18n) { case 'template': rtnTitle = i18n._('TEMPLATES'); break; + case 'application': + rtnTitle = i18n._('APPLICATIONS'); + break; } return rtnTitle; diff --git a/awx/ui/client/src/activity-stream/model-to-base-path-key.factory.js b/awx/ui/client/src/activity-stream/model-to-base-path-key.factory.js index 10285871d0..f6a45e6bdd 100644 --- a/awx/ui/client/src/activity-stream/model-to-base-path-key.factory.js +++ b/awx/ui/client/src/activity-stream/model-to-base-path-key.factory.js @@ -18,6 +18,9 @@ export default function ModelToBasePathKey() { var basePathKey; switch(model) { + case 'application': + basePathKey = 'applications'; + break; case 'project': basePathKey = 'projects'; break; diff --git a/awx/ui/client/src/activity-stream/streamDropdownNav/stream-dropdown-nav.directive.js b/awx/ui/client/src/activity-stream/streamDropdownNav/stream-dropdown-nav.directive.js index 7231cdd9c2..601e65f1ac 100644 --- a/awx/ui/client/src/activity-stream/streamDropdownNav/stream-dropdown-nav.directive.js +++ b/awx/ui/client/src/activity-stream/streamDropdownNav/stream-dropdown-nav.directive.js @@ -21,6 +21,7 @@ export default ['templateUrl', 'i18n', function(templateUrl, i18n) { $scope.options = [ {label: i18n._('All Activity'), value: 'dashboard'}, + {label: i18n._('Applications'), value: 'application'}, {label: i18n._('Credentials'), value: 'credential'}, {label: i18n._('Hosts'), value: 'host'}, {label: i18n._('Inventories'), value: 'inventory'}, From 61757fb2b1b535dcc2a9595fbfd3077d4ab0b203 Mon Sep 17 00:00:00 2001 From: Jake McDermott Date: Thu, 26 Apr 2018 13:24:41 -0400 Subject: [PATCH 006/169] improve traceback details label --- awx/ui/client/features/output/details.directive.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/ui/client/features/output/details.directive.js b/awx/ui/client/features/output/details.directive.js index eb2b7277c7..b19475f70a 100644 --- a/awx/ui/client/features/output/details.directive.js +++ b/awx/ui/client/features/output/details.directive.js @@ -290,7 +290,7 @@ function getResultTracebackDetails () { } const limit = 150; - const label = 'Results Traceback'; + const label = 'Error Details'; const more = traceback; const less = $filter('limitTo')(more, limit); From f99792e604d326732c163cd30a97af3be3f2da69 Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Thu, 26 Apr 2018 17:06:20 -0400 Subject: [PATCH 007/169] make selects work with ng required --- awx/ui/client/lib/components/input/base.controller.js | 3 ++- awx/ui/client/lib/models/Application.js | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/awx/ui/client/lib/components/input/base.controller.js b/awx/ui/client/lib/components/input/base.controller.js index 75caad2f07..32fa30b458 100644 --- a/awx/ui/client/lib/components/input/base.controller.js +++ b/awx/ui/client/lib/components/input/base.controller.js @@ -49,7 +49,8 @@ function BaseInputController (strings) { scope.state._touched = true; } - if (scope.state._required && !scope.state._value && !scope.state._displayValue) { + if (scope.state._required && (!scope.state._value || !scope.state._value[0]) && + !scope.state._displayValue) { isValid = false; message = vm.strings.get('message.REQUIRED_INPUT_MISSING'); } else if (scope.state._validate) { diff --git a/awx/ui/client/lib/models/Application.js b/awx/ui/client/lib/models/Application.js index 1fa79a3661..2a364eb9e7 100644 --- a/awx/ui/client/lib/models/Application.js +++ b/awx/ui/client/lib/models/Application.js @@ -2,7 +2,7 @@ let Base; function createFormSchema (method, config) { function mungeSelectFromOptions (configObj, value) { - configObj.choices = [['', '']].concat(configObj.choices); + configObj.choices = [[null, '']].concat(configObj.choices); configObj._data = configObj.choices; configObj._exp = 'choice[1] for choice in state._data'; configObj._format = 'selectFromOptions'; From 35bd98eb49807e82c2ac113b66b6cc08c4e11cde Mon Sep 17 00:00:00 2001 From: Marliana Lara Date: Thu, 26 Apr 2018 16:36:53 -0400 Subject: [PATCH 008/169] Fix job result detail and standard out panel styles --- awx/ui/client/features/output/_index.less | 169 ++----- .../features/output/details.partial.html | 429 +++++++++--------- awx/ui/client/features/output/index.view.html | 114 +++-- .../client/features/output/stats.partial.html | 21 +- 4 files changed, 315 insertions(+), 418 deletions(-) diff --git a/awx/ui/client/features/output/_index.less b/awx/ui/client/features/output/_index.less index 4238573fb3..0bb8a72aba 100644 --- a/awx/ui/client/features/output/_index.less +++ b/awx/ui/client/features/output/_index.less @@ -6,6 +6,7 @@ border-top-left-radius: 4px; border-top-right-radius: 4px; border-bottom: none; + margin-top: 15px; & > div { user-select: none; @@ -123,7 +124,7 @@ &-container { font-family: monospace; - height: calc(~"100vh - 240px"); + height: 100%; overflow-y: scroll; font-size: 15px; border: 1px solid @at-gray-b7; @@ -143,6 +144,11 @@ } } } + + &--fullscreen { + grid-column-start: 1; + grid-column-end: 3; + } } .at-mixin-event() { @@ -201,12 +207,12 @@ flex-wrap: wrap; } - // Status Bar ----------------------------------------------------------------------------- .HostStatusBar { display: flex; flex: 0 0 auto; width: 100%; + margin-bottom: 15px; } .HostStatusBar-ok, @@ -282,49 +288,28 @@ } +.HostStatusBar-tooltip.top { + margin-top: 4px; +} + // Job Details --------------------------------------------------------------------------------- @breakpoint-md: 1200px; -.JobResults { - .OnePlusTwo-container(100%, @breakpoint-md); +.JobResults-container { + display: grid; + grid-gap: 20px; + grid-template-columns: minmax(300px, 1fr) minmax(500px, 2fr); + grid-template-rows: minmax(500px, ~"calc(100vh - 140px)"); - &.fullscreen { - .JobResults-rightSide { - max-width: 100%; - } + .at-Panel { + overflow-y: scroll; } } -.JobResults-leftSide { - .OnePlusTwo-left--panel(100%, @breakpoint-md); - max-width: 30%; - height: ~"calc(100vh - 177px)"; - - @media screen and (max-width: @breakpoint-md) { - max-width: 100%; - } -} - -.JobResults-rightSide { - .OnePlusTwo-right--panel(100%, @breakpoint-md); - height: ~"calc(100vh - 177px)"; - - @media (max-width: @breakpoint-md - 1px) { - padding-right: 15px; - } -} - -.JobResults-detailsPanel{ - overflow-y: scroll; -} - -.JobResults-stdoutActionButton--active { - display: none; - visibility: hidden; - flex:none; - width:0px; - padding-right: 0px; +.JobResults-detailsPanel { + display: flex; + flex-direction: column; } .JobResults-panelHeader { @@ -352,8 +337,8 @@ flex-wrap: wrap; } -.JobResults-codeMirrorResultRowLabel{ - font-size: 12px; +.JobResults-resultRow #cm-variables-container { + width: 100%; } .JobResults-resultRowLabel { @@ -416,109 +401,10 @@ padding-right: 10px; } -.JobResults-badgeRow { - display: flex; - align-items: center; - margin-right: 5px; -} - -.JobResults-badgeTitle{ - color: @default-interface-txt; - font-size: 14px; - margin-right: 10px; - font-weight: normal; - text-transform: uppercase; - margin-left: 20px; -} - -@media (max-width: @breakpoint-md) { - .JobResults-detailsPanel { - overflow-y: auto; - } - - .JobResults-rightSide { - height: inherit; - } -} - -.JobResults-timeBadge { - float:right; - font-size: 11px; - font-weight: normal; - padding: 1px 10px; - height: 14px; - margin: 3px 15px; - width: 80px; - background-color: @default-bg; - border-radius: 5px; - color: @default-interface-txt; - margin-right: -5px; -} - -.JobResults-panelRight { - display: flex; - flex-direction: column; -} - -.JobResults-panelRight .SmartSearch-bar { - width: 100%; -} - -.JobResults-panelRightTitle{ - flex-wrap: wrap; -} - -.JobResults-panelRightTitleText{ - word-wrap: break-word; - word-break: break-all; - max-width: 100%; -} - -.JobResults-badgeAndActionRow{ - display:flex; - flex: 1 0 auto; - justify-content: flex-end; - flex-wrap: wrap; - max-width: 100%; -} - .StandardOut-panelHeader { flex: initial; } -.StandardOut-panelHeader--jobIsRunning { - margin-bottom: 20px; -} - -host-status-bar { - flex: initial; - margin-bottom: 20px; -} - -smart-search { - flex: initial; -} - -job-results-standard-out { - flex: 1; - flex-basis: auto; - height: ~"calc(100% - 800px)"; - display: flex; - border: 1px solid @d7grey; - border-radius: 5px; - margin-top: 20px; -} -@media screen and (max-width: @breakpoint-md) { - job-results-standard-out { - height: auto; - } -} - -.JobResults-extraVarsHelp { - margin-left: 10px; - color: @default-icon; -} - .JobResults-seeMoreLess { color: #337AB7; margin: 4px 0px; @@ -528,3 +414,10 @@ job-results-standard-out { border-radius: 5px; font-size: 11px; } + +@media screen and (max-width: @breakpoint-md) { + .JobResults-container { + display: flex; + flex-direction: column; + } +} \ No newline at end of file diff --git a/awx/ui/client/features/output/details.partial.html b/awx/ui/client/features/output/details.partial.html index d873c4fbb5..5eb4f5b146 100644 --- a/awx/ui/client/features/output/details.partial.html +++ b/awx/ui/client/features/output/details.partial.html @@ -40,248 +40,247 @@ -
- -
- -
- - {{ vm.status.value }} -
+ +
+ +
+ + {{ vm.status.value }}
+
- -
- -
- {{ vm.jobExplanation.less }} - ... - - Show More - -
-
- {{ vm.jobExplanation.more }} - - Show Less - -
+ +
+ +
+ {{ vm.jobExplanation.less }} + ... + + Show More +
- - -
- -
- {{ vm.started.value }} -
+
+ {{ vm.jobExplanation.more }} + + Show Less +
+
- -
- -
- {{ vm.finished.value }} -
+ +
+ +
+ {{ vm.started.value }}
+
- -
- -
{{ vm.moduleArgs.value }}
+ +
+ +
+ {{ vm.finished.value }}
+
- -
- -
- {{ vm.resultTraceback.less }} - ... - - Show More - -
-
- {{ vm.resultTraceback.more }} - - Show Less - -
+ +
+ +
{{ vm.moduleArgs.value }}
+
+ + +
+ +
+ {{ vm.resultTraceback.less }} + ... + + Show More +
- - -
- - +
+ {{ vm.resultTraceback.more }} + + Show Less +
+
- -
- -
{{ vm.jobType.value }}
+ +
+ + +
- -
- - -
+ +
+ +
{{ vm.jobType.value }}
+
+ + +
+ + +
- - -
- - +
+ {{ vm.launchedBy.value }}
+
- -
- - + +
+ + +
- -
- - + +
+ + +
- -
- -
{{ vm.playbook.value }}
+ +
+ + +
+ + +
+ +
{{ vm.playbook.value }}
+
+ + +
+ + +
- -
- - + +
+ +
{{ vm.forks.value }}
+
+ + +
+ +
{{ vm.limit.value }}
+
+ + +
+ +
{{ vm.verbosity.value }}
+
+ + +
+ +
+ {{ vm.instanceGroup.value }} + + {{ vm.instanceGroup.isolated }} +
+
- -
- -
{{ vm.forks.value }}
+ +
+ +
{{ vm.jobTags.value }}
+
+ + +
+ +
{{ vm.skipTags.value }}
+
+ + + + + + +
+ - - -
- -
{{ vm.limit.value }}
-
- - -
- -
{{ vm.verbosity.value }}
-
- - -
- -
- {{ vm.instanceGroup.value }} - - {{ vm.instanceGroup.isolated }} - -
-
- - -
- -
{{ vm.jobTags.value }}
-
- - -
- -
{{ vm.skipTags.value }}
-
- - - - - - -
- -
-
-
{{ label }}
-
+
+
+
{{ label }}
diff --git a/awx/ui/client/features/output/index.view.html b/awx/ui/client/features/output/index.view.html index b3bb6e95bc..f25171f9f2 100644 --- a/awx/ui/client/features/output/index.view.html +++ b/awx/ui/client/features/output/index.view.html @@ -1,64 +1,62 @@ -
-
-
- - - -
+
+
+ + + -
- -
{{ vm.title }}
- - - + +
+ {{ vm.title }} +
+ + + -
-
- -
- -
- -
-
- -
-
- -
-
- -
- -
+
+
+
-
-                
-                    
-                        
-                            
-                            
-                            
-                        
-                    
-                    
-                
 
-
- -
-
-

-

Back to Top

-
- -
+
+
- -
+
+ +
+
+ +
+
+ +
+ +
+
+ +
+            
+                
+                    
+                        
+                        
+                        
+                    
+                
+                
+            
 
+
+ +
+
+

+

Back to Top

+
+ +
+
+
diff --git a/awx/ui/client/features/output/stats.partial.html b/awx/ui/client/features/output/stats.partial.html index 70d980ed33..6427735236 100644 --- a/awx/ui/client/features/output/stats.partial.html +++ b/awx/ui/client/features/output/stats.partial.html @@ -42,40 +42,47 @@ ng-show="!vm.running" data-placement="top" aw-tool-tip="{{ vm.tooltips.ok }}" - data-tip-watch="vm.tooltips.ok"> + data-tip-watch="vm.tooltips.ok" + tooltip-outer-class="HostStatusBar-tooltip">
+ data-tip-watch="vm.tooltips.skipped" + tooltip-outer-class="HostStatusBar-tooltip">
+ data-tip-watch="vm.tooltips.changed" + tooltip-outer-class="HostStatusBar-tooltip">
+ data-tip-watch="vm.tooltips.failures" + tooltip-outer-class="HostStatusBar-tooltip">
+ data-tip-watch="vm.tooltips.dark" + tooltip-outer-class="HostStatusBar-tooltip">
+ aw-tool-tip="{{:: vm.tooltips.running }}" + tooltip-outer-class="HostStatusBar-tooltip">
+ aw-tool-tip="{{:: vm.tooltips.unavailable }}" + tooltip-outer-class="HostStatusBar-tooltip">
From 6ab64590d745a601b69f7907ecd02be416fa3e39 Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Fri, 27 Apr 2018 11:37:17 -0400 Subject: [PATCH 009/169] update app activity stream target to o_auth2_application --- awx/ui/client/features/applications/index.js | 9 ++++----- .../src/activity-stream/get-target-title.factory.js | 2 +- .../activity-stream/model-to-base-path-key.factory.js | 2 +- .../streamDropdownNav/stream-dropdown-nav.directive.js | 2 +- 4 files changed, 7 insertions(+), 8 deletions(-) diff --git a/awx/ui/client/features/applications/index.js b/awx/ui/client/features/applications/index.js index 448ea12f88..af4783884f 100644 --- a/awx/ui/client/features/applications/index.js +++ b/awx/ui/client/features/applications/index.js @@ -62,7 +62,7 @@ function ApplicationsRun ($stateExtender, strings) { }, data: { activityStream: true, - activityStreamTarget: 'application' + activityStreamTarget: 'o_auth2_application' }, views: { '@': { @@ -110,7 +110,7 @@ function ApplicationsRun ($stateExtender, strings) { }, data: { activityStream: true, - activityStreamTarget: 'application' + activityStreamTarget: 'o_auth2_application' }, views: { 'add@applications': { @@ -132,7 +132,7 @@ function ApplicationsRun ($stateExtender, strings) { }, data: { activityStream: true, - activityStreamTarget: 'application', + activityStreamTarget: 'o_auth2_application', activityStreamId: 'application_id' }, views: { @@ -262,8 +262,7 @@ function ApplicationsRun ($stateExtender, strings) { }, data: { activityStream: true, - // TODO: double-check activity stream works - activityStreamTarget: 'application' + activityStreamTarget: 'o_auth2_application' }, views: { 'userList@applications.edit': { diff --git a/awx/ui/client/src/activity-stream/get-target-title.factory.js b/awx/ui/client/src/activity-stream/get-target-title.factory.js index f782769c5c..88d274f58f 100644 --- a/awx/ui/client/src/activity-stream/get-target-title.factory.js +++ b/awx/ui/client/src/activity-stream/get-target-title.factory.js @@ -43,7 +43,7 @@ export default function GetTargetTitle(i18n) { case 'template': rtnTitle = i18n._('TEMPLATES'); break; - case 'application': + case 'o_auth2_application': rtnTitle = i18n._('APPLICATIONS'); break; } diff --git a/awx/ui/client/src/activity-stream/model-to-base-path-key.factory.js b/awx/ui/client/src/activity-stream/model-to-base-path-key.factory.js index f6a45e6bdd..2ce145c4dc 100644 --- a/awx/ui/client/src/activity-stream/model-to-base-path-key.factory.js +++ b/awx/ui/client/src/activity-stream/model-to-base-path-key.factory.js @@ -18,7 +18,7 @@ export default function ModelToBasePathKey() { var basePathKey; switch(model) { - case 'application': + case 'o_auth2_application': basePathKey = 'applications'; break; case 'project': diff --git a/awx/ui/client/src/activity-stream/streamDropdownNav/stream-dropdown-nav.directive.js b/awx/ui/client/src/activity-stream/streamDropdownNav/stream-dropdown-nav.directive.js index 601e65f1ac..e60170c310 100644 --- a/awx/ui/client/src/activity-stream/streamDropdownNav/stream-dropdown-nav.directive.js +++ b/awx/ui/client/src/activity-stream/streamDropdownNav/stream-dropdown-nav.directive.js @@ -21,7 +21,7 @@ export default ['templateUrl', 'i18n', function(templateUrl, i18n) { $scope.options = [ {label: i18n._('All Activity'), value: 'dashboard'}, - {label: i18n._('Applications'), value: 'application'}, + {label: i18n._('Applications'), value: 'o_auth2_application'}, {label: i18n._('Credentials'), value: 'credential'}, {label: i18n._('Hosts'), value: 'host'}, {label: i18n._('Inventories'), value: 'inventory'}, From c8e416f0b7dd84fd1f42fa9cad013c14897d2fe9 Mon Sep 17 00:00:00 2001 From: Ryan Petrello Date: Fri, 27 Apr 2018 11:40:58 -0400 Subject: [PATCH 010/169] restore the celery hostname --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 14564df430..e7df5b3d48 100644 --- a/Makefile +++ b/Makefile @@ -323,7 +323,7 @@ celeryd: @if [ "$(VENV_BASE)" ]; then \ . $(VENV_BASE)/awx/bin/activate; \ fi; \ - celery worker -A awx -l DEBUG -B -Ofair --autoscale=100,4 --schedule=$(CELERY_SCHEDULE_FILE) --pidfile /tmp/celery_pid + celery worker -A awx -l DEBUG -B -Ofair --autoscale=100,4 --schedule=$(CELERY_SCHEDULE_FILE) -n celery@$(COMPOSE_HOST) --pidfile /tmp/celery_pid # Run to start the zeromq callback receiver receiver: From 11560ab7bbdcd28b7337d04be9254d0f4256cedb Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Fri, 27 Apr 2018 13:05:34 -0400 Subject: [PATCH 011/169] populate org and description on app detail view --- .../add-applications.controller.js | 2 +- .../applications/applications.strings.js | 4 +++ .../edit-applications.controller.js | 32 ++++++------------- 3 files changed, 15 insertions(+), 23 deletions(-) diff --git a/awx/ui/client/features/applications/add-applications.controller.js b/awx/ui/client/features/applications/add-applications.controller.js index 23453734fe..486c20dffb 100644 --- a/awx/ui/client/features/applications/add-applications.controller.js +++ b/awx/ui/client/features/applications/add-applications.controller.js @@ -42,7 +42,7 @@ function AddApplicationsController (models, $state, strings) { vm.form.organization._resource = 'organization'; vm.form.organization._route = 'applications.add.organization'; vm.form.organization._model = organization; - vm.form.organization._placeholder = strings.get('SELECT AN ORGANIZATION'); + vm.form.organization._placeholder = strings.get('inputs.ORGANIZATION_PLACEHOLDER'); vm.form.name.required = true; vm.form.organization.required = true; diff --git a/awx/ui/client/features/applications/applications.strings.js b/awx/ui/client/features/applications/applications.strings.js index 18158c4d4e..0194efbf42 100644 --- a/awx/ui/client/features/applications/applications.strings.js +++ b/awx/ui/client/features/applications/applications.strings.js @@ -25,6 +25,10 @@ function ApplicationsStrings (BaseString) { ROW_ITEM_LABEL_ORGANIZATION: t.s('ORG'), ROW_ITEM_LABEL_MODIFIED: t.s('LAST MODIFIED') }; + + ns.inputs = { + ORGANIZATION_PLACEHOLDER: t.s('SELECT AN ORGANIZATION') + }; } ApplicationsStrings.$inject = ['BaseStringService']; diff --git a/awx/ui/client/features/applications/edit-applications.controller.js b/awx/ui/client/features/applications/edit-applications.controller.js index 6b7e21aed4..60b08be6ba 100644 --- a/awx/ui/client/features/applications/edit-applications.controller.js +++ b/awx/ui/client/features/applications/edit-applications.controller.js @@ -52,37 +52,25 @@ function EditApplicationsController (models, $state, strings, $scope) { vm.form.disabled = !isEditable; + vm.form.name.required = true; + vm.form.redirect_uris.required = true; + const isOrgAdmin = _.some(me.get('related.admin_of_organizations.results'), (org) => org.id === organization.get('id')); const isSuperuser = me.get('is_superuser'); const isCurrentAuthor = Boolean(application.get('summary_fields.created_by.id') === me.get('id')); - - vm.form.organization = { - type: 'field', - label: 'Organization', - id: 'organization' - }; - vm.form.description = { - type: 'String', - label: 'Description', - id: 'description' - }; - - vm.form.organization._resource = 'organization'; - vm.form.organization._route = 'applications.edit.organization'; - vm.form.organization._model = organization; - vm.form.organization._placeholder = strings.get('SELECT AN ORGANIZATION'); - - // TODO: org not returned via api endpoint, check on this - vm.form.organization._value = application.get('organization'); - vm.form.organization._disabled = true; + if (isSuperuser || isOrgAdmin || (application.get('organization') === null && isCurrentAuthor)) { vm.form.organization._disabled = false; } - vm.form.name.required = true; + vm.form.organization._resource = 'organization'; + vm.form.organization._model = organization; + vm.form.organization._route = 'applications.edit.organization'; + vm.form.organization._value = application.get('summary_fields.organization.id'); + vm.form.organization._displayValue = application.get('summary_fields.organization.name'); + vm.form.organization._placeholder = strings.get('inputs.ORGANIZATION_PLACEHOLDER'); vm.form.organization.required = true; - vm.form.redirect_uris.required = true; delete vm.form.name.help_text; From f5c4f9a9dffe27bd2ce4d209c4a265f2fe1e9922 Mon Sep 17 00:00:00 2001 From: Jared Tabor Date: Fri, 27 Apr 2018 11:15:20 -0700 Subject: [PATCH 012/169] Adds liveUpdates flag for the UI back in This line was lost in the 3.2.4 merge --- awx/ui/client/index.template.ejs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/awx/ui/client/index.template.ejs b/awx/ui/client/index.template.ejs index 58ee4d85a0..6f6a5c2cba 100644 --- a/awx/ui/client/index.template.ejs +++ b/awx/ui/client/index.template.ejs @@ -6,7 +6,10 @@ - + <% htmlWebpackPlugin.files.css.forEach(file => {%> <% }) %> From aa6fc6d8bfe7e5f45fa7721f3e74f70ff50bd220 Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Fri, 27 Apr 2018 15:38:12 -0400 Subject: [PATCH 013/169] only show close icon in templates panel header when applicable --- .../features/templates/index.controller.js | 4 ++-- .../client/features/templates/index.view.html | 24 +++++++++++++------ 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/awx/ui/client/features/templates/index.controller.js b/awx/ui/client/features/templates/index.controller.js index 5f305345a9..c859255070 100644 --- a/awx/ui/client/features/templates/index.controller.js +++ b/awx/ui/client/features/templates/index.controller.js @@ -11,9 +11,9 @@ function IndexTemplatesController ($scope, strings, dataset) { } IndexTemplatesController.$inject = [ - '$scope', + '$scope', 'TemplatesStrings', - 'Dataset', + 'Dataset' ]; export default IndexTemplatesController; diff --git a/awx/ui/client/features/templates/index.view.html b/awx/ui/client/features/templates/index.view.html index 6323fd8129..5f6ae703f6 100644 --- a/awx/ui/client/features/templates/index.view.html +++ b/awx/ui/client/features/templates/index.view.html @@ -1,10 +1,20 @@
- - - {{:: vm.strings.get('list.PANEL_TITLE') }} -
- {{ vm.count }} -
-
+ +
+ + {{:: vm.strings.get('list.PANEL_TITLE') }} +
+ {{ vm.count }} +
+
+
+
+ + {{:: vm.strings.get('list.PANEL_TITLE') }} +
+ {{ vm.count }} +
+
+
From 10fd65bea0668b68738663e58a0505840710329a Mon Sep 17 00:00:00 2001 From: Ben Thomasson Date: Mon, 30 Apr 2018 10:23:14 -0400 Subject: [PATCH 014/169] Fixes dragging devices out of the inventory toolbox Fixes a defect where devices were dropped onto the canvas if they were moved a small amount after being selected in the inventory toolbox. This fix checks that the mouse has cleared the boundary of the inventory toolbox before dropping it onto the canvas. --- awx/ui/client/src/network-ui/toolbox.fsm.js | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/awx/ui/client/src/network-ui/toolbox.fsm.js b/awx/ui/client/src/network-ui/toolbox.fsm.js index c8a80c6463..562c38b167 100644 --- a/awx/ui/client/src/network-ui/toolbox.fsm.js +++ b/awx/ui/client/src/network-ui/toolbox.fsm.js @@ -234,8 +234,19 @@ _Start.prototype.start.transitions = ['Ready']; _Move.prototype.onMouseUp = function (controller) { - controller.changeState(Dropping); + var i = 0; + var toolbox = controller.toolbox; + if (controller.scope.mouseX < controller.toolbox.width) { + for(i = 0; i < toolbox.items.length; i++) { + toolbox.items[i].selected = false; + } + toolbox.selected_item = null; + controller.changeState(Ready); + + } else { + controller.changeState(Dropping); + } }; _Move.prototype.onMouseUp.transitions = ['Dropping']; From 456bf5d04d3e647e892f0351f93e2cde6cc513e2 Mon Sep 17 00:00:00 2001 From: Ben Thomasson Date: Mon, 30 Apr 2018 10:40:30 -0400 Subject: [PATCH 015/169] Fixes scroll of devices over inventory toolbox title. Fixes a defect where the devices in the inventory toolbox would scroll over the toolbox title. This moves the title later in the rendering order and adds a background to the title. --- .../src/network-ui/inventory_toolbox.partial.svg | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/awx/ui/client/src/network-ui/inventory_toolbox.partial.svg b/awx/ui/client/src/network-ui/inventory_toolbox.partial.svg index 7709b30228..88d4c67cfe 100644 --- a/awx/ui/client/src/network-ui/inventory_toolbox.partial.svg +++ b/awx/ui/client/src/network-ui/inventory_toolbox.partial.svg @@ -6,11 +6,7 @@ ng-attr-width="{{toolbox.width}}" ng-attr-height="{{toolbox.height}}" rx=5> - - {{toolbox.name}} - + @@ -92,4 +88,10 @@ + + {{toolbox.name}} + From 24e363888a89babd3faf798d375ed73fc32d9eee Mon Sep 17 00:00:00 2001 From: mabashian Date: Mon, 30 Apr 2018 10:47:34 -0400 Subject: [PATCH 016/169] Fixed error showing vault password prompts on relaunch --- .../relaunchButton.component.js | 11 +++- awx/ui/client/lib/models/Job.js | 10 +++ .../src/templates/prompt/prompt.controller.js | 66 +++++++++---------- .../src/templates/prompt/prompt.partial.html | 2 +- .../src/templates/prompt/prompt.service.js | 4 +- .../prompt-credential.controller.js | 16 ++--- .../credential/prompt-credential.partial.html | 18 ++--- 7 files changed, 70 insertions(+), 57 deletions(-) diff --git a/awx/ui/client/lib/components/relaunchButton/relaunchButton.component.js b/awx/ui/client/lib/components/relaunchButton/relaunchButton.component.js index df97883dc4..aa29a2b700 100644 --- a/awx/ui/client/lib/components/relaunchButton/relaunchButton.component.js +++ b/awx/ui/client/lib/components/relaunchButton/relaunchButton.component.js @@ -33,11 +33,12 @@ function atRelaunchCtrl ( ) { const jobPromises = [ jobObj.request('get', vm.job.id), - jobTemplate.optionsLaunch(vm.job.unified_job_template) + jobTemplate.optionsLaunch(vm.job.unified_job_template), + jobObj.getCredentials(vm.job.id) ]; $q.all(jobPromises) - .then(([jobRes, launchOptions]) => { + .then(([jobRes, launchOptions, jobCreds]) => { const populatedJob = jobRes.data; const jobTypeChoices = _.get( launchOptions, @@ -68,7 +69,11 @@ function atRelaunchCtrl ( relaunchHostType: option ? (option.name).toLowerCase() : null, prompts: { credentials: { - value: populatedJob.summary_fields.credentials || [] + value: populatedJob.summary_fields.credentials ? + _.merge( + jobCreds.data.results, + populatedJob.summary_fields.credentials + ) : [] }, variables: { value: populatedJob.extra_vars diff --git a/awx/ui/client/lib/models/Job.js b/awx/ui/client/lib/models/Job.js index 7dd58482a5..1e466b0e6d 100644 --- a/awx/ui/client/lib/models/Job.js +++ b/awx/ui/client/lib/models/Job.js @@ -48,6 +48,15 @@ function getStats () { }); } +function getCredentials (id) { + const req = { + method: 'GET', + url: `${this.path}${id}/credentials/` + }; + + return $http(req); +} + function JobModel (method, resource, config) { BaseModel.call(this, 'jobs'); @@ -56,6 +65,7 @@ function JobModel (method, resource, config) { this.postRelaunch = postRelaunch.bind(this); this.getRelaunch = getRelaunch.bind(this); this.getStats = getStats.bind(this); + this.getCredentials = getCredentials.bind(this); return this.create(method, resource, config); } diff --git a/awx/ui/client/src/templates/prompt/prompt.controller.js b/awx/ui/client/src/templates/prompt/prompt.controller.js index 6ea5c61aad..362e1223fe 100644 --- a/awx/ui/client/src/templates/prompt/prompt.controller.js +++ b/awx/ui/client/src/templates/prompt/prompt.controller.js @@ -56,10 +56,12 @@ export default [ 'Rest', 'GetBasePath', 'ProcessErrors', 'CredentialTypeModel', .then( (response) => { vm.promptDataClone.prompts.credentials.credentialTypes = {}; vm.promptDataClone.prompts.credentials.credentialTypeOptions = []; + let machineCredTypeId = null; response.data.results.forEach((credentialTypeRow => { vm.promptDataClone.prompts.credentials.credentialTypes[credentialTypeRow.id] = credentialTypeRow.kind; if(credentialTypeRow.kind.match(/^(cloud|net|ssh|vault)$/)) { if(credentialTypeRow.kind === 'ssh') { + machineCredTypeId = credentialTypeRow.id; vm.promptDataClone.prompts.credentials.credentialKind = credentialTypeRow.id.toString(); } vm.promptDataClone.prompts.credentials.credentialTypeOptions.push({ @@ -72,40 +74,33 @@ export default [ 'Rest', 'GetBasePath', 'ProcessErrors', 'CredentialTypeModel', vm.promptDataClone.prompts.credentials.passwords = {}; if(vm.promptDataClone.launchConf.passwords_needed_to_start) { + let machineCredPassObj = null; vm.promptDataClone.launchConf.passwords_needed_to_start.forEach((passwordNeeded) => { - if (passwordNeeded === "ssh_password") { - vm.promptDataClone.prompts.credentials.value.forEach((defaultCredential) => { - defaultCredential.passwords_needed.forEach((neededPassword) => { - if (neededPassword === "ssh_password") { - vm.promptDataClone.prompts.credentials.passwords.ssh = { + if (passwordNeeded === "ssh_password" || + passwordNeeded === "become_password" || + passwordNeeded === "ssh_key_unlock" + ) { + if (!machineCredPassObj) { + vm.promptDataClone.prompts.credentials.value.forEach((defaultCredential) => { + if (defaultCredential.kind && defaultCredential.kind === "ssh") { + machineCredPassObj = { id: defaultCredential.id, name: defaultCredential.name }; + } else if (defaultCredential.passwords_needed) { + defaultCredential.passwords_needed.forEach((neededPassword) => { + if (neededPassword === passwordNeeded) { + machineCredPassObj = { + id: defaultCredential.id, + name: defaultCredential.name + }; + } + }); } }); - }); - } else if (passwordNeeded === "become_password") { - vm.promptDataClone.prompts.credentials.value.forEach((defaultCredential) => { - defaultCredential.passwords_needed.forEach((neededPassword) => { - if (neededPassword === "become_password") { - vm.promptDataClone.prompts.credentials.passwords.become = { - id: defaultCredential.id, - name: defaultCredential.name - }; - } - }); - }); - } else if (passwordNeeded === "ssh_key_unlock") { - vm.promptDataClone.prompts.credentials.value.forEach((defaultCredential) => { - defaultCredential.passwords_needed.forEach((neededPassword) => { - if (neededPassword === "ssh_key_unlock") { - vm.promptDataClone.prompts.credentials.passwords.ssh_key_unlock = { - id: defaultCredential.id, - name: defaultCredential.name - }; - } - }); - }); + } + + vm.promptDataClone.prompts.credentials.passwords[passwordNeeded] = angular.copy(machineCredPassObj); } else if (passwordNeeded.startsWith("vault_password")) { let vault_id = null; if (passwordNeeded.includes('.')) { @@ -119,12 +114,15 @@ export default [ 'Rest', 'GetBasePath', 'ProcessErrors', 'CredentialTypeModel', // Loop across the default credentials to find the name of the // credential that requires a password vm.promptDataClone.prompts.credentials.value.forEach((defaultCredential) => { - if (defaultCredential.vault_id === vault_id) { - vm.promptDataClone.prompts.credentials.passwords.vault.push({ - id: defaultCredential.id, - name: defaultCredential.name, - vault_id: defaultCredential.vault_id - }); + if (vm.promptDataClone.prompts.credentials.credentialTypes[defaultCredential.credential_type] === "vault") { + let defaultCredVaultId = defaultCredential.vault_id || _.get(defaultCredential, 'inputs.vault_id') || null; + if (defaultCredVaultId === vault_id) { + vm.promptDataClone.prompts.credentials.passwords.vault.push({ + id: defaultCredential.id, + name: defaultCredential.name, + vault_id: defaultCredVaultId + }); + } } }); } diff --git a/awx/ui/client/src/templates/prompt/prompt.partial.html b/awx/ui/client/src/templates/prompt/prompt.partial.html index a25bef7e0b..79b23eedd8 100644 --- a/awx/ui/client/src/templates/prompt/prompt.partial.html +++ b/awx/ui/client/src/templates/prompt/prompt.partial.html @@ -34,7 +34,7 @@ diff --git a/awx/ui/client/src/templates/prompt/prompt.service.js b/awx/ui/client/src/templates/prompt/prompt.service.js index 2044b7f440..f29e3ca856 100644 --- a/awx/ui/client/src/templates/prompt/prompt.service.js +++ b/awx/ui/client/src/templates/prompt/prompt.service.js @@ -171,7 +171,7 @@ function PromptService (Empty, $filter) { if (key === "ssh_key_unlock") { launchData.credential_passwords.ssh_key_unlock = val.value; } else if (key !== "vault") { - launchData.credential_passwords[`${key}_password`] = val.value; + launchData.credential_passwords[`${key}`] = val.value; } else { _.each(val, (vaultCred) => { launchData.credential_passwords[vaultCred.vault_id ? `${key}_password.${vaultCred.vault_id}` : `${key}_password`] = vaultCred.value; @@ -198,7 +198,7 @@ function PromptService (Empty, $filter) { if (key === "ssh_key_unlock") { launchData.credential_passwords.ssh_key_unlock = val.value; } else if (key !== "vault") { - launchData.credential_passwords[`${key}_password`] = val.value; + launchData.credential_passwords[`${key}`] = val.value; } else { _.each(val, (vaultCred) => { launchData.credential_passwords[vaultCred.vault_id ? `${key}_password.${vaultCred.vault_id}` : `${key}_password`] = vaultCred.value; diff --git a/awx/ui/client/src/templates/prompt/steps/credential/prompt-credential.controller.js b/awx/ui/client/src/templates/prompt/steps/credential/prompt-credential.controller.js index 9a5c8feec3..f42792a822 100644 --- a/awx/ui/client/src/templates/prompt/steps/credential/prompt-credential.controller.js +++ b/awx/ui/client/src/templates/prompt/steps/credential/prompt-credential.controller.js @@ -35,9 +35,9 @@ export default if(cred.passwords_needed) { cred.passwords_needed.forEach((passwordNeeded => { if(passwordNeeded === 'ssh_password') { - delete scope.promptData.prompts.credentials.passwords.ssh; + delete scope.promptData.prompts.credentials.passwords.ssh_password; } else if(passwordNeeded === 'become_password') { - delete scope.promptData.prompts.credentials.passwords.become; + delete scope.promptData.prompts.credentials.passwords.become_password; } else if(passwordNeeded === 'ssh_key_unlock') { delete scope.promptData.prompts.credentials.passwords.ssh_key_unlock; } else if(passwordNeeded.startsWith("vault_password")) { @@ -50,10 +50,10 @@ export default })); } else if(cred.inputs && !_.isEmpty(cred.inputs)) { if(cred.inputs.password && cred.inputs.password === "ASK") { - delete scope.promptData.prompts.credentials.passwords.ssh; + delete scope.promptData.prompts.credentials.passwords.ssh_password; } if(cred.inputs.become_password && cred.inputs.become_password === "ASK") { - delete scope.promptData.prompts.credentials.passwords.become; + delete scope.promptData.prompts.credentials.passwords.become_password; } if(cred.inputs.ssh_key_unlock && cred.inputs.ssh_key_unlock === "ASK") { delete scope.promptData.prompts.credentials.passwords.ssh_key_unlock; @@ -72,13 +72,13 @@ export default let updateNeededPasswords = (cred) => { if(cred.inputs) { if(cred.inputs.password && cred.inputs.password === "ASK") { - scope.promptData.prompts.credentials.passwords.ssh = { + scope.promptData.prompts.credentials.passwords.ssh_password = { id: cred.id, name: cred.name }; } if(cred.inputs.become_password && cred.inputs.become_password === "ASK") { - scope.promptData.prompts.credentials.passwords.become = { + scope.promptData.prompts.credentials.passwords.become_password = { id: cred.id, name: cred.name }; @@ -291,10 +291,10 @@ export default }; if(passwordNeeded === "ssh_password") { - scope.promptData.prompts.credentials.passwords.ssh = credPassObj; + scope.promptData.prompts.credentials.passwords.ssh_password = credPassObj; } if(passwordNeeded === "become_password") { - scope.promptData.prompts.credentials.passwords.become = credPassObj; + scope.promptData.prompts.credentials.passwords.become_password = credPassObj; } if(passwordNeeded === "ssh_key_unlock") { scope.promptData.prompts.credentials.passwords.ssh_key_unlock = credPassObj; diff --git a/awx/ui/client/src/templates/prompt/steps/credential/prompt-credential.partial.html b/awx/ui/client/src/templates/prompt/steps/credential/prompt-credential.partial.html index 214401b55f..7b1ca2b093 100644 --- a/awx/ui/client/src/templates/prompt/steps/credential/prompt-credential.partial.html +++ b/awx/ui/client/src/templates/prompt/steps/credential/prompt-credential.partial.html @@ -45,13 +45,13 @@
-
+
{{ vm.strings.get('prompt.CREDENTIAL_PASSWORD_WARNING')}}
-
{{promptData.prompts.credentials.passwords.ssh.name || promptData.prompts.credentials.passwords.become.name || promptData.prompts.credentials.passwords.ssh_key_unlock.name}}
+
{{promptData.prompts.credentials.passwords.ssh_password.name || promptData.prompts.credentials.passwords.become_password.name || promptData.prompts.credentials.passwords.ssh_key_unlock.name}}
{{vaultCred.name}}
@@ -65,10 +65,10 @@
-
+
{{:: vm.strings.get('prompt.PASSWORDS_REQUIRED_HELP') }}
-
+
{{:: vm.strings.get('prompt.PLEASE_ENTER_PASSWORD') }}
@@ -96,7 +96,7 @@
{{:: vm.strings.get('prompt.PLEASE_ENTER_PASSWORD') }}
-
+
{{:: vm.strings.get('prompt.PLEASE_ENTER_PASSWORD') }}
@@ -118,9 +118,9 @@
- + - +
{{:: vm.strings.get('prompt.PLEASE_ENTER_PASSWORD') }}
From fd38c62e7e4288b640723861f1335f5f68085590 Mon Sep 17 00:00:00 2001 From: Ben Thomasson Date: Mon, 30 Apr 2018 10:59:54 -0400 Subject: [PATCH 017/169] Fixes 301 redirects on topology websocket connection Fixes 301 redirects on creation of the topology websocket connection by adding a trailing slash to the websocket URI. --- awx/ui/client/src/network-ui/network.ui.controller.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/ui/client/src/network-ui/network.ui.controller.js b/awx/ui/client/src/network-ui/network.ui.controller.js index a0875b0d64..a261b326bb 100644 --- a/awx/ui/client/src/network-ui/network.ui.controller.js +++ b/awx/ui/client/src/network-ui/network.ui.controller.js @@ -52,7 +52,7 @@ var NetworkUIController = function($scope, $scope.initial_messages = []; if (!$scope.disconnected) { - $scope.control_socket = new ReconnectingWebSocket(protocol + "://" + window.location.host + "/network_ui/topology?inventory_id=" + $scope.inventory_id, + $scope.control_socket = new ReconnectingWebSocket(protocol + "://" + window.location.host + "/network_ui/topology/?inventory_id=" + $scope.inventory_id, null, {debug: false, reconnectInterval: 300}); if ($scope.tests_enabled) { From fcc5549ec999b1f718abe507df34e6c8f518de37 Mon Sep 17 00:00:00 2001 From: mabashian Date: Mon, 30 Apr 2018 11:41:26 -0400 Subject: [PATCH 018/169] Reload state after copying inventory to update list --- .../inventories/list/inventory-list.controller.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/awx/ui/client/src/inventories-hosts/inventories/list/inventory-list.controller.js b/awx/ui/client/src/inventories-hosts/inventories/list/inventory-list.controller.js index 9f3ba01557..9fc16afdd0 100644 --- a/awx/ui/client/src/inventories-hosts/inventories/list/inventory-list.controller.js +++ b/awx/ui/client/src/inventories-hosts/inventories/list/inventory-list.controller.js @@ -77,7 +77,7 @@ function InventoriesList($scope, Wait('start'); new Inventory('get', inventory.id) .then(model => model.copy()) - .then(copy => $scope.editInventory(copy)) + .then(copy => $scope.editInventory(copy, true)) .catch(({ data, status }) => { const params = { hdr: 'Error!', msg: `Call to copy failed. Return status: ${status}` }; ProcessErrors($scope, data, status, null, params); @@ -89,12 +89,13 @@ function InventoriesList($scope, $state.go('inventories.edit.networking', {inventory_id: inventory.id, inventory_name: inventory.name}); }; - $scope.editInventory = function (inventory) { + $scope.editInventory = function (inventory, reload) { + const goOptions = reload ? { reload: true } : null; if(inventory.kind && inventory.kind === 'smart') { - $state.go('inventories.editSmartInventory', {smartinventory_id: inventory.id}); + $state.go('inventories.editSmartInventory', {smartinventory_id: inventory.id}, goOptions); } else { - $state.go('inventories.edit', {inventory_id: inventory.id}); + $state.go('inventories.edit', {inventory_id: inventory.id}, goOptions); } }; From 4197a9fd35f64db4d249feabfe2c930481c97c7a Mon Sep 17 00:00:00 2001 From: adamscmRH Date: Fri, 27 Apr 2018 16:39:37 -0400 Subject: [PATCH 019/169] granularly prevent filtering oauth secrets --- awx/api/filters.py | 2 -- awx/main/models/__init__.py | 6 ++++++ awx/main/tests/unit/api/test_filters.py | 6 +++--- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/awx/api/filters.py b/awx/api/filters.py index c7425f5e75..81290c377b 100644 --- a/awx/api/filters.py +++ b/awx/api/filters.py @@ -121,8 +121,6 @@ def get_field_from_path(model, path): new_parts.append(name_alt) else: field = model._meta.get_field(name) - if 'auth' in name or 'token' in name: - raise PermissionDenied(_('Filtering on %s is not allowed.' % name)) if isinstance(field, ForeignObjectRel) and getattr(field.field, '__prevent_search__', False): raise PermissionDenied(_('Filtering on %s is not allowed.' % name)) elif getattr(field, '__prevent_search__', False): diff --git a/awx/main/models/__init__.py b/awx/main/models/__init__.py index 0bbbc08254..7764655419 100644 --- a/awx/main/models/__init__.py +++ b/awx/main/models/__init__.py @@ -169,3 +169,9 @@ activity_stream_registrar.connect(OAuth2AccessToken) # prevent API filtering on certain Django-supplied sensitive fields prevent_search(User._meta.get_field('password')) +prevent_search(OAuth2AccessToken._meta.get_field('token')) +prevent_search(RefreshToken._meta.get_field('token')) +prevent_search(OAuth2Application._meta.get_field('client_secret')) +prevent_search(OAuth2Application._meta.get_field('client_id')) +prevent_search(Grant._meta.get_field('code')) + diff --git a/awx/main/tests/unit/api/test_filters.py b/awx/main/tests/unit/api/test_filters.py index 1a70b9716c..cc53234e97 100644 --- a/awx/main/tests/unit/api/test_filters.py +++ b/awx/main/tests/unit/api/test_filters.py @@ -10,6 +10,7 @@ from awx.main.models import (AdHocCommand, ActivityStream, WorkflowJob, WorkflowJobTemplate, WorkflowJobOptions, InventorySource, JobEvent) +from awx.main.models.oauth import OAuth2Application from awx.main.models.jobs import JobOptions # Django @@ -82,7 +83,6 @@ def test_filter_on_password_field(password_field, lookup_suffix): (User, 'password__icontains'), (User, 'settings__value__icontains'), (User, 'main_oauth2accesstoken__token__gt'), - (User, 'main_oauth2application__name__gt'), (UnifiedJob, 'job_args__icontains'), (UnifiedJob, 'job_env__icontains'), (UnifiedJob, 'start_args__icontains'), @@ -95,8 +95,8 @@ def test_filter_on_password_field(password_field, lookup_suffix): (JobTemplate, 'survey_spec__icontains'), (WorkflowJobTemplate, 'survey_spec__icontains'), (CustomInventoryScript, 'script__icontains'), - (ActivityStream, 'o_auth2_access_token__gt'), - (ActivityStream, 'o_auth2_application__gt') + (ActivityStream, 'o_auth2_application__client_secret__gt'), + (OAuth2Application, 'grant__code__gt') ]) def test_filter_sensitive_fields_and_relations(model, query): field_lookup = FieldLookupBackend() From f8a9402d4b3d2bd36b5486ea5af15a369395d5ed Mon Sep 17 00:00:00 2001 From: mabashian Date: Mon, 30 Apr 2018 11:55:54 -0400 Subject: [PATCH 020/169] Fixed team list org sorting --- awx/ui/client/src/teams/list/teams-list.controller.js | 3 --- awx/ui/client/src/teams/teams.list.js | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/awx/ui/client/src/teams/list/teams-list.controller.js b/awx/ui/client/src/teams/list/teams-list.controller.js index 254629d30b..024f74361b 100644 --- a/awx/ui/client/src/teams/list/teams-list.controller.js +++ b/awx/ui/client/src/teams/list/teams-list.controller.js @@ -28,9 +28,6 @@ export default ['$scope', 'Rest', 'TeamList', 'Prompt', $scope.list = list; $scope[`${list.iterator}_dataset`] = Dataset.data; $scope[list.name] = $scope[`${list.iterator}_dataset`].results; - _.forEach($scope[list.name], (team) => { - team.organization_name = team.summary_fields.organization.name; - }); $scope.selected = []; } diff --git a/awx/ui/client/src/teams/teams.list.js b/awx/ui/client/src/teams/teams.list.js index 1af52a8084..e1d9c5af35 100644 --- a/awx/ui/client/src/teams/teams.list.js +++ b/awx/ui/client/src/teams/teams.list.js @@ -28,7 +28,7 @@ export default ['i18n', function(i18n) { }, organization: { label: i18n._('Organization'), - ngBind: 'team.organization_name', + ngBind: 'team.summary_fields.organization.name', sourceModel: 'organization', sourceField: 'name', columnClass: 'col-md-3 hidden-sm hidden-xs', From e45be8155ea9664416ea120a14d89137670d1658 Mon Sep 17 00:00:00 2001 From: mabashian Date: Mon, 30 Apr 2018 14:56:17 -0400 Subject: [PATCH 021/169] Clear invalid password error if password field is completely cleared --- awx/ui/client/src/users/add/users-add.controller.js | 7 ++++--- awx/ui/client/src/users/edit/users-edit.controller.js | 7 ++++--- awx/ui/client/src/users/users.form.js | 2 +- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/awx/ui/client/src/users/add/users-add.controller.js b/awx/ui/client/src/users/add/users-add.controller.js index 081ae21a90..c822b7bf9d 100644 --- a/awx/ui/client/src/users/add/users-add.controller.js +++ b/awx/ui/client/src/users/add/users-add.controller.js @@ -109,10 +109,11 @@ export default ['$scope', '$rootScope', 'UserForm', 'GenerateForm', 'Rest', }; // Password change - $scope.clearPWConfirm = function(fld) { + $scope.clearPWConfirm = function() { // If password value changes, make sure password_confirm must be re-entered - $scope[fld] = ''; - $scope[form.name + '_form'][fld].$setValidity('awpassmatch', false); + $scope.password_confirm = ''; + let passValidity = (!$scope.password || $scope.password === '') ? true : false; + $scope[form.name + '_form'].password_confirm.$setValidity('awpassmatch', passValidity); }; } ]; diff --git a/awx/ui/client/src/users/edit/users-edit.controller.js b/awx/ui/client/src/users/edit/users-edit.controller.js index 82ae48af1d..f2294a1703 100644 --- a/awx/ui/client/src/users/edit/users-edit.controller.js +++ b/awx/ui/client/src/users/edit/users-edit.controller.js @@ -189,10 +189,11 @@ export default ['$scope', '$rootScope', '$stateParams', 'UserForm', 'Rest', } }; - $scope.clearPWConfirm = function(fld) { + $scope.clearPWConfirm = function() { // If password value changes, make sure password_confirm must be re-entered - $scope[fld] = ''; - $scope[form.name + '_form'][fld].$setValidity('awpassmatch', false); + $scope.password_confirm = ''; + let passValidity = (!$scope.password || $scope.password === '') ? true : false; + $scope[form.name + '_form'].password_confirm.$setValidity('awpassmatch', passValidity); $rootScope.flashMessage = null; }; } diff --git a/awx/ui/client/src/users/users.form.js b/awx/ui/client/src/users/users.form.js index 93a1264edd..900e1995ef 100644 --- a/awx/ui/client/src/users/users.form.js +++ b/awx/ui/client/src/users/users.form.js @@ -76,7 +76,7 @@ export default ['i18n', function(i18n) { reqExpression: "isAddForm", init: false }, - ngChange: "clearPWConfirm('password_confirm')", + ngChange: "clearPWConfirm()", autocomplete: false, ngDisabled: '!(user_obj.summary_fields.user_capabilities.edit || canAdd)' }, From 421293c8c5bf6ca15233f766f89a4dd430bb932b Mon Sep 17 00:00:00 2001 From: Jared Tabor Date: Mon, 30 Apr 2018 15:11:34 -0700 Subject: [PATCH 022/169] fixes error handling with groups service, sources service --- .../inventories/related/groups/add/groups-add.controller.js | 4 ++-- .../nested-groups/group-nested-groups-add.controller.js | 4 ++-- .../inventories/related/sources/sources.service.js | 6 +++--- .../client/src/inventories-hosts/shared/groups.service.js | 6 +++--- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/awx/ui/client/src/inventories-hosts/inventories/related/groups/add/groups-add.controller.js b/awx/ui/client/src/inventories-hosts/inventories/related/groups/add/groups-add.controller.js index 9ede17bcf5..80a1d917b7 100644 --- a/awx/ui/client/src/inventories-hosts/inventories/related/groups/add/groups-add.controller.js +++ b/awx/ui/client/src/inventories-hosts/inventories/related/groups/add/groups-add.controller.js @@ -49,10 +49,10 @@ export default ['$state', '$stateParams', '$scope', 'GroupForm', }; GroupsService.post(group).then(res => { - if ($stateParams.group_id) { + if ($stateParams.group_id && _.has(res, 'data')) { return GroupsService.associateGroup(res.data, $stateParams.group_id) .then(() => $state.go('^', null, { reload: true })); - } else { + } else if(_.has(res, 'data.id')){ $state.go('^.edit', { group_id: res.data.id }, { reload: true }); } }); diff --git a/awx/ui/client/src/inventories-hosts/inventories/related/groups/related/nested-groups/group-nested-groups-add.controller.js b/awx/ui/client/src/inventories-hosts/inventories/related/groups/related/nested-groups/group-nested-groups-add.controller.js index de61ced6b3..25633199bd 100644 --- a/awx/ui/client/src/inventories-hosts/inventories/related/groups/related/nested-groups/group-nested-groups-add.controller.js +++ b/awx/ui/client/src/inventories-hosts/inventories/related/groups/related/nested-groups/group-nested-groups-add.controller.js @@ -45,10 +45,10 @@ export default ['$state', '$stateParams', '$scope', 'NestedGroupForm', }; GroupsService.post(group).then(res => { - if ($stateParams.group_id) { + if ($stateParams.group_id && _.has(res, 'data')) { return GroupsService.associateGroup(res.data, $stateParams.group_id) .then(() => $state.go('^', null, { reload: true })); - } else { + } else if(_.has(res, 'data.id')){ $state.go('^.edit', { group_id: res.data.id }, { reload: true }); } }); diff --git a/awx/ui/client/src/inventories-hosts/inventories/related/sources/sources.service.js b/awx/ui/client/src/inventories-hosts/inventories/related/sources/sources.service.js index 8ac9b39248..ce65a7dae5 100644 --- a/awx/ui/client/src/inventories-hosts/inventories/related/sources/sources.service.js +++ b/awx/ui/client/src/inventories-hosts/inventories/related/sources/sources.service.js @@ -10,9 +10,9 @@ export default url: function(){ return ''; }, - error: function(data, status) { - ProcessErrors($rootScope, data, status, null, { hdr: 'Error!', - msg: 'Call to ' + this.url + '. GET returned: ' + status }); + error: function(data) { + ProcessErrors($rootScope, data.data, data.status, null, { hdr: 'Error!', + msg: 'Call to ' + this.url + '. GET returned: ' + data.status }); }, success: function(data){ return data; diff --git a/awx/ui/client/src/inventories-hosts/shared/groups.service.js b/awx/ui/client/src/inventories-hosts/shared/groups.service.js index caf31eca2f..0c6e1f8bef 100644 --- a/awx/ui/client/src/inventories-hosts/shared/groups.service.js +++ b/awx/ui/client/src/inventories-hosts/shared/groups.service.js @@ -10,9 +10,9 @@ export default url: function(){ return ''; }, - error: function(data, status) { - ProcessErrors($rootScope, data, status, null, { hdr: 'Error!', - msg: 'Call to ' + this.url + '. GET returned: ' + status }); + error: function(data) { + ProcessErrors($rootScope, data.data, data.status, null, { hdr: 'Error!', + msg: 'Call to ' + this.url + '. GET returned: ' + data.status }); }, success: function(data){ return data; From 5ef297aec17cca3e964ead2a60a954a1416ac1ec Mon Sep 17 00:00:00 2001 From: mabashian Date: Tue, 1 May 2018 12:13:16 -0400 Subject: [PATCH 023/169] Update relaunch button on details page in real time as status changes --- .../features/output/details.directive.js | 8 +++- .../relaunchButton.component.js | 38 +++++++++---------- .../relaunchButton.partial.html | 5 ++- 3 files changed, 28 insertions(+), 23 deletions(-) diff --git a/awx/ui/client/features/output/details.directive.js b/awx/ui/client/features/output/details.directive.js index b19475f70a..d4e590e1c3 100644 --- a/awx/ui/client/features/output/details.directive.js +++ b/awx/ui/client/features/output/details.directive.js @@ -556,7 +556,7 @@ function AtJobDetailsController ( vm.labels = getLabelDetails(); // Relaunch and Delete Components - vm.job = _.get(resource.model, 'model.GET', {}); + vm.job = angular.copy(_.get(resource.model, 'model.GET', {})); vm.canDelete = resource.model.get('summary_fields.user_capabilities.delete'); vm.cancelJob = cancelJob; @@ -568,10 +568,14 @@ function AtJobDetailsController ( }; observe(status.getStarted, getStartDetails, 'started'); - observe(status.getJobStatus, getStatusDetails, 'status'); observe(status.getFinished, getFinishDetails, 'finished'); observe(status.getProjectUpdateId, getProjectUpdateDetails, 'projectUpdate'); observe(status.getProjectStatus, getProjectStatusDetails, 'projectStatus'); + + $scope.$watch(status.getJobStatus, jobStatus => { + vm.status = getStatusDetails(jobStatus); + vm.job.status = jobStatus; + }); }; } diff --git a/awx/ui/client/lib/components/relaunchButton/relaunchButton.component.js b/awx/ui/client/lib/components/relaunchButton/relaunchButton.component.js index df97883dc4..1f258a94ba 100644 --- a/awx/ui/client/lib/components/relaunchButton/relaunchButton.component.js +++ b/awx/ui/client/lib/components/relaunchButton/relaunchButton.component.js @@ -23,6 +23,14 @@ function atRelaunchCtrl ( const jobObj = new Job(); const jobTemplate = new JobTemplate(); + const updateTooltip = () => { + if (vm.job.type === 'job' && vm.job.status === 'failed') { + vm.tooltip = strings.get('relaunch.HOSTS'); + } else { + vm.tooltip = strings.get('relaunch.DEFAULT'); + } + }; + const checkRelaunchPlaybook = (option) => { jobObj.getRelaunch({ id: vm.job.id @@ -113,7 +121,7 @@ function atRelaunchCtrl ( jobObj.postRelaunch(launchParams) .then((launchRes) => { - if (!$state.includes('jobs')) { + if (!$state.is('jobs')) { const relaunchType = launchRes.data.type === 'job' ? 'playbook' : launchRes.data.type; $state.go('jobz', { id: launchRes.data.id, type: relaunchType }, { reload: true }); } @@ -129,13 +137,7 @@ function atRelaunchCtrl ( vm.$onInit = () => { vm.showRelaunch = vm.job.type !== 'system_job' && vm.job.summary_fields.user_capabilities.start; - vm.showDropdown = vm.job.type === 'job' && vm.job.status === 'failed'; - vm.createDropdown(); - vm.createTooltips(); - }; - - vm.createDropdown = () => { vm.icon = 'icon-launch'; vm.dropdownTitle = strings.get('relaunch.DROPDOWN_TITLE'); vm.dropdownOptions = [ @@ -148,14 +150,12 @@ function atRelaunchCtrl ( icon: 'icon-host-failed' } ]; - }; - vm.createTooltips = () => { - if (vm.showDropdown) { - vm.tooltip = strings.get('relaunch.HOSTS'); - } else { - vm.tooltip = strings.get('relaunch.DEFAULT'); - } + updateTooltip(); + + $scope.$watch('vm.job.status', () => { + updateTooltip(); + }); }; vm.relaunchJob = () => { @@ -167,7 +167,7 @@ function atRelaunchCtrl ( if (getUpdateRes.data.can_update) { inventorySource.postUpdate(vm.job.inventory_source) .then((postUpdateRes) => { - if (!$state.includes('jobs')) { + if (!$state.is('jobs')) { $state.go('jobz', { id: postUpdateRes.data.id, type: 'inventory' }, { reload: true }); } }).catch(({ data, status, config }) => { @@ -191,7 +191,7 @@ function atRelaunchCtrl ( if (getUpdateRes.data.can_update) { project.postUpdate(vm.job.project) .then((postUpdateRes) => { - if (!$state.includes('jobs')) { + if (!$state.is('jobs')) { $state.go('jobz', { id: postUpdateRes.data.id, type: 'project' }, { reload: true }); } }).catch(({ data, status, config }) => { @@ -213,7 +213,7 @@ function atRelaunchCtrl ( workflowJob.postRelaunch({ id: vm.job.id }).then((launchRes) => { - if (!$state.includes('jobs')) { + if (!$state.is('jobs')) { $state.go('workflowResults', { id: launchRes.data.id }, { reload: true }); } }).catch(({ data, status, config }) => { @@ -237,7 +237,7 @@ function atRelaunchCtrl ( adHocCommand.postRelaunch({ id: vm.job.id }).then((launchRes) => { - if (!$state.includes('jobs')) { + if (!$state.is('jobs')) { $state.go('jobz', { id: launchRes.data.id, type: 'command' }, { reload: true }); } }).catch(({ data, status, config }) => { @@ -262,7 +262,7 @@ function atRelaunchCtrl ( id: vm.promptData.job, relaunchData: PromptService.bundlePromptDataForRelaunch(vm.promptData) }).then((launchRes) => { - if (!$state.includes('jobs')) { + if (!$state.is('jobs')) { $state.go('jobz', { id: launchRes.data.job, type: 'playbook' }, { reload: true }); } }).catch(({ data, status }) => { diff --git a/awx/ui/client/lib/components/relaunchButton/relaunchButton.partial.html b/awx/ui/client/lib/components/relaunchButton/relaunchButton.partial.html index 280380dcbc..22f7120152 100644 --- a/awx/ui/client/lib/components/relaunchButton/relaunchButton.partial.html +++ b/awx/ui/client/lib/components/relaunchButton/relaunchButton.partial.html @@ -1,9 +1,10 @@
-
+
From 648d9165ff7491bc03a5ba023d27b82a74ac16d7 Mon Sep 17 00:00:00 2001 From: chris meyers Date: Mon, 30 Apr 2018 15:18:05 -0400 Subject: [PATCH 024/169] broadcast queues get a per-node stable queue name * Using Kombu's default Broadcast() constructor requires only 1 parameter. That parameter defines the exchange name and the queue name is randomly generated per-node. * This caused problems if/when celery enters an infinite restart loop because too many rabbit queues get created and rabbit OOM's (gracefully). * To remedy this we tell Broadcast the queue name to use, which is derived from some constant + the node name so that the per-node queue name is stable. --- awx/main/tasks.py | 4 ++-- awx/main/tests/unit/utils/test_ha.py | 29 ++++++++++++++++------------ awx/main/utils/ha.py | 28 +++++++++++++++++++-------- awx/settings/defaults.py | 6 +----- 4 files changed, 40 insertions(+), 27 deletions(-) diff --git a/awx/main/tasks.py b/awx/main/tasks.py index 31edcc38d7..ea3c187d05 100644 --- a/awx/main/tasks.py +++ b/awx/main/tasks.py @@ -189,7 +189,7 @@ def apply_cluster_membership_policies(self): handle_ha_toplogy_changes.apply([]) -@shared_task(queue='tower_broadcast_all', bind=True) +@shared_task(exchange='tower_broadcast_all', bind=True) def handle_setting_changes(self, setting_keys): orig_len = len(setting_keys) for i in range(orig_len): @@ -208,7 +208,7 @@ def handle_setting_changes(self, setting_keys): restart_local_services(['uwsgi']) -@shared_task(bind=True, queue='tower_broadcast_all') +@shared_task(bind=True, exchange='tower_broadcast_all') def handle_ha_toplogy_changes(self): (changed, instance) = Instance.objects.get_or_register() if changed: diff --git a/awx/main/tests/unit/utils/test_ha.py b/awx/main/tests/unit/utils/test_ha.py index edd44b7958..6432be7a32 100644 --- a/awx/main/tests/unit/utils/test_ha.py +++ b/awx/main/tests/unit/utils/test_ha.py @@ -6,6 +6,7 @@ # python import pytest import mock +from contextlib import nested # AWX from awx.main.utils.ha import ( @@ -47,22 +48,26 @@ class TestAddRemoveCeleryWorkerQueues(): app.control.cancel_consumer = mocker.MagicMock() return app - @pytest.mark.parametrize("static_queues,_worker_queues,groups,hostname,added_expected,removed_expected", [ - (['east', 'west'], ['east', 'west', 'east-1'], [], 'east-1', [], []), - ([], ['east', 'west', 'east-1'], ['east', 'west'], 'east-1', [], []), - ([], ['east', 'west'], ['east', 'west'], 'east-1', ['east-1'], []), - ([], [], ['east', 'west'], 'east-1', ['east', 'west', 'east-1'], []), - ([], ['china', 'russia'], ['east', 'west'], 'east-1', ['east', 'west', 'east-1'], ['china', 'russia']), + @pytest.mark.parametrize("broadcast_queues,static_queues,_worker_queues,groups,hostname,added_expected,removed_expected", [ + (['tower_broadcast_all'], ['east', 'west'], ['east', 'west', 'east-1'], [], 'east-1', ['tower_broadcast_all_east-1'], []), + ([], [], ['east', 'west', 'east-1'], ['east', 'west'], 'east-1', [], []), + ([], [], ['east', 'west'], ['east', 'west'], 'east-1', ['east-1'], []), + ([], [], [], ['east', 'west'], 'east-1', ['east', 'west', 'east-1'], []), + ([], [], ['china', 'russia'], ['east', 'west'], 'east-1', ['east', 'west', 'east-1'], ['china', 'russia']), ]) def test__add_remove_celery_worker_queues_noop(self, mock_app, - instance_generator, - worker_queues_generator, - static_queues, _worker_queues, + instance_generator, + worker_queues_generator, + broadcast_queues, + static_queues, _worker_queues, groups, hostname, added_expected, removed_expected): instance = instance_generator(groups=groups, hostname=hostname) worker_queues = worker_queues_generator(_worker_queues) - with mock.patch('awx.main.utils.ha.settings.AWX_CELERY_QUEUES_STATIC', static_queues): + with nested( + mock.patch('awx.main.utils.ha.settings.AWX_CELERY_QUEUES_STATIC', static_queues), + mock.patch('awx.main.utils.ha.settings.AWX_CELERY_BCAST_QUEUES_STATIC', broadcast_queues), + mock.patch('awx.main.utils.ha.settings.CLUSTER_HOST_ID', hostname)): (added_queues, removed_queues) = _add_remove_celery_worker_queues(mock_app, [instance], worker_queues, hostname) assert set(added_queues) == set(added_expected) assert set(removed_queues) == set(removed_expected) @@ -71,11 +76,11 @@ class TestAddRemoveCeleryWorkerQueues(): class TestUpdateCeleryWorkerRoutes(): @pytest.mark.parametrize("is_controller,expected_routes", [ - (False, { + (False, { 'awx.main.tasks.cluster_node_heartbeat': {'queue': 'east-1', 'routing_key': 'east-1'}, 'awx.main.tasks.purge_old_stdout_files': {'queue': 'east-1', 'routing_key': 'east-1'} }), - (True, { + (True, { 'awx.main.tasks.cluster_node_heartbeat': {'queue': 'east-1', 'routing_key': 'east-1'}, 'awx.main.tasks.purge_old_stdout_files': {'queue': 'east-1', 'routing_key': 'east-1'}, 'awx.main.tasks.awx_isolated_heartbeat': {'queue': 'east-1', 'routing_key': 'east-1'}, diff --git a/awx/main/utils/ha.py b/awx/main/utils/ha.py index 93a7f8dd24..1e3bee15fd 100644 --- a/awx/main/utils/ha.py +++ b/awx/main/utils/ha.py @@ -10,6 +10,10 @@ from django.conf import settings from awx.main.models import Instance +def construct_bcast_queue_name(common_name): + return common_name.encode('utf8') + '_' + settings.CLUSTER_HOST_ID + + def _add_remove_celery_worker_queues(app, controlled_instances, worker_queues, worker_name): removed_queues = [] added_queues = [] @@ -19,17 +23,14 @@ def _add_remove_celery_worker_queues(app, controlled_instances, worker_queues, w ig_names.update(instance.rampart_groups.values_list('name', flat=True)) worker_queue_names = set([q['name'] for q in worker_queues]) + bcast_queue_names = set([construct_bcast_queue_name(n) for n in settings.AWX_CELERY_BCAST_QUEUES_STATIC]) all_queue_names = ig_names | hostnames | set(settings.AWX_CELERY_QUEUES_STATIC) # Remove queues that aren't in the instance group - for queue in worker_queues: - if queue['name'] in settings.AWX_CELERY_QUEUES_STATIC or \ - queue['alias'] in settings.AWX_CELERY_BCAST_QUEUES_STATIC: - continue - - if queue['name'] not in all_queue_names or not instance.enabled: - app.control.cancel_consumer(queue['name'].encode("utf8"), reply=True, destination=[worker_name]) - removed_queues.append(queue['name'].encode("utf8")) + for queue_name in worker_queue_names: + if queue_name not in all_queue_names | bcast_queue_names or not instance.enabled: + app.control.cancel_consumer(queue_name.encode("utf8"), reply=True, destination=[worker_name]) + removed_queues.append(queue_name.encode("utf8")) # Add queues for instance and instance groups for queue_name in all_queue_names: @@ -37,6 +38,17 @@ def _add_remove_celery_worker_queues(app, controlled_instances, worker_queues, w app.control.add_consumer(queue_name.encode("utf8"), reply=True, destination=[worker_name]) added_queues.append(queue_name.encode("utf8")) + # Add stable-named broadcast queues + for queue_name in settings.AWX_CELERY_BCAST_QUEUES_STATIC: + bcast_queue_name = construct_bcast_queue_name(queue_name) + if bcast_queue_name not in worker_queue_names: + app.control.add_consumer(bcast_queue_name, + exchange=queue_name.encode("utf8"), + exchange_type='fanout', + routing_key=queue_name.encode("utf8"), + reply=True) + added_queues.append(bcast_queue_name) + return (added_queues, removed_queues) diff --git a/awx/settings/defaults.py b/awx/settings/defaults.py index 354c2b9e74..20c8a82a1f 100644 --- a/awx/settings/defaults.py +++ b/awx/settings/defaults.py @@ -9,8 +9,6 @@ import djcelery import six from datetime import timedelta -from kombu.common import Broadcast - # global settings from django.conf import global_settings # ugettext lazy @@ -466,9 +464,7 @@ CELERYD_POOL_RESTARTS = True CELERYD_AUTOSCALER = 'awx.main.utils.autoscale:DynamicAutoScaler' CELERY_RESULT_BACKEND = 'djcelery.backends.database:DatabaseBackend' CELERY_IMPORTS = ('awx.main.scheduler.tasks',) -CELERY_QUEUES = ( - Broadcast('tower_broadcast_all'), -) +CELERY_QUEUES = () CELERY_ROUTES = {} From 202ddae813eca7c88a962f728c0a67f15d9bff95 Mon Sep 17 00:00:00 2001 From: chris meyers Date: Tue, 1 May 2018 13:13:52 -0400 Subject: [PATCH 025/169] make tower instance group name field read-only --- awx/api/serializers.py | 5 +++++ awx/main/tests/functional/api/test_instance_group.py | 9 ++++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/awx/api/serializers.py b/awx/api/serializers.py index c774898d0d..aa63d0997e 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -4627,6 +4627,11 @@ class InstanceGroupSerializer(BaseSerializer): raise serializers.ValidationError(_('{} is not a valid hostname of an existing instance.').format(instance_name)) return value + def validate_name(self, value): + if self.instance and self.instance.name == 'tower' and value != 'tower': + raise serializers.ValidationError(_('tower instance group name may not be changed.')) + return value + def get_jobs_qs(self): # Store running jobs queryset in context, so it will be shared in ListView if 'running_jobs' not in self.context: diff --git a/awx/main/tests/functional/api/test_instance_group.py b/awx/main/tests/functional/api/test_instance_group.py index 3dfd554f11..cd78d0de33 100644 --- a/awx/main/tests/functional/api/test_instance_group.py +++ b/awx/main/tests/functional/api/test_instance_group.py @@ -87,7 +87,7 @@ def test_delete_instance_group_jobs_running(delete, instance_group_jobs_running, @pytest.mark.django_db -def test_modify_delete_tower_instance_group_prevented(delete, options, tower_instance_group, user, patch, put): +def test_delete_rename_tower_instance_group_prevented(delete, options, tower_instance_group, instance_group, user, patch): url = reverse("api:instance_group_detail", kwargs={'pk': tower_instance_group.pk}) super_user = user('bob', True) @@ -99,6 +99,13 @@ def test_modify_delete_tower_instance_group_prevented(delete, options, tower_ins assert 'GET' in resp.data['actions'] assert 'PUT' in resp.data['actions'] + # Rename 'tower' instance group denied + patch(url, {'name': 'tower_prime'}, super_user, expect=400) + + # Rename, other instance group OK + url = reverse("api:instance_group_detail", kwargs={'pk': instance_group.pk}) + patch(url, {'name': 'foobar'}, super_user, expect=200) + @pytest.mark.django_db def test_prevent_delete_iso_and_control_groups(delete, isolated_instance_group, admin): From 1963ab689af0627a38a036604abf681fb23dc1bc Mon Sep 17 00:00:00 2001 From: adamscmRH Date: Tue, 1 May 2018 13:31:00 -0400 Subject: [PATCH 026/169] rm an unnecessary uwsgi restart --- awx/api/conf.py | 3 ++- awx/api/serializers.py | 9 ++++----- awx/conf/apps.py | 8 -------- awx/main/tasks.py | 2 -- 4 files changed, 6 insertions(+), 16 deletions(-) diff --git a/awx/api/conf.py b/awx/api/conf.py index 34bf305f20..58aa9b4cc8 100644 --- a/awx/api/conf.py +++ b/awx/api/conf.py @@ -4,6 +4,7 @@ from django.utils.translation import ugettext_lazy as _ # AWX from awx.conf import fields, register from awx.api.fields import OAuth2ProviderField +from oauth2_provider.settings import oauth2_settings register( @@ -36,7 +37,7 @@ register( register( 'OAUTH2_PROVIDER', field_class=OAuth2ProviderField, - default={'ACCESS_TOKEN_EXPIRE_SECONDS': 315360000000, + default={'ACCESS_TOKEN_EXPIRE_SECONDS': oauth2_settings.ACCESS_TOKEN_EXPIRE_SECONDS, 'AUTHORIZATION_CODE_EXPIRE_SECONDS': 600}, label=_('OAuth 2 Timeout Settings'), help_text=_('Dictionary for customizing OAuth 2 timeouts, available items are ' diff --git a/awx/api/serializers.py b/awx/api/serializers.py index c774898d0d..bc254cb760 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -14,7 +14,6 @@ from datetime import timedelta # OAuth2 from oauthlib.common import generate_token -from oauth2_provider.settings import oauth2_settings # Django from django.conf import settings @@ -1024,7 +1023,7 @@ class UserAuthorizedTokenSerializer(BaseSerializer): validated_data['user'] = current_user validated_data['token'] = generate_token() validated_data['expires'] = now() + timedelta( - seconds=oauth2_settings.ACCESS_TOKEN_EXPIRE_SECONDS + seconds=settings.OAUTH2_PROVIDER['ACCESS_TOKEN_EXPIRE_SECONDS'] ) obj = super(OAuth2TokenSerializer, self).create(validated_data) obj.save() @@ -1176,7 +1175,7 @@ class OAuth2TokenSerializer(BaseSerializer): validated_data['user'] = current_user validated_data['token'] = generate_token() validated_data['expires'] = now() + timedelta( - seconds=oauth2_settings.ACCESS_TOKEN_EXPIRE_SECONDS + seconds=settings.OAUTH2_PROVIDER['ACCESS_TOKEN_EXPIRE_SECONDS'] ) obj = super(OAuth2TokenSerializer, self).create(validated_data) if obj.application and obj.application.user: @@ -1239,7 +1238,7 @@ class OAuth2AuthorizedTokenSerializer(BaseSerializer): validated_data['user'] = current_user validated_data['token'] = generate_token() validated_data['expires'] = now() + timedelta( - seconds=oauth2_settings.ACCESS_TOKEN_EXPIRE_SECONDS + seconds=settings.OAUTH2_PROVIDER['ACCESS_TOKEN_EXPIRE_SECONDS'] ) obj = super(OAuth2AuthorizedTokenSerializer, self).create(validated_data) if obj.application and obj.application.user: @@ -1306,7 +1305,7 @@ class OAuth2PersonalTokenSerializer(BaseSerializer): validated_data['user'] = self.context['request'].user validated_data['token'] = generate_token() validated_data['expires'] = now() + timedelta( - seconds=oauth2_settings.ACCESS_TOKEN_EXPIRE_SECONDS + seconds=settings.OAUTH2_PROVIDER['ACCESS_TOKEN_EXPIRE_SECONDS'] ) validated_data['application'] = None obj = super(OAuth2PersonalTokenSerializer, self).create(validated_data) diff --git a/awx/conf/apps.py b/awx/conf/apps.py index 06c2facb7a..a70d21326c 100644 --- a/awx/conf/apps.py +++ b/awx/conf/apps.py @@ -11,16 +11,8 @@ class ConfConfig(AppConfig): name = 'awx.conf' verbose_name = _('Configuration') - def configure_oauth2_provider(self, settings): - from oauth2_provider import settings as o_settings - o_settings.oauth2_settings = o_settings.OAuth2ProviderSettings( - settings.OAUTH2_PROVIDER, o_settings.DEFAULTS, - o_settings.IMPORT_STRINGS, o_settings.MANDATORY - ) - def ready(self): self.module.autodiscover() from .settings import SettingsWrapper SettingsWrapper.initialize() configure_external_logger(settings) - self.configure_oauth2_provider(settings) diff --git a/awx/main/tasks.py b/awx/main/tasks.py index 31edcc38d7..4f6032b12c 100644 --- a/awx/main/tasks.py +++ b/awx/main/tasks.py @@ -204,8 +204,6 @@ def handle_setting_changes(self, setting_keys): if key.startswith('LOG_AGGREGATOR_'): restart_local_services(['uwsgi', 'celery', 'beat', 'callback']) break - elif key == 'OAUTH2_PROVIDER': - restart_local_services(['uwsgi']) @shared_task(bind=True, queue='tower_broadcast_all') From 079d8e25657d9e87169cfbd29d791a935ebeb2df Mon Sep 17 00:00:00 2001 From: Jared Tabor Date: Tue, 1 May 2018 12:36:59 -0700 Subject: [PATCH 027/169] fixes issue with ui-view for network UI and closes network UI socket connections after closing the network UI --- awx/ui/client/index.template.ejs | 5 +-- .../components/layout/side-nav.partial.html | 4 +- .../network-nav/network.nav.block.less | 2 + .../network-nav/network.nav.controller.js | 1 + .../src/network-ui/network.ui.controller.js | 39 +++++++++++-------- awx/ui/client/src/network-ui/style.less | 1 + 6 files changed, 31 insertions(+), 21 deletions(-) diff --git a/awx/ui/client/index.template.ejs b/awx/ui/client/index.template.ejs index 6f6a5c2cba..f16282bd91 100644 --- a/awx/ui/client/index.template.ejs +++ b/awx/ui/client/index.template.ejs @@ -19,14 +19,13 @@ -
+
-
-
+
diff --git a/awx/ui/client/lib/components/layout/side-nav.partial.html b/awx/ui/client/lib/components/layout/side-nav.partial.html index 8996df899f..5786c13537 100644 --- a/awx/ui/client/lib/components/layout/side-nav.partial.html +++ b/awx/ui/client/lib/components/layout/side-nav.partial.html @@ -1,7 +1,7 @@
+ ng-class="{'at-Layout-side--expanded': vm.isExpanded && layoutVm.isLoggedIn}" ng-show="layoutVm.isLoggedIn && !layoutVm.licenseIsMissing && layoutVm.currentState !== 'inventories.edit.networking'">
+ ng-show="layoutVm.isLoggedIn && !layoutVm.licenseIsMissing && layoutVm.currentState !== 'inventories.edit.networking'">
diff --git a/awx/ui/client/src/network-ui/network-nav/network.nav.block.less b/awx/ui/client/src/network-ui/network-nav/network.nav.block.less index 8cb4006389..552c0151bc 100644 --- a/awx/ui/client/src/network-ui/network-nav/network.nav.block.less +++ b/awx/ui/client/src/network-ui/network-nav/network.nav.block.less @@ -4,6 +4,7 @@ width:100%; align-items: flex-end; position:absolute; + z-index: 1100; } .Networking-top{ @@ -135,6 +136,7 @@ .Networking-dropDown{ left:-2px!important; + z-index: 1101; } .Networking-searchButton{ diff --git a/awx/ui/client/src/network-ui/network-nav/network.nav.controller.js b/awx/ui/client/src/network-ui/network-nav/network.nav.controller.js index f05bee4dd7..820ed7e37d 100644 --- a/awx/ui/client/src/network-ui/network-nav/network.nav.controller.js +++ b/awx/ui/client/src/network-ui/network-nav/network.nav.controller.js @@ -16,6 +16,7 @@ function NetworkingController (models, $state, $scope, strings) { vm.groups = []; $scope.devices = []; vm.close = () => { + $scope.$broadcast('awxNet-closeNetworkUI'); $state.go('inventories'); }; diff --git a/awx/ui/client/src/network-ui/network.ui.controller.js b/awx/ui/client/src/network-ui/network.ui.controller.js index a261b326bb..7f324988c7 100644 --- a/awx/ui/client/src/network-ui/network.ui.controller.js +++ b/awx/ui/client/src/network-ui/network.ui.controller.js @@ -25,6 +25,7 @@ var NetworkUIController = function($scope, $http, $q, $state, + $log, ProcessErrors, ConfigService, rbacUiControlService) { @@ -269,19 +270,19 @@ var NetworkUIController = function($scope, $scope.for_each_page('/api/v2/inventories/' + $scope.inventory_id + '/hosts/', function(all_results) { let hosts = all_results; - console.log(hosts.length); + $log.debug(hosts.length); for(var i = 0; i Date: Tue, 1 May 2018 16:57:17 -0400 Subject: [PATCH 028/169] add auth cookies --- awx/api/generics.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/awx/api/generics.py b/awx/api/generics.py index e12949515b..eae3fe62f8 100644 --- a/awx/api/generics.py +++ b/awx/api/generics.py @@ -6,6 +6,7 @@ import inspect import logging import time import six +import urllib # Django from django.conf import settings @@ -29,6 +30,7 @@ from rest_framework.response import Response from rest_framework import status from rest_framework import views from rest_framework.permissions import AllowAny +from rest_framework.renderers import JSONRenderer # cryptography from cryptography.fernet import InvalidToken @@ -39,7 +41,7 @@ from awx.main.models import * # noqa from awx.main.access import access_registry from awx.main.utils import * # noqa from awx.main.utils.db import get_all_field_names -from awx.api.serializers import ResourceAccessListElementSerializer, CopySerializer +from awx.api.serializers import ResourceAccessListElementSerializer, CopySerializer, UserSerializer from awx.api.versioning import URLPathVersioning, get_request_version from awx.api.metadata import SublistAttachDetatchMetadata, Metadata @@ -70,6 +72,13 @@ class LoggedLoginView(auth_views.LoginView): if current_user and getattr(current_user, 'pk', None) and current_user != original_user: logger.info("User {} logged in.".format(current_user.username)) if request.user.is_authenticated: + logger.info(smart_text(u"User {} logged in".format(self.request.user.username))) + ret.set_cookie('userLoggedIn', 'true') + current_user = UserSerializer(self.request.user) + current_user = JSONRenderer().render(current_user.data) + current_user = urllib.quote('%s' % current_user, '') + ret.set_cookie('current_user', current_user) + return ret else: ret.status_code = 401 @@ -82,6 +91,7 @@ class LoggedLogoutView(auth_views.LogoutView): original_user = getattr(request, 'user', None) ret = super(LoggedLogoutView, self).dispatch(request, *args, **kwargs) current_user = getattr(request, 'user', None) + ret.set_cookie('userLoggedIn', 'false') if (not current_user or not getattr(current_user, 'pk', True)) \ and current_user != original_user: logger.info("User {} logged out.".format(original_user.username)) From a79632968cf4e42af7bbf99ddf1c56a79c0a0d9b Mon Sep 17 00:00:00 2001 From: Jared Tabor Date: Tue, 1 May 2018 15:19:44 -0700 Subject: [PATCH 029/169] Fixes job template schedule link from Activity Stream --- .../src/activity-stream/factories/build-anchor.factory.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/ui/client/src/activity-stream/factories/build-anchor.factory.js b/awx/ui/client/src/activity-stream/factories/build-anchor.factory.js index 2b01398280..1fc031e0e6 100644 --- a/awx/ui/client/src/activity-stream/factories/build-anchor.factory.js +++ b/awx/ui/client/src/activity-stream/factories/build-anchor.factory.js @@ -38,7 +38,7 @@ export default function BuildAnchor($log, $filter) { case 'schedule': // schedule urls depend on the resource they're associated with if (activity.summary_fields.job_template){ - url += 'job_templates/' + activity.summary_fields.job_template.id + '/schedules/' + obj.id; + url += 'templates/job_template/' + activity.summary_fields.job_template.id + '/schedules/' + obj.id; } else if (activity.summary_fields.project){ url += 'projects/' + activity.summary_fields.project.id + '/schedules/' + obj.id; From a840ec408931418c92ea6a43107496a7f6521bb1 Mon Sep 17 00:00:00 2001 From: Jared Tabor Date: Tue, 1 May 2018 15:36:47 -0700 Subject: [PATCH 030/169] Fixes jobs' page activity stream link, and remove AS button on job results page --- awx/ui/client/features/jobs/routes/jobs.route.js | 2 ++ awx/ui/client/features/output/index.js | 3 +-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/awx/ui/client/features/jobs/routes/jobs.route.js b/awx/ui/client/features/jobs/routes/jobs.route.js index c3ee0b4fef..9129fed3d1 100644 --- a/awx/ui/client/features/jobs/routes/jobs.route.js +++ b/awx/ui/client/features/jobs/routes/jobs.route.js @@ -22,6 +22,8 @@ export default { } }, data: { + activityStream: true, + activityStreamTarget: 'job', socket: { groups: { jobs: ['status_changed'], diff --git a/awx/ui/client/features/output/index.js b/awx/ui/client/features/output/index.js index 3aaadc2553..35fa91ef6e 100644 --- a/awx/ui/client/features/output/index.js +++ b/awx/ui/client/features/output/index.js @@ -169,8 +169,7 @@ function JobsRun ($stateRegistry) { url: '/jobz/:type/:id?job_event_search', route: '/jobz/:type/:id?job_event_search', data: { - activityStream: true, - activityStreamTarget: 'jobs' + activityStream: false, }, views: { '@': { From 63542c3b436fa5a02e566fb9774c1a024a6ed650 Mon Sep 17 00:00:00 2001 From: Jared Tabor Date: Tue, 1 May 2018 16:05:22 -0700 Subject: [PATCH 031/169] Don't render tooltips that shouldn't exist --- awx/ui/client/lib/components/list/row.partial.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/ui/client/lib/components/list/row.partial.html b/awx/ui/client/lib/components/list/row.partial.html index 0f1d88d22d..dc6dfbd9f0 100644 --- a/awx/ui/client/lib/components/list/row.partial.html +++ b/awx/ui/client/lib/components/list/row.partial.html @@ -1,6 +1,6 @@
- +
From cf0efe969ef52507205d85a60f480650a8116dc2 Mon Sep 17 00:00:00 2001 From: Jared Tabor Date: Tue, 1 May 2018 16:20:44 -0700 Subject: [PATCH 032/169] Changes "Cancel" to "OK" on cancel-job-prompt modal --- awx/ui/client/features/jobs/jobsList.controller.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/ui/client/features/jobs/jobsList.controller.js b/awx/ui/client/features/jobs/jobsList.controller.js index 4959d41b5e..dbb32a93c3 100644 --- a/awx/ui/client/features/jobs/jobsList.controller.js +++ b/awx/ui/client/features/jobs/jobsList.controller.js @@ -174,7 +174,7 @@ function ListJobsController ( resourceName: $filter('sanitize')(job.name), body: deleteModalBody, action, - actionText: strings.get('CANCEL') + actionText: strings.get('OK') }); }; From 83d5fef67cea897e533bed81ae255f3d89eb2268 Mon Sep 17 00:00:00 2001 From: Jake McDermott Date: Wed, 2 May 2018 01:02:56 -0400 Subject: [PATCH 033/169] conditionally hide scroll --- awx/ui/client/features/output/_index.less | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/ui/client/features/output/_index.less b/awx/ui/client/features/output/_index.less index 0bb8a72aba..524951ecbc 100644 --- a/awx/ui/client/features/output/_index.less +++ b/awx/ui/client/features/output/_index.less @@ -303,7 +303,7 @@ grid-template-rows: minmax(500px, ~"calc(100vh - 140px)"); .at-Panel { - overflow-y: scroll; + overflow-y: auto; } } From 0c8b2a98723e524eb0d0ef86a9aef1a42ffb448e Mon Sep 17 00:00:00 2001 From: Jake McDermott Date: Wed, 2 May 2018 01:03:13 -0400 Subject: [PATCH 034/169] remove unused service --- awx/ui/client/features/output/details.directive.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/awx/ui/client/features/output/details.directive.js b/awx/ui/client/features/output/details.directive.js index d4e590e1c3..e55f3a8d81 100644 --- a/awx/ui/client/features/output/details.directive.js +++ b/awx/ui/client/features/output/details.directive.js @@ -509,8 +509,7 @@ function AtJobDetailsController ( _strings_, _status_, _wait_, - ParseTypeChange, - ParseVariableString, + _parse_, ) { vm = this || {}; @@ -519,7 +518,7 @@ function AtJobDetailsController ( $state = _$state_; error = _error_; - parse = ParseVariableString; + parse = _parse_; prompt = _prompt_; strings = _strings_; status = _status_; @@ -588,7 +587,6 @@ AtJobDetailsController.$inject = [ 'JobStrings', 'JobStatusService', 'Wait', - 'ParseTypeChange', 'ParseVariableString', ]; From dce52d05525759889d0bcda68b01d37a38f988e4 Mon Sep 17 00:00:00 2001 From: Marliana Lara Date: Tue, 1 May 2018 14:14:28 -0400 Subject: [PATCH 035/169] Update the Add button content and styles * Change the text from "Add" to "+" * Update the e2e test to check for an id #button-add --- awx/ui/client/features/_index.less | 1 - .../applications/list-applications.view.html | 1 + awx/ui/client/features/credentials/_index.less | 3 --- .../features/credentials/legacy.credentials.js | 4 ++-- .../features/templates/templates.strings.js | 1 - .../features/templates/templatesList.view.html | 6 ++---- .../users/tokens/users-tokens-list.partial.html | 1 + awx/ui/client/legacy/styles/ansible-ui.less | 6 ++++-- awx/ui/client/legacy/styles/lists.less | 17 +++-------------- awx/ui/client/lib/components/list/_index.less | 11 ----------- awx/ui/client/lib/theme/_global.less | 5 +++-- .../credential-types/credential-types.list.js | 4 ++-- .../client/src/credentials/credentials.form.js | 5 +++-- .../client/src/credentials/credentials.list.js | 4 ++-- .../instances/instances-list.partial.html | 1 + .../list/instance-groups-list.partial.html | 1 + .../related/groups/hosts-related-groups.list.js | 4 ++-- .../inventories/inventory.list.js | 4 ++-- .../inventories/related/groups/groups.list.js | 4 ++-- .../nested-groups/group-nested-groups.list.js | 4 ++-- .../nested-hosts/group-nested-hosts.list.js | 4 ++-- .../related/hosts/related-host.list.js | 4 ++-- .../nested-groups/host-nested-groups.list.js | 4 ++-- .../inventories/related/sources/sources.list.js | 4 ++-- .../smart-inventory/smart-inventory.form.js | 4 ++-- .../standard-inventory/inventory.form.js | 4 ++-- .../inventory-scripts/inventory-scripts.list.js | 4 ++-- .../notifications/notificationTemplates.list.js | 4 ++-- .../linkout/organizations-linkout.route.js | 7 +++---- .../list/organizations-list.partial.html | 7 +++---- .../src/organizations/organizations.form.js | 4 ++-- .../src/organizations/organizations.list.js | 4 ++-- awx/ui/client/src/projects/projects.form.js | 4 ++-- awx/ui/client/src/projects/projects.list.js | 4 ++-- awx/ui/client/src/scheduler/schedules.list.js | 4 ++-- awx/ui/client/src/shared/generator-helpers.js | 1 + .../list-generator/list-actions.partial.html | 12 ++++++------ awx/ui/client/src/teams/teams.form.js | 8 ++++---- awx/ui/client/src/teams/teams.list.js | 4 ++-- .../job_templates/job-template.form.js | 4 ++-- awx/ui/client/src/templates/templates.list.js | 4 ++-- awx/ui/client/src/templates/workflows.form.js | 4 ++-- awx/ui/client/src/users/users.form.js | 4 ++-- awx/ui/client/src/users/users.list.js | 4 ++-- awx/ui/test/e2e/objects/credentialTypes.js | 2 +- awx/ui/test/e2e/objects/credentials.js | 2 +- awx/ui/test/e2e/objects/inventories.js | 6 +++--- awx/ui/test/e2e/objects/inventoryScripts.js | 2 +- .../test/e2e/objects/notificationTemplates.js | 2 +- awx/ui/test/e2e/objects/organizations.js | 2 +- awx/ui/test/e2e/objects/projects.js | 2 +- awx/ui/test/e2e/objects/sections/permissions.js | 2 +- awx/ui/test/e2e/objects/teams.js | 2 +- awx/ui/test/e2e/objects/templates.js | 4 ++-- awx/ui/test/e2e/objects/users.js | 2 +- awx/ui/test/e2e/tests/smoke.js | 5 ++--- 56 files changed, 104 insertions(+), 127 deletions(-) delete mode 100644 awx/ui/client/features/credentials/_index.less diff --git a/awx/ui/client/features/_index.less b/awx/ui/client/features/_index.less index 59e8e4630b..04be5b31bb 100644 --- a/awx/ui/client/features/_index.less +++ b/awx/ui/client/features/_index.less @@ -1,3 +1,2 @@ -@import 'credentials/_index'; @import 'output/_index'; @import 'users/tokens/_index'; diff --git a/awx/ui/client/features/applications/list-applications.view.html b/awx/ui/client/features/applications/list-applications.view.html index 1803e5ab66..1dd96b2dc1 100644 --- a/awx/ui/client/features/applications/list-applications.view.html +++ b/awx/ui/client/features/applications/list-applications.view.html @@ -23,6 +23,7 @@ type="button" ui-sref="applications.add" class="at-Button--add" + id="button-add" aria-haspopup="true" aria-expanded="false"> diff --git a/awx/ui/client/features/credentials/_index.less b/awx/ui/client/features/credentials/_index.less deleted file mode 100644 index 87f746b2c3..0000000000 --- a/awx/ui/client/features/credentials/_index.less +++ /dev/null @@ -1,3 +0,0 @@ -.at-CredentialsPermissions { - margin-top: 50px; -} diff --git a/awx/ui/client/features/credentials/legacy.credentials.js b/awx/ui/client/features/credentials/legacy.credentials.js index 07cee1329d..27428feb7b 100644 --- a/awx/ui/client/features/credentials/legacy.credentials.js +++ b/awx/ui/client/features/credentials/legacy.credentials.js @@ -69,8 +69,8 @@ function LegacyCredentialsService () { ngClick: '$state.go(\'.add\')', label: 'Add', awToolTip: N_('Add a permission'), - actionClass: 'btn List-buttonSubmit', - buttonContent: `+ ${N_('ADD')}`, + actionClass: 'at-Button--add', + actionId: 'button-add', ngShow: '(credential_obj.summary_fields.user_capabilities.edit || canAdd)' } }, diff --git a/awx/ui/client/features/templates/templates.strings.js b/awx/ui/client/features/templates/templates.strings.js index 896cfd87bd..3bb2d38b66 100644 --- a/awx/ui/client/features/templates/templates.strings.js +++ b/awx/ui/client/features/templates/templates.strings.js @@ -10,7 +10,6 @@ function TemplatesStrings (BaseString) { ns.list = { PANEL_TITLE: t.s('TEMPLATES'), - ADD_BUTTON_LABEL: t.s('ADD'), ADD_DD_JT_LABEL: t.s('Job Template'), ADD_DD_WF_LABEL: t.s('Workflow Template'), ROW_ITEM_LABEL_ACTIVITY: t.s('Activity'), diff --git a/awx/ui/client/features/templates/templatesList.view.html b/awx/ui/client/features/templates/templatesList.view.html index e68ddef8fb..2608a04177 100644 --- a/awx/ui/client/features/templates/templatesList.view.html +++ b/awx/ui/client/features/templates/templatesList.view.html @@ -14,13 +14,11 @@