From 096a0a262ac021d406a889c5cb01a03ec64ccc66 Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Tue, 28 Jun 2016 15:42:56 -0400 Subject: [PATCH 001/123] Ported old/ha.py test --- awx/main/tests/old/ha.py | 24 ------------------------ awx/main/tests/unit/test_ha.py | 15 +++++++++++++++ 2 files changed, 15 insertions(+), 24 deletions(-) delete mode 100644 awx/main/tests/old/ha.py create mode 100644 awx/main/tests/unit/test_ha.py diff --git a/awx/main/tests/old/ha.py b/awx/main/tests/old/ha.py deleted file mode 100644 index 4fd0ae1c40..0000000000 --- a/awx/main/tests/old/ha.py +++ /dev/null @@ -1,24 +0,0 @@ -# Copyright (c) 2015 Ansible, Inc. - -# Python -import mock - -# Django -from django.test import SimpleTestCase - -# AWX -from awx.main.models import * # noqa -from awx.main.ha import * # noqa - -__all__ = ['HAUnitTest',] - -class HAUnitTest(SimpleTestCase): - - @mock.patch('awx.main.models.Instance.objects.count', return_value=2) - def test_multiple_instances(self, ignore): - self.assertTrue(is_ha_environment()) - - @mock.patch('awx.main.models.Instance.objects.count', return_value=1) - def test_db_localhost(self, ignore): - self.assertFalse(is_ha_environment()) - diff --git a/awx/main/tests/unit/test_ha.py b/awx/main/tests/unit/test_ha.py new file mode 100644 index 0000000000..07249a67fb --- /dev/null +++ b/awx/main/tests/unit/test_ha.py @@ -0,0 +1,15 @@ +# Copyright (c) 2016 Ansible, Inc. + +# Python +import mock + +# AWX +from awx.main.ha import is_ha_environment + +@mock.patch('awx.main.models.Instance.objects.count', lambda: 2) +def test_multiple_instances(): + assert is_ha_environment() + +@mock.patch('awx.main.models.Instance.objects.count', lambda: 1) +def test_db_localhost(): + assert is_ha_environment() is False From 66f86de3fa29d57fd4da32ffb41867a18ccef0fc Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Tue, 28 Jun 2016 16:58:14 -0400 Subject: [PATCH 002/123] Ported old test_licences.py tests --- .../tests/functional/core/test_licenses.py | 132 ++++++++++++++++ awx/main/tests/old/licenses.py | 147 ------------------ 2 files changed, 132 insertions(+), 147 deletions(-) create mode 100644 awx/main/tests/functional/core/test_licenses.py delete mode 100644 awx/main/tests/old/licenses.py diff --git a/awx/main/tests/functional/core/test_licenses.py b/awx/main/tests/functional/core/test_licenses.py new file mode 100644 index 0000000000..37f3c63fa9 --- /dev/null +++ b/awx/main/tests/functional/core/test_licenses.py @@ -0,0 +1,132 @@ +# Copyright (c) 2015 Ansible, Inc. +# All Rights Reserved. + +import json +import mock +import os +import tempfile +import time +import pytest +from datetime import datetime + +from awx.main.models import Host +from awx.main.task_engine import TaskSerializer, TaskEngager + + +@pytest.mark.django_db +def test_license_writer(inventory, admin): + writer = TaskEngager( + company_name='acmecorp', + contact_name='Michael DeHaan', + contact_email='michael@ansibleworks.com', + license_date=25000, # seconds since epoch + instance_count=500) + + data = writer.get_data() + + Host.objects.bulk_create( + [ + Host( + name='host.%d' % n, + inventory=inventory, + created_by=admin, + modified=datetime.now(), + created=datetime.now()) + for n in range(12) + ] + ) + + assert data['instance_count'] == 500 + assert data['contact_name'] == 'Michael DeHaan' + assert data['contact_email'] == 'michael@ansibleworks.com' + assert data['license_date'] == 25000 + assert data['license_key'] == "11bae31f31c6a6cdcb483a278cdbe98bd8ac5761acd7163a50090b0f098b3a13" + + strdata = writer.get_string() + strdata_loaded = json.loads(strdata) + assert strdata_loaded == data + + reader = TaskSerializer() + + vdata = reader.from_string(strdata) + + assert vdata['available_instances'] == 500 + assert vdata['current_instances'] == 12 + assert vdata['free_instances'] == 488 + assert vdata['date_warning'] is True + assert vdata['date_expired'] is True + assert vdata['license_date'] == 25000 + assert vdata['time_remaining'] < 0 + assert vdata['valid_key'] is True + assert vdata['compliant'] is False + assert vdata['subscription_name'] + +@pytest.mark.django_db +def test_expired_licenses(): + reader = TaskSerializer() + writer = TaskEngager( + company_name='Tower', + contact_name='Tower Admin', + contact_email='tower@ansible.com', + license_date=int(time.time() - 3600), + instance_count=100, + trial=True) + strdata = writer.get_string() + vdata = reader.from_string(strdata) + + assert vdata['compliant'] is False + assert vdata['grace_period_remaining'] < 0 + + writer = TaskEngager( + company_name='Tower', + contact_name='Tower Admin', + contact_email='tower@ansible.com', + license_date=int(time.time() - 2592001), + instance_count=100, + trial=False) + strdata = writer.get_string() + vdata = reader.from_string(strdata) + + assert vdata['compliant'] is False + assert vdata['grace_period_remaining'] < 0 + + writer = TaskEngager( + company_name='Tower', + contact_name='Tower Admin', + contact_email='tower@ansible.com', + license_date=int(time.time() - 3600), + instance_count=100, + trial=False) + strdata = writer.get_string() + vdata = reader.from_string(strdata) + + assert vdata['compliant'] is False + assert vdata['grace_period_remaining'] > 0 + +@pytest.mark.django_db +def test_aws_license(): + os.environ['AWX_LICENSE_FILE'] = 'non-existent-license-file.json' + + h, path = tempfile.mkstemp() + with os.fdopen(h, 'w') as f: + json.dump({'instance_count': 100}, f) + + def fetch_ami(_self): + _self.attributes['ami-id'] = 'ami-00000000' + return True + + def fetch_instance(_self): + _self.attributes['instance-id'] = 'i-00000000' + return True + + with mock.patch('awx.main.task_engine.TEMPORARY_TASK_FILE', path): + with mock.patch('awx.main.task_engine.TemporaryTaskEngine.fetch_ami', fetch_ami): + with mock.patch('awx.main.task_engine.TemporaryTaskEngine.fetch_instance', fetch_instance): + reader = TaskSerializer() + license = reader.from_file() + assert license['is_aws'] + assert license['time_remaining'] + assert license['free_instances'] > 0 + assert license['grace_period_remaining'] > 0 + + os.unlink(path) diff --git a/awx/main/tests/old/licenses.py b/awx/main/tests/old/licenses.py deleted file mode 100644 index 136c3ce8a5..0000000000 --- a/awx/main/tests/old/licenses.py +++ /dev/null @@ -1,147 +0,0 @@ -# Copyright (c) 2015 Ansible, Inc. -# All Rights Reserved. - -import json -import os -import tempfile - -from awx.main.models import Host, Inventory, Organization -from awx.main.tests.base import BaseTest -import awx.main.task_engine -from awx.main.task_engine import * # noqa - -class LicenseTests(BaseTest): - - def setUp(self): - self.start_redis() - self.setup_instances() - super(LicenseTests, self).setUp() - self.setup_users() - u = self.super_django_user - org = Organization.objects.create(name='o1', created_by=u) - org.admin_role.members.add(self.normal_django_user) - self.inventory = Inventory.objects.create(name='hi', organization=org, created_by=u) - Host.objects.create(name='a1', inventory=self.inventory, created_by=u) - Host.objects.create(name='a2', inventory=self.inventory, created_by=u) - Host.objects.create(name='a3', inventory=self.inventory, created_by=u) - Host.objects.create(name='a4', inventory=self.inventory, created_by=u) - Host.objects.create(name='a5', inventory=self.inventory, created_by=u) - Host.objects.create(name='a6', inventory=self.inventory, created_by=u) - Host.objects.create(name='a7', inventory=self.inventory, created_by=u) - Host.objects.create(name='a8', inventory=self.inventory, created_by=u) - Host.objects.create(name='a9', inventory=self.inventory, created_by=u) - Host.objects.create(name='a10', inventory=self.inventory, created_by=u) - Host.objects.create(name='a11', inventory=self.inventory, created_by=u) - Host.objects.create(name='a12', inventory=self.inventory, created_by=u) - self._temp_task_file = awx.main.task_engine.TEMPORARY_TASK_FILE - self._temp_task_fetch_ami = awx.main.task_engine.TemporaryTaskEngine.fetch_ami - self._temp_task_fetch_instance = awx.main.task_engine.TemporaryTaskEngine.fetch_instance - - def tearDown(self): - awx.main.task_engine.TEMPORARY_TASK_FILE = self._temp_task_file - awx.main.task_engine.TemporaryTaskEngine.fetch_ami = self._temp_task_fetch_ami - awx.main.task_engine.TemporaryTaskEngine.fetch_instance = self._temp_task_fetch_instance - super(LicenseTests, self).tearDown() - self.stop_redis() - - def test_license_writer(self): - - writer = TaskEngager( - company_name='acmecorp', - contact_name='Michael DeHaan', - contact_email='michael@ansibleworks.com', - license_date=25000, # seconds since epoch - instance_count=500) - - data = writer.get_data() - - assert data['instance_count'] == 500 - assert data['contact_name'] == 'Michael DeHaan' - assert data['contact_email'] == 'michael@ansibleworks.com' - assert data['license_date'] == 25000 - assert data['license_key'] == "11bae31f31c6a6cdcb483a278cdbe98bd8ac5761acd7163a50090b0f098b3a13" - - strdata = writer.get_string() - strdata_loaded = json.loads(strdata) - assert strdata_loaded == data - - reader = TaskSerializer() - - vdata = reader.from_string(strdata) - - assert vdata['available_instances'] == 500 - assert vdata['current_instances'] == 12 - assert vdata['free_instances'] == 488 - assert vdata['date_warning'] is True - assert vdata['date_expired'] is True - assert vdata['license_date'] == 25000 - assert vdata['time_remaining'] < 0 - assert vdata['valid_key'] is True - assert vdata['compliant'] is False - assert vdata['subscription_name'] - - def test_expired_licenses(self): - reader = TaskSerializer() - writer = TaskEngager( - company_name='Tower', - contact_name='Tower Admin', - contact_email='tower@ansible.com', - license_date=int(time.time() - 3600), - instance_count=100, - trial=True) - strdata = writer.get_string() - vdata = reader.from_string(strdata) - - assert vdata['compliant'] is False - assert vdata['grace_period_remaining'] < 0 - - writer = TaskEngager( - company_name='Tower', - contact_name='Tower Admin', - contact_email='tower@ansible.com', - license_date=int(time.time() - 2592001), - instance_count=100, - trial=False) - strdata = writer.get_string() - vdata = reader.from_string(strdata) - - assert vdata['compliant'] is False - assert vdata['grace_period_remaining'] < 0 - - writer = TaskEngager( - company_name='Tower', - contact_name='Tower Admin', - contact_email='tower@ansible.com', - license_date=int(time.time() - 3600), - instance_count=100, - trial=False) - strdata = writer.get_string() - vdata = reader.from_string(strdata) - - assert vdata['compliant'] is False - assert vdata['grace_period_remaining'] > 0 - - def test_aws_license(self): - os.environ['AWX_LICENSE_FILE'] = 'non-existent-license-file.json' - h, path = tempfile.mkstemp() - self._temp_paths.append(path) - with os.fdopen(h, 'w') as f: - json.dump({'instance_count': 100}, f) - awx.main.task_engine.TEMPORARY_TASK_FILE = path - - def fetch_ami(_self): - _self.attributes['ami-id'] = 'ami-00000000' - return True - - def fetch_instance(_self): - _self.attributes['instance-id'] = 'i-00000000' - return True - - awx.main.task_engine.TemporaryTaskEngine.fetch_ami = fetch_ami - awx.main.task_engine.TemporaryTaskEngine.fetch_instance = fetch_instance - reader = TaskSerializer() - license = reader.from_file() - self.assertTrue(license['is_aws']) - self.assertTrue(license['time_remaining']) - self.assertTrue(license['free_instances'] > 0) - self.assertTrue(license['grace_period_remaining'] > 0) From 172b6b48b4216f6f86b3116c978d62ba2960c7a6 Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Wed, 29 Jun 2016 14:36:25 -0400 Subject: [PATCH 003/123] Ported old redact.py tests --- .../{old/redact.py => unit/test_redact.py} | 124 +++++++++++------- 1 file changed, 76 insertions(+), 48 deletions(-) rename awx/main/tests/{old/redact.py => unit/test_redact.py} (54%) diff --git a/awx/main/tests/old/redact.py b/awx/main/tests/unit/test_redact.py similarity index 54% rename from awx/main/tests/old/redact.py rename to awx/main/tests/unit/test_redact.py index 3ce38bc7ad..47d6419642 100644 --- a/awx/main/tests/old/redact.py +++ b/awx/main/tests/unit/test_redact.py @@ -1,11 +1,9 @@ - import textwrap +import re # AWX from awx.main.redact import UriCleaner -from awx.main.tests.base import BaseTest, URI - -__all__ = ['UriCleanTests'] +from awx.main.tests.base import URI TEST_URIS = [ URI('no host', scheme='https', username='myusername', password='mypass', host=None), @@ -80,59 +78,89 @@ TEST_CLEARTEXT.append({ 'host_occurrences' : 4 }) -class UriCleanTests(BaseTest): - # should redact sensitive usernames and passwords - def test_uri_scm_simple_redacted(self): - for uri in TEST_URIS: - redacted_str = UriCleaner.remove_sensitive(str(uri)) - if uri.username: - self.check_not_found(redacted_str, uri.username, uri.description) - if uri.password: - self.check_not_found(redacted_str, uri.password, uri.description) +def check_found(string, substr, count=-1, description=None, word_boundary=False): + if word_boundary: + count_actual = len(re.findall(r'\b%s\b' % re.escape(substr), string)) + else: + count_actual = string.count(substr) - # should replace secret data with safe string, UriCleaner.REPLACE_STR - def test_uri_scm_simple_replaced(self): - for uri in TEST_URIS: - redacted_str = UriCleaner.remove_sensitive(str(uri)) - self.check_found(redacted_str, UriCleaner.REPLACE_STR, uri.get_secret_count()) + msg = '' + if description: + msg = 'Test "%s".\n' % description + if count == -1: + assert count_actual > 0 + else: + msg += 'Found %d occurances of "%s" instead of %d in: "%s"' % (count_actual, substr, count, string) + if count_actual != count: + raise Exception(msg) - # should redact multiple uris in text - def test_uri_scm_multiple(self): - cleartext = '' - for uri in TEST_URIS: - cleartext += str(uri) + ' ' - for uri in TEST_URIS: - cleartext += str(uri) + '\n' +def check_not_found(string, substr, description=None, word_boundary=False): + if word_boundary: + count = len(re.findall(r'\b%s\b' % re.escape(substr), string)) + else: + count = string.find(substr) + if count == -1: + count = 0 + msg = '' + if description: + msg = 'Test "%s".\n' % description + msg += '"%s" found in: "%s"' % (substr, string) + if count != 0: + raise Exception(msg) + + +# should redact sensitive usernames and passwords +def test_uri_scm_simple_redacted(): + for uri in TEST_URIS: redacted_str = UriCleaner.remove_sensitive(str(uri)) if uri.username: - self.check_not_found(redacted_str, uri.username, uri.description) + check_not_found(redacted_str, uri.username, uri.description) if uri.password: - self.check_not_found(redacted_str, uri.password, uri.description) + check_not_found(redacted_str, uri.password, uri.description) - # should replace multiple secret data with safe string - def test_uri_scm_multiple_replaced(self): - cleartext = '' - find_count = 0 - for uri in TEST_URIS: - cleartext += str(uri) + ' ' - find_count += uri.get_secret_count() +# should replace secret data with safe string, UriCleaner.REPLACE_STR +def test_uri_scm_simple_replaced(): + for uri in TEST_URIS: + redacted_str = UriCleaner.remove_sensitive(str(uri)) + check_found(redacted_str, UriCleaner.REPLACE_STR, uri.get_secret_count()) - for uri in TEST_URIS: - cleartext += str(uri) + '\n' - find_count += uri.get_secret_count() +# should redact multiple uris in text +def test_uri_scm_multiple(): + cleartext = '' + for uri in TEST_URIS: + cleartext += str(uri) + ' ' + for uri in TEST_URIS: + cleartext += str(uri) + '\n' - redacted_str = UriCleaner.remove_sensitive(cleartext) - self.check_found(redacted_str, UriCleaner.REPLACE_STR, find_count) + redacted_str = UriCleaner.remove_sensitive(str(uri)) + if uri.username: + check_not_found(redacted_str, uri.username, uri.description) + if uri.password: + check_not_found(redacted_str, uri.password, uri.description) - # should redact and replace multiple secret data within a complex cleartext blob - def test_uri_scm_cleartext_redact_and_replace(self): - for test_data in TEST_CLEARTEXT: - uri = test_data['uri'] - redacted_str = UriCleaner.remove_sensitive(test_data['text']) - self.check_not_found(redacted_str, uri.username, uri.description) - self.check_not_found(redacted_str, uri.password, uri.description) - # Ensure the host didn't get redacted - self.check_found(redacted_str, uri.host, test_data['host_occurrences'], uri.description) +# should replace multiple secret data with safe string +def test_uri_scm_multiple_replaced(): + cleartext = '' + find_count = 0 + for uri in TEST_URIS: + cleartext += str(uri) + ' ' + find_count += uri.get_secret_count() + for uri in TEST_URIS: + cleartext += str(uri) + '\n' + find_count += uri.get_secret_count() + + redacted_str = UriCleaner.remove_sensitive(cleartext) + check_found(redacted_str, UriCleaner.REPLACE_STR, find_count) + +# should redact and replace multiple secret data within a complex cleartext blob +def test_uri_scm_cleartext_redact_and_replace(): + for test_data in TEST_CLEARTEXT: + uri = test_data['uri'] + redacted_str = UriCleaner.remove_sensitive(test_data['text']) + check_not_found(redacted_str, uri.username, uri.description) + check_not_found(redacted_str, uri.password, uri.description) + # Ensure the host didn't get redacted + check_found(redacted_str, uri.host, test_data['host_occurrences'], uri.description) From 9742d02ee8cf6516aeef5b3ba7650860ee4d2ef4 Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Thu, 30 Jun 2016 11:31:59 -0400 Subject: [PATCH 004/123] Eliminated redundant http request code in test suite --- awx/main/tests/functional/conftest.py | 135 +++++--------------------- 1 file changed, 22 insertions(+), 113 deletions(-) diff --git a/awx/main/tests/functional/conftest.py b/awx/main/tests/functional/conftest.py index 5e6ef333bb..c363d6f1d3 100644 --- a/awx/main/tests/functional/conftest.py +++ b/awx/main/tests/functional/conftest.py @@ -293,17 +293,23 @@ def permissions(): 'update':False, 'delete':False, 'scm_update':False, 'execute':False, 'use':True,}, } -@pytest.fixture -def post(): - def rf(url, data, user=None, middleware=None, expect=None, **kwargs): - view, view_args, view_kwargs = resolve(urlparse(url)[2]) + +def _request(verb): + def rf(url, data_or_user=None, user=None, middleware=None, expect=None, **kwargs): + if type(data_or_user) is User and user is None: + user = data_or_user + elif 'data' not in kwargs: + kwargs['data'] = data_or_user if 'format' not in kwargs: kwargs['format'] = 'json' - request = APIRequestFactory().post(url, data, **kwargs) + + view, view_args, view_kwargs = resolve(urlparse(url)[2]) + request = getattr(APIRequestFactory(), verb)(url, **kwargs) if middleware: middleware.process_request(request) if user: force_authenticate(request, user=user) + response = view(request, *view_args, **view_kwargs) if middleware: middleware.process_response(request, response) @@ -311,134 +317,37 @@ def post(): if response.status_code != expect: print(response.data) assert response.status_code == expect + response.render() return response return rf +@pytest.fixture +def post(): + return _request('post') + @pytest.fixture def get(): - def rf(url, user=None, middleware=None, expect=None, **kwargs): - view, view_args, view_kwargs = resolve(urlparse(url)[2]) - if 'format' not in kwargs: - kwargs['format'] = 'json' - request = APIRequestFactory().get(url, **kwargs) - if middleware: - middleware.process_request(request) - if user: - force_authenticate(request, user=user) - response = view(request, *view_args, **view_kwargs) - if middleware: - middleware.process_response(request, response) - if expect: - if response.status_code != expect: - print(response.data) - assert response.status_code == expect - return response - return rf + return _request('get') @pytest.fixture def put(): - def rf(url, data, user=None, middleware=None, expect=None, **kwargs): - view, view_args, view_kwargs = resolve(urlparse(url)[2]) - if 'format' not in kwargs: - kwargs['format'] = 'json' - request = APIRequestFactory().put(url, data, **kwargs) - if middleware: - middleware.process_request(request) - if user: - force_authenticate(request, user=user) - response = view(request, *view_args, **view_kwargs) - if middleware: - middleware.process_response(request, response) - if expect: - if response.status_code != expect: - print(response.data) - assert response.status_code == expect - return response - return rf + return _request('put') @pytest.fixture def patch(): - def rf(url, data, user=None, middleware=None, expect=None, **kwargs): - view, view_args, view_kwargs = resolve(urlparse(url)[2]) - if 'format' not in kwargs: - kwargs['format'] = 'json' - request = APIRequestFactory().patch(url, data, **kwargs) - if middleware: - middleware.process_request(request) - if user: - force_authenticate(request, user=user) - response = view(request, *view_args, **view_kwargs) - if middleware: - middleware.process_response(request, response) - if expect: - if response.status_code != expect: - print(response.data) - assert response.status_code == expect - return response - return rf + return _request('patch') @pytest.fixture def delete(): - def rf(url, user=None, middleware=None, expect=None, **kwargs): - view, view_args, view_kwargs = resolve(urlparse(url)[2]) - if 'format' not in kwargs: - kwargs['format'] = 'json' - request = APIRequestFactory().delete(url, **kwargs) - if middleware: - middleware.process_request(request) - if user: - force_authenticate(request, user=user) - response = view(request, *view_args, **view_kwargs) - if middleware: - middleware.process_response(request, response) - if expect: - if response.status_code != expect: - print(response.data) - assert response.status_code == expect - return response - return rf + return _request('delete') @pytest.fixture def head(): - def rf(url, user=None, middleware=None, expect=None, **kwargs): - view, view_args, view_kwargs = resolve(urlparse(url)[2]) - if 'format' not in kwargs: - kwargs['format'] = 'json' - request = APIRequestFactory().head(url, **kwargs) - if middleware: - middleware.process_request(request) - if user: - force_authenticate(request, user=user) - response = view(request, *view_args, **view_kwargs) - if middleware: - middleware.process_response(request, response) - if expect: - if response.status_code != expect: - print(response.data) - assert response.status_code == expect - return response - return rf + return _request('head') @pytest.fixture def options(): - def rf(url, data, user=None, middleware=None, expect=None, **kwargs): - view, view_args, view_kwargs = resolve(urlparse(url)[2]) - if 'format' not in kwargs: - kwargs['format'] = 'json' - request = APIRequestFactory().options(url, data, **kwargs) - if middleware: - middleware.process_request(request) - if user: - force_authenticate(request, user=user) - response = view(request, *view_args, **view_kwargs) - if middleware: - middleware.process_response(request, response) - if expect: - if response.status_code != expect: - print(response.data) - assert response.status_code == expect - return response - return rf + return _request('options') From e2c9d26e0d4b7128b949906de0be2723c139bf62 Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Thu, 30 Jun 2016 11:38:06 -0400 Subject: [PATCH 005/123] Ported old/views.py --- .../functional/api/test_unified_jobs_view.py | 79 +++++++++++- awx/main/tests/functional/conftest.py | 6 + awx/main/tests/old/views.py | 113 ------------------ 3 files changed, 83 insertions(+), 115 deletions(-) delete mode 100644 awx/main/tests/old/views.py diff --git a/awx/main/tests/functional/api/test_unified_jobs_view.py b/awx/main/tests/functional/api/test_unified_jobs_view.py index ab9487bc38..174bb60080 100644 --- a/awx/main/tests/functional/api/test_unified_jobs_view.py +++ b/awx/main/tests/functional/api/test_unified_jobs_view.py @@ -1,8 +1,84 @@ import pytest -from awx.main.models import UnifiedJob from django.core.urlresolvers import reverse +from awx.main.models import UnifiedJob, ProjectUpdate +from awx.main.tests.base import URI + + +TEST_STDOUTS = [] +uri = URI(scheme="https", username="Dhh3U47nmC26xk9PKscV", password="PXPfWW8YzYrgS@E5NbQ2H@", host="github.ginger.com/theirrepo.git/info/refs") +TEST_STDOUTS.append({ + 'description': 'uri in a plain text document', + 'uri' : uri, + 'text' : 'hello world %s goodbye world' % uri, + 'occurrences' : 1 +}) + +uri = URI(scheme="https", username="applepie@@@", password="thatyouknow@@@@", host="github.ginger.com/theirrepo.git/info/refs") +TEST_STDOUTS.append({ + 'description': 'uri appears twice in a multiline plain text document', + 'uri' : uri, + 'text' : 'hello world %s \n\nyoyo\n\nhello\n%s' % (uri, uri), + 'occurrences' : 2 +}) + + + +@pytest.fixture +def test_cases(project): + ret = [] + for e in TEST_STDOUTS: + e['project'] = ProjectUpdate(project=project) + e['project'].result_stdout_text = e['text'] + e['project'].save() + ret.append(e) + return ret + +@pytest.fixture +def negative_test_cases(job_factory): + ret = [] + for e in TEST_STDOUTS: + e['job'] = job_factory() + e['job'].result_stdout_text = e['text'] + e['job'].save() + ret.append(e) + return ret + + +formats = [ + ('json', 'application/json'), + ('ansi', 'text/plain'), + ('txt', 'text/plain'), + ('html', 'text/html'), +] + +@pytest.mark.parametrize("format,content_type", formats) +@pytest.mark.django_db +def test_project_update_redaction_enabled(get, format, content_type, test_cases, admin): + for test_data in test_cases: + job = test_data['project'] + response = get(reverse("api:project_update_stdout", args=(job.pk,)) + "?format=" + format, user=admin, expect=200, accept=content_type) + assert content_type in response['CONTENT-TYPE'] + print(response.data) + assert response.data is not None + content = response.data['content'] if format == 'json' else response.data + assert test_data['uri'].username not in content + assert test_data['uri'].password not in content + assert content.count(test_data['uri'].host) == test_data['occurrences'] + +@pytest.mark.parametrize("format,content_type", formats) +@pytest.mark.django_db +def test_job_redaction_disabled(get, format, content_type, negative_test_cases, admin): + for test_data in negative_test_cases: + job = test_data['job'] + response = get(reverse("api:job_stdout", args=(job.pk,)) + "?format=" + format, user=admin, expect=200, format=format) + content = response.data['content'] if format == 'json' else response.data + assert response.data is not None + assert test_data['uri'].username in content + assert test_data['uri'].password in content + + @pytest.mark.django_db def test_options_fields_choices(instance, options, user): @@ -14,4 +90,3 @@ def test_options_fields_choices(instance, options, user): assert UnifiedJob.LAUNCH_TYPE_CHOICES == response.data['actions']['GET']['launch_type']['choices'] assert 'choice' == response.data['actions']['GET']['status']['type'] assert UnifiedJob.STATUS_CHOICES == response.data['actions']['GET']['status']['choices'] - diff --git a/awx/main/tests/functional/conftest.py b/awx/main/tests/functional/conftest.py index c363d6f1d3..65ccb9a443 100644 --- a/awx/main/tests/functional/conftest.py +++ b/awx/main/tests/functional/conftest.py @@ -124,6 +124,12 @@ def project_factory(organization): return prj return factory +@pytest.fixture +def job_factory(job_template, admin): + def factory(job_template=job_template, initial_state='new', created_by=admin): + return job_template.create_job(created_by=created_by, status=initial_state) + return factory + @pytest.fixture def team_factory(organization): def factory(name): diff --git a/awx/main/tests/old/views.py b/awx/main/tests/old/views.py deleted file mode 100644 index 4ccd6f64ec..0000000000 --- a/awx/main/tests/old/views.py +++ /dev/null @@ -1,113 +0,0 @@ -# Django -from django.core.urlresolvers import reverse - -# Reuse Test code -from awx.main.tests.base import ( - BaseLiveServerTest, - QueueStartStopTestMixin, - URI, -) -from awx.main.models.projects import * # noqa - -__all__ = ['UnifiedJobStdoutRedactedTests'] - - -TEST_STDOUTS = [] -uri = URI(scheme="https", username="Dhh3U47nmC26xk9PKscV", password="PXPfWW8YzYrgS@E5NbQ2H@", host="github.ginger.com/theirrepo.git/info/refs") -TEST_STDOUTS.append({ - 'description': 'uri in a plain text document', - 'uri' : uri, - 'text' : 'hello world %s goodbye world' % uri, - 'occurrences' : 1 -}) - -uri = URI(scheme="https", username="applepie@@@", password="thatyouknow@@@@", host="github.ginger.com/theirrepo.git/info/refs") -TEST_STDOUTS.append({ - 'description': 'uri appears twice in a multiline plain text document', - 'uri' : uri, - 'text' : 'hello world %s \n\nyoyo\n\nhello\n%s' % (uri, uri), - 'occurrences' : 2 -}) - -class UnifiedJobStdoutRedactedTests(BaseLiveServerTest, QueueStartStopTestMixin): - - def setUp(self): - super(UnifiedJobStdoutRedactedTests, self).setUp() - self.setup_instances() - self.setup_users() - self.test_cases = [] - self.negative_test_cases = [] - - proj = self.make_project() - - for e in TEST_STDOUTS: - e['project'] = ProjectUpdate(project=proj) - e['project'].result_stdout_text = e['text'] - e['project'].save() - self.test_cases.append(e) - for d in TEST_STDOUTS: - d['job'] = self.make_job() - d['job'].result_stdout_text = d['text'] - d['job'].save() - self.negative_test_cases.append(d) - - # This is more of a functional test than a unit test. - # should filter out username and password - def check_sensitive_redacted(self, test_data, response): - uri = test_data['uri'] - self.assertIsNotNone(response['content']) - self.check_not_found(response['content'], uri.username, test_data['description']) - self.check_not_found(response['content'], uri.password, test_data['description']) - # Ensure the host didn't get redacted - self.check_found(response['content'], uri.host, test_data['occurrences'], test_data['description']) - - def check_sensitive_not_redacted(self, test_data, response): - uri = test_data['uri'] - self.assertIsNotNone(response['content']) - self.check_found(response['content'], uri.username, description=test_data['description']) - self.check_found(response['content'], uri.password, description=test_data['description']) - - def _get_url_job_stdout(self, job, url_base, format='json'): - formats = { - 'json': 'application/json', - 'ansi': 'text/plain', - 'txt': 'text/plain', - 'html': 'text/html', - } - content_type = formats[format] - project_update_stdout_url = reverse(url_base, args=(job.pk,)) + "?format=" + format - return self.get(project_update_stdout_url, expect=200, auth=self.get_super_credentials(), accept=content_type) - - def _test_redaction_enabled(self, format): - for test_data in self.test_cases: - response = self._get_url_job_stdout(test_data['project'], "api:project_update_stdout", format=format) - self.check_sensitive_redacted(test_data, response) - - def _test_redaction_disabled(self, format): - for test_data in self.negative_test_cases: - response = self._get_url_job_stdout(test_data['job'], "api:job_stdout", format=format) - self.check_sensitive_not_redacted(test_data, response) - - def test_project_update_redaction_enabled_json(self): - self._test_redaction_enabled('json') - - def test_project_update_redaction_enabled_ansi(self): - self._test_redaction_enabled('ansi') - - def test_project_update_redaction_enabled_html(self): - self._test_redaction_enabled('html') - - def test_project_update_redaction_enabled_txt(self): - self._test_redaction_enabled('txt') - - def test_job_redaction_disabled_json(self): - self._test_redaction_disabled('json') - - def test_job_redaction_disabled_ansi(self): - self._test_redaction_disabled('ansi') - - def test_job_redaction_disabled_html(self): - self._test_redaction_disabled('html') - - def test_job_redaction_disabled_txt(self): - self._test_redaction_disabled('txt') From fa58ca44b13ad4462adbee90f34663f3eabd0a0a Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Thu, 30 Jun 2016 11:52:56 -0400 Subject: [PATCH 006/123] Ported old/unified_jobs.py --- .../functional/api/test_unified_jobs_view.py | 3 +- awx/main/tests/old/unified_jobs.py | 55 ------------------- awx/main/tests/unit/test_unified_jobs.py | 48 ++++++++++++++++ 3 files changed, 50 insertions(+), 56 deletions(-) delete mode 100644 awx/main/tests/old/unified_jobs.py create mode 100644 awx/main/tests/unit/test_unified_jobs.py diff --git a/awx/main/tests/functional/api/test_unified_jobs_view.py b/awx/main/tests/functional/api/test_unified_jobs_view.py index 174bb60080..ed7034a28e 100644 --- a/awx/main/tests/functional/api/test_unified_jobs_view.py +++ b/awx/main/tests/functional/api/test_unified_jobs_view.py @@ -60,7 +60,6 @@ def test_project_update_redaction_enabled(get, format, content_type, test_cases, job = test_data['project'] response = get(reverse("api:project_update_stdout", args=(job.pk,)) + "?format=" + format, user=admin, expect=200, accept=content_type) assert content_type in response['CONTENT-TYPE'] - print(response.data) assert response.data is not None content = response.data['content'] if format == 'json' else response.data assert test_data['uri'].username not in content @@ -90,3 +89,5 @@ def test_options_fields_choices(instance, options, user): assert UnifiedJob.LAUNCH_TYPE_CHOICES == response.data['actions']['GET']['launch_type']['choices'] assert 'choice' == response.data['actions']['GET']['status']['type'] assert UnifiedJob.STATUS_CHOICES == response.data['actions']['GET']['status']['choices'] + + diff --git a/awx/main/tests/old/unified_jobs.py b/awx/main/tests/old/unified_jobs.py deleted file mode 100644 index dad9acb3ea..0000000000 --- a/awx/main/tests/old/unified_jobs.py +++ /dev/null @@ -1,55 +0,0 @@ -# Copyright (c) 2015 Ansible, Inc. -# All Rights Reserved - -# Python -import mock -from mock import Mock -from StringIO import StringIO -from django.utils.timezone import now - -# Django -from django.test import SimpleTestCase - -# AWX -from awx.main.models import * # noqa - -__all__ = ['UnifiedJobsUnitTest',] - -class UnifiedJobsUnitTest(SimpleTestCase): - - # stdout file present - @mock.patch('os.path.exists', return_value=True) - @mock.patch('codecs.open', return_value='my_file_handler') - def test_result_stdout_raw_handle_file__found(self, exists, open): - unified_job = UnifiedJob() - unified_job.result_stdout_file = 'dummy' - - with mock.patch('os.stat', return_value=Mock(st_size=1)): - result = unified_job.result_stdout_raw_handle() - - self.assertEqual(result, 'my_file_handler') - - # stdout file missing, job finished - @mock.patch('os.path.exists', return_value=False) - def test_result_stdout_raw_handle__missing(self, exists): - unified_job = UnifiedJob() - unified_job.result_stdout_file = 'dummy' - unified_job.finished = now() - - result = unified_job.result_stdout_raw_handle() - - self.assertIsInstance(result, StringIO) - self.assertEqual(result.read(), 'stdout capture is missing') - - # stdout file missing, job not finished - @mock.patch('os.path.exists', return_value=False) - def test_result_stdout_raw_handle__pending(self, exists): - unified_job = UnifiedJob() - unified_job.result_stdout_file = 'dummy' - unified_job.finished = None - - result = unified_job.result_stdout_raw_handle() - - self.assertIsInstance(result, StringIO) - self.assertEqual(result.read(), 'Waiting for results...') - diff --git a/awx/main/tests/unit/test_unified_jobs.py b/awx/main/tests/unit/test_unified_jobs.py new file mode 100644 index 0000000000..edd6978b47 --- /dev/null +++ b/awx/main/tests/unit/test_unified_jobs.py @@ -0,0 +1,48 @@ +# Copyright (c) 2015 Ansible, Inc. +# All Rights Reserved + +# Python +import mock +from mock import Mock +from StringIO import StringIO +from django.utils.timezone import now + +# AWX +from awx.main.models import UnifiedJob + + +# stdout file present +@mock.patch('os.path.exists', return_value=True) +@mock.patch('codecs.open', return_value='my_file_handler') +def test_result_stdout_raw_handle_file__found(exists, open): + unified_job = UnifiedJob() + unified_job.result_stdout_file = 'dummy' + + with mock.patch('os.stat', return_value=Mock(st_size=1)): + result = unified_job.result_stdout_raw_handle() + + assert result == 'my_file_handler' + +# stdout file missing, job finished +@mock.patch('os.path.exists', return_value=False) +def test_result_stdout_raw_handle__missing(exists): + unified_job = UnifiedJob() + unified_job.result_stdout_file = 'dummy' + unified_job.finished = now() + + result = unified_job.result_stdout_raw_handle() + + assert isinstance(result, StringIO) + assert result.read() == 'stdout capture is missing' + +# stdout file missing, job not finished +@mock.patch('os.path.exists', return_value=False) +def test_result_stdout_raw_handle__pending(exists): + unified_job = UnifiedJob() + unified_job.result_stdout_file = 'dummy' + unified_job.finished = None + + result = unified_job.result_stdout_raw_handle() + + assert isinstance(result, StringIO) + assert result.read() == 'Waiting for results...' From 6e022ae183daf0c36d7b00eabb726eb9a00168e8 Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Thu, 30 Jun 2016 12:04:23 -0400 Subject: [PATCH 007/123] Broke out URI test helper from test base.py --- awx/main/tests/URI.py | 43 +++++++++++++++++++++++++++++++++++++++ awx/main/tests/base.py | 46 +----------------------------------------- 2 files changed, 44 insertions(+), 45 deletions(-) create mode 100644 awx/main/tests/URI.py diff --git a/awx/main/tests/URI.py b/awx/main/tests/URI.py new file mode 100644 index 0000000000..d04da03436 --- /dev/null +++ b/awx/main/tests/URI.py @@ -0,0 +1,43 @@ +# Helps with test cases. +# Save all components of a uri (i.e. scheme, username, password, etc.) so that +# when we construct a uri string and decompose it, we can verify the decomposition +class URI(object): + DEFAULTS = { + 'scheme' : 'http', + 'username' : 'MYUSERNAME', + 'password' : 'MYPASSWORD', + 'host' : 'host.com', + } + + def __init__(self, description='N/A', scheme=DEFAULTS['scheme'], username=DEFAULTS['username'], password=DEFAULTS['password'], host=DEFAULTS['host']): + self.description = description + self.scheme = scheme + self.username = username + self.password = password + self.host = host + + def get_uri(self): + uri = "%s://" % self.scheme + if self.username: + uri += "%s" % self.username + if self.password: + uri += ":%s" % self.password + if (self.username or self.password) and self.host is not None: + uri += "@%s" % self.host + elif self.host is not None: + uri += "%s" % self.host + return uri + + def get_secret_count(self): + secret_count = 0 + if self.username: + secret_count += 1 + if self.password: + secret_count += 1 + return secret_count + + def __string__(self): + return self.get_uri() + + def __repr__(self): + return self.get_uri() diff --git a/awx/main/tests/base.py b/awx/main/tests/base.py index 6257e26438..cd3754b23f 100644 --- a/awx/main/tests/base.py +++ b/awx/main/tests/base.py @@ -35,6 +35,7 @@ from awx.main.management.commands.run_task_system import run_taskmanager from awx.main.utils import get_ansible_version from awx.main.task_engine import TaskEngager as LicenseWriter from awx.sso.backends import LDAPSettings +from awx.main.tests.URI import URI # noqa TEST_PLAYBOOK = '''- hosts: mygroup gather_facts: false @@ -732,48 +733,3 @@ class BaseJobExecutionTest(QueueStartStopTestMixin, BaseLiveServerTest): ''' Base class for celery task tests. ''' - -# Helps with test cases. -# Save all components of a uri (i.e. scheme, username, password, etc.) so that -# when we construct a uri string and decompose it, we can verify the decomposition -class URI(object): - DEFAULTS = { - 'scheme' : 'http', - 'username' : 'MYUSERNAME', - 'password' : 'MYPASSWORD', - 'host' : 'host.com', - } - - def __init__(self, description='N/A', scheme=DEFAULTS['scheme'], username=DEFAULTS['username'], password=DEFAULTS['password'], host=DEFAULTS['host']): - self.description = description - self.scheme = scheme - self.username = username - self.password = password - self.host = host - - def get_uri(self): - uri = "%s://" % self.scheme - if self.username: - uri += "%s" % self.username - if self.password: - uri += ":%s" % self.password - if (self.username or self.password) and self.host is not None: - uri += "@%s" % self.host - elif self.host is not None: - uri += "%s" % self.host - return uri - - def get_secret_count(self): - secret_count = 0 - if self.username: - secret_count += 1 - if self.password: - secret_count += 1 - return secret_count - - def __string__(self): - return self.get_uri() - - def __repr__(self): - return self.get_uri() - From d79188f865acd85826224afae1caea3d41c4bc2b Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Thu, 30 Jun 2016 12:04:51 -0400 Subject: [PATCH 008/123] Cleaned up unit/test_redact.py --- awx/main/tests/unit/test_redact.py | 52 ++++++------------------------ 1 file changed, 10 insertions(+), 42 deletions(-) diff --git a/awx/main/tests/unit/test_redact.py b/awx/main/tests/unit/test_redact.py index 47d6419642..3535869ee1 100644 --- a/awx/main/tests/unit/test_redact.py +++ b/awx/main/tests/unit/test_redact.py @@ -1,9 +1,8 @@ import textwrap -import re # AWX from awx.main.redact import UriCleaner -from awx.main.tests.base import URI +from awx.main.tests.URI import URI TEST_URIS = [ URI('no host', scheme='https', username='myusername', password='mypass', host=None), @@ -79,52 +78,21 @@ TEST_CLEARTEXT.append({ }) -def check_found(string, substr, count=-1, description=None, word_boundary=False): - if word_boundary: - count_actual = len(re.findall(r'\b%s\b' % re.escape(substr), string)) - else: - count_actual = string.count(substr) - - msg = '' - if description: - msg = 'Test "%s".\n' % description - if count == -1: - assert count_actual > 0 - else: - msg += 'Found %d occurances of "%s" instead of %d in: "%s"' % (count_actual, substr, count, string) - if count_actual != count: - raise Exception(msg) - -def check_not_found(string, substr, description=None, word_boundary=False): - if word_boundary: - count = len(re.findall(r'\b%s\b' % re.escape(substr), string)) - else: - count = string.find(substr) - if count == -1: - count = 0 - - msg = '' - if description: - msg = 'Test "%s".\n' % description - msg += '"%s" found in: "%s"' % (substr, string) - if count != 0: - raise Exception(msg) - # should redact sensitive usernames and passwords def test_uri_scm_simple_redacted(): for uri in TEST_URIS: redacted_str = UriCleaner.remove_sensitive(str(uri)) if uri.username: - check_not_found(redacted_str, uri.username, uri.description) + assert uri.username not in redacted_str if uri.password: - check_not_found(redacted_str, uri.password, uri.description) + assert uri.username not in redacted_str # should replace secret data with safe string, UriCleaner.REPLACE_STR def test_uri_scm_simple_replaced(): for uri in TEST_URIS: redacted_str = UriCleaner.remove_sensitive(str(uri)) - check_found(redacted_str, UriCleaner.REPLACE_STR, uri.get_secret_count()) + assert redacted_str.count(UriCleaner.REPLACE_STR) == uri.get_secret_count() # should redact multiple uris in text def test_uri_scm_multiple(): @@ -136,9 +104,9 @@ def test_uri_scm_multiple(): redacted_str = UriCleaner.remove_sensitive(str(uri)) if uri.username: - check_not_found(redacted_str, uri.username, uri.description) + assert uri.username not in redacted_str if uri.password: - check_not_found(redacted_str, uri.password, uri.description) + assert uri.username not in redacted_str # should replace multiple secret data with safe string def test_uri_scm_multiple_replaced(): @@ -153,14 +121,14 @@ def test_uri_scm_multiple_replaced(): find_count += uri.get_secret_count() redacted_str = UriCleaner.remove_sensitive(cleartext) - check_found(redacted_str, UriCleaner.REPLACE_STR, find_count) + assert redacted_str.count(UriCleaner.REPLACE_STR) == find_count # should redact and replace multiple secret data within a complex cleartext blob def test_uri_scm_cleartext_redact_and_replace(): for test_data in TEST_CLEARTEXT: uri = test_data['uri'] redacted_str = UriCleaner.remove_sensitive(test_data['text']) - check_not_found(redacted_str, uri.username, uri.description) - check_not_found(redacted_str, uri.password, uri.description) + assert uri.username not in redacted_str + assert uri.password not in redacted_str # Ensure the host didn't get redacted - check_found(redacted_str, uri.host, test_data['host_occurrences'], uri.description) + assert redacted_str.count(uri.host) == test_data['host_occurrences'] From 58c493653a0c8393c8979702903a8c2147f133a3 Mon Sep 17 00:00:00 2001 From: Jared Tabor Date: Wed, 6 Jul 2016 12:32:16 -0700 Subject: [PATCH 009/123] Cloud credential should not be required for manual groups the cloud credential-required flag was true for manual group types, which isn't required. --- .../src/inventories/manage/groups/groups-add.controller.js | 2 +- .../src/inventories/manage/groups/groups-edit.controller.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/awx/ui/client/src/inventories/manage/groups/groups-add.controller.js b/awx/ui/client/src/inventories/manage/groups/groups-add.controller.js index 7cc1920daf..179f460d91 100644 --- a/awx/ui/client/src/inventories/manage/groups/groups-add.controller.js +++ b/awx/ui/client/src/inventories/manage/groups/groups-add.controller.js @@ -118,7 +118,7 @@ $scope.group_by_choices = source === 'ec2' ? $scope.ec2_group_by : null; // azure_rm regions choices are keyed as "azure" in an OPTIONS request to the inventory_sources endpoint $scope.source_region_choices = source === 'azure_rm' ? $scope.azure_regions : $scope[source + '_regions']; - $scope.cloudCredentialRequired = source !== 'manual' && source !== 'custom' ? true : false; + $scope.cloudCredentialRequired = source !== '' && source !== 'custom' ? true : false; $scope.group_by = null; $scope.source_regions = null; $scope.credential = null; diff --git a/awx/ui/client/src/inventories/manage/groups/groups-edit.controller.js b/awx/ui/client/src/inventories/manage/groups/groups-edit.controller.js index 266eef430a..e7f7b67db3 100644 --- a/awx/ui/client/src/inventories/manage/groups/groups-edit.controller.js +++ b/awx/ui/client/src/inventories/manage/groups/groups-edit.controller.js @@ -117,7 +117,7 @@ // reset fields // azure_rm regions choices are keyed as "azure" in an OPTIONS request to the inventory_sources endpoint $scope.source_region_choices = source.value === 'azure_rm' ? $scope.azure_regions : $scope[source.value + '_regions']; - $scope.cloudCredentialRequired = source.value !== 'manual' && source.value !== 'custom' ? true : false; + $scope.cloudCredentialRequired = source.value !== '' && source.value !== 'custom' ? true : false; $scope.group_by = null; $scope.source_regions = null; $scope.credential = null; From 0c3ba0d6b3da1293db6ed84d62ec9e7d4b92e235 Mon Sep 17 00:00:00 2001 From: Jared Tabor Date: Wed, 6 Jul 2016 14:38:52 -0700 Subject: [PATCH 010/123] Activity Stream toggle should close AS Clicking the toggle while on the AS page wouldn't always remove the AS --- awx/ui/client/src/bread-crumb/bread-crumb.directive.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/awx/ui/client/src/bread-crumb/bread-crumb.directive.js b/awx/ui/client/src/bread-crumb/bread-crumb.directive.js index 1cf035550a..790ab24674 100644 --- a/awx/ui/client/src/bread-crumb/bread-crumb.directive.js +++ b/awx/ui/client/src/bread-crumb/bread-crumb.directive.js @@ -6,7 +6,7 @@ export default templateUrl: templateUrl('bread-crumb/bread-crumb'), link: function(scope) { - var streamConfig = {}; + var streamConfig = {}, originalRoute; scope.showActivityStreamButton = false; scope.showRefreshButton = false; @@ -30,7 +30,7 @@ export default stateGoParams.id = $state.params[streamConfig.activityStreamId]; } } - + originalRoute = $state.current; $state.go('activityStream', stateGoParams); } // The user is navigating away from the activity stream - take them back from whence they came @@ -38,7 +38,10 @@ export default // Pull the previous state out of local storage var previousState = Store('previous_state'); - if(previousState && !Empty(previousState.name)) { + if(originalRoute) { + $state.go(originalRoute.name, originalRoute.fromParams); + } + else if(previousState && !Empty(previousState.name)) { $state.go(previousState.name, previousState.fromParams); } else { From e90f744e30b6621ab22b82958bb57af330d77862 Mon Sep 17 00:00:00 2001 From: Ken Hoes Date: Thu, 7 Jul 2016 10:49:08 -0400 Subject: [PATCH 011/123] Edit indicator removed after changing URLS and making new object --- awx/ui/client/src/app.js | 1 + 1 file changed, 1 insertion(+) diff --git a/awx/ui/client/src/app.js b/awx/ui/client/src/app.js index 8300d7d09c..2229f4ca77 100644 --- a/awx/ui/client/src/app.js +++ b/awx/ui/client/src/app.js @@ -859,6 +859,7 @@ var tower = angular.module('Tower', [ $rootScope.$broadcast("EditIndicatorChange", list, id); } else if ($rootScope.addedAnItem) { delete $rootScope.addedAnItem; + $rootScope.$broadcast("RemoveIndicator"); } else { $rootScope.$broadcast("RemoveIndicator"); } From 7732e1168fc9c9e2acd7f89b254f814e0076d679 Mon Sep 17 00:00:00 2001 From: Bill Nottingham Date: Thu, 7 Jul 2016 11:18:16 -0400 Subject: [PATCH 012/123] Rename 'Host Pattern' to 'Limit', and fix paste-o in 'Verbosity' popover title. (#2776) --- awx/ui/client/src/inventories/manage/adhoc/adhoc.form.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/awx/ui/client/src/inventories/manage/adhoc/adhoc.form.js b/awx/ui/client/src/inventories/manage/adhoc/adhoc.form.js index 13e74be4d9..20c1aa6941 100644 --- a/awx/ui/client/src/inventories/manage/adhoc/adhoc.form.js +++ b/awx/ui/client/src/inventories/manage/adhoc/adhoc.form.js @@ -44,7 +44,7 @@ export default function() { autocomplete: false }, limit: { - label: 'Host Pattern', + label: 'Limit', type: 'text', addRequired: false, awPopOver: '

The pattern used to target hosts in the ' + @@ -54,7 +54,7 @@ export default function() { 'here.

', - dataTitle: 'Host Pattern', + dataTitle: 'Limit', dataPlacement: 'right', dataContainer: 'body' }, @@ -98,7 +98,7 @@ export default function() { addRequired: true, awPopOver:'

These are the verbosity levels for standard ' + 'out of the command run that are supported.', - dataTitle: 'Module', + dataTitle: 'Verbosity', dataPlacement: 'right', dataContainer: 'body', "default": 1 From 55730f873094f5d910124584d1188ea58f02adf8 Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Thu, 7 Jul 2016 11:58:38 -0400 Subject: [PATCH 013/123] Ported old/organizations.py tests --- .../functional/api/test_organizations.py | 182 ++++++++ .../tests/functional/test_auth_token_limit.py | 39 ++ awx/main/tests/old/organizations.py | 413 ------------------ 3 files changed, 221 insertions(+), 413 deletions(-) create mode 100644 awx/main/tests/functional/api/test_organizations.py create mode 100644 awx/main/tests/functional/test_auth_token_limit.py delete mode 100644 awx/main/tests/old/organizations.py diff --git a/awx/main/tests/functional/api/test_organizations.py b/awx/main/tests/functional/api/test_organizations.py new file mode 100644 index 0000000000..8a9da1c662 --- /dev/null +++ b/awx/main/tests/functional/api/test_organizations.py @@ -0,0 +1,182 @@ +# Copyright (c) 2015 Ansible, Inc. +# All Rights Reserved. + +# Python +import pytest + +# Django +from django.core.urlresolvers import reverse + +# AWX +from awx.main.models import * # noqa + + +@pytest.mark.django_db +def test_organization_list_access_tests(options, head, get, admin, alice): + options(reverse('api:organization_list'), user=admin, expect=200) + head(reverse('api:organization_list'), user=admin, expect=200) + get(reverse('api:organization_list'), user=admin, expect=200) + options(reverse('api:organization_list'), user=alice, expect=200) + head(reverse('api:organization_list'), user=alice, expect=200) + get(reverse('api:organization_list'), user=alice, expect=200) + options(reverse('api:organization_list'), user=None, expect=401) + head(reverse('api:organization_list'), user=None, expect=401) + get(reverse('api:organization_list'), user=None, expect=401) + + +@pytest.mark.django_db +def test_organization_access_tests(organization, get, admin, alice, bob): + organization.member_role.members.add(alice) + get(reverse('api:organization_detail', args=(organization.id,)), user=admin, expect=200) + get(reverse('api:organization_detail', args=(organization.id,)), user=alice, expect=200) + get(reverse('api:organization_detail', args=(organization.id,)), user=bob, expect=403) + get(reverse('api:organization_detail', args=(organization.id,)), user=None, expect=401) + + +@pytest.mark.django_db +def test_organization_list_integrity(organization, get, admin, alice): + res = get(reverse('api:organization_list'), user=admin) + for field in ['id', 'url', 'name', 'description', 'created']: + assert field in res.data['results'][0] + + +@pytest.mark.django_db +def test_organization_list_visibility(organizations, get, admin, alice): + orgs = organizations(2) + + res = get(reverse('api:organization_list'), user=admin) + assert res.data['count'] == 2 + assert len(res.data['results']) == 2 + + res = get(reverse('api:organization_list'), user=alice) + assert res.data['count'] == 0 + + orgs[1].member_role.members.add(alice) + + res = get(reverse('api:organization_list'), user=alice) + assert res.data['count'] == 1 + assert len(res.data['results']) == 1 + assert res.data['results'][0]['id'] == orgs[1].id + + +@pytest.mark.django_db +def test_organization_project_list(organization, project_factory, get, alice, bob, rando): + prj1 = project_factory('project-one') + project_factory('project-two') + organization.admin_role.members.add(alice) + organization.member_role.members.add(bob) + prj1.use_role.members.add(bob) + assert get(reverse('api:organization_projects_list', args=(organization.id,)), user=alice).data['count'] == 2 + assert get(reverse('api:organization_projects_list', args=(organization.id,)), user=bob).data['count'] == 1 + assert get(reverse('api:organization_projects_list', args=(organization.id,)), user=rando).status_code == 403 + + +@pytest.mark.django_db +def test_organization_user_list(organization, get, admin, alice, bob): + organization.admin_role.members.add(alice) + organization.member_role.members.add(alice) + organization.member_role.members.add(bob) + assert get(reverse('api:organization_users_list', args=(organization.id,)), user=admin).data['count'] == 2 + assert get(reverse('api:organization_users_list', args=(organization.id,)), user=alice).data['count'] == 2 + assert get(reverse('api:organization_users_list', args=(organization.id,)), user=bob).data['count'] == 2 + assert get(reverse('api:organization_admins_list', args=(organization.id,)), user=admin).data['count'] == 1 + assert get(reverse('api:organization_admins_list', args=(organization.id,)), user=alice).data['count'] == 1 + assert get(reverse('api:organization_admins_list', args=(organization.id,)), user=bob).data['count'] == 1 + + +@pytest.mark.django_db +def test_organization_inventory_list(organization, inventory_factory, get, alice, bob, rando): + inv1 = inventory_factory('inventory-one') + inventory_factory('inventory-two') + organization.admin_role.members.add(alice) + organization.member_role.members.add(bob) + inv1.use_role.members.add(bob) + assert get(reverse('api:organization_inventories_list', args=(organization.id,)), user=alice).data['count'] == 2 + assert get(reverse('api:organization_inventories_list', args=(organization.id,)), user=bob).data['count'] == 1 + get(reverse('api:organization_inventories_list', args=(organization.id,)), user=rando, expect=403) + + +@pytest.mark.django_db +def test_create_organization(post, admin, alice): + new_org = { + 'name': 'new org', + 'description': 'my description' + } + res = post(reverse('api:organization_list'), new_org, user=admin, expect=201) + assert res.data['name'] == new_org['name'] + res = post(reverse('api:organization_list'), new_org, user=admin, expect=400) + + +@pytest.mark.django_db +def test_create_organization_xfail(post, alice): + new_org = { + 'name': 'new org', + 'description': 'my description' + } + post(reverse('api:organization_list'), new_org, user=alice, expect=403) + + +@pytest.mark.django_db +def test_add_user_to_organization(post, organization, alice, bob): + organization.admin_role.members.add(alice) + post(reverse('api:organization_users_list', args=(organization.id,)), {'id': bob.id}, user=alice, expect=204) + assert bob in organization.member_role + post(reverse('api:organization_users_list', args=(organization.id,)), {'id': bob.id, 'disassociate': True} , user=alice, expect=204) + assert bob not in organization.member_role + + +@pytest.mark.django_db +def test_add_user_to_organization_xfail(post, organization, alice, bob): + organization.member_role.members.add(alice) + post(reverse('api:organization_users_list', args=(organization.id,)), {'id': bob.id}, user=alice, expect=403) + + +@pytest.mark.django_db +def test_add_admin_to_organization(post, organization, alice, bob): + organization.admin_role.members.add(alice) + post(reverse('api:organization_admins_list', args=(organization.id,)), {'id': bob.id}, user=alice, expect=204) + assert bob in organization.admin_role + assert bob in organization.member_role + post(reverse('api:organization_admins_list', args=(organization.id,)), {'id': bob.id, 'disassociate': True} , user=alice, expect=204) + assert bob not in organization.admin_role + assert bob not in organization.member_role + + +@pytest.mark.django_db +def test_add_admin_to_organization_xfail(post, organization, alice, bob): + organization.member_role.members.add(alice) + post(reverse('api:organization_admins_list', args=(organization.id,)), {'id': bob.id}, user=alice, expect=403) + + +@pytest.mark.django_db +def test_update_organization(get, put, organization, alice, bob): + organization.admin_role.members.add(alice) + data = get(reverse('api:organization_detail', args=(organization.id,)), user=alice, expect=200).data + data['description'] = 'hi' + put(reverse('api:organization_detail', args=(organization.id,)), data, user=alice, expect=200) + organization.refresh_from_db() + assert organization.description == 'hi' + data['description'] = 'bye' + put(reverse('api:organization_detail', args=(organization.id,)), data, user=bob, expect=403) + + +@pytest.mark.django_db +def test_delete_organization(delete, organization, admin): + delete(reverse('api:organization_detail', args=(organization.id,)), user=admin, expect=204) + + +@pytest.mark.django_db +def test_delete_organization2(delete, organization, alice): + organization.admin_role.members.add(alice) + delete(reverse('api:organization_detail', args=(organization.id,)), user=alice, expect=204) + + +@pytest.mark.django_db +def test_delete_organization_xfail1(delete, organization, alice): + organization.member_role.members.add(alice) + delete(reverse('api:organization_detail', args=(organization.id,)), user=alice, expect=403) + + +@pytest.mark.django_db +def test_delete_organization_xfail2(delete, organization): + delete(reverse('api:organization_detail', args=(organization.id,)), user=None, expect=401) diff --git a/awx/main/tests/functional/test_auth_token_limit.py b/awx/main/tests/functional/test_auth_token_limit.py new file mode 100644 index 0000000000..bbe30320c4 --- /dev/null +++ b/awx/main/tests/functional/test_auth_token_limit.py @@ -0,0 +1,39 @@ +import pytest +from datetime import timedelta + +from django.utils.timezone import now as tz_now +from django.test.utils import override_settings + +from awx.main.models import AuthToken, User + + +@override_settings(AUTH_TOKEN_PER_USER=3) +@pytest.mark.django_db +def test_get_tokens_over_limit(): + now = tz_now() + # Times are relative to now + # (key, created on in seconds , expiration in seconds) + test_data = [ + # a is implicitly expired + ("a", -1000, -10), + # b's are invalid due to session limit of 3 + ("b", -100, 60), + ("bb", -100, 60), + ("c", -90, 70), + ("d", -80, 80), + ("e", -70, 90), + ] + user = User.objects.create_superuser('admin', 'foo@bar.com', 'password') + for key, t_create, t_expire in test_data: + AuthToken.objects.create( + user=user, + key=key, + request_hash='this_is_a_hash', + created=now + timedelta(seconds=t_create), + expires=now + timedelta(seconds=t_expire), + ) + invalid_tokens = AuthToken.get_tokens_over_limit(user, now=now) + invalid_keys = [x.key for x in invalid_tokens] + assert len(invalid_keys) == 2 + assert 'b' in invalid_keys + assert 'bb' in invalid_keys diff --git a/awx/main/tests/old/organizations.py b/awx/main/tests/old/organizations.py deleted file mode 100644 index 136e2603cc..0000000000 --- a/awx/main/tests/old/organizations.py +++ /dev/null @@ -1,413 +0,0 @@ -# Copyright (c) 2015 Ansible, Inc. -# All Rights Reserved. - -# Python -from datetime import timedelta - -# Django -from django.core.urlresolvers import reverse -from django.test.utils import override_settings -from django.contrib.auth.models import User -from django.utils.timezone import now as tz_now - -# AWX -from awx.main.models import * # noqa -from awx.main.tests.base import BaseTest - -__all__ = ['AuthTokenLimitUnitTest', 'OrganizationsTest'] - -class AuthTokenLimitUnitTest(BaseTest): - - def setUp(self): - self.now = tz_now() - # Times are relative to now - # (key, created on in seconds , expiration in seconds) - self.test_data = [ - # a is implicitly expired - ("a", -1000, -10), - # b's are invalid due to session limit of 3 - ("b", -100, 60), - ("bb", -100, 60), - ("c", -90, 70), - ("d", -80, 80), - ("e", -70, 90), - ] - self.user = User.objects.create_superuser('admin', 'foo@bar.com', 'password') - for key, t_create, t_expire in self.test_data: - AuthToken.objects.create( - user=self.user, - key=key, - request_hash='this_is_a_hash', - created=self.now + timedelta(seconds=t_create), - expires=self.now + timedelta(seconds=t_expire), - ) - super(AuthTokenLimitUnitTest, self).setUp() - - @override_settings(AUTH_TOKEN_PER_USER=3) - def test_get_tokens_over_limit(self): - invalid_tokens = AuthToken.get_tokens_over_limit(self.user, now=self.now) - invalid_keys = [x.key for x in invalid_tokens] - self.assertEqual(len(invalid_keys), 2) - self.assertIn('b', invalid_keys) - self.assertIn('bb', invalid_keys) - -class OrganizationsTest(BaseTest): - - def collection(self): - return reverse('api:organization_list') - - def setUp(self): - super(OrganizationsTest, self).setUp() - self.setup_instances() - # TODO: Test non-enterprise license - self.create_test_license_file() - self.setup_users() - - self.organizations = self.make_organizations(self.super_django_user, 10) - self.projects = self.make_projects(self.normal_django_user, 10) - - # add projects to organizations in a more or less arbitrary way - for project in self.projects[0:2]: - self.organizations[0].projects.add(project) - for project in self.projects[3:8]: - self.organizations[1].projects.add(project) - for project in self.projects[9:10]: - self.organizations[2].projects.add(project) - self.organizations[0].projects.add(self.projects[-1]) - self.organizations[9].projects.add(self.projects[-2]) - - # get the URL for various organization records - self.a_detail_url = "%s%s" % (self.collection(), self.organizations[0].pk) - self.b_detail_url = "%s%s" % (self.collection(), self.organizations[1].pk) - self.c_detail_url = "%s%s" % (self.collection(), self.organizations[2].pk) - - # configuration: - # admin_user is an admin and regular user in all organizations - # other_user is all organizations - # normal_user is a user in organization 0, and an admin of organization 1 - # nobody_user is a user not a member of any organizations - - for x in self.organizations: - x.admin_role.members.add(self.super_django_user) - x.member_role.members.add(self.super_django_user) - x.member_role.members.add(self.other_django_user) - - self.organizations[0].member_role.members.add(self.normal_django_user) - self.organizations[1].admin_role.members.add(self.normal_django_user) - - def test_get_organization_list(self): - url = reverse('api:organization_list') - - # no credentials == 401 - self.options(url, expect=401) - self.head(url, expect=401) - self.get(url, expect=401) - - # wrong credentials == 401 - with self.current_user(self.get_invalid_credentials()): - self.options(url, expect=401) - self.head(url, expect=401) - self.get(url, expect=401) - - # superuser credentials == 200, full list - with self.current_user(self.super_django_user): - self.options(url, expect=200) - self.head(url, expect=200) - response = self.get(url, expect=200) - self.check_pagination_and_size(response, 10, previous=None, next=None) - self.assertEqual(len(response['results']), - Organization.objects.count()) - for field in ['id', 'url', 'name', 'description', 'created']: - self.assertTrue(field in response['results'][0], - 'field %s not in result' % field) - - # check that the related URL functionality works - related = response['results'][0]['related'] - for x in ['projects', 'users', 'admins']: - self.assertTrue(x in related and related[x].endswith("/%s/" % x), "looking for %s in related" % x) - - # normal credentials == 200, get only organizations of which user is a member - with self.current_user(self.normal_django_user): - self.options(url, expect=200) - self.head(url, expect=200) - response = self.get(url, expect=200) - self.check_pagination_and_size(response, 2, previous=None, next=None) - - # no admin rights? get empty list - with self.current_user(self.other_django_user): - response = self.get(url, expect=200) - self.check_pagination_and_size(response, len(self.organizations), previous=None, next=None) - - # not a member of any orgs? get empty list - with self.current_user(self.nobody_django_user): - response = self.get(url, expect=200) - self.check_pagination_and_size(response, 0, previous=None, next=None) - - def test_get_item(self): - - # first get all the URLs - data = self.get(self.collection(), expect=200, auth=self.get_super_credentials()) - urls = [item['url'] for item in data['results']] - - # make sure super user can fetch records - data = self.get(urls[0], expect=200, auth=self.get_super_credentials()) - [self.assertTrue(key in data) for key in ['name', 'description', 'url']] - - # make sure invalid user cannot - data = self.get(urls[0], expect=401, auth=self.get_invalid_credentials()) - - # normal user should be able to get org 0 and org 1 but not org 9 (as he's not a user or admin of it) - data = self.get(urls[0], expect=200, auth=self.get_normal_credentials()) - data = self.get(urls[1], expect=200, auth=self.get_normal_credentials()) - data = self.get(urls[9], expect=403, auth=self.get_normal_credentials()) - - # other user is a member, but not admin, can access org - data = self.get(urls[0], expect=200, auth=self.get_other_credentials()) - - # nobody user is not a member, cannot access org - data = self.get(urls[0], expect=403, auth=self.get_nobody_credentials()) - - def test_get_item_subobjects_projects(self): - - # first get all the orgs - orgs = self.get(self.collection(), expect=200, auth=self.get_super_credentials()) - - # find projects attached to the first org - projects0_url = orgs['results'][0]['related']['projects'] - projects1_url = orgs['results'][1]['related']['projects'] - projects9_url = orgs['results'][9]['related']['projects'] - - self.get(projects0_url, expect=401, auth=None) - self.get(projects0_url, expect=401, auth=self.get_invalid_credentials()) - - # normal user is just a member of the first org, so can see all projects under the org - self.get(projects0_url, expect=200, auth=self.get_normal_credentials()) - - # however in the second org, he's an admin and should see all of them - projects1a = self.get(projects1_url, expect=200, auth=self.get_normal_credentials()) - self.assertEquals(projects1a['count'], 5) - - # but the non-admin cannot access the list of projects in the org. He should use /projects/ instead! - self.get(projects1_url, expect=200, auth=self.get_other_credentials()) - - # superuser should be able to read anything - projects9a = self.get(projects9_url, expect=200, auth=self.get_super_credentials()) - self.assertEquals(projects9a['count'], 1) - - # nobody user is not a member of any org, so can't see projects... - self.get(projects0_url, expect=403, auth=self.get_nobody_credentials()) - projects1a = self.get(projects1_url, expect=403, auth=self.get_nobody_credentials()) - - def test_get_item_subobjects_users(self): - - # see if we can list the users added to the organization - orgs = self.get(self.collection(), expect=200, auth=self.get_super_credentials()) - org1_users_url = orgs['results'][1]['related']['users'] - org1_users = self.get(org1_users_url, expect=200, auth=self.get_normal_credentials()) - self.assertEquals(org1_users['count'], 2) - org1_users = self.get(org1_users_url, expect=200, auth=self.get_super_credentials()) - self.assertEquals(org1_users['count'], 2) - org1_users = self.get(org1_users_url, expect=200, auth=self.get_other_credentials()) - self.assertEquals(org1_users['count'], 2) - - def test_get_item_subobjects_admins(self): - - # see if we can list the users added to the organization - orgs = self.get(self.collection(), expect=200, auth=self.get_super_credentials()) - org1_users_url = orgs['results'][1]['related']['admins'] - org1_users = self.get(org1_users_url, expect=200, auth=self.get_normal_credentials()) - self.assertEquals(org1_users['count'], 2) - org1_users = self.get(org1_users_url, expect=200, auth=self.get_super_credentials()) - self.assertEquals(org1_users['count'], 2) - - def test_get_organization_inventories_list(self): - pass - - def _test_get_item_subobjects_tags(self): - # FIXME: Update to support taggit! - - # put some tags on the org - org1 = Organization.objects.get(pk=2) - tag1 = Tag.objects.create(name='atag') - tag2 = Tag.objects.create(name='btag') - org1.tags.add(tag1) - org1.tags.add(tag2) - - # see if we can list the users added to the organization - orgs = self.get(self.collection(), expect=200, auth=self.get_super_credentials()) - org1_tags_url = orgs['results'][1]['related']['tags'] - org1_tags = self.get(org1_tags_url, expect=200, auth=self.get_normal_credentials()) - self.assertEquals(org1_tags['count'], 2) - org1_tags = self.get(org1_tags_url, expect=200, auth=self.get_super_credentials()) - self.assertEquals(org1_tags['count'], 2) - org1_tags = self.get(org1_tags_url, expect=403, auth=self.get_other_credentials()) - - def _test_get_item_subobjects_audit_trail(self): - # FIXME: Update to support whatever audit trail framework is used. - url = '/api/v1/organizations/2/audit_trail/' - self.get(url, expect=200, auth=self.get_normal_credentials()) - # FIXME: verify that some audit trail records are auto-created on save AND post - - def test_post_item(self): - - new_org = dict(name='magic test org', description='8675309') - - # need to be a valid user - self.post(self.collection(), new_org, expect=401, auth=None) - self.post(self.collection(), new_org, expect=401, auth=self.get_invalid_credentials()) - - # only super users can create organizations - self.post(self.collection(), new_org, expect=403, auth=self.get_normal_credentials()) - self.post(self.collection(), new_org, expect=403, auth=self.get_other_credentials()) - data1 = self.post(self.collection(), new_org, expect=201, auth=self.get_super_credentials()) - - # duplicate post results in 400 - response = self.post(self.collection(), new_org, expect=400, auth=self.get_super_credentials()) - self.assertTrue('name' in response, response) - self.assertTrue('Name' in response['name'][0], response) - - # look at what we got back from the post, make sure we added an org - last_org = Organization.objects.order_by('-pk')[0] - self.assertTrue(data1['url'].endswith("/%d/" % last_org.pk)) - - # Test that not even super users can create an organization with a basic license - self.create_basic_license_file() - cant_org = dict(name='silly user org', description='4815162342') - self.post(self.collection(), cant_org, expect=402, auth=self.get_super_credentials()) - - def test_post_item_subobjects_users(self): - - url = reverse('api:organization_users_list', args=(self.organizations[1].pk,)) - users = self.get(url, expect=200, auth=self.get_normal_credentials()) - self.assertEqual(users['count'], 2) - self.post(url, dict(id=self.normal_django_user.pk), expect=204, auth=self.get_normal_credentials()) - users = self.get(url, expect=200, auth=self.get_normal_credentials()) - self.assertEqual(users['count'], 3) - self.post(url, dict(id=self.normal_django_user.pk, disassociate=True), expect=204, auth=self.get_normal_credentials()) - users = self.get(url, expect=200, auth=self.get_normal_credentials()) - self.assertEqual(users['count'], 2) - - # post a completely new user to verify we can add users to the subcollection directly - new_user = dict(username='NewUser9000', password='NewPassword9000') - which_org = Organization.accessible_objects(self.normal_django_user, 'admin_role')[0] - url = reverse('api:organization_users_list', args=(which_org.pk,)) - self.post(url, new_user, expect=201, auth=self.get_normal_credentials()) - - all_users = self.get(url, expect=200, auth=self.get_normal_credentials()) - self.assertEqual(all_users['count'], 3) - - def test_post_item_subobjects_admins(self): - - url = reverse('api:organization_admins_list', args=(self.organizations[1].pk,)) - admins = self.get(url, expect=200, auth=self.get_normal_credentials()) - self.assertEqual(admins['count'], 2) - self.post(url, dict(id=self.other_django_user.pk), expect=204, auth=self.get_normal_credentials()) - admins = self.get(url, expect=200, auth=self.get_normal_credentials()) - self.assertEqual(admins['count'], 3) - self.post(url, dict(id=self.other_django_user.pk, disassociate=1), expect=204, auth=self.get_normal_credentials()) - admins = self.get(url, expect=200, auth=self.get_normal_credentials()) - self.assertEqual(admins['count'], 2) - - def _test_post_item_subobjects_tags(self): - # FIXME: Update to support taggit! - - tag = Tag.objects.create(name='blippy') - url = '/api/v1/organizations/2/tags/' - tags = self.get(url, expect=200, auth=self.get_normal_credentials()) - self.assertEqual(tags['count'], 0) - self.post(url, dict(id=tag.pk), expect=204, auth=self.get_normal_credentials()) - tags = self.get(url, expect=200, auth=self.get_normal_credentials()) - self.assertEqual(tags['count'], 1) - self.assertEqual(tags['results'][0]['id'], tag.pk) - self.post(url, dict(id=tag.pk, disassociate=1), expect=204, auth=self.get_normal_credentials()) - tags = self.get(url, expect=200, auth=self.get_normal_credentials()) - self.assertEqual(tags['count'], 0) - - def _test_post_item_subobjects_audit_trail(self): - # FIXME: Update to support whatever audit trail framework is used. - # audit trails are system things, and no user can post to them. - url = '/api/v1/organizations/2/audit_trail/' - self.post(url, dict(id=1), expect=405, auth=self.get_super_credentials()) - - def test_put_item(self): - - # first get some urls and data to put back to them - urls = self.get_urls(self.collection(), auth=self.get_super_credentials()) - self.get(urls[0], expect=200, auth=self.get_super_credentials()) - data1 = self.get(urls[1], expect=200, auth=self.get_super_credentials()) - - # test that an unauthenticated user cannot do a put - new_data1 = data1.copy() - new_data1['description'] = 'updated description' - self.put(urls[0], new_data1, expect=401, auth=None) - self.put(urls[0], new_data1, expect=401, auth=self.get_invalid_credentials()) - - # user normal is an admin of org 0 and a member of org 1 so should be able to put only org 1 - self.put(urls[0], new_data1, expect=403, auth=self.get_normal_credentials()) - self.put(urls[1], new_data1, expect=200, auth=self.get_normal_credentials()) - - # get back org 1 and see if it changed - get_result = self.get(urls[1], expect=200, auth=self.get_normal_credentials()) - self.assertEquals(get_result['description'], 'updated description') - - # super user can also put even though they aren't added to the org users or admins list - self.put(urls[1], new_data1, expect=200, auth=self.get_super_credentials()) - - # make sure posting to this URL is not supported - self.post(urls[1], new_data1, expect=405, auth=self.get_super_credentials()) - - def test_put_item_subobjects_projects(self): - - # any attempt to put a subobject should be a 405, edit the actual resource or POST with 'disassociate' to delete - # this is against a collection URL anyway, so we really need not repeat this test for other object types - # as a PUT against a collection doesn't make much sense. - - orgs = self.get(self.collection(), expect=200, auth=self.get_super_credentials()) - projects0_url = orgs['results'][0]['related']['projects'] - sub_projects = self.get(projects0_url, expect=200, auth=self.get_super_credentials()) - self.assertEquals(sub_projects['count'], 3) - first_sub_project = sub_projects['results'][0] - self.put(projects0_url, first_sub_project, expect=405, auth=self.get_super_credentials()) - - def test_delete_item(self): - - # first get some urls - urls = self.get_urls(self.collection(), auth=self.get_super_credentials()) - urldata1 = self.get(urls[1], auth=self.get_super_credentials()) - - # check authentication -- admins of the org and superusers can delete objects only - self.delete(urls[0], expect=401, auth=None) - self.delete(urls[0], expect=401, auth=self.get_invalid_credentials()) - self.delete(urls[8], expect=403, auth=self.get_normal_credentials()) - self.delete(urls[1], expect=204, auth=self.get_normal_credentials()) - self.delete(urls[0], expect=204, auth=self.get_super_credentials()) - - # check that when we have deleted an object it comes back 404 via GET - self.get(urls[1], expect=404, auth=self.get_normal_credentials()) - assert Organization.objects.filter(pk=urldata1['id']).count() == 0 - - # also check that DELETE on the collection doesn't work - self.delete(self.collection(), expect=405, auth=self.get_super_credentials()) - - # Test that not even super users can delete an organization with a basic license - self.create_basic_license_file() - self.delete(urls[2], expect=402, auth=self.get_super_credentials()) - - def test_invalid_post_data(self): - url = reverse('api:organization_list') - # API should gracefully handle data of an invalid type. - self.post(url, expect=400, data=None, auth=self.get_super_credentials()) - self.post(url, expect=400, data=99, auth=self.get_super_credentials()) - self.post(url, expect=400, data='abcd', auth=self.get_super_credentials()) - self.post(url, expect=400, data=3.14, auth=self.get_super_credentials()) - self.post(url, expect=400, data=True, auth=self.get_super_credentials()) - self.post(url, expect=400, data=[1,2,3], auth=self.get_super_credentials()) - url = reverse('api:organization_users_list', args=(self.organizations[0].pk,)) - self.post(url, expect=400, data=None, auth=self.get_super_credentials()) - self.post(url, expect=400, data=99, auth=self.get_super_credentials()) - self.post(url, expect=400, data='abcd', auth=self.get_super_credentials()) - self.post(url, expect=400, data=3.14, auth=self.get_super_credentials()) - self.post(url, expect=400, data=True, auth=self.get_super_credentials()) - self.post(url, expect=400, data=[1,2,3], auth=self.get_super_credentials()) - -# TODO: tests for tag disassociation From 735a75729fb4e831365923833453aae0d4ca1169 Mon Sep 17 00:00:00 2001 From: Chris Meyers Date: Thu, 7 Jul 2016 11:59:09 -0400 Subject: [PATCH 014/123] Revert "fixes Save with update_fields did not affect any rows" This reverts commit c6d282fce27d23317d2649ccf00b75799dad1a86. This didn't fix the underlying issue. The issue is that the inventory object may have been deleted by the time save is called. --- awx/main/models/inventory.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/awx/main/models/inventory.py b/awx/main/models/inventory.py index b51e558a8b..8dde9f3b3b 100644 --- a/awx/main/models/inventory.py +++ b/awx/main/models/inventory.py @@ -309,8 +309,7 @@ class Inventory(CommonModel, ResourceMixin): else: computed_fields.pop(field) if computed_fields: - if len(computed_fields) > 0: - iobj.save(update_fields=computed_fields.keys()) + iobj.save(update_fields=computed_fields.keys()) logger.debug("Finished updating inventory computed fields") @property From 8bc4e2e06306166b85acd3428fe064cf2fb2bcc2 Mon Sep 17 00:00:00 2001 From: Leigh Johnson Date: Thu, 7 Jul 2016 12:53:35 -0400 Subject: [PATCH 015/123] re-init activity data on sort/filter/paginate changes, resolves #2795 --- .../src/smart-status/smart-status.controller.js | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/awx/ui/client/src/smart-status/smart-status.controller.js b/awx/ui/client/src/smart-status/smart-status.controller.js index 00343eafd6..e222509d30 100644 --- a/awx/ui/client/src/smart-status/smart-status.controller.js +++ b/awx/ui/client/src/smart-status/smart-status.controller.js @@ -7,13 +7,13 @@ export default ['$scope', '$filter', function ($scope, $filter) { - var recentJobs = $scope.jobs; - function isFailureState(status) { return status === 'failed' || status === 'error' || status === 'canceled'; } - var sparkData = + function init(){ + var recentJobs = $scope.jobs; + var sparkData = _.sortBy(recentJobs.map(function(job) { var data = {}; @@ -36,7 +36,12 @@ export default ['$scope', '$filter', return data; }), "sortDate").reverse(); - $scope.sparkArray = sparkData; + $scope.sparkArray = sparkData; + } + $scope.$watchCollection('jobs', function(){ + init(); + }); + }]; // From 4abae312f1580407f977d1955363a732f66a8ecd Mon Sep 17 00:00:00 2001 From: Jared Tabor Date: Thu, 7 Jul 2016 10:12:36 -0700 Subject: [PATCH 016/123] adding "missing" status for project status tooltip the tooltip for "missing" projects read "Failed" instead of "Missing" --- awx/ui/client/src/helpers/Projects.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/awx/ui/client/src/helpers/Projects.js b/awx/ui/client/src/helpers/Projects.js index 37f2d5823b..816fd73ba3 100644 --- a/awx/ui/client/src/helpers/Projects.js +++ b/awx/ui/client/src/helpers/Projects.js @@ -70,8 +70,11 @@ export default result = 'Success! Click for details'; break; case 'failed': - case 'missing': result = 'Failed. Click for details'; + break; + case 'missing': + result = 'Missing. Click for details'; + break; } return result; }; From 856ad80c214c9f70f3ef904daba6904a8cacc9ad Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Thu, 7 Jul 2016 13:53:21 -0400 Subject: [PATCH 017/123] Mock feature_enabled for organization tests --- awx/main/tests/functional/api/test_organizations.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/awx/main/tests/functional/api/test_organizations.py b/awx/main/tests/functional/api/test_organizations.py index 8a9da1c662..7aa266deb3 100644 --- a/awx/main/tests/functional/api/test_organizations.py +++ b/awx/main/tests/functional/api/test_organizations.py @@ -3,6 +3,8 @@ # Python import pytest +import mock + # Django from django.core.urlresolvers import reverse @@ -97,6 +99,7 @@ def test_organization_inventory_list(organization, inventory_factory, get, alice @pytest.mark.django_db +@mock.patch('awx.api.views.feature_enabled', lambda feature,bypass_db=None: True) def test_create_organization(post, admin, alice): new_org = { 'name': 'new org', @@ -108,6 +111,7 @@ def test_create_organization(post, admin, alice): @pytest.mark.django_db +@mock.patch('awx.api.views.feature_enabled', lambda feature,bypass_db=None: True) def test_create_organization_xfail(post, alice): new_org = { 'name': 'new org', @@ -161,22 +165,26 @@ def test_update_organization(get, put, organization, alice, bob): @pytest.mark.django_db +@mock.patch('awx.api.views.feature_enabled', lambda feature,bypass_db=None: True) def test_delete_organization(delete, organization, admin): delete(reverse('api:organization_detail', args=(organization.id,)), user=admin, expect=204) @pytest.mark.django_db +@mock.patch('awx.api.views.feature_enabled', lambda feature,bypass_db=None: True) def test_delete_organization2(delete, organization, alice): organization.admin_role.members.add(alice) delete(reverse('api:organization_detail', args=(organization.id,)), user=alice, expect=204) @pytest.mark.django_db +@mock.patch('awx.api.views.feature_enabled', lambda feature,bypass_db=None: True) def test_delete_organization_xfail1(delete, organization, alice): organization.member_role.members.add(alice) delete(reverse('api:organization_detail', args=(organization.id,)), user=alice, expect=403) @pytest.mark.django_db +@mock.patch('awx.api.views.feature_enabled', lambda feature,bypass_db=None: True) def test_delete_organization_xfail2(delete, organization): delete(reverse('api:organization_detail', args=(organization.id,)), user=None, expect=401) From 94c4db1d6a5fe703514a7299ba2e043121dd731d Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Thu, 7 Jul 2016 13:43:13 -0400 Subject: [PATCH 018/123] Show the project list POST text box for org admins in API browser --- awx/main/access.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/awx/main/access.py b/awx/main/access.py index e6f692a005..0503501d86 100644 --- a/awx/main/access.py +++ b/awx/main/access.py @@ -722,6 +722,8 @@ class ProjectAccess(BaseAccess): @check_superuser def can_add(self, data): + if not data or '_method' in data: + return Organization.accessible_objects(self.user, 'admin_role').exists() organization_pk = get_pk_from_dict(data, 'organization') org = get_object_or_400(Organization, pk=organization_pk) return self.user in org.admin_role From 5b76df4d7f615c59cd126d445e0185a0e5b31e27 Mon Sep 17 00:00:00 2001 From: Ken Hoes Date: Thu, 7 Jul 2016 14:17:28 -0400 Subject: [PATCH 019/123] Default to descending order on job view toggle --- awx/ui/client/src/portal-mode/portal-mode-jobs.controller.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/awx/ui/client/src/portal-mode/portal-mode-jobs.controller.js b/awx/ui/client/src/portal-mode/portal-mode-jobs.controller.js index 34669c74e9..d6f6a7e42c 100644 --- a/awx/ui/client/src/portal-mode/portal-mode-jobs.controller.js +++ b/awx/ui/client/src/portal-mode/portal-mode-jobs.controller.js @@ -52,17 +52,16 @@ export function PortalModeJobsController($scope, $rootScope, GetBasePath, Genera }; - $scope.filterUser = function(){ $scope.activeFilter = 'user'; defaultUrl = GetBasePath('jobs') + '?created_by=' + $rootScope.current_user.id; - init(); + init(true); }; $scope.filterAll = function(){ $scope.activeFilter = 'all'; defaultUrl = GetBasePath('jobs'); - init(); + init(true); }; $scope.refresh = function(){ From 521fa13662ba2f5bee63f48cf39792d231538f0d Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Thu, 7 Jul 2016 14:41:11 -0400 Subject: [PATCH 020/123] More mock fixes for organizations.py tests --- awx/main/tests/functional/api/test_organizations.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/awx/main/tests/functional/api/test_organizations.py b/awx/main/tests/functional/api/test_organizations.py index 7aa266deb3..d141ddd6b5 100644 --- a/awx/main/tests/functional/api/test_organizations.py +++ b/awx/main/tests/functional/api/test_organizations.py @@ -165,26 +165,26 @@ def test_update_organization(get, put, organization, alice, bob): @pytest.mark.django_db -@mock.patch('awx.api.views.feature_enabled', lambda feature,bypass_db=None: True) +@mock.patch('awx.main.access.BaseAccess.check_license', lambda *a, **kw: True) def test_delete_organization(delete, organization, admin): delete(reverse('api:organization_detail', args=(organization.id,)), user=admin, expect=204) @pytest.mark.django_db -@mock.patch('awx.api.views.feature_enabled', lambda feature,bypass_db=None: True) +@mock.patch('awx.main.access.BaseAccess.check_license', lambda *a, **kw: True) def test_delete_organization2(delete, organization, alice): organization.admin_role.members.add(alice) delete(reverse('api:organization_detail', args=(organization.id,)), user=alice, expect=204) @pytest.mark.django_db -@mock.patch('awx.api.views.feature_enabled', lambda feature,bypass_db=None: True) +@mock.patch('awx.main.access.BaseAccess.check_license', lambda *a, **kw: True) def test_delete_organization_xfail1(delete, organization, alice): organization.member_role.members.add(alice) delete(reverse('api:organization_detail', args=(organization.id,)), user=alice, expect=403) @pytest.mark.django_db -@mock.patch('awx.api.views.feature_enabled', lambda feature,bypass_db=None: True) +@mock.patch('awx.main.access.BaseAccess.check_license', lambda *a, **kw: True) def test_delete_organization_xfail2(delete, organization): delete(reverse('api:organization_detail', args=(organization.id,)), user=None, expect=401) From a6d08eef1a203730756e94994848b124a9017ecc Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Thu, 7 Jul 2016 14:51:50 -0400 Subject: [PATCH 021/123] put team name in tooltip in rolelist --- awx/ui/client/src/access/roleList.partial.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/ui/client/src/access/roleList.partial.html b/awx/ui/client/src/access/roleList.partial.html index f050af8b38..10fc725b41 100644 --- a/awx/ui/client/src/access/roleList.partial.html +++ b/awx/ui/client/src/access/roleList.partial.html @@ -9,6 +9,6 @@

{{ entry.name }} - +
From 4c0a0f7936479f1400bab85dbfeb3d4a3dbd522f Mon Sep 17 00:00:00 2001 From: Jared Tabor Date: Thu, 7 Jul 2016 12:00:19 -0700 Subject: [PATCH 022/123] Searching for disabled hosts was searching for enabled hosts The API can only search hosts by "enabled": yes or no (t/f). However on the UI we want to search by "disabled" so needed to override the boolean values for this particular search --- awx/ui/client/src/lists/InventoryHosts.js | 4 ++++ awx/ui/client/src/search/tagSearch.service.js | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/awx/ui/client/src/lists/InventoryHosts.js b/awx/ui/client/src/lists/InventoryHosts.js index 16c8989832..f0cc803fe5 100644 --- a/awx/ui/client/src/lists/InventoryHosts.js +++ b/awx/ui/client/src/lists/InventoryHosts.js @@ -54,6 +54,10 @@ export default label: 'Disabled?', searchSingleValue: true, searchType: 'boolean', + typeOptions: [ + {label: "Yes", value: false}, + {label: "No", value: true} + ], searchValue: 'false', searchOnly: true }, diff --git a/awx/ui/client/src/search/tagSearch.service.js b/awx/ui/client/src/search/tagSearch.service.js index fbc87c4e52..777c4c6441 100644 --- a/awx/ui/client/src/search/tagSearch.service.js +++ b/awx/ui/client/src/search/tagSearch.service.js @@ -25,7 +25,7 @@ export default ['Rest', '$q', 'GetBasePath', 'Wait', 'ProcessErrors', '$log', fu typeOptions = field.searchOptions || []; } else if (field.searchType === 'boolean') { type = 'select'; - typeOptions = [{label: "Yes", value: true}, + typeOptions = field.typeOptions || [{label: "Yes", value: true}, {label: "No", value: false}]; } else { type = 'text'; From e0d76ec3789e963a0d75d45814163d05ef6b27a7 Mon Sep 17 00:00:00 2001 From: Bill Nottingham Date: Thu, 7 Jul 2016 16:12:51 -0400 Subject: [PATCH 023/123] Tweak system job wording. --- awx/main/migrations/0010_v300_create_system_job_templates.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/awx/main/migrations/0010_v300_create_system_job_templates.py b/awx/main/migrations/0010_v300_create_system_job_templates.py index 61092ed60b..abf336efaa 100644 --- a/awx/main/migrations/0010_v300_create_system_job_templates.py +++ b/awx/main/migrations/0010_v300_create_system_job_templates.py @@ -24,7 +24,7 @@ def create_system_job_templates(apps, schema_editor): job_type='cleanup_jobs', defaults=dict( name='Cleanup Job Details', - description='Remove job history older than X days', + description='Remove job history', created=now_dt, modified=now_dt, polymorphic_ctype=sjt_ct, @@ -51,7 +51,7 @@ def create_system_job_templates(apps, schema_editor): job_type='cleanup_activitystream', defaults=dict( name='Cleanup Activity Stream', - description='Remove activity stream history older than X days', + description='Remove activity stream history', created=now_dt, modified=now_dt, polymorphic_ctype=sjt_ct, From 145d9bb7583867ea18591e0602c22db781801cd3 Mon Sep 17 00:00:00 2001 From: Leigh Johnson Date: Fri, 8 Jul 2016 10:11:43 -0400 Subject: [PATCH 024/123] update tooltips in job list on status change, resolves #2811 --- awx/ui/client/src/helpers/Jobs.js | 7 +++++++ awx/ui/client/src/lists/AllJobs.js | 3 ++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/awx/ui/client/src/helpers/Jobs.js b/awx/ui/client/src/helpers/Jobs.js index 5be8d23d06..28727b700e 100644 --- a/awx/ui/client/src/helpers/Jobs.js +++ b/awx/ui/client/src/helpers/Jobs.js @@ -231,6 +231,12 @@ export default search_params = params.searchParams, spinner = (params.spinner === undefined) ? true : params.spinner, key; + var buildTooltips = function(data){ + data.forEach((val) => { + val.status_tip = 'Job ' + val.status + ". Click for details."; + }); + }; + GenerateList.inject(list, { mode: 'edit', id: id, @@ -260,6 +266,7 @@ export default scope.$on('PostRefresh', function(){ JobsControllerInit({ scope: scope, parent_scope: parent_scope }); JobsListUpdate({ scope: scope, parent_scope: parent_scope, list: list }); + buildTooltips(scope.jobs); parent_scope.$emit('listLoaded'); }); diff --git a/awx/ui/client/src/lists/AllJobs.js b/awx/ui/client/src/lists/AllJobs.js index 04bbaad493..955d239dc4 100644 --- a/awx/ui/client/src/lists/AllJobs.js +++ b/awx/ui/client/src/lists/AllJobs.js @@ -21,7 +21,8 @@ export default label: '', searchLabel: 'Status', columnClass: 'col-lg-1 col-md-1 col-sm-2 col-xs-2 List-staticColumn--smallStatus', - awToolTip: "Job {{ all_job.status }}. Click for details.", + dataTipWatch: 'all_job.status_tip', + awToolTip: "{{ all_job.status_tip }}", awTipPlacement: "right", dataTitle: "{{ all_job.status_popover_title }}", icon: 'icon-job-{{ all_job.status }}', From 6296e717cb68e1a5aaf879864f25d487e35cd183 Mon Sep 17 00:00:00 2001 From: Leigh Johnson Date: Fri, 8 Jul 2016 11:00:46 -0400 Subject: [PATCH 025/123] clear labels column if no label results are found, resolves #2865 --- .../client/src/job-templates/labels/labelsList.directive.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/awx/ui/client/src/job-templates/labels/labelsList.directive.js b/awx/ui/client/src/job-templates/labels/labelsList.directive.js index bb1a825273..a5f055bb6e 100644 --- a/awx/ui/client/src/job-templates/labels/labelsList.directive.js +++ b/awx/ui/client/src/job-templates/labels/labelsList.directive.js @@ -74,13 +74,17 @@ export default }); }; - scope.$watch(scope.$parent.list.iterator+".summary_fields.labels.results", function() { + scope.$watchCollection(scope.$parent.list.iterator, function() { // To keep the array of labels fresh, we need to set up a watcher - otherwise, the // array will get set initially and then never be updated as labels are removed if (scope[scope.$parent.list.iterator].summary_fields.labels){ scope.labels = scope[scope.$parent.list.iterator].summary_fields.labels.results; scope.count = scope[scope.$parent.list.iterator].summary_fields.labels.count; } + else{ + scope.labels = null; + scope.count = null; + } }); } }; From ad4841cc9403920121daa3a4633757e4343c8299 Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Fri, 8 Jul 2016 11:12:59 -0400 Subject: [PATCH 026/123] fix tooltip placement on permission role list team item --- awx/ui/client/src/access/roleList.block.less | 4 ++++ awx/ui/client/src/access/roleList.partial.html | 6 ++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/awx/ui/client/src/access/roleList.block.less b/awx/ui/client/src/access/roleList.block.less index 9b185148b0..5cacfb1814 100644 --- a/awx/ui/client/src/access/roleList.block.less +++ b/awx/ui/client/src/access/roleList.block.less @@ -53,6 +53,10 @@ cursor: pointer; } +.RoleList-tag--team { + cursor: default; +} + .RoleList-tagDelete { font-size: 13px; color: @default-bg; diff --git a/awx/ui/client/src/access/roleList.partial.html b/awx/ui/client/src/access/roleList.partial.html index 10fc725b41..d98c13d53b 100644 --- a/awx/ui/client/src/access/roleList.partial.html +++ b/awx/ui/client/src/access/roleList.partial.html @@ -7,8 +7,10 @@ class="fa fa-times RoleList-tagDelete">
+ ng-class="{'RoleList-tag--deletable': entry.explicit, + 'RoleList-tag--team': entry.team_id}" + aw-tool-tip='{{entry.team_name}}' aw-tip-placement='bottom'> {{ entry.name }} - +
From ffdcd1f20d40789265f0e5edbf58043b2ca288a6 Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Fri, 8 Jul 2016 11:14:01 -0400 Subject: [PATCH 027/123] Don't fetch credential for inventory groups when all we need is the name and it exists in the summary fields This implicitly solves #2803 by simply not doing the get that was causing the problem. --- .../src/inventories/manage/groups/groups-edit.controller.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/ui/client/src/inventories/manage/groups/groups-edit.controller.js b/awx/ui/client/src/inventories/manage/groups/groups-edit.controller.js index e7f7b67db3..4875e3abca 100644 --- a/awx/ui/client/src/inventories/manage/groups/groups-edit.controller.js +++ b/awx/ui/client/src/inventories/manage/groups/groups-edit.controller.js @@ -247,7 +247,7 @@ {inventory_script: inventorySourceData.source_script} ); if (inventorySourceData.credential){ - GroupManageService.getCredential(inventorySourceData.credential).then(res => $scope.credential_name = res.data.name); + $scope.credential_name = inventorySourceData.summary_fields.credential.name; } $scope = angular.extend($scope, groupData); From 1aa6ae5307d83d9a187f54a778c00090566f0463 Mon Sep 17 00:00:00 2001 From: Leigh Johnson Date: Fri, 8 Jul 2016 11:17:54 -0400 Subject: [PATCH 028/123] fix outdated order_by params in jobs > schedules list, resolves #2862 --- awx/ui/client/src/lists/ScheduledJobs.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/ui/client/src/lists/ScheduledJobs.js b/awx/ui/client/src/lists/ScheduledJobs.js index 1be131b6ca..c42ff8c1e6 100644 --- a/awx/ui/client/src/lists/ScheduledJobs.js +++ b/awx/ui/client/src/lists/ScheduledJobs.js @@ -45,7 +45,7 @@ export default sourceModel: 'unified_job_template', sourceField: 'unified_job_type', ngBind: 'schedule.type_label', - searchField: 'unified_job_template__polymorphic_ctype__name', + searchField: 'unified_job_template__name', searchLable: 'Type', searchable: true, searchType: 'select', From ee3d4dc42dc3abc8575009d99d87b046fd96c758 Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Fri, 8 Jul 2016 12:09:39 -0400 Subject: [PATCH 029/123] Allow anyone who can read an inventory to see adhoc commands run on that inventory This implicitly solves one of the issues with #2804, but is in general a better behavior in general we believe. --- awx/main/access.py | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/awx/main/access.py b/awx/main/access.py index 0503501d86..724291f2fd 100644 --- a/awx/main/access.py +++ b/awx/main/access.py @@ -1075,10 +1075,7 @@ class AdHocCommandAccess(BaseAccess): ''' I can only see/run ad hoc commands when: - I am a superuser. - - I am an org admin and have permission to read the credential. - - I am a normal user with a user/team permission that has at least read - permission on the inventory and the run_ad_hoc_commands flag set, and I - can read the credential. + - I have read access to the inventory ''' model = AdHocCommand @@ -1089,11 +1086,8 @@ class AdHocCommandAccess(BaseAccess): if self.user.is_superuser: return qs.all() - credential_ids = set(self.user.get_queryset(Credential).values_list('id', flat=True)) inventory_qs = Inventory.accessible_objects(self.user, 'read_role') - - return qs.filter(credential_id__in=credential_ids, - inventory__in=inventory_qs) + return qs.filter(inventory__in=inventory_qs) def can_add(self, data): if not data or '_method' in data: # So the browseable API will work? @@ -1101,11 +1095,11 @@ class AdHocCommandAccess(BaseAccess): self.check_license() - # If a credential is provided, the user should have read access to it. + # If a credential is provided, the user should have use access to it. credential_pk = get_pk_from_dict(data, 'credential') if credential_pk: credential = get_object_or_400(Credential, pk=credential_pk) - if self.user not in credential.read_role: + if self.user not in credential.use_role: return False # Check that the user has the run ad hoc command permission on the From a0017eb07480719c99f0bf8aa7b4c7dd3dac97f6 Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Fri, 8 Jul 2016 12:33:32 -0400 Subject: [PATCH 030/123] Skip troublesome old job tests These are actively being worked on by matburt and cmeyers, merging these skips because the otherwise unrelated test failures are causing headaches for the UI team. --- awx/main/tests/old/api/job_tasks.py | 4 ++++ awx/main/tests/old/jobs/job_relaunch.py | 3 +++ 2 files changed, 7 insertions(+) diff --git a/awx/main/tests/old/api/job_tasks.py b/awx/main/tests/old/api/job_tasks.py index 93861f8d56..2471f9e614 100644 --- a/awx/main/tests/old/api/job_tasks.py +++ b/awx/main/tests/old/api/job_tasks.py @@ -1,6 +1,9 @@ # Copyright (c) 2015 Ansible, Inc. # All Rights Reserved. +import os +import unittest2 as unittest + from django.conf import settings from django.test import LiveServerTestCase from django.test.utils import override_settings @@ -8,6 +11,7 @@ from django.test.utils import override_settings from awx.main.tests.job_base import BaseJobTestMixin +@unittest.skipIf(os.environ.get('SKIP_SLOW_TESTS', False), 'Skipping slow test') @override_settings(CELERY_ALWAYS_EAGER=True, CELERY_EAGER_PROPAGATES_EXCEPTIONS=True, ANSIBLE_TRANSPORT='local') diff --git a/awx/main/tests/old/jobs/job_relaunch.py b/awx/main/tests/old/jobs/job_relaunch.py index 437c6ed099..3a9e050288 100644 --- a/awx/main/tests/old/jobs/job_relaunch.py +++ b/awx/main/tests/old/jobs/job_relaunch.py @@ -4,6 +4,8 @@ # Python from __future__ import absolute_import import json +import os +import unittest2 as unittest # Django from django.core.urlresolvers import reverse @@ -15,6 +17,7 @@ from awx.main.tests.job_base import BaseJobTestMixin __all__ = ['JobRelaunchTest',] +@unittest.skipIf(os.environ.get('SKIP_SLOW_TESTS', False), 'Skipping slow test') class JobRelaunchTest(BaseJobTestMixin, BaseLiveServerTest): def test_job_relaunch(self): From 4c7f54cf7205417a926a75d54aa4d769d05185af Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Fri, 8 Jul 2016 12:54:42 -0400 Subject: [PATCH 031/123] Fix an activity stream associate with deleted obj In some scenarios the activity stream associate could be invoked where obj2 doesn't exist... the .get() will fail and tank the caller preventing the save. This guards that and blocks the activity stream event, saving the save (whew!) --- awx/main/signals.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/awx/main/signals.py b/awx/main/signals.py index 6138ee17bd..1c57fea8ae 100644 --- a/awx/main/signals.py +++ b/awx/main/signals.py @@ -410,7 +410,10 @@ def activity_stream_associate(sender, instance, **kwargs): for entity_acted in kwargs['pk_set']: obj2 = kwargs['model'] obj2_id = entity_acted - obj2_actual = obj2.objects.get(id=obj2_id) + obj2_actual = obj2.objects.filter(id=obj2_id) + if not obj2_actual.exists(): + continue + obj2_actual = obj2_actual[0] if isinstance(obj2_actual, Role) and obj2_actual.content_object is not None: obj2_actual = obj2_actual.content_object object2 = camelcase_to_underscore(obj2_actual.__class__.__name__) From 81d32e216def0d92ed5179d81859e9187581d4ba Mon Sep 17 00:00:00 2001 From: Jared Tabor Date: Thu, 7 Jul 2016 14:27:39 -0700 Subject: [PATCH 032/123] fixing tooltip for host events on job detail panel --- awx/ui/client/src/job-detail/job-detail.partial.html | 2 +- awx/ui/client/src/job-detail/job-detail.service.js | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/awx/ui/client/src/job-detail/job-detail.partial.html b/awx/ui/client/src/job-detail/job-detail.partial.html index 578e17510f..18c90cf646 100644 --- a/awx/ui/client/src/job-detail/job-detail.partial.html +++ b/awx/ui/client/src/job-detail/job-detail.partial.html @@ -354,7 +354,7 @@ - {{ result.name }}{{ result.name }} + {{ result.name }}{{ result.name }} {{ result.item }} {{ result.msg }} diff --git a/awx/ui/client/src/job-detail/job-detail.service.js b/awx/ui/client/src/job-detail/job-detail.service.js index 080339e7dd..e5578349a5 100644 --- a/awx/ui/client/src/job-detail/job-detail.service.js +++ b/awx/ui/client/src/job-detail/job-detail.service.js @@ -39,6 +39,13 @@ export default } catch(err){return;} }, + processsEventTip: function(event, status){ + try{ + var string = `Event ID: ${ event.id }
Status: ${ _.capitalize(status.status)}. Click for details`; + return typeof item === 'object' ? JSON.stringify(string) : string; + } + catch(err){return;} + }, // Generate a helper class for job_event statuses // the stack for which status to display is // unreachable > failed > changed > ok @@ -88,6 +95,7 @@ export default var status = self.processEventStatus(event); var msg = self.processEventMsg(event); var item = self.processEventItem(event); + var tip = self.processsEventTip(event, status); results.push({ id: event.id, status: status.status, @@ -96,6 +104,7 @@ export default task_id: event.parent, name: event.event_data.host, created: event.created, + tip: typeof tip === 'undefined' ? undefined : tip, msg: typeof msg === 'undefined' ? undefined : msg, item: typeof item === 'undefined' ? undefined : item }); From 4f219e27756b294d8be34ddeec9c688f06808e50 Mon Sep 17 00:00:00 2001 From: Jared Tabor Date: Thu, 7 Jul 2016 14:45:46 -0700 Subject: [PATCH 033/123] redirect user to dashboard if refresh if changing targets on the activity stream page and then clicking the toggle --- awx/ui/client/src/bread-crumb/bread-crumb.directive.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/awx/ui/client/src/bread-crumb/bread-crumb.directive.js b/awx/ui/client/src/bread-crumb/bread-crumb.directive.js index 790ab24674..cfc6630412 100644 --- a/awx/ui/client/src/bread-crumb/bread-crumb.directive.js +++ b/awx/ui/client/src/bread-crumb/bread-crumb.directive.js @@ -36,14 +36,10 @@ export default // The user is navigating away from the activity stream - take them back from whence they came else { // Pull the previous state out of local storage - var previousState = Store('previous_state'); if(originalRoute) { $state.go(originalRoute.name, originalRoute.fromParams); } - else if(previousState && !Empty(previousState.name)) { - $state.go(previousState.name, previousState.fromParams); - } else { // If for some reason something went wrong (like local storage was wiped, etc) take the // user back to the dashboard From 1c860b81ace9d8c2b2d550bd6875071058e71d33 Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Fri, 8 Jul 2016 13:03:34 -0400 Subject: [PATCH 034/123] Fix up hook conditions for rbac activity stream --- awx/main/signals.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/awx/main/signals.py b/awx/main/signals.py index 1c57fea8ae..b74c99dcba 100644 --- a/awx/main/signals.py +++ b/awx/main/signals.py @@ -159,7 +159,7 @@ def org_admin_edit_members(instance, action, model, reverse, pk_set, **kwargs): def rbac_activity_stream(instance, sender, **kwargs): user_type = ContentType.objects.get_for_model(User) # Only if we are associating/disassociating - if kwargs['action'] in ['pre_add', 'pre_remove']: + if kwargs['action'] in ['post_add', 'pre_remove']: # Only if this isn't for the User.admin_role if hasattr(instance, 'content_type'): if instance.content_type in [None, user_type]: @@ -396,7 +396,7 @@ def activity_stream_delete(sender, instance, **kwargs): def activity_stream_associate(sender, instance, **kwargs): if not activity_stream_enabled: return - if kwargs['action'] in ['pre_add', 'pre_remove']: + if kwargs['action'] in ['post_add', 'pre_remove']: if kwargs['action'] == 'pre_add': action = 'associate' elif kwargs['action'] == 'pre_remove': From 26b0d345f890a2090f9a6ec618d1e01b364159e2 Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Fri, 8 Jul 2016 13:04:42 -0400 Subject: [PATCH 035/123] Notification guarding for deleted inventory --- awx/main/models/jobs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/main/models/jobs.py b/awx/main/models/jobs.py index 19a03febee..b9464385ac 100644 --- a/awx/main/models/jobs.py +++ b/awx/main/models/jobs.py @@ -681,7 +681,7 @@ class Job(UnifiedJob, JobOptions): ok=h.ok, processed=h.processed, skipped=h.skipped) - data.update(dict(inventory=self.inventory.name, + data.update(dict(inventory=self.inventory.name if self.inventory else None, project=self.project.name if self.project else None, playbook=self.playbook, credential=self.credential.name, From ae9b4d3e58b128bd5913341781c1d878bfb49971 Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Fri, 8 Jul 2016 13:13:22 -0400 Subject: [PATCH 036/123] Revert "Fix up hook conditions for rbac activity stream" This reverts commit 36c14f83bcb11fcdbd31140ce182ab87e3db4644. --- awx/main/signals.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/awx/main/signals.py b/awx/main/signals.py index b74c99dcba..1c57fea8ae 100644 --- a/awx/main/signals.py +++ b/awx/main/signals.py @@ -159,7 +159,7 @@ def org_admin_edit_members(instance, action, model, reverse, pk_set, **kwargs): def rbac_activity_stream(instance, sender, **kwargs): user_type = ContentType.objects.get_for_model(User) # Only if we are associating/disassociating - if kwargs['action'] in ['post_add', 'pre_remove']: + if kwargs['action'] in ['pre_add', 'pre_remove']: # Only if this isn't for the User.admin_role if hasattr(instance, 'content_type'): if instance.content_type in [None, user_type]: @@ -396,7 +396,7 @@ def activity_stream_delete(sender, instance, **kwargs): def activity_stream_associate(sender, instance, **kwargs): if not activity_stream_enabled: return - if kwargs['action'] in ['post_add', 'pre_remove']: + if kwargs['action'] in ['pre_add', 'pre_remove']: if kwargs['action'] == 'pre_add': action = 'associate' elif kwargs['action'] == 'pre_remove': From ac42f7a0d592bc458cea8df70a22e12581adece4 Mon Sep 17 00:00:00 2001 From: Jared Tabor Date: Fri, 8 Jul 2016 10:38:00 -0700 Subject: [PATCH 037/123] Using $state to determine to launch or relaunch jobs instead of $location --- .../src/job-submission/job-submission.controller.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/awx/ui/client/src/job-submission/job-submission.controller.js b/awx/ui/client/src/job-submission/job-submission.controller.js index acefe6aa07..00c7e971d3 100644 --- a/awx/ui/client/src/job-submission/job-submission.controller.js +++ b/awx/ui/client/src/job-submission/job-submission.controller.js @@ -128,12 +128,14 @@ export default // This gets things started - goes out and hits the launch endpoint (based on launch/relaunch) and // prepares the form fields, defauts, etc. $scope.init = function() { - $scope.forms = {}; $scope.passwords = {}; - var base = $location.path().replace(/^\//, '').split('/')[0], - isRelaunch = !(base === 'job_templates' || base === 'portal' || base === 'inventories' || base === 'home'); + var base = $state.current.name, + // As of 3.0, the only place the user can relaunch a + // playbook is on jobTemplates.edit (completed_jobs tab), + // jobs, and jobDetails $states. + isRelaunch = !(base === 'jobTemplates' || base === 'portalMode' || base === 'dashboard'); if (!isRelaunch) { launch_url = GetBasePath('job_templates') + $scope.submitJobId + '/launch/'; From 8cfb25b2409044d5f3c12b69fedad1216087834c Mon Sep 17 00:00:00 2001 From: Leigh Johnson Date: Fri, 8 Jul 2016 15:09:09 -0400 Subject: [PATCH 038/123] consolidate host events filter/search requests, resolves #2858, #2859, kickback on #2761 --- .../host-events/host-events.controller.js | 121 ++++++------------ 1 file changed, 37 insertions(+), 84 deletions(-) diff --git a/awx/ui/client/src/job-detail/host-events/host-events.controller.js b/awx/ui/client/src/job-detail/host-events/host-events.controller.js index 2edea0a622..55abf51e04 100644 --- a/awx/ui/client/src/job-detail/host-events/host-events.controller.js +++ b/awx/ui/client/src/job-detail/host-events/host-events.controller.js @@ -18,93 +18,49 @@ $scope.search = function(){ Wait('start'); - if ($scope.searchStr === undefined){ - return; - } //http://docs.ansible.com/ansible-tower/latest/html/towerapi/intro.html#filtering // SELECT WHERE host_name LIKE str OR WHERE play LIKE str OR WHERE task LIKE str AND host_name NOT "" // selecting non-empty host_name fields prevents us from displaying non-runner events, like playbook_on_task_start - JobDetailService.getRelatedJobEvents($stateParams.id, { - or__host_name__icontains: $scope.searchStr, - or__play__icontains: $scope.searchStr, - or__task__icontains: $scope.searchStr, - not__host_name: "" , - page_size: $scope.pageSize}) - .success(function(res){ - $scope.results = res.results; - Wait('stop'); - }); + var params = { + host_name: $scope.hostName, + }; + if ($scope.searchStr && $scope.searchStr !== ''){ + params.or__play__icontains = $scope.searchStr; + params.or__task__icontains = $scope.searchStr; + } + + switch($scope.activeFilter){ + case 'skipped': + params.event = 'runner_on_skipped'; + break; + case 'unreachable': + params.event = 'runner_on_unreachable'; + break; + case 'ok': + params.event = 'runner_on_ok'; + break; + case 'failed': + params.failed = true; + break; + case 'changed': + params.changed = true; + break; + default: + break; + } + JobDetailService.getRelatedJobEvents($stateParams.id, params) + .success(function(res){ + $scope.results = res.results; + Wait('stop'); + }); }; $scope.filters = ['all', 'changed', 'failed', 'ok', 'unreachable', 'skipped']; - var filter = function(filter){ - Wait('start'); - - if (filter === 'all'){ - return JobDetailService.getRelatedJobEvents($stateParams.id, { - host_name: $stateParams.hostName, - page_size: $scope.pageSize}) - .success(function(res){ - $scope.results = res.results; - Wait('stop'); - }); - } - // handle runner cases - if (filter === 'skipped'){ - return JobDetailService.getRelatedJobEvents($stateParams.id, { - host_name: $stateParams.hostName, - event: 'runner_on_skipped'}) - .success(function(res){ - $scope.results = res.results; - Wait('stop'); - }); - } - if (filter === 'unreachable'){ - return JobDetailService.getRelatedJobEvents($stateParams.id, { - host_name: $stateParams.hostName, - event: 'runner_on_unreachable'}) - .success(function(res){ - $scope.results = res.results; - Wait('stop'); - }); - } - if (filter === 'ok'){ - return JobDetailService.getRelatedJobEvents($stateParams.id, { - host_name: $stateParams.hostName, - event__startswith: 'runner_on_ok' - }) - .success(function(res){ - $scope.results = res.results; - Wait('stop'); - }); - } - // handle convience properties .changed .failed - if (filter === 'changed'){ - return JobDetailService.getRelatedJobEvents($stateParams.id, { - host_name: $stateParams.hostName, - changed: true}) - .success(function(res){ - $scope.results = res.results; - Wait('stop'); - }); - } - if (filter === 'failed'){ - return JobDetailService.getRelatedJobEvents($stateParams.id, { - host_name: $stateParams.hostName, - failed: true, - event__startswith: 'runner_on_failed' - }) - .success(function(res){ - $scope.results = res.results; - Wait('stop'); - }); - } - }; - // watch select2 for changes $('.HostEvents-select').on("select2:select", function () { - filter($('.HostEvents-select').val()); + $scope.activeFilter = $('.HostEvents-select').val(); + $scope.search(); }); var init = function(){ @@ -116,12 +72,9 @@ }); // process the filter if one was passed if ($stateParams.filter){ - Wait('start'); - filter($stateParams.filter).success(function(res){ - $scope.results = res.results; - Wait('stop'); - $('#HostEvents').modal('show'); - }); + $scope.activeFilter = $stateParams.filter; + $scope.search(); + $('#HostEvents').modal('show'); } else{ $scope.results = hosts.data.results; From 5027027aba0a55a3d1e41918222999fcf254cdd2 Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Fri, 8 Jul 2016 13:36:38 -0400 Subject: [PATCH 039/123] update behavior of access.py to newer DRF, display POST box for most list endpoints --- awx/main/access.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/awx/main/access.py b/awx/main/access.py index 0503501d86..16e6cfbde1 100644 --- a/awx/main/access.py +++ b/awx/main/access.py @@ -589,8 +589,9 @@ class CredentialAccess(BaseAccess): def can_read(self, obj): return self.user in obj.read_role + @check_superuser def can_add(self, data): - if self.user.is_superuser: + if not data: # So the browseable API will work return True user_pk = get_pk_from_dict(data, 'user') if user_pk: @@ -660,6 +661,8 @@ class TeamAccess(BaseAccess): @check_superuser def can_add(self, data): + if not data: # So the browseable API will work + return Organization.accessible_objects(self.user, 'admin_role').exists() org_pk = get_pk_from_dict(data, 'organization') org = get_object_or_400(Organization, pk=org_pk) if self.user in org.admin_role: @@ -722,7 +725,7 @@ class ProjectAccess(BaseAccess): @check_superuser def can_add(self, data): - if not data or '_method' in data: + if not data: # So the browseable API will work return Organization.accessible_objects(self.user, 'admin_role').exists() organization_pk = get_pk_from_dict(data, 'organization') org = get_object_or_400(Organization, pk=organization_pk) @@ -804,7 +807,7 @@ class JobTemplateAccess(BaseAccess): given action as well as the 'create' deploy permission. Users who are able to create deploy jobs can also run normal and check (dry run) jobs. ''' - if not data or '_method' in data: # So the browseable API will work? + if not data: # So the browseable API will work return True # if reference_obj is provided, determine if it can be coppied @@ -994,7 +997,7 @@ class JobAccess(BaseAccess): Q(project__organization__in=org_access_qs)).distinct() def can_add(self, data): - if not data or '_method' in data: # So the browseable API will work? + if not data: # So the browseable API will work return True if not self.user.is_superuser: return False @@ -1096,7 +1099,7 @@ class AdHocCommandAccess(BaseAccess): inventory__in=inventory_qs) def can_add(self, data): - if not data or '_method' in data: # So the browseable API will work? + if not data: # So the browseable API will work return True self.check_license() @@ -1445,7 +1448,7 @@ class LabelAccess(BaseAccess): @check_superuser def can_add(self, data): - if not data or '_method' in data: # So the browseable API will work? + if not data: # So the browseable API will work return True org_pk = get_pk_from_dict(data, 'organization') @@ -1552,6 +1555,8 @@ class CustomInventoryScriptAccess(BaseAccess): @check_superuser def can_add(self, data): + if not data: # So the browseable API will work + return Organization.accessible_objects(self.user, 'admin_role').exists() org_pk = get_pk_from_dict(data, 'organization') org = get_object_or_400(Organization, pk=org_pk) return self.user in org.admin_role From f106b596ea7e0c2541f9cbd245bb75bd52bc6620 Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Fri, 8 Jul 2016 16:27:43 -0400 Subject: [PATCH 040/123] give description column top/bottom padding, and center action cell for tall rows --- awx/ui/client/legacy-styles/lists.less | 5 +++++ .../job-templates-list.partial.html | 16 +++++++++------- awx/ui/client/src/shared/form-generator.js | 4 ++-- .../list-generator/list-generator.factory.js | 4 ++-- 4 files changed, 18 insertions(+), 11 deletions(-) diff --git a/awx/ui/client/legacy-styles/lists.less b/awx/ui/client/legacy-styles/lists.less index f2120c139e..6282ab340e 100644 --- a/awx/ui/client/legacy-styles/lists.less +++ b/awx/ui/client/legacy-styles/lists.less @@ -80,6 +80,11 @@ table, tbody { border-top:0px!important; } +.List-tableCell.description-column { + padding-top: 15px; + padding-bottom: 15px; +} + .List-actionButtonCell { padding-top:5px; padding-right: 15px; diff --git a/awx/ui/client/src/dashboard/lists/job-templates/job-templates-list.partial.html b/awx/ui/client/src/dashboard/lists/job-templates/job-templates-list.partial.html index da60bce8cf..8329747c6c 100644 --- a/awx/ui/client/src/dashboard/lists/job-templates/job-templates-list.partial.html +++ b/awx/ui/client/src/dashboard/lists/job-templates/job-templates-list.partial.html @@ -32,13 +32,15 @@ - - - + +
+ + +
diff --git a/awx/ui/client/src/shared/form-generator.js b/awx/ui/client/src/shared/form-generator.js index 5417829724..4b889eeb75 100644 --- a/awx/ui/client/src/shared/form-generator.js +++ b/awx/ui/client/src/shared/form-generator.js @@ -1960,7 +1960,7 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat // Row level actions if (collection.fieldActions) { - html += ""; + html += "
"; for (act in collection.fieldActions) { fAction = collection.fieldActions[act]; html += ""; } - html += ""; + html += "
"; html += "\n"; } diff --git a/awx/ui/client/src/shared/list-generator/list-generator.factory.js b/awx/ui/client/src/shared/list-generator/list-generator.factory.js index 30c4625a59..9fc63e0969 100644 --- a/awx/ui/client/src/shared/list-generator/list-generator.factory.js +++ b/awx/ui/client/src/shared/list-generator/list-generator.factory.js @@ -524,7 +524,7 @@ export default ['$location', '$compile', '$rootScope', 'SearchWidget', 'Paginate // Row level actions - innerTable += ""; + innerTable += "
"; for (field_action in list.fieldActions) { if (field_action !== 'columnClass') { @@ -573,7 +573,7 @@ export default ['$location', '$compile', '$rootScope', 'SearchWidget', 'Paginate } } } - innerTable += "\n"; + innerTable += "
\n"; } innerTable += "\n"; From 7733167693d914188b114c6888245682f3d8d216 Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Fri, 8 Jul 2016 16:29:14 -0400 Subject: [PATCH 041/123] Updated tests to reflect new expected behavior --- awx/main/tests/functional/api/test_adhoc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/main/tests/functional/api/test_adhoc.py b/awx/main/tests/functional/api/test_adhoc.py index 43326afcb4..e7029b0c79 100644 --- a/awx/main/tests/functional/api/test_adhoc.py +++ b/awx/main/tests/functional/api/test_adhoc.py @@ -122,7 +122,7 @@ def test_get_inventory_ad_hoc_command_list(admin, alice, post_adhoc, get, invent inv1.adhoc_role.members.add(alice) res = get(reverse('api:inventory_ad_hoc_commands_list', args=(inv1.id,)), alice, expect=200) - assert res.data['count'] == 0 + assert res.data['count'] == 1 machine_credential.use_role.members.add(alice) res = get(reverse('api:inventory_ad_hoc_commands_list', args=(inv1.id,)), alice, expect=200) From 03af6b350756548b5663db9d9c7ced1a78d91051 Mon Sep 17 00:00:00 2001 From: Leigh Johnson Date: Fri, 8 Jul 2016 16:29:48 -0400 Subject: [PATCH 042/123] fix activity stream inventory script url, handle update operations in a separate case, resolves #1721 kickback --- awx/ui/client/src/widgets/Stream.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/awx/ui/client/src/widgets/Stream.js b/awx/ui/client/src/widgets/Stream.js index 4a8c6aeb61..56e92e384f 100644 --- a/awx/ui/client/src/widgets/Stream.js +++ b/awx/ui/client/src/widgets/Stream.js @@ -28,11 +28,15 @@ angular.module('StreamWidget', ['RestServices', 'Utilities', 'StreamListDefiniti // arguments are: a summary_field object, a resource type, an activity stream object function ($log, $filter) { return function (obj, resource, activity) { + console.log(obj, resource) var url = '/#/'; // try/except pattern asserts that: // if we encounter a case where a UI url can't or shouldn't be generated, just supply the name of the resource try { switch (resource) { + case 'custom_inventory_script': + url += 'inventory_scripts/' + obj.id + '/'; + break; case 'group': if (activity.operation === 'create' || activity.operation === 'delete'){ // the API formats the changes.inventory field as str 'myInventoryName-PrimaryKey' @@ -47,7 +51,7 @@ angular.module('StreamWidget', ['RestServices', 'Utilities', 'StreamListDefiniti url += 'home/hosts/' + obj.id; break; case 'job': - url += 'jobs/?id=' + obj.id; + url += 'jobs/' + obj.id; break; case 'inventory': url += 'inventories/' + obj.id + '/'; @@ -192,11 +196,13 @@ angular.module('StreamWidget', ['RestServices', 'Utilities', 'StreamListDefiniti case 'delete': activity.description += activity.object1 + BuildAnchor(activity.changes, activity.object1, activity); break; - // equivalent to 'create' or 'update' // expected outcome: "operation " - default: + case 'update': activity.description += activity.object1 + BuildAnchor(activity.summary_fields[activity.object1][0], activity.object1, activity); break; + case 'create': + activity.description += activity.object1 + BuildAnchor(activity.changes, activity.object1, activity); + break; } break; } From 0b8bec3da67a769fe134b35bac8fdc0957eb03a2 Mon Sep 17 00:00:00 2001 From: Leigh Johnson Date: Fri, 8 Jul 2016 17:01:25 -0400 Subject: [PATCH 043/123] remove log --- awx/ui/client/src/widgets/Stream.js | 1 - 1 file changed, 1 deletion(-) diff --git a/awx/ui/client/src/widgets/Stream.js b/awx/ui/client/src/widgets/Stream.js index 56e92e384f..f3e9271264 100644 --- a/awx/ui/client/src/widgets/Stream.js +++ b/awx/ui/client/src/widgets/Stream.js @@ -28,7 +28,6 @@ angular.module('StreamWidget', ['RestServices', 'Utilities', 'StreamListDefiniti // arguments are: a summary_field object, a resource type, an activity stream object function ($log, $filter) { return function (obj, resource, activity) { - console.log(obj, resource) var url = '/#/'; // try/except pattern asserts that: // if we encounter a case where a UI url can't or shouldn't be generated, just supply the name of the resource From 6acf6b80d708185939bb69e3d1d757c4b442eac3 Mon Sep 17 00:00:00 2001 From: Leigh Johnson Date: Fri, 8 Jul 2016 17:11:52 -0400 Subject: [PATCH 044/123] fix Inventory Manage > Hosts list disabled indicator style, resolves #2855 --- awx/ui/client/legacy-styles/text-label.less | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/awx/ui/client/legacy-styles/text-label.less b/awx/ui/client/legacy-styles/text-label.less index 63aee6c9a3..57d50f3f8b 100644 --- a/awx/ui/client/legacy-styles/text-label.less +++ b/awx/ui/client/legacy-styles/text-label.less @@ -1,7 +1,17 @@ -@import "src/shared/text-label.less"; +@import "src/shared/branding/colors.default.less"; .host-disabled-label { &:after { - .include-text-label(#676767; white; "disabled"); + display: inline-block; + content: "disabled"; + border-radius: 3px; + color: @default-icon; + text-transform: uppercase; + font-size: .7em; + font-style: normal; + margin-left: 0.5em; + padding: 0.35em; + padding-bottom: 0.2em; + line-height: 1.1; } } From 393c668ffbc05399ee304cc88be58eab61101ed2 Mon Sep 17 00:00:00 2001 From: Jared Tabor Date: Fri, 8 Jul 2016 14:32:14 -0700 Subject: [PATCH 045/123] Update all fa fa-rocket icons to new 3.0 launch icon --- awx/ui/client/src/job-detail/job-detail.partial.html | 2 +- awx/ui/client/src/management-jobs/card/card.partial.html | 2 +- awx/ui/client/src/shared/generator-helpers.js | 4 ++-- .../src/standard-out/adhoc/standard-out-adhoc.partial.html | 2 +- .../inventory-sync/standard-out-inventory-sync.partial.html | 2 +- .../scm-update/standard-out-scm-update.partial.html | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/awx/ui/client/src/job-detail/job-detail.partial.html b/awx/ui/client/src/job-detail/job-detail.partial.html index 18c90cf646..a5590ebd5e 100644 --- a/awx/ui/client/src/job-detail/job-detail.partial.html +++ b/awx/ui/client/src/job-detail/job-detail.partial.html @@ -15,7 +15,7 @@
- +
diff --git a/awx/ui/client/src/management-jobs/card/card.partial.html b/awx/ui/client/src/management-jobs/card/card.partial.html index fc0f2b0c00..ae0d660b50 100644 --- a/awx/ui/client/src/management-jobs/card/card.partial.html +++ b/awx/ui/client/src/management-jobs/card/card.partial.html @@ -18,7 +18,7 @@ + diff --git a/awx/ui/client/src/standard-out/inventory-sync/standard-out-inventory-sync.partial.html b/awx/ui/client/src/standard-out/inventory-sync/standard-out-inventory-sync.partial.html index 250706e1c5..5c1d2bc983 100644 --- a/awx/ui/client/src/standard-out/inventory-sync/standard-out-inventory-sync.partial.html +++ b/awx/ui/client/src/standard-out/inventory-sync/standard-out-inventory-sync.partial.html @@ -8,7 +8,7 @@ RESULTS
- +
diff --git a/awx/ui/client/src/standard-out/scm-update/standard-out-scm-update.partial.html b/awx/ui/client/src/standard-out/scm-update/standard-out-scm-update.partial.html index b4459ba4bf..909eac91a3 100644 --- a/awx/ui/client/src/standard-out/scm-update/standard-out-scm-update.partial.html +++ b/awx/ui/client/src/standard-out/scm-update/standard-out-scm-update.partial.html @@ -8,7 +8,7 @@ RESULTS
- +
From 34523d59a2562549c57c4d2253322da866469daa Mon Sep 17 00:00:00 2001 From: Leigh Johnson Date: Mon, 11 Jul 2016 09:48:12 -0400 Subject: [PATCH 046/123] fix parseTypeChange params in Projects, Inventory Source, Job Templates extra vars fields, resolves #2866 (#2878) --- awx/ui/client/src/scheduler/schedulerAdd.controller.js | 4 ++-- awx/ui/client/src/scheduler/schedulerForm.partial.html | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/awx/ui/client/src/scheduler/schedulerAdd.controller.js b/awx/ui/client/src/scheduler/schedulerAdd.controller.js index 8f3ae65621..48dcbb764b 100644 --- a/awx/ui/client/src/scheduler/schedulerAdd.controller.js +++ b/awx/ui/client/src/scheduler/schedulerAdd.controller.js @@ -78,11 +78,11 @@ export default ['$compile', '$filter', '$state', '$stateParams', 'AddSchedule', // extra_data field is not manifested in the UI when scheduling a Management Job if ($state.current.name === 'jobTemplateSchedules.add'){ $scope.parseType = 'yaml'; + // grab any existing extra_vars from parent job_template var defaultUrl = GetBasePath('job_templates') + $stateParams.id + '/'; Rest.setUrl(defaultUrl); Rest.get().then(function(res){ - // sanitize - var data = JSON.parse(JSON.stringify(res.data.extra_vars)); + var data = res.data.extra_vars; $scope.extraVars = data === '' ? '---' : data; ParseTypeChange({ scope: $scope, diff --git a/awx/ui/client/src/scheduler/schedulerForm.partial.html b/awx/ui/client/src/scheduler/schedulerForm.partial.html index cd8c563b17..e3fffecec7 100644 --- a/awx/ui/client/src/scheduler/schedulerForm.partial.html +++ b/awx/ui/client/src/scheduler/schedulerForm.partial.html @@ -630,8 +630,8 @@
- YAML - JSON + YAML + JSON
From d40d7c90f547a84cf21a0210756a1e276dbf1642 Mon Sep 17 00:00:00 2001 From: Michael Abashian Date: Mon, 11 Jul 2016 09:57:02 -0400 Subject: [PATCH 047/123] Added row/list being edited logic to home/hosts --- .../hosts/dashboard-hosts-list.controller.js | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/awx/ui/client/src/dashboard/hosts/dashboard-hosts-list.controller.js b/awx/ui/client/src/dashboard/hosts/dashboard-hosts-list.controller.js index 43970c30b4..bbc57dc275 100644 --- a/awx/ui/client/src/dashboard/hosts/dashboard-hosts-list.controller.js +++ b/awx/ui/client/src/dashboard/hosts/dashboard-hosts-list.controller.js @@ -6,8 +6,8 @@ export default ['$scope', '$state', '$stateParams', 'PageRangeSetup', 'GetBasePath', 'DashboardHostsList', - 'generateList', 'PaginateInit', 'SetStatus', 'DashboardHostService', 'hosts', - function($scope, $state, $stateParams, PageRangeSetup, GetBasePath, DashboardHostsList, GenerateList, PaginateInit, SetStatus, DashboardHostService, hosts){ + 'generateList', 'PaginateInit', 'SetStatus', 'DashboardHostService', 'hosts', '$rootScope', + function($scope, $state, $stateParams, PageRangeSetup, GetBasePath, DashboardHostsList, GenerateList, PaginateInit, SetStatus, DashboardHostService, hosts, $rootScope){ var setJobStatus = function(){ _.forEach($scope.hosts, function(value){ SetStatus({ @@ -38,6 +38,20 @@ export default }); setJobStatus(); }); + var cleanUpStateChangeListener = $rootScope.$on('$stateChangeSuccess', function(event, toState, toParams) { + if (toState.name === "dashboardHosts.edit") { + $scope.rowBeingEdited = toParams.id; + $scope.listBeingEdited = "hosts"; + } + else { + delete $scope.rowBeingEdited; + delete $scope.listBeingEdited; + } + }); + // Remove the listener when the scope is destroyed to avoid a memory leak + $scope.$on('$destroy', function() { + cleanUpStateChangeListener(); + }); var init = function(){ $scope.list = list; $scope.host_active_search = false; @@ -59,6 +73,10 @@ export default iterator: list.iterator }); $scope.hostLoading = false; + if($state.current.name === "dashboardHosts.edit") { + $scope.rowBeingEdited = $state.params.id; + $scope.listBeingEdited = "hosts"; + } }; init(); }]; From 6447434aa1c13fb8a165fd9f8cf4d528e50ed3fa Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Mon, 11 Jul 2016 10:16:08 -0400 Subject: [PATCH 048/123] Hanele credential in notifications when deleted --- awx/main/models/jobs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/main/models/jobs.py b/awx/main/models/jobs.py index b9464385ac..ac67bf8d67 100644 --- a/awx/main/models/jobs.py +++ b/awx/main/models/jobs.py @@ -684,7 +684,7 @@ class Job(UnifiedJob, JobOptions): data.update(dict(inventory=self.inventory.name if self.inventory else None, project=self.project.name if self.project else None, playbook=self.playbook, - credential=self.credential.name, + credential=self.credential.name if self.credential else None, limit=self.limit, extra_vars=self.extra_vars, hosts=all_hosts)) From 0506162ba3a411a79d5d97cacaee931aac0c4ba0 Mon Sep 17 00:00:00 2001 From: Michael Abashian Date: Mon, 11 Jul 2016 10:43:59 -0400 Subject: [PATCH 049/123] Take the searchActive flag into account when hiding the event summary hosts table --- .../src/job-detail/host-summary/host-summary.partial.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/awx/ui/client/src/job-detail/host-summary/host-summary.partial.html b/awx/ui/client/src/job-detail/host-summary/host-summary.partial.html index 669ba5c344..5452990e76 100644 --- a/awx/ui/client/src/job-detail/host-summary/host-summary.partial.html +++ b/awx/ui/client/src/job-detail/host-summary/host-summary.partial.html @@ -1,7 +1,7 @@
-
4 Please select a host below to view a summary of all associated tasks.
-
+
4 Please select a host below to view a summary of all associated tasks.
+
@@ -22,7 +22,7 @@
-
+
From 3cfc0131e12bb281a493f553b9bcff35e1369429 Mon Sep 17 00:00:00 2001 From: Bill Nottingham Date: Mon, 11 Jul 2016 10:55:45 -0400 Subject: [PATCH 050/123] Fix paste-o in tooltip. Related: #2892 --- awx/ui/client/src/inventory-scripts/inventory-scripts.list.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/awx/ui/client/src/inventory-scripts/inventory-scripts.list.js b/awx/ui/client/src/inventory-scripts/inventory-scripts.list.js index 98f2576054..213ff84dbf 100644 --- a/awx/ui/client/src/inventory-scripts/inventory-scripts.list.js +++ b/awx/ui/client/src/inventory-scripts/inventory-scripts.list.js @@ -55,7 +55,7 @@ export default function(){ icon: 'fa-edit', label: 'Edit', "class": 'btn-sm', - awToolTip: 'Edit credential', + awToolTip: 'Edit inventory script', dataPlacement: 'top' }, "delete": { @@ -63,7 +63,7 @@ export default function(){ icon: 'fa-trash', label: 'Delete', "class": 'btn-sm', - awToolTip: 'Delete credential', + awToolTip: 'Delete inventory script', dataPlacement: 'top' } } From 02732a26df0b80c0d2a28388de892bd23e4e9eb0 Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Mon, 11 Jul 2016 11:02:03 -0400 Subject: [PATCH 051/123] Swap order of save / cancel buttons for add permissions modal #2893 --- .../access/addPermissions/addPermissions.partial.html | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/awx/ui/client/src/access/addPermissions/addPermissions.partial.html b/awx/ui/client/src/access/addPermissions/addPermissions.partial.html index 725a6e273e..cc2f7ee0c7 100644 --- a/awx/ui/client/src/access/addPermissions/addPermissions.partial.html +++ b/awx/ui/client/src/access/addPermissions/addPermissions.partial.html @@ -105,17 +105,17 @@ From 37fdfb34448053a2dc35fb2874f46c36ea9d02fb Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Mon, 11 Jul 2016 11:10:02 -0400 Subject: [PATCH 052/123] fixed list labels on dashboard --- .../lists/job-templates/job-templates-list.partial.html | 2 +- awx/ui/client/src/dashboard/lists/jobs/jobs-list.partial.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/awx/ui/client/src/dashboard/lists/job-templates/job-templates-list.partial.html b/awx/ui/client/src/dashboard/lists/job-templates/job-templates-list.partial.html index 8329747c6c..9215dc8332 100644 --- a/awx/ui/client/src/dashboard/lists/job-templates/job-templates-list.partial.html +++ b/awx/ui/client/src/dashboard/lists/job-templates/job-templates-list.partial.html @@ -11,7 +11,7 @@
- Title + Name Activity diff --git a/awx/ui/client/src/dashboard/lists/jobs/jobs-list.partial.html b/awx/ui/client/src/dashboard/lists/jobs/jobs-list.partial.html index 27be4c729e..313a799ab7 100644 --- a/awx/ui/client/src/dashboard/lists/jobs/jobs-list.partial.html +++ b/awx/ui/client/src/dashboard/lists/jobs/jobs-list.partial.html @@ -10,7 +10,7 @@
- + Date: Mon, 11 Jul 2016 11:27:08 -0400 Subject: [PATCH 053/123] Remove old method of appending errors because it makes attrs None, causing server errors --- awx/api/serializers.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/awx/api/serializers.py b/awx/api/serializers.py index 9ce7aee670..f305370f64 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -1926,12 +1926,11 @@ class JobSerializer(UnifiedJobSerializer, JobOptionsSerializer): def to_internal_value(self, data): # When creating a new job and a job template is specified, populate any # fields not provided in data from the job template. - if not self.instance and isinstance(data, dict) and 'job_template' in data: + if not self.instance and isinstance(data, dict) and data.get('job_template', False): try: job_template = JobTemplate.objects.get(pk=data['job_template']) except JobTemplate.DoesNotExist: - self._errors = {'job_template': 'Invalid job template.'} - return + raise serializers.ValidationError({'job_template': 'Invalid job template.'}) data.setdefault('name', job_template.name) data.setdefault('description', job_template.description) data.setdefault('job_type', job_template.job_type) @@ -2694,8 +2693,7 @@ class TowerSettingsSerializer(BaseSerializer): def to_internal_value(self, data): if data['key'] not in settings.TOWER_SETTINGS_MANIFEST: - self._errors = {'key': 'Key {0} is not a valid settings key.'.format(data['key'])} - return + raise serializers.ValidationError({'key': ['Key {0} is not a valid settings key.'.format(data['key'])]}) ret = super(TowerSettingsSerializer, self).to_internal_value(data) manifest_val = settings.TOWER_SETTINGS_MANIFEST[data['key']] ret['description'] = manifest_val['description'] From fed8a83b08ce5d9b7a863c161be8eacef2bf75fb Mon Sep 17 00:00:00 2001 From: Leigh Johnson Date: Mon, 11 Jul 2016 12:09:16 -0400 Subject: [PATCH 054/123] fixed kickback on #2795 (#2884) --- awx/ui/client/src/smart-status/smart-status.controller.js | 1 + awx/ui/client/src/smart-status/smart-status.partial.html | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/awx/ui/client/src/smart-status/smart-status.controller.js b/awx/ui/client/src/smart-status/smart-status.controller.js index e222509d30..ff18ba9300 100644 --- a/awx/ui/client/src/smart-status/smart-status.controller.js +++ b/awx/ui/client/src/smart-status/smart-status.controller.js @@ -32,6 +32,7 @@ export default ['$scope', '$filter', data.jobId = job.id; data.sortDate = job.finished || "running" + data.jobId; data.finished = $filter('longDate')(job.finished) || job.status+""; + data.status_tip = "JOB ID: " + data.jobId + "
STATUS: " + data.smartStatus + "
FINISHED: " + data.finished; return data; }), "sortDate").reverse(); diff --git a/awx/ui/client/src/smart-status/smart-status.partial.html b/awx/ui/client/src/smart-status/smart-status.partial.html index e100caf913..e8fa03bc56 100644 --- a/awx/ui/client/src/smart-status/smart-status.partial.html +++ b/awx/ui/client/src/smart-status/smart-status.partial.html @@ -1,8 +1,8 @@
Date: Mon, 11 Jul 2016 12:23:30 -0400 Subject: [PATCH 055/123] resolves kickback on #2862 --- awx/ui/client/src/lists/ScheduledJobs.js | 13 ++++++++----- awx/ui/client/src/search/tagSearch.service.js | 7 ++++++- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/awx/ui/client/src/lists/ScheduledJobs.js b/awx/ui/client/src/lists/ScheduledJobs.js index c42ff8c1e6..30658404ba 100644 --- a/awx/ui/client/src/lists/ScheduledJobs.js +++ b/awx/ui/client/src/lists/ScheduledJobs.js @@ -45,14 +45,17 @@ export default sourceModel: 'unified_job_template', sourceField: 'unified_job_type', ngBind: 'schedule.type_label', - searchField: 'unified_job_template__name', - searchLable: 'Type', + searchField: 'unified_job_template__polymorphic_ctype__model', + filterBySearchField: true, + searchLabel: 'Type', searchable: true, searchType: 'select', searchOptions: [ - { value: 'inventory source', label: 'Inventory Sync' }, - { value: 'job template', label: 'Playbook Run' }, - { value: 'project', label: 'SCM Update' } + { value: 'inventorysource', label: 'Inventory Sync' }, + { value: 'jobtemplate', label: 'Playbook Run' }, + { value: 'project', label: 'SCM Update' }, + { value: 'systemjobtemplate', label: 'Management Job'} + ] }, next_run: { diff --git a/awx/ui/client/src/search/tagSearch.service.js b/awx/ui/client/src/search/tagSearch.service.js index 777c4c6441..5b1473a00e 100644 --- a/awx/ui/client/src/search/tagSearch.service.js +++ b/awx/ui/client/src/search/tagSearch.service.js @@ -6,7 +6,12 @@ export default ['Rest', '$q', 'GetBasePath', 'Wait', 'ProcessErrors', '$log', fu var obj = {}; // build the value (key) var value; - if (field.sourceModel && field.sourceField) { + console.log(field) + if (field.searchField && field.filterBySearchField === true){ + value = field.searchField; + } + else if (field.sourceModel && field.sourceField) { + console.log('condition met 2') value = field.sourceModel + '__' + field.sourceField; obj.related = true; } else if (typeof(field.key) === String) { From b4d489ff1bb998ba355ad129fa9a8136782224c8 Mon Sep 17 00:00:00 2001 From: Jared Tabor Date: Mon, 11 Jul 2016 09:28:25 -0700 Subject: [PATCH 056/123] XSS sanitize permissions popover and remove-permissions modal --- awx/ui/client/src/access/roleList.partial.html | 2 +- awx/ui/client/src/app.js | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/awx/ui/client/src/access/roleList.partial.html b/awx/ui/client/src/access/roleList.partial.html index d98c13d53b..1478c9dc37 100644 --- a/awx/ui/client/src/access/roleList.partial.html +++ b/awx/ui/client/src/access/roleList.partial.html @@ -9,7 +9,7 @@
+ aw-tool-tip='{{entry.team_name | sanitize}}' aw-tip-placement='bottom'> {{ entry.name }}
diff --git a/awx/ui/client/src/app.js b/awx/ui/client/src/app.js index 2229f4ca77..b6f43018fc 100644 --- a/awx/ui/client/src/app.js +++ b/awx/ui/client/src/app.js @@ -521,11 +521,12 @@ var tower = angular.module('Tower', [ 'ClearScope', 'Socket', 'LoadConfig', 'Store', 'ShowSocketHelp', 'pendoService', 'Prompt', 'Rest', 'Wait', 'ProcessErrors', '$state', 'GetBasePath', 'ConfigService', - 'FeaturesService', + 'FeaturesService', '$filter', function ($q, $compile, $cookieStore, $rootScope, $log, CheckLicense, $location, Authorization, LoadBasePaths, Timer, ClearScope, Socket, LoadConfig, Store, ShowSocketHelp, pendoService, Prompt, Rest, Wait, - ProcessErrors, $state, GetBasePath, ConfigService, FeaturesService) { + ProcessErrors, $state, GetBasePath, ConfigService, FeaturesService, + $filter) { var sock; $rootScope.addPermission = function (scope) { $compile("")(scope); @@ -563,7 +564,7 @@ var tower = angular.module('Tower', [ if (accessListEntry.team_id) { Prompt({ hdr: `Team access removal`, - body: `
Please confirm that you would like to remove ${entry.name} access from the team ${entry.team_name}. This will affect all members of the team. If you would like to only remove access for this particular user, please remove them from the team.
`, + body: `
Please confirm that you would like to remove ${entry.name} access from the team ${$filter('sanitize')(entry.team_name)}. This will affect all members of the team. If you would like to only remove access for this particular user, please remove them from the team.
`, action: action, actionText: 'REMOVE TEAM ACCESS' }); From 62351f18ceec36a514d959c7066c109838005155 Mon Sep 17 00:00:00 2001 From: Michael Abashian Date: Mon, 11 Jul 2016 13:07:23 -0400 Subject: [PATCH 057/123] Fixed bug where created_by filter was lost when adding a search tag --- awx/ui/client/src/portal-mode/portal-mode-jobs.controller.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/awx/ui/client/src/portal-mode/portal-mode-jobs.controller.js b/awx/ui/client/src/portal-mode/portal-mode-jobs.controller.js index d6f6a7e42c..c9a1fa60e0 100644 --- a/awx/ui/client/src/portal-mode/portal-mode-jobs.controller.js +++ b/awx/ui/client/src/portal-mode/portal-mode-jobs.controller.js @@ -24,6 +24,11 @@ export function PortalModeJobsController($scope, $rootScope, GetBasePath, Genera $scope.activeFilter = 'user'; var init = function(sort){ + // We need to explicitly set the lists base path so that tag searching will keep the '?created_by' + // query param when it's present. If we don't do this, then tag search will just grab the base + // path for this list (/api/v1/jobs) and lose the created_by filter + list.basePath = defaultUrl; + view.inject(list, { id: 'portal-jobs', mode: 'edit', From d3aadd4847b61c60ca3698c1c775b6596b8e7b6f Mon Sep 17 00:00:00 2001 From: Shane McDonald Date: Mon, 11 Jul 2016 13:13:27 -0400 Subject: [PATCH 058/123] Removing console.logs --- awx/ui/client/src/search/tagSearch.service.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/awx/ui/client/src/search/tagSearch.service.js b/awx/ui/client/src/search/tagSearch.service.js index 5b1473a00e..6b02b8fdf8 100644 --- a/awx/ui/client/src/search/tagSearch.service.js +++ b/awx/ui/client/src/search/tagSearch.service.js @@ -6,12 +6,10 @@ export default ['Rest', '$q', 'GetBasePath', 'Wait', 'ProcessErrors', '$log', fu var obj = {}; // build the value (key) var value; - console.log(field) if (field.searchField && field.filterBySearchField === true){ value = field.searchField; } else if (field.sourceModel && field.sourceField) { - console.log('condition met 2') value = field.sourceModel + '__' + field.sourceField; obj.related = true; } else if (typeof(field.key) === String) { From 0eee38ab1038194623b9e42bf8e1f9752b99f4b1 Mon Sep 17 00:00:00 2001 From: Jared Tabor Date: Mon, 11 Jul 2016 10:17:43 -0700 Subject: [PATCH 059/123] 1px white border for any status icon within a tooltip --- awx/ui/client/src/inventories/list/inventory-list.controller.js | 2 +- .../notification-templates-list/list.controller.js | 2 +- awx/ui/client/src/smart-status/smart-status.block.less | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/awx/ui/client/src/inventories/list/inventory-list.controller.js b/awx/ui/client/src/inventories/list/inventory-list.controller.js index 096ea2ad31..e57a7beb32 100644 --- a/awx/ui/client/src/inventories/list/inventory-list.controller.js +++ b/awx/ui/client/src/inventories/list/inventory-list.controller.js @@ -230,7 +230,7 @@ function InventoriesList($scope, $rootScope, $location, $log, data.results.forEach( function(row) { if (row.related.last_update) { html += "
"; - html += ""; + html += ``; html += ""; html += ""; html += "\n"; diff --git a/awx/ui/client/src/notifications/notification-templates-list/list.controller.js b/awx/ui/client/src/notifications/notification-templates-list/list.controller.js index 9ea29b7374..7a4bd4b408 100644 --- a/awx/ui/client/src/notifications/notification-templates-list/list.controller.js +++ b/awx/ui/client/src/notifications/notification-templates-list/list.controller.js @@ -131,7 +131,7 @@ export default recent_notifications.forEach(function(row) { html += "\n"; - html += "\n"; + html += ``; html += "\n"; html += "\n"; }); diff --git a/awx/ui/client/src/smart-status/smart-status.block.less b/awx/ui/client/src/smart-status/smart-status.block.less index 92b5244e4f..0757529d45 100644 --- a/awx/ui/client/src/smart-status/smart-status.block.less +++ b/awx/ui/client/src/smart-status/smart-status.block.less @@ -38,6 +38,7 @@ line-height: 22px; } +.SmartStatus-tooltip--successful, .SmartStatus-tooltip--success{ color: @default-succ; padding-left: 5px; From 5a9af010bf2f98ad6b081b5c7d930bdd6efd86e7 Mon Sep 17 00:00:00 2001 From: Chris Meyers Date: Mon, 11 Jul 2016 13:32:53 -0400 Subject: [PATCH 060/123] fix label docs --- awx/api/templates/api/job_template_label_list.md | 9 +++++++++ awx/api/templates/api/sub_list_create_api_view.md | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 awx/api/templates/api/job_template_label_list.md diff --git a/awx/api/templates/api/job_template_label_list.md b/awx/api/templates/api/job_template_label_list.md new file mode 100644 index 0000000000..76c520eab5 --- /dev/null +++ b/awx/api/templates/api/job_template_label_list.md @@ -0,0 +1,9 @@ +{% include "api/sub_list_create_api_view.md" %} + +Labels not associated with any other resources are deleted. A label can become disassociated with a resource as a result of 3 events. + +1. A label is explicitly diassociated with a related job template +2. A job is deleted with labels +3. A cleanup job deletes a job with labels + +{% include "api/_new_in_awx.md" %} diff --git a/awx/api/templates/api/sub_list_create_api_view.md b/awx/api/templates/api/sub_list_create_api_view.md index 75c5940f4d..2d8571c1d6 100644 --- a/awx/api/templates/api/sub_list_create_api_view.md +++ b/awx/api/templates/api/sub_list_create_api_view.md @@ -34,7 +34,7 @@ existing {{ model_verbose_name }} with this {{ parent_model_verbose_name }}. Make a POST request to this resource with `id` and `disassociate` fields to remove the {{ model_verbose_name }} from this {{ parent_model_verbose_name }} -without deleting the {{ model_verbose_name }}. +{% if model_verbose_name != "label" %}}without deleting the {{ model_verbose_name }}{% endif %}. {% endif %} {% endif %} From 3f19f3c6d28c24b5a9baeae01996430d8d3a7b37 Mon Sep 17 00:00:00 2001 From: Michael Abashian Date: Mon, 11 Jul 2016 13:34:20 -0400 Subject: [PATCH 061/123] Deep clone the PortalJobsList so that we don't modify the base config --- awx/ui/client/src/portal-mode/portal-mode-jobs.controller.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/ui/client/src/portal-mode/portal-mode-jobs.controller.js b/awx/ui/client/src/portal-mode/portal-mode-jobs.controller.js index c9a1fa60e0..3334a23940 100644 --- a/awx/ui/client/src/portal-mode/portal-mode-jobs.controller.js +++ b/awx/ui/client/src/portal-mode/portal-mode-jobs.controller.js @@ -7,7 +7,7 @@ export function PortalModeJobsController($scope, $rootScope, GetBasePath, GenerateList, PortalJobsList, SearchInit, PaginateInit){ - var list = PortalJobsList, + var list = _.cloneDeep(PortalJobsList), view = GenerateList, // show user jobs by default defaultUrl = GetBasePath('jobs') + '?created_by=' + $rootScope.current_user.id, From 0d1f5f1e54a2569f86c254f2e2cb1fcb25f45614 Mon Sep 17 00:00:00 2001 From: Leigh Johnson Date: Mon, 11 Jul 2016 13:40:55 -0400 Subject: [PATCH 062/123] resolves kickback on #2841 --- awx/ui/client/src/lists/PortalJobs.js | 1 + .../portal-mode/portal-mode-jobs.controller.js | 16 ++++++---------- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/awx/ui/client/src/lists/PortalJobs.js b/awx/ui/client/src/lists/PortalJobs.js index 49ca8a3ec2..f92497e7a4 100644 --- a/awx/ui/client/src/lists/PortalJobs.js +++ b/awx/ui/client/src/lists/PortalJobs.js @@ -43,6 +43,7 @@ export default searchable: false, filter: "longDate", key: true, + desc: true, columnClass: "col-lg-4 col-md-4 col-sm-3" } }, diff --git a/awx/ui/client/src/portal-mode/portal-mode-jobs.controller.js b/awx/ui/client/src/portal-mode/portal-mode-jobs.controller.js index d6f6a7e42c..61e43df08e 100644 --- a/awx/ui/client/src/portal-mode/portal-mode-jobs.controller.js +++ b/awx/ui/client/src/portal-mode/portal-mode-jobs.controller.js @@ -23,7 +23,8 @@ export function PortalModeJobsController($scope, $rootScope, GetBasePath, Genera $scope.iterator = list.iterator; $scope.activeFilter = 'user'; - var init = function(sort){ + var init = function(url){ + defaultUrl = url ? url : defaultUrl; view.inject(list, { id: 'portal-jobs', mode: 'edit', @@ -44,31 +45,26 @@ export function PortalModeJobsController($scope, $rootScope, GetBasePath, Genera url: defaultUrl, pageSize: pageSize }); - $scope.search (list.iterator); - if(sort) { - // hack to default to descending sort order - $scope.sort('job','finished'); - } - + $scope.search(list.iterator); }; $scope.filterUser = function(){ $scope.activeFilter = 'user'; defaultUrl = GetBasePath('jobs') + '?created_by=' + $rootScope.current_user.id; - init(true); + init(defaultUrl); }; $scope.filterAll = function(){ $scope.activeFilter = 'all'; defaultUrl = GetBasePath('jobs'); - init(true); + init(defaultUrl); }; $scope.refresh = function(){ $scope.search(list.iterator); }; - init(true); + init(); } PortalModeJobsController.$inject = ['$scope', '$rootScope', 'GetBasePath', 'generateList', 'PortalJobsList', 'SearchInit', From 134b60dbed5914ad13f1456a4cfbd3304c61ca9d Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Mon, 11 Jul 2016 14:06:40 -0400 Subject: [PATCH 063/123] Switch to explicit checks for system auditor for all applicable get_queryset calls Solves #2918 and probably a couple other corner cases where orphan situations could happen --- awx/main/access.py | 36 +++++++++++++++++------------------- 1 file changed, 17 insertions(+), 19 deletions(-) diff --git a/awx/main/access.py b/awx/main/access.py index f47198e4b0..ae1fb20f34 100644 --- a/awx/main/access.py +++ b/awx/main/access.py @@ -139,7 +139,7 @@ class BaseAccess(object): self.user = user def get_queryset(self): - if self.user.is_superuser: + if self.user.is_superuser or self.user.is_system_auditor: return self.model.objects.all() else: return self.model.objects.none() @@ -221,7 +221,7 @@ class UserAccess(BaseAccess): model = User def get_queryset(self): - if self.user.is_superuser: + if self.user.is_superuser or self.user.is_system_auditor: return User.objects.all() if tower_settings.ORG_ADMINS_CAN_SEE_ALL_USERS and \ @@ -718,7 +718,7 @@ class ProjectAccess(BaseAccess): model = Project def get_queryset(self): - if self.user.is_superuser: + if self.user.is_superuser or self.user.is_system_auditor: return self.model.objects.all() qs = self.model.accessible_objects(self.user, 'read_role') return qs.select_related('modified_by', 'credential', 'current_job', 'last_job').all() @@ -752,7 +752,7 @@ class ProjectUpdateAccess(BaseAccess): model = ProjectUpdate def get_queryset(self): - if self.user.is_superuser: + if self.user.is_superuser or self.user.is_system_auditor: return self.model.objects.all() qs = ProjectUpdate.objects.distinct() qs = qs.select_related('created_by', 'modified_by', 'project') @@ -788,7 +788,7 @@ class JobTemplateAccess(BaseAccess): model = JobTemplate def get_queryset(self): - if self.user.is_superuser: + if self.user.is_superuser or self.user.is_system_auditor: qs = self.model.objects.all() else: qs = self.model.accessible_objects(self.user, 'read_role') @@ -979,7 +979,7 @@ class JobAccess(BaseAccess): qs = qs.select_related('created_by', 'modified_by', 'job_template', 'inventory', 'project', 'credential', 'cloud_credential', 'job_template') qs = qs.prefetch_related('unified_job_template') - if self.user.is_superuser: + if self.user.is_superuser or self.user.is_system_auditor: return qs.all() qs_jt = qs.filter( @@ -1086,7 +1086,7 @@ class AdHocCommandAccess(BaseAccess): qs = self.model.objects.distinct() qs = qs.select_related('created_by', 'modified_by', 'inventory', 'credential') - if self.user.is_superuser: + if self.user.is_superuser or self.user.is_system_auditor: return qs.all() inventory_qs = Inventory.accessible_objects(self.user, 'read_role') @@ -1147,7 +1147,7 @@ class AdHocCommandEventAccess(BaseAccess): qs = self.model.objects.distinct() qs = qs.select_related('ad_hoc_command', 'host') - if self.user.is_superuser: + if self.user.is_superuser or self.user.is_system_auditor: return qs.all() ad_hoc_command_qs = self.user.get_queryset(AdHocCommand) host_qs = self.user.get_queryset(Host) @@ -1173,7 +1173,7 @@ class JobHostSummaryAccess(BaseAccess): def get_queryset(self): qs = self.model.objects qs = qs.select_related('job', 'job__job_template', 'host') - if self.user.is_superuser: + if self.user.is_superuser or self.user.is_system_auditor: return qs.all() job_qs = self.user.get_queryset(Job) host_qs = self.user.get_queryset(Host) @@ -1205,7 +1205,7 @@ class JobEventAccess(BaseAccess): event_data__icontains='"ansible_job_id": "', event_data__contains='"module_name": "async_status"') - if self.user.is_superuser: + if self.user.is_superuser or self.user.is_system_auditor: return qs.all() job_qs = self.user.get_queryset(Job) @@ -1318,7 +1318,7 @@ class ScheduleAccess(BaseAccess): qs = self.model.objects.all() qs = qs.select_related('created_by', 'modified_by') qs = qs.prefetch_related('unified_job_template') - if self.user.is_superuser: + if self.user.is_superuser or self.user.is_system_auditor: return qs.all() job_template_qs = self.user.get_queryset(JobTemplate) inventory_source_qs = self.user.get_queryset(InventorySource) @@ -1369,7 +1369,7 @@ class NotificationTemplateAccess(BaseAccess): def get_queryset(self): qs = self.model.objects.all() - if self.user.is_superuser: + if self.user.is_superuser or self.user.is_system_auditor: return qs return self.model.objects.filter(organization__in=Organization.accessible_objects(self.user, 'admin_role').all()) @@ -1413,7 +1413,7 @@ class NotificationAccess(BaseAccess): def get_queryset(self): qs = self.model.objects.all() - if self.user.is_superuser: + if self.user.is_superuser or self.user.is_system_auditor: return qs return self.model.objects.filter(notification_template__organization__in=Organization.accessible_objects(self.user, 'admin_role')) @@ -1430,7 +1430,7 @@ class LabelAccess(BaseAccess): model = Label def get_queryset(self): - if self.user.is_superuser: + if self.user.is_superuser or self.user.is_system_auditor: return self.model.objects.all() return self.model.objects.filter( organization__in=Organization.accessible_objects(self.user, 'read_role') @@ -1493,9 +1493,7 @@ class ActivityStreamAccess(BaseAccess): 'inventory_update', 'credential', 'team', 'project', 'project_update', 'permission', 'job_template', 'job', 'ad_hoc_command', 'notification_template', 'notification', 'label', 'role') - if self.user.is_superuser: - return qs.all() - if self.user in Role.singleton('system_auditor'): + if self.user.is_superuser or self.user.is_system_auditor: return qs.all() inventory_set = Inventory.accessible_objects(self.user, 'read_role') @@ -1543,7 +1541,7 @@ class CustomInventoryScriptAccess(BaseAccess): model = CustomInventoryScript def get_queryset(self): - if self.user.is_superuser: + if self.user.is_superuser or self.user.is_system_auditor: return self.model.objects.distinct().all() return self.model.accessible_objects(self.user, 'read_role').all() @@ -1599,7 +1597,7 @@ class RoleAccess(BaseAccess): def can_read(self, obj): if not obj: return False - if self.user.is_superuser: + if self.user.is_superuser or self.user.is_system_auditor: return True if obj.object_id: From 148b59d6282baad524ac2f5f73439ca8eb84012d Mon Sep 17 00:00:00 2001 From: Jared Tabor Date: Mon, 11 Jul 2016 11:11:53 -0700 Subject: [PATCH 064/123] Add additional polling if test notification is "pending" --- .../list.controller.js | 64 +++++++++++-------- 1 file changed, 38 insertions(+), 26 deletions(-) diff --git a/awx/ui/client/src/notifications/notification-templates-list/list.controller.js b/awx/ui/client/src/notifications/notification-templates-list/list.controller.js index 9ea29b7374..c180d8c6a9 100644 --- a/awx/ui/client/src/notifications/notification-templates-list/list.controller.js +++ b/awx/ui/client/src/notifications/notification-templates-list/list.controller.js @@ -147,7 +147,9 @@ export default }; scope.testNotification = function(){ - var name = $filter('sanitize')(this.notification_template.name); + var name = $filter('sanitize')(this.notification_template.name), + pending_count = 10; + Rest.setUrl(defaultUrl + this.notification_template.id +'/test/'); Rest.post({}) .then(function (data) { @@ -156,31 +158,7 @@ export default // Using a setTimeout here to wait for the // notification to be processed and for a status // to be returned from the API. - setTimeout(function(){ - var id = data.data.notification, - url = GetBasePath('notifications') + id; - Rest.setUrl(url); - Rest.get() - .then(function (res) { - Wait('stop'); - if(res && res.data && res.data.status && res.data.status === "successful"){ - scope.search(list.iterator); - ngToast.success({ - content: ` ${name}: Notification sent.` - }); - } - else if(res && res.data && res.data.status && res.data.status === "failed"){ - scope.search(list.iterator); - ngToast.danger({ - content: ` ${name}: Notification failed.` - }); - } - else { - ProcessErrors(scope, data, status, null, { hdr: 'Error!', - msg: 'Call to ' + url + ' failed. Notification returned status: ' + status }); - } - }); - } , 5000); + retrieveStatus(data.data.notification); } else { ProcessErrors(scope, data, status, null, { hdr: 'Error!', @@ -192,6 +170,40 @@ export default content: ` ${name}: Notification Failed.`, }); }); + + function retrieveStatus(id){ + setTimeout(function(){ + var url = GetBasePath('notifications') + id; + Rest.setUrl(url); + Rest.get() + .then(function (res) { + if(res && res.data && res.data.status && res.data.status === "successful"){ + scope.search(list.iterator); + ngToast.success({ + content: ` ${name}: Notification sent.` + }); + Wait('stop'); + } + else if(res && res.data && res.data.status && res.data.status === "failed"){ + scope.search(list.iterator); + ngToast.danger({ + content: ` ${name}: Notification failed.` + }); + Wait('stop'); + } + else if(res && res.data && res.data.status && res.data.status === "pending" && pending_count>0){ + pending_count--; + retrieveStatus(id); + } + else { + Wait('stop'); + ProcessErrors(scope, null, status, null, { hdr: 'Error!', + msg: 'Call to test notifications failed.' }); + } + + }); + } , 5000); + } }; scope.addNotification = function(){ From 999c3d829273bcdfa1a6e5d3bbc0126cf44e4c81 Mon Sep 17 00:00:00 2001 From: Jared Tabor Date: Mon, 11 Jul 2016 11:15:38 -0700 Subject: [PATCH 065/123] capping pending retries at 10 --- .../notification-templates-list/list.controller.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/awx/ui/client/src/notifications/notification-templates-list/list.controller.js b/awx/ui/client/src/notifications/notification-templates-list/list.controller.js index c180d8c6a9..462d888c13 100644 --- a/awx/ui/client/src/notifications/notification-templates-list/list.controller.js +++ b/awx/ui/client/src/notifications/notification-templates-list/list.controller.js @@ -148,7 +148,7 @@ export default scope.testNotification = function(){ var name = $filter('sanitize')(this.notification_template.name), - pending_count = 10; + pending_retries = 10; Rest.setUrl(defaultUrl + this.notification_template.id +'/test/'); Rest.post({}) @@ -191,8 +191,8 @@ export default }); Wait('stop'); } - else if(res && res.data && res.data.status && res.data.status === "pending" && pending_count>0){ - pending_count--; + else if(res && res.data && res.data.status && res.data.status === "pending" && pending_retries>0){ + pending_retries--; retrieveStatus(id); } else { From 6de5cceb8f3f34dc52426a43010cbe87e615eabb Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Mon, 11 Jul 2016 14:28:26 -0400 Subject: [PATCH 066/123] More is_system_auditor checks in views.py --- awx/api/views.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/awx/api/views.py b/awx/api/views.py index 4ad0a9cc58..d452758ce6 100644 --- a/awx/api/views.py +++ b/awx/api/views.py @@ -1401,7 +1401,7 @@ class OrganizationCredentialList(SubListCreateAPIView): user_visible = Credential.accessible_objects(self.request.user, 'read_role').all() org_set = Credential.accessible_objects(organization.admin_role, 'read_role').all() - if self.request.user.is_superuser: + if self.request.user.is_superuser or self.request.user.is_system_auditor: return org_set return org_set & user_visible @@ -2591,7 +2591,7 @@ class SystemJobTemplateList(ListAPIView): serializer_class = SystemJobTemplateSerializer def get(self, request, *args, **kwargs): - if not request.user.is_superuser: + if not request.user.is_superuser and not request.user.is_system_auditor: raise PermissionDenied("Superuser privileges needed.") return super(SystemJobTemplateList, self).get(request, *args, **kwargs) @@ -3321,7 +3321,7 @@ class SystemJobList(ListCreateAPIView): serializer_class = SystemJobListSerializer def get(self, request, *args, **kwargs): - if not request.user.is_superuser: + if not request.user.is_superuser and not request.user.is_system_auditor: raise PermissionDenied("Superuser privileges needed.") return super(SystemJobList, self).get(request, *args, **kwargs) @@ -3625,8 +3625,6 @@ class RoleList(ListAPIView): new_in_300 = True def get_queryset(self): - if self.request.user.is_superuser: - return Role.objects.all() return Role.visible_roles(self.request.user) From 985c23dbb4b2bc020153a3271345e093fda129c8 Mon Sep 17 00:00:00 2001 From: Leigh Johnson Date: Mon, 11 Jul 2016 14:29:59 -0400 Subject: [PATCH 067/123] incorporate mabashians changes in #2896 --- .../client/src/portal-mode/portal-mode-jobs.controller.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/awx/ui/client/src/portal-mode/portal-mode-jobs.controller.js b/awx/ui/client/src/portal-mode/portal-mode-jobs.controller.js index 61e43df08e..f6bd8ff9df 100644 --- a/awx/ui/client/src/portal-mode/portal-mode-jobs.controller.js +++ b/awx/ui/client/src/portal-mode/portal-mode-jobs.controller.js @@ -7,7 +7,7 @@ export function PortalModeJobsController($scope, $rootScope, GetBasePath, GenerateList, PortalJobsList, SearchInit, PaginateInit){ - var list = PortalJobsList, + var list = _.cloneDeep(PortalJobsList), view = GenerateList, // show user jobs by default defaultUrl = GetBasePath('jobs') + '?created_by=' + $rootScope.current_user.id, @@ -25,6 +25,11 @@ export function PortalModeJobsController($scope, $rootScope, GetBasePath, Genera var init = function(url){ defaultUrl = url ? url : defaultUrl; + // We need to explicitly set the lists base path so that tag searching will keep the '?created_by' + // query param when it's present. If we don't do this, then tag search will just grab the base + // path for this list (/api/v1/jobs) and lose the created_by filter + list.basePath = defaultUrl; + view.inject(list, { id: 'portal-jobs', mode: 'edit', From 43c5105a577dc0de221f53183c818785a24b3686 Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Mon, 11 Jul 2016 14:36:10 -0400 Subject: [PATCH 068/123] No more Mr. Nice Tower or how I learned to stop worrying and use SIGKILL. Ansible has a bug that could potentially leave tower's jobs hanging around indefinitely requiring manual shell intervention. I'm going to put this here and until Ansible can get to the bottom of it. --- awx/main/tasks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/main/tasks.py b/awx/main/tasks.py index 04efceb3ae..f4d442fbeb 100644 --- a/awx/main/tasks.py +++ b/awx/main/tasks.py @@ -599,7 +599,7 @@ class BaseTask(Task): else: child_procs = main_proc.get_children(recursive=True) for child_proc in child_procs: - os.kill(child_proc.pid, signal.SIGTERM) + os.kill(child_proc.pid, signal.SIGKILL) except TypeError: os.kill(child.pid, signal.SIGKILL) else: From 4c67c50373f5f0474aeee855960312c94761910b Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Mon, 11 Jul 2016 14:42:01 -0400 Subject: [PATCH 069/123] Allow system auditors to see notification templates and management jobs in the UI --- awx/ui/client/src/app.js | 1 + awx/ui/client/src/login/loginModal/loginModal.controller.js | 1 + awx/ui/client/src/setup-menu/setup-menu.partial.html | 4 ++-- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/awx/ui/client/src/app.js b/awx/ui/client/src/app.js index 2229f4ca77..ab39dd8415 100644 --- a/awx/ui/client/src/app.js +++ b/awx/ui/client/src/app.js @@ -880,6 +880,7 @@ var tower = angular.module('Tower', [ } // If browser refresh, set the user_is_superuser value $rootScope.user_is_superuser = Authorization.getUserInfo('is_superuser'); + $rootScope.user_is_system_auditor = Authorization.getUserInfo('is_system_auditor'); // state the user refreshes we want to open the socket, except if the user is on the login page, which should happen after the user logs in (see the AuthService module for that call to OpenSocket) if(!_.contains($location.$$url, '/login')){ ConfigService.getConfig().then(function(){ diff --git a/awx/ui/client/src/login/loginModal/loginModal.controller.js b/awx/ui/client/src/login/loginModal/loginModal.controller.js index c87aafba41..9bd5132ab1 100644 --- a/awx/ui/client/src/login/loginModal/loginModal.controller.js +++ b/awx/ui/client/src/login/loginModal/loginModal.controller.js @@ -137,6 +137,7 @@ export default ['$log', '$cookieStore', '$compile', '$window', '$rootScope', $rootScope.sessionTimer = timer; $rootScope.$emit('OpenSocket'); $rootScope.user_is_superuser = data.results[0].is_superuser; + $rootScope.user_is_system_auditor = data.results[0].is_system_auditor; scope.$emit('AuthorizationGetLicense'); }); }) diff --git a/awx/ui/client/src/setup-menu/setup-menu.partial.html b/awx/ui/client/src/setup-menu/setup-menu.partial.html index ae1cdb0ee2..fc9a04d4ed 100644 --- a/awx/ui/client/src/setup-menu/setup-menu.partial.html +++ b/awx/ui/client/src/setup-menu/setup-menu.partial.html @@ -24,7 +24,7 @@ Add passwords, SSH keys, etc. for Tower to use when launching jobs against machines, or when syncing inventories or projects.

- +

Management Jobs

Manage the cleanup of old job history, activity streams, data marked for deletion, and system tracking info. @@ -37,7 +37,7 @@

+ ng-if="orgAdmin || user_is_system_auditor || user_is_superuser">

Notifications

Create templates for sending notifications with Email, HipChat, Slack, and SMS. From ec37703ce85ed6d0bd30b71852e30342d883b59a Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Mon, 11 Jul 2016 14:44:36 -0400 Subject: [PATCH 070/123] Allow SA's to read all notification templates --- awx/main/access.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/awx/main/access.py b/awx/main/access.py index ae1fb20f34..38d50ba1c9 100644 --- a/awx/main/access.py +++ b/awx/main/access.py @@ -1375,6 +1375,8 @@ class NotificationTemplateAccess(BaseAccess): @check_superuser def can_read(self, obj): + if self.user.is_superuser or self.user.is_system_auditor: + return True if obj.organization is not None: return self.user in obj.organization.admin_role return False From 2f843d15e0bdd62b728d0fb686671a7cb651135f Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Mon, 11 Jul 2016 14:45:21 -0400 Subject: [PATCH 071/123] Also forcefully kill the main process --- awx/main/tasks.py | 1 + 1 file changed, 1 insertion(+) diff --git a/awx/main/tasks.py b/awx/main/tasks.py index f4d442fbeb..2325702da6 100644 --- a/awx/main/tasks.py +++ b/awx/main/tasks.py @@ -600,6 +600,7 @@ class BaseTask(Task): child_procs = main_proc.get_children(recursive=True) for child_proc in child_procs: os.kill(child_proc.pid, signal.SIGKILL) + os.kill(main_proc.pid, signal.SIGKILL) except TypeError: os.kill(child.pid, signal.SIGKILL) else: From fd461a9768f939f28e1cac75429186da4c8df1d7 Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Mon, 11 Jul 2016 15:05:35 -0400 Subject: [PATCH 072/123] Remove redundant check --- awx/main/access.py | 1 - 1 file changed, 1 deletion(-) diff --git a/awx/main/access.py b/awx/main/access.py index 38d50ba1c9..85d4a9d980 100644 --- a/awx/main/access.py +++ b/awx/main/access.py @@ -1373,7 +1373,6 @@ class NotificationTemplateAccess(BaseAccess): return qs return self.model.objects.filter(organization__in=Organization.accessible_objects(self.user, 'admin_role').all()) - @check_superuser def can_read(self, obj): if self.user.is_superuser or self.user.is_system_auditor: return True From d0aa28e9b8582794628ed4a18a5050f356b23789 Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Mon, 11 Jul 2016 15:16:15 -0400 Subject: [PATCH 073/123] fix scheduler datepicker issues --- awx/ui/client/src/scheduler/schedulerDatePicker.directive.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/awx/ui/client/src/scheduler/schedulerDatePicker.directive.js b/awx/ui/client/src/scheduler/schedulerDatePicker.directive.js index 7718bdb40a..8eacdefda0 100644 --- a/awx/ui/client/src/scheduler/schedulerDatePicker.directive.js +++ b/awx/ui/client/src/scheduler/schedulerDatePicker.directive.js @@ -36,6 +36,7 @@ export default mustUpdateValue = false; scope.dateValueMoment = moment(newValue, ['MM/DD/YYYY'], moment.locale()); scope.dateValue = scope.dateValueMoment.format('L'); + element.find(".DatePicker").systemTrackingDP('update', scope.dateValue); } }, true); @@ -54,7 +55,7 @@ export default element.find(".DatePicker").addClass("input-prepend date"); element.find(".DatePicker").find(".DatePicker-icon").addClass("add-on"); - $(".date").systemTrackingDP({ + element.find(".DatePicker").systemTrackingDP({ autoclose: true, language: localeKey, format: dateFormat From f7e9d07dad669295e894abd061d484e26e394496 Mon Sep 17 00:00:00 2001 From: Wayne Witzel III Date: Mon, 11 Jul 2016 15:25:35 -0400 Subject: [PATCH 074/123] ensure CustomInventoryScripts render correctly in the ActivityStream --- awx/api/serializers.py | 7 ++++++- awx/main/signals.py | 1 + 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/awx/api/serializers.py b/awx/api/serializers.py index 9ce7aee670..973c40f3d5 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -87,6 +87,7 @@ SUMMARIZABLE_FK_FIELDS = { 'current_update': DEFAULT_SUMMARY_FIELDS + ('status', 'failed', 'license_error'), 'current_job': DEFAULT_SUMMARY_FIELDS + ('status', 'failed', 'license_error'), 'inventory_source': ('source', 'last_updated', 'status'), + 'custom_inventory_script': DEFAULT_SUMMARY_FIELDS, 'source_script': ('name', 'description'), 'role': ('id', 'role_field'), 'notification_template': DEFAULT_SUMMARY_FIELDS, @@ -2621,7 +2622,11 @@ class ActivityStreamSerializer(BaseSerializer): if getattr(obj, fk).exists(): rel[fk] = [] for thisItem in allm2m: - rel[fk].append(reverse('api:' + fk + '_detail', args=(thisItem.id,))) + if fk == 'custom_inventory_script': + rel[fk].append(reverse('api:inventory_script_detail', args=(thisItem.id,))) + else: + rel[fk].append(reverse('api:' + fk + '_detail', args=(thisItem.id,))) + if fk == 'schedule': rel['unified_job_template'] = thisItem.unified_job_template.get_absolute_url() return rel diff --git a/awx/main/signals.py b/awx/main/signals.py index 1c57fea8ae..7389f01763 100644 --- a/awx/main/signals.py +++ b/awx/main/signals.py @@ -323,6 +323,7 @@ model_serializer_mapping = { Host: HostSerializer, Group: GroupSerializer, InventorySource: InventorySourceSerializer, + CustomInventoryScript: CustomInventoryScriptSerializer, Credential: CredentialSerializer, Team: TeamSerializer, Project: ProjectSerializer, From 1b613d919b709f11ec7f5166cf678a0d118e402e Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Mon, 11 Jul 2016 16:51:29 -0400 Subject: [PATCH 075/123] include all roles in the select_related --- awx/api/views.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/awx/api/views.py b/awx/api/views.py index 4ad0a9cc58..16a0bbb719 100644 --- a/awx/api/views.py +++ b/awx/api/views.py @@ -958,6 +958,7 @@ class ProjectList(ListCreateAPIView): 'admin_role', 'use_role', 'update_role', + 'read_role', ) return projects_qs @@ -1487,7 +1488,7 @@ class InventoryList(ListCreateAPIView): def get_queryset(self): qs = Inventory.accessible_objects(self.request.user, 'read_role') - qs = qs.select_related('admin_role', 'read_role', 'update_role', 'use_role') + qs = qs.select_related('admin_role', 'read_role', 'update_role', 'use_role', 'adhoc_role') return qs class InventoryDetail(RetrieveUpdateDestroyAPIView): From f68495cf58fb1cf4b6dfef3cbb37d6ee2eacbcc4 Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Tue, 12 Jul 2016 09:11:44 -0400 Subject: [PATCH 076/123] allow org auditors to view notification templates --- awx/main/access.py | 8 ++++++-- awx/main/tests/functional/test_rbac_notifications.py | 5 +++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/awx/main/access.py b/awx/main/access.py index 85d4a9d980..b1e7c2fd7d 100644 --- a/awx/main/access.py +++ b/awx/main/access.py @@ -1371,13 +1371,17 @@ class NotificationTemplateAccess(BaseAccess): qs = self.model.objects.all() if self.user.is_superuser or self.user.is_system_auditor: return qs - return self.model.objects.filter(organization__in=Organization.accessible_objects(self.user, 'admin_role').all()) + return self.model.objects.filter( + Q(organization__in=self.user.admin_of_organizations) | + Q(organization__in=self.user.auditor_of_organizations) + ).distinct() def can_read(self, obj): if self.user.is_superuser or self.user.is_system_auditor: return True if obj.organization is not None: - return self.user in obj.organization.admin_role + if self.user in obj.organization.admin_role or self.user in obj.organization.auditor_role: + return True return False @check_superuser diff --git a/awx/main/tests/functional/test_rbac_notifications.py b/awx/main/tests/functional/test_rbac_notifications.py index 35cbd43814..cafef084e6 100644 --- a/awx/main/tests/functional/test_rbac_notifications.py +++ b/awx/main/tests/functional/test_rbac_notifications.py @@ -24,6 +24,11 @@ def test_notification_template_get_queryset_orgadmin(notification_template, user notification_template.organization.admin_role.members.add(user('admin', False)) assert access.get_queryset().count() == 1 +@pytest.mark.django_db +def test_notification_template_get_queryset_org_auditor(notification_template, org_auditor): + access = NotificationTemplateAccess(org_auditor) + assert access.get_queryset().count() == 1 + @pytest.mark.django_db def test_notification_template_access_superuser(notification_template_factory): nf_objects = notification_template_factory('test-orphaned', organization='test', superusers=['admin']) From 8a5ddef6fb47c6d7dc19c100b01396e3c054b7d5 Mon Sep 17 00:00:00 2001 From: Ken Hoes Date: Tue, 12 Jul 2016 09:48:22 -0400 Subject: [PATCH 077/123] Correctly redraw codemirror instance on invalid json error --- awx/ui/client/src/helpers/Parse.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/awx/ui/client/src/helpers/Parse.js b/awx/ui/client/src/helpers/Parse.js index 8057270b4a..c484963162 100644 --- a/awx/ui/client/src/helpers/Parse.js +++ b/awx/ui/client/src/helpers/Parse.js @@ -71,7 +71,7 @@ export default } catch (e) { Alert('Parse Error', 'Failed to parse valid YAML. ' + e.message); - setTimeout( function() { scope.$apply( function() { scope[model] = 'yaml'; createField(); }); }, 500); + setTimeout( function() { scope.$apply( function() { scope[model] = 'yaml'; createField(onReady, onChange, fld); }); }, 500); } } else { @@ -89,7 +89,7 @@ export default } catch (e) { Alert('Parse Error', 'Failed to parse valid JSON. ' + e.message); - setTimeout( function() { scope.$apply( function() { scope[model] = 'json'; createField(); }); }, 500 ); + setTimeout( function() { scope.$apply( function() { scope[model] = 'json'; createField(onReady, onChange, fld); }); }, 500 ); } } }; From 5141fc6e6ab51bdab278cf2541c9decf8000f963 Mon Sep 17 00:00:00 2001 From: Michael Abashian Date: Tue, 12 Jul 2016 09:56:29 -0400 Subject: [PATCH 078/123] Fix bug where filtering failed tasks was throwing JS error --- awx/ui/client/src/job-detail/job-detail.controller.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/awx/ui/client/src/job-detail/job-detail.controller.js b/awx/ui/client/src/job-detail/job-detail.controller.js index ee40db21ed..4791803ebc 100644 --- a/awx/ui/client/src/job-detail/job-detail.controller.js +++ b/awx/ui/client/src/job-detail/job-detail.controller.js @@ -841,7 +841,7 @@ export default }; scope.searchTasks = function() { - var params; + var params = {}; if (scope.search_task_name) { scope.searchTasksEnabled = false; } @@ -866,7 +866,7 @@ export default }; scope.searchHosts = function() { - var params; + var params = {}; if (scope.search_host_name) { scope.searchHostsEnabled = false; } From 0fcc9abd696929817a3bc40b36b21fe954372dea Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Tue, 12 Jul 2016 10:27:30 -0400 Subject: [PATCH 079/123] allow org auditors to see notifications --- awx/main/access.py | 5 ++++- awx/main/tests/functional/conftest.py | 14 +++++++++++++- .../tests/functional/test_rbac_notifications.py | 17 ++++++++++++++++- 3 files changed, 33 insertions(+), 3 deletions(-) diff --git a/awx/main/access.py b/awx/main/access.py index b1e7c2fd7d..40eb3db2e1 100644 --- a/awx/main/access.py +++ b/awx/main/access.py @@ -1420,7 +1420,10 @@ class NotificationAccess(BaseAccess): qs = self.model.objects.all() if self.user.is_superuser or self.user.is_system_auditor: return qs - return self.model.objects.filter(notification_template__organization__in=Organization.accessible_objects(self.user, 'admin_role')) + return self.model.objects.filter( + Q(notification_template__organization__in=self.user.admin_of_organizations) | + Q(notification_template__organization__in=self.user.auditor_of_organizations) + ).distinct() def can_read(self, obj): return self.user.can_access(NotificationTemplate, 'read', obj.notification_template) diff --git a/awx/main/tests/functional/conftest.py b/awx/main/tests/functional/conftest.py index 3c335a2840..fdc3d3d95e 100644 --- a/awx/main/tests/functional/conftest.py +++ b/awx/main/tests/functional/conftest.py @@ -38,7 +38,10 @@ from awx.main.models.organization import ( Team, ) -from awx.main.models.notifications import NotificationTemplate +from awx.main.models.notifications import ( + NotificationTemplate, + Notification +) ''' Disable all django model signals. @@ -193,6 +196,15 @@ def notification_template(organization): notification_configuration=dict(url="http://localhost", headers={"Test": "Header"})) +@pytest.fixture +def notification(notification_template): + return Notification.objects.create(notification_template=notification_template, + status='successful', + notifications_sent=1, + notification_type='email', + recipients='admin@admin.com', + subject='email subject') + @pytest.fixture def job_with_secret_key(job_with_secret_key_factory): return job_with_secret_key_factory(persisted=True) diff --git a/awx/main/tests/functional/test_rbac_notifications.py b/awx/main/tests/functional/test_rbac_notifications.py index cafef084e6..1af0d06818 100644 --- a/awx/main/tests/functional/test_rbac_notifications.py +++ b/awx/main/tests/functional/test_rbac_notifications.py @@ -1,6 +1,9 @@ import pytest -from awx.main.access import NotificationTemplateAccess +from awx.main.access import ( + NotificationTemplateAccess, + NotificationAccess +) @pytest.mark.django_db def test_notification_template_get_queryset_orgmember(notification_template, user): @@ -86,3 +89,15 @@ def test_notificaiton_template_orphan_access_org_admin(notification_template, or notification_template.organization = None access = NotificationTemplateAccess(org_admin) assert not access.can_change(notification_template, {'organization': organization.id}) + +@pytest.mark.django_db +def test_notification_access_org_admin(notification, org_admin): + access = NotificationAccess(org_admin) + assert access.get_queryset().count() == 1 + assert access.can_read(notification) + +@pytest.mark.django_db +def test_notification_access_org_auditor(notification, org_auditor): + access = NotificationAccess(org_auditor) + assert access.get_queryset().count() == 1 + assert access.can_read(notification) From 36286bcda2f778148ec098b37ca544ba8773b41c Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Tue, 12 Jul 2016 11:14:29 -0400 Subject: [PATCH 080/123] notifier rbac test made consistent with others --- awx/main/tests/functional/conftest.py | 2 +- .../functional/test_rbac_notifications.py | 20 +++++++++++++++++-- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/awx/main/tests/functional/conftest.py b/awx/main/tests/functional/conftest.py index fdc3d3d95e..f970adc2e7 100644 --- a/awx/main/tests/functional/conftest.py +++ b/awx/main/tests/functional/conftest.py @@ -202,7 +202,7 @@ def notification(notification_template): status='successful', notifications_sent=1, notification_type='email', - recipients='admin@admin.com', + recipients='admin@redhat.com', subject='email subject') @pytest.fixture diff --git a/awx/main/tests/functional/test_rbac_notifications.py b/awx/main/tests/functional/test_rbac_notifications.py index 1af0d06818..a9a5e7c5f9 100644 --- a/awx/main/tests/functional/test_rbac_notifications.py +++ b/awx/main/tests/functional/test_rbac_notifications.py @@ -91,13 +91,29 @@ def test_notificaiton_template_orphan_access_org_admin(notification_template, or assert not access.can_change(notification_template, {'organization': organization.id}) @pytest.mark.django_db -def test_notification_access_org_admin(notification, org_admin): +def test_notification_access_get_queryset_org_admin(notification, org_admin): access = NotificationAccess(org_admin) assert access.get_queryset().count() == 1 + +@pytest.mark.django_db +def test_notification_access_get_queryset_org_auditor(notification, org_auditor): + access = NotificationAccess(org_auditor) + assert access.get_queryset().count() == 1 + +@pytest.mark.django_db +def test_notification_access_system_admin(notification, admin): + access = NotificationAccess(admin) assert access.can_read(notification) + assert access.can_delete(notification) + +@pytest.mark.django_db +def test_notification_access_org_admin(notification, org_admin): + access = NotificationAccess(org_admin) + assert access.can_read(notification) + assert access.can_delete(notification) @pytest.mark.django_db def test_notification_access_org_auditor(notification, org_auditor): access = NotificationAccess(org_auditor) - assert access.get_queryset().count() == 1 assert access.can_read(notification) + assert not access.can_delete(notification) From 5930ad03fa66ee932c07558a99520dc9b3a7c285 Mon Sep 17 00:00:00 2001 From: Michael Abashian Date: Tue, 12 Jul 2016 12:58:28 -0400 Subject: [PATCH 081/123] Updated the tab active background color on the job launch modal to match other tabs --- awx/ui/client/src/job-submission/job-submission.block.less | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/awx/ui/client/src/job-submission/job-submission.block.less b/awx/ui/client/src/job-submission/job-submission.block.less index 60272deda5..eed6b797a5 100644 --- a/awx/ui/client/src/job-submission/job-submission.block.less +++ b/awx/ui/client/src/job-submission/job-submission.block.less @@ -103,8 +103,8 @@ } .JobSubmission-step--active { color: @default-bg!important; - background-color: @d7grey!important; - border-color: @d7grey!important; + background-color: @default-icon!important; + border-color: @default-icon!important; cursor: default!important; } .JobSubmission-step--disabled { From 15f06ec9d3e756595a4164acac93ae62d6fd02d2 Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Tue, 12 Jul 2016 15:03:33 -0400 Subject: [PATCH 082/123] allow system auditors to see orphan inventory scripts --- awx/main/access.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/awx/main/access.py b/awx/main/access.py index 40eb3db2e1..27f7c80cda 100644 --- a/awx/main/access.py +++ b/awx/main/access.py @@ -1573,10 +1573,6 @@ class CustomInventoryScriptAccess(BaseAccess): def can_delete(self, obj): return self.can_admin(obj) - @check_superuser - def can_read(self, obj): - return self.user in obj.read_role - class TowerSettingsAccess(BaseAccess): ''' From e6f2b0416a2e3403e09b845c6e55d5177203e9f8 Mon Sep 17 00:00:00 2001 From: Michael Abashian Date: Tue, 12 Jul 2016 15:33:28 -0400 Subject: [PATCH 083/123] Fixed bug where normal job launch on job template edit was attempting to relaunch. --- awx/ui/client/src/helpers/Jobs.js | 2 +- .../client/src/job-detail/job-detail.controller.js | 6 +++--- .../initiateplaybookrun.factory.js | 3 ++- .../job-submission/job-submission.controller.js | 14 ++++++-------- .../src/job-submission/job-submission.directive.js | 3 ++- 5 files changed, 14 insertions(+), 14 deletions(-) diff --git a/awx/ui/client/src/helpers/Jobs.js b/awx/ui/client/src/helpers/Jobs.js index 28727b700e..1e2db376fb 100644 --- a/awx/ui/client/src/helpers/Jobs.js +++ b/awx/ui/client/src/helpers/Jobs.js @@ -450,7 +450,7 @@ export default return function(params) { var scope = params.scope, id = params.id; - InitiatePlaybookRun({ scope: scope, id: id }); + InitiatePlaybookRun({ scope: scope, id: id, relaunch: true }); }; }]) diff --git a/awx/ui/client/src/job-detail/job-detail.controller.js b/awx/ui/client/src/job-detail/job-detail.controller.js index 4791803ebc..3f6e8f7209 100644 --- a/awx/ui/client/src/job-detail/job-detail.controller.js +++ b/awx/ui/client/src/job-detail/job-detail.controller.js @@ -13,13 +13,13 @@ export default [ '$location', '$rootScope', '$filter', '$scope', '$compile', '$state', '$stateParams', '$log', 'ClearScope', 'GetBasePath', 'Wait', 'ProcessErrors', 'SelectPlay', 'SelectTask', 'GetElapsed', 'JobIsFinished', - 'SetTaskStyles', 'DigestEvent', 'UpdateDOM', 'DeleteJob', 'InitiatePlaybookRun', 'LoadPlays', 'LoadTasks', + 'SetTaskStyles', 'DigestEvent', 'UpdateDOM', 'DeleteJob', 'RelaunchPlaybook', 'LoadPlays', 'LoadTasks', 'ParseVariableString', 'GetChoices', 'fieldChoices', 'fieldLabels', 'EditSchedule', 'ParseTypeChange', 'JobDetailService', function( $location, $rootScope, $filter, $scope, $compile, $state, $stateParams, $log, ClearScope, GetBasePath, Wait, ProcessErrors, SelectPlay, SelectTask, GetElapsed, JobIsFinished, - SetTaskStyles, DigestEvent, UpdateDOM, DeleteJob, InitiatePlaybookRun, LoadPlays, LoadTasks, + SetTaskStyles, DigestEvent, UpdateDOM, DeleteJob, RelaunchPlaybook, LoadPlays, LoadTasks, ParseVariableString, GetChoices, fieldChoices, fieldLabels, EditSchedule, ParseTypeChange, JobDetailService ) { @@ -920,7 +920,7 @@ export default }; scope.relaunchJob = function() { - InitiatePlaybookRun({ + RelaunchPlaybook({ scope: scope, id: scope.job.id }); diff --git a/awx/ui/client/src/job-submission/job-submission-factories/initiateplaybookrun.factory.js b/awx/ui/client/src/job-submission/job-submission-factories/initiateplaybookrun.factory.js index 34743b99ea..ed16e745e5 100644 --- a/awx/ui/client/src/job-submission/job-submission-factories/initiateplaybookrun.factory.js +++ b/awx/ui/client/src/job-submission/job-submission-factories/initiateplaybookrun.factory.js @@ -9,10 +9,11 @@ export default return function (params) { var scope = params.scope.$new(), id = params.id, + relaunch = params.relaunch || false, system_job = params.system_job || false; scope.job_template_id = id; - var el = $compile( "" )( scope ); + var el = $compile( "" )( scope ); $('#content-container').remove('submit-job').append( el ); }; } diff --git a/awx/ui/client/src/job-submission/job-submission.controller.js b/awx/ui/client/src/job-submission/job-submission.controller.js index 00c7e971d3..d059f9ec96 100644 --- a/awx/ui/client/src/job-submission/job-submission.controller.js +++ b/awx/ui/client/src/job-submission/job-submission.controller.js @@ -131,13 +131,11 @@ export default $scope.forms = {}; $scope.passwords = {}; - var base = $state.current.name, - // As of 3.0, the only place the user can relaunch a - // playbook is on jobTemplates.edit (completed_jobs tab), - // jobs, and jobDetails $states. - isRelaunch = !(base === 'jobTemplates' || base === 'portalMode' || base === 'dashboard'); + // As of 3.0, the only place the user can relaunch a + // playbook is on jobTemplates.edit (completed_jobs tab), + // jobs, and jobDetails $states. - if (!isRelaunch) { + if (!$scope.submitJobRelaunch) { launch_url = GetBasePath('job_templates') + $scope.submitJobId + '/launch/'; } else { @@ -189,7 +187,7 @@ export default updateRequiredPasswords(); } - if( (isRelaunch && !$scope.password_needed) || (!isRelaunch && $scope.can_start_without_user_input && !$scope.ask_inventory_on_launch && !$scope.ask_credential_on_launch && !$scope.has_other_prompts && !$scope.survey_enabled)) { + if( ($scope.submitJobRelaunch && !$scope.password_needed) || (!$scope.submitJobRelaunch && $scope.can_start_without_user_input && !$scope.ask_inventory_on_launch && !$scope.ask_credential_on_launch && !$scope.has_other_prompts && !$scope.survey_enabled)) { // The job can be launched if // a) It's a relaunch and no passwords are needed // or @@ -217,7 +215,7 @@ export default $scope.openLaunchModal(); }; - if(isRelaunch) { + if($scope.submitJobRelaunch) { // Go out and get some of the job details like inv, cred, name Rest.setUrl(GetBasePath('jobs') + $scope.submitJobId); Rest.get() diff --git a/awx/ui/client/src/job-submission/job-submission.directive.js b/awx/ui/client/src/job-submission/job-submission.directive.js index d31279f00d..bd1c90ff92 100644 --- a/awx/ui/client/src/job-submission/job-submission.directive.js +++ b/awx/ui/client/src/job-submission/job-submission.directive.js @@ -11,7 +11,8 @@ export default [ 'templateUrl', 'CreateDialog', 'Wait', 'CreateSelect2', 'ParseT return { scope: { submitJobId: '=', - submitJobSystem: '=' + submitJobSystem: '=', + submitJobRelaunch: '=' }, templateUrl: templateUrl('job-submission/job-submission'), controller: jobSubmissionController, From 646f848cfb12da3ad20d69b2b8d2ce8e652a1ba3 Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Tue, 12 Jul 2016 15:46:48 -0400 Subject: [PATCH 084/123] fix incorrect error message --- awx/api/views.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/awx/api/views.py b/awx/api/views.py index 43da567e8f..4af8a6561e 100644 --- a/awx/api/views.py +++ b/awx/api/views.py @@ -3651,10 +3651,10 @@ class RoleUsersList(SubListCreateAttachDetachAPIView): return role.members.all() def post(self, request, *args, **kwargs): - # Forbid implicit role creation here + # Forbid implicit user creation here sub_id = request.data.get('id', None) if not sub_id: - data = dict(msg="Role 'id' field is missing.") + data = dict(msg="User 'id' field is missing.") return Response(data, status=status.HTTP_400_BAD_REQUEST) return super(RoleUsersList, self).post(request, *args, **kwargs) From 745209d3625ebfb0e48f4b29e999361158196771 Mon Sep 17 00:00:00 2001 From: Leigh Johnson Date: Tue, 12 Jul 2016 15:49:25 -0400 Subject: [PATCH 085/123] init search with correct query param, resolves #2947 --- awx/ui/client/src/helpers/JobTemplates.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/awx/ui/client/src/helpers/JobTemplates.js b/awx/ui/client/src/helpers/JobTemplates.js index 1dbbe33584..7db3e3cb2e 100644 --- a/awx/ui/client/src/helpers/JobTemplates.js +++ b/awx/ui/client/src/helpers/JobTemplates.js @@ -162,6 +162,8 @@ angular.module('JobTemplatesHelper', ['Utilities']) input_type: "radio" }); + CredentialList.basePath = GetBasePath('credentials') + '?kind=ssh'; + LookUpInit({ url: GetBasePath('credentials') + '?kind=ssh', scope: scope, From 9389d9014b039a7343581bd557bec78821c8ab92 Mon Sep 17 00:00:00 2001 From: Jared Tabor Date: Tue, 12 Jul 2016 13:17:58 -0700 Subject: [PATCH 086/123] Jobs and Scheduled Jobs dynamic tooltip fix Both tooltips would break if sorting/filtering the jobs list and the scheduled jobs list --- awx/ui/client/src/lists/AllJobs.js | 1 + awx/ui/client/src/lists/ScheduledJobs.js | 1 + 2 files changed, 2 insertions(+) diff --git a/awx/ui/client/src/lists/AllJobs.js b/awx/ui/client/src/lists/AllJobs.js index 955d239dc4..ee28221c1d 100644 --- a/awx/ui/client/src/lists/AllJobs.js +++ b/awx/ui/client/src/lists/AllJobs.js @@ -49,6 +49,7 @@ export default ngClick: "viewJobDetails(all_job)", defaultSearchField: true, awToolTip: "{{ all_job.name | sanitize }}", + dataTipWatch: 'all_job.name', dataPlacement: 'top' }, type: { diff --git a/awx/ui/client/src/lists/ScheduledJobs.js b/awx/ui/client/src/lists/ScheduledJobs.js index 30658404ba..be4e7bd22d 100644 --- a/awx/ui/client/src/lists/ScheduledJobs.js +++ b/awx/ui/client/src/lists/ScheduledJobs.js @@ -35,6 +35,7 @@ export default sourceField: 'name', ngClick: "editSchedule(schedule)", awToolTip: "{{ schedule.nameTip | sanitize}}", + dataTipWatch: 'schedule.nameTip', dataPlacement: "top", defaultSearchField: true }, From d6679936dae2fe3a7cdb44cbe935c561481e4541 Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Tue, 12 Jul 2016 16:29:46 -0400 Subject: [PATCH 087/123] Update license info, add tool for helpsies --- .../ip-associations-python-novaclient-ext | 15 ++ ...ip-associations-python-novaclient-ext.txt} | 0 docs/licenses/irc.txt | 10 + ...> os-diskconfig-python-novaclient-ext.txt} | 0 ...> os-networksv2-python-novaclient-ext.txt} | 0 ...al-interfacesv2-python-novaclient-ext.txt} | 0 docs/licenses/python-heatclient.txt | 176 ++++++++++++++++++ docs/licenses/python-openstackclient.txt | 176 ++++++++++++++++++ docs/licenses/pywinrmkerberos.txt | 20 ++ ...t-network-flags-python-novaclient-ext.txt} | 0 ...cheduled-images-python-novaclient-ext.txt} | 0 docs/licenses/requestsexceptions.txt | 14 ++ docs/licenses/total-ordering.txt | 28 +++ tools/audit-api-license.sh | 20 ++ 14 files changed, 459 insertions(+) create mode 100644 docs/licenses/ip-associations-python-novaclient-ext rename docs/licenses/{ip_associations_python_novaclient_ext.txt => ip-associations-python-novaclient-ext.txt} (100%) create mode 100644 docs/licenses/irc.txt rename docs/licenses/{os_diskconfig_python_novaclient_ext.txt => os-diskconfig-python-novaclient-ext.txt} (100%) rename docs/licenses/{os_networksv2_python_novaclient_ext.txt => os-networksv2-python-novaclient-ext.txt} (100%) rename docs/licenses/{os_virtual_interfacesv2_python_novaclient_ext.txt => os-virtual-interfacesv2-python-novaclient-ext.txt} (100%) create mode 100644 docs/licenses/python-heatclient.txt create mode 100644 docs/licenses/python-openstackclient.txt create mode 100644 docs/licenses/pywinrmkerberos.txt rename docs/licenses/{rax_default_network_flags_python_novaclient_ext.txt => rax-default-network-flags-python-novaclient-ext.txt} (100%) rename docs/licenses/{rax_scheduled_images_python_novaclient_ext.txt => rax-scheduled-images-python-novaclient-ext.txt} (100%) create mode 100644 docs/licenses/requestsexceptions.txt create mode 100644 docs/licenses/total-ordering.txt create mode 100755 tools/audit-api-license.sh diff --git a/docs/licenses/ip-associations-python-novaclient-ext b/docs/licenses/ip-associations-python-novaclient-ext new file mode 100644 index 0000000000..b3fc50d03b --- /dev/null +++ b/docs/licenses/ip-associations-python-novaclient-ext @@ -0,0 +1,15 @@ +# As displayed: https://pypi.python.org/pypi/ip_associations_python_novaclient_ext + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. \ No newline at end of file diff --git a/docs/licenses/ip_associations_python_novaclient_ext.txt b/docs/licenses/ip-associations-python-novaclient-ext.txt similarity index 100% rename from docs/licenses/ip_associations_python_novaclient_ext.txt rename to docs/licenses/ip-associations-python-novaclient-ext.txt diff --git a/docs/licenses/irc.txt b/docs/licenses/irc.txt new file mode 100644 index 0000000000..921ae9dd2b --- /dev/null +++ b/docs/licenses/irc.txt @@ -0,0 +1,10 @@ +# As listed on https://pypi.python.org/pypi/irc + +The MIT License (MIT) +Copyright (c) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/docs/licenses/os_diskconfig_python_novaclient_ext.txt b/docs/licenses/os-diskconfig-python-novaclient-ext.txt similarity index 100% rename from docs/licenses/os_diskconfig_python_novaclient_ext.txt rename to docs/licenses/os-diskconfig-python-novaclient-ext.txt diff --git a/docs/licenses/os_networksv2_python_novaclient_ext.txt b/docs/licenses/os-networksv2-python-novaclient-ext.txt similarity index 100% rename from docs/licenses/os_networksv2_python_novaclient_ext.txt rename to docs/licenses/os-networksv2-python-novaclient-ext.txt diff --git a/docs/licenses/os_virtual_interfacesv2_python_novaclient_ext.txt b/docs/licenses/os-virtual-interfacesv2-python-novaclient-ext.txt similarity index 100% rename from docs/licenses/os_virtual_interfacesv2_python_novaclient_ext.txt rename to docs/licenses/os-virtual-interfacesv2-python-novaclient-ext.txt diff --git a/docs/licenses/python-heatclient.txt b/docs/licenses/python-heatclient.txt new file mode 100644 index 0000000000..68c771a099 --- /dev/null +++ b/docs/licenses/python-heatclient.txt @@ -0,0 +1,176 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + diff --git a/docs/licenses/python-openstackclient.txt b/docs/licenses/python-openstackclient.txt new file mode 100644 index 0000000000..68c771a099 --- /dev/null +++ b/docs/licenses/python-openstackclient.txt @@ -0,0 +1,176 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + diff --git a/docs/licenses/pywinrmkerberos.txt b/docs/licenses/pywinrmkerberos.txt new file mode 100644 index 0000000000..77b1811346 --- /dev/null +++ b/docs/licenses/pywinrmkerberos.txt @@ -0,0 +1,20 @@ +Copyright (c) 2013 Alexey Diyan + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + diff --git a/docs/licenses/rax_default_network_flags_python_novaclient_ext.txt b/docs/licenses/rax-default-network-flags-python-novaclient-ext.txt similarity index 100% rename from docs/licenses/rax_default_network_flags_python_novaclient_ext.txt rename to docs/licenses/rax-default-network-flags-python-novaclient-ext.txt diff --git a/docs/licenses/rax_scheduled_images_python_novaclient_ext.txt b/docs/licenses/rax-scheduled-images-python-novaclient-ext.txt similarity index 100% rename from docs/licenses/rax_scheduled_images_python_novaclient_ext.txt rename to docs/licenses/rax-scheduled-images-python-novaclient-ext.txt diff --git a/docs/licenses/requestsexceptions.txt b/docs/licenses/requestsexceptions.txt new file mode 100644 index 0000000000..37a3165011 --- /dev/null +++ b/docs/licenses/requestsexceptions.txt @@ -0,0 +1,14 @@ +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + diff --git a/docs/licenses/total-ordering.txt b/docs/licenses/total-ordering.txt new file mode 100644 index 0000000000..16c40f4f4f --- /dev/null +++ b/docs/licenses/total-ordering.txt @@ -0,0 +1,28 @@ +Copyright (c) 2012, Konsta Vesterinen + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* The names of the contributors may not be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY DIRECT, +INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE +OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + diff --git a/tools/audit-api-license.sh b/tools/audit-api-license.sh new file mode 100755 index 0000000000..6cf93f4022 --- /dev/null +++ b/tools/audit-api-license.sh @@ -0,0 +1,20 @@ +#!/bin/bash + +echo "---------- requirements.txt --------------" +for each in `cat requirements/requirements.txt| awk -F= '{print $1}' | tr -d "[]"` +do + if [ ! -f docs/licenses/$each.txt ]; then + echo No license for $each + fi +done +echo "---------- end requirements.txt --------------" + + +echo "---------- requirements_ansible.txt --------------" +for each in `cat requirements/requirements_ansible.txt| awk -F= '{print $1}' | tr -d "[]"` +do + if [ ! -f docs/licenses/$each.txt ]; then + echo No license for $each + fi +done +echo "---------- end requirements_ansible.txt --------------" From 03a04268cf2e174de63f01e361e1033749777214 Mon Sep 17 00:00:00 2001 From: Leigh Johnson Date: Tue, 12 Jul 2016 17:11:10 -0400 Subject: [PATCH 088/123] fix prompt-for machine cred query params too --- awx/ui/client/src/job-submission/job-submission.controller.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/awx/ui/client/src/job-submission/job-submission.controller.js b/awx/ui/client/src/job-submission/job-submission.controller.js index 00c7e971d3..7c3421e551 100644 --- a/awx/ui/client/src/job-submission/job-submission.controller.js +++ b/awx/ui/client/src/job-submission/job-submission.controller.js @@ -134,7 +134,7 @@ export default var base = $state.current.name, // As of 3.0, the only place the user can relaunch a // playbook is on jobTemplates.edit (completed_jobs tab), - // jobs, and jobDetails $states. + // jobs, and jobDetails $states. isRelaunch = !(base === 'jobTemplates' || base === 'portalMode' || base === 'dashboard'); if (!isRelaunch) { @@ -333,6 +333,7 @@ export default var credential_url = GetBasePath('credentials') + '?kind=ssh'; var credList = _.cloneDeep(CredentialList); + credList.basePath = GetBasePath('credentials') + '?kind=ssh'; credList.fields.description.searchable = false; credList.fields.kind.searchable = false; From b6504da90e05d0f0fab8495dbdb4313eab5a122d Mon Sep 17 00:00:00 2001 From: Leigh Johnson Date: Tue, 12 Jul 2016 17:32:21 -0400 Subject: [PATCH 089/123] fix home > hosts codemirror instance, resolves #2954 (#2960) --- .../src/dashboard/hosts/dashboard-hosts-edit.controller.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/awx/ui/client/src/dashboard/hosts/dashboard-hosts-edit.controller.js b/awx/ui/client/src/dashboard/hosts/dashboard-hosts-edit.controller.js index c70d7d39ad..f77764903b 100644 --- a/awx/ui/client/src/dashboard/hosts/dashboard-hosts-edit.controller.js +++ b/awx/ui/client/src/dashboard/hosts/dashboard-hosts-edit.controller.js @@ -22,7 +22,7 @@ $scope.formSave = function(){ var host = { id: $scope.host.id, - variables: $scope.extraVars === '---' || $scope.extraVars === '{}' ? null : $scope.extraVars, + variables: $scope.variables === '---' || $scope.variables === '{}' ? null : $scope.variables, name: $scope.name, description: $scope.description, enabled: $scope.host.enabled @@ -34,15 +34,14 @@ }; var init = function(){ $scope.host = host; - $scope.extraVars = host.variables === '' ? '---' : host.variables; generator.inject(form, {mode: 'edit', related: false, scope: $scope}); - $scope.extraVars = $scope.host.variables === '' ? '---' : $scope.host.variables; $scope.name = host.name; $scope.description = host.description; + $scope.variables = host.variables === '' ? '---' : host.variables; ParseTypeChange({ scope: $scope, field_id: 'host_variables', - variable: 'extraVars', + variable: 'variables', }); }; From affd66521d94de886639bea3a0238dc7d43011cb Mon Sep 17 00:00:00 2001 From: Leigh Johnson Date: Tue, 12 Jul 2016 17:33:55 -0400 Subject: [PATCH 090/123] avoid building urls to deleted resources, resolves #2948 (#2956) --- awx/ui/client/src/widgets/Stream.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/awx/ui/client/src/widgets/Stream.js b/awx/ui/client/src/widgets/Stream.js index f3e9271264..bd9b0011a4 100644 --- a/awx/ui/client/src/widgets/Stream.js +++ b/awx/ui/client/src/widgets/Stream.js @@ -32,6 +32,11 @@ angular.module('StreamWidget', ['RestServices', 'Utilities', 'StreamListDefiniti // try/except pattern asserts that: // if we encounter a case where a UI url can't or shouldn't be generated, just supply the name of the resource try { + // catch-all case to avoid generating urls if a resource has been deleted + // if a resource still exists, it'll be serialized in the activity's summary_fields + if (!activity.summary_fields[resource]){ + throw {name : 'ResourceDeleted', message: 'The referenced resource no longer exists'}; + } switch (resource) { case 'custom_inventory_script': url += 'inventory_scripts/' + obj.id + '/'; From f88e9cfec1f7319c160862ece98dca8a343bc79c Mon Sep 17 00:00:00 2001 From: Jared Tabor Date: Tue, 12 Jul 2016 15:44:13 -0700 Subject: [PATCH 091/123] Check for object keys to avoid console error --- awx/ui/client/src/job-detail/job-detail.controller.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/awx/ui/client/src/job-detail/job-detail.controller.js b/awx/ui/client/src/job-detail/job-detail.controller.js index 3f6e8f7209..acb473b088 100644 --- a/awx/ui/client/src/job-detail/job-detail.controller.js +++ b/awx/ui/client/src/job-detail/job-detail.controller.js @@ -428,7 +428,8 @@ export default var params = { order_by: 'id' }; - if (scope.job.summary_fields.unified_job_template.unified_job_type === 'job'){ + + if (scope.job && scope.job.summary_fields && scope.job.summary_fields.unified_job_template && scope.job.summary_fields.unified_job_template.unified_job_type === 'job'){ JobDetailService.getJobPlays(scope.job.id, params) .success( function(data) { scope.next_plays = data.next; From c6aa2b43921d7e877cf1a4e3c3f77fee42a18b25 Mon Sep 17 00:00:00 2001 From: Jared Tabor Date: Tue, 12 Jul 2016 16:03:31 -0700 Subject: [PATCH 092/123] removing logic in job detail page related to scm updates job detail page is only for job runs, scm updates have their own stdout page --- awx/ui/client/src/job-detail/job-detail.controller.js | 2 -- awx/ui/client/src/job-detail/job-detail.partial.html | 4 ++-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/awx/ui/client/src/job-detail/job-detail.controller.js b/awx/ui/client/src/job-detail/job-detail.controller.js index acb473b088..02cb21a5f1 100644 --- a/awx/ui/client/src/job-detail/job-detail.controller.js +++ b/awx/ui/client/src/job-detail/job-detail.controller.js @@ -429,7 +429,6 @@ export default order_by: 'id' }; - if (scope.job && scope.job.summary_fields && scope.job.summary_fields.unified_job_template && scope.job.summary_fields.unified_job_template.unified_job_type === 'job'){ JobDetailService.getJobPlays(scope.job.id, params) .success( function(data) { scope.next_plays = data.next; @@ -515,7 +514,6 @@ export default } scope.$emit('LoadTasks', events_url); }); - } }); diff --git a/awx/ui/client/src/job-detail/job-detail.partial.html b/awx/ui/client/src/job-detail/job-detail.partial.html index a5590ebd5e..0be9e4eb6b 100644 --- a/awx/ui/client/src/job-detail/job-detail.partial.html +++ b/awx/ui/client/src/job-detail/job-detail.partial.html @@ -164,7 +164,7 @@ -

+
@@ -379,7 +379,7 @@ -
+
From 99bd6ac38c8e34358448b35fc1e05a62a8ce6842 Mon Sep 17 00:00:00 2001 From: James Laska Date: Wed, 13 Jul 2016 09:00:49 -0400 Subject: [PATCH 093/123] Fix typo in sub_list_create_api_view Relates #2961 --- awx/api/templates/api/sub_list_create_api_view.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/api/templates/api/sub_list_create_api_view.md b/awx/api/templates/api/sub_list_create_api_view.md index 2d8571c1d6..74b91b5084 100644 --- a/awx/api/templates/api/sub_list_create_api_view.md +++ b/awx/api/templates/api/sub_list_create_api_view.md @@ -34,7 +34,7 @@ existing {{ model_verbose_name }} with this {{ parent_model_verbose_name }}. Make a POST request to this resource with `id` and `disassociate` fields to remove the {{ model_verbose_name }} from this {{ parent_model_verbose_name }} -{% if model_verbose_name != "label" %}}without deleting the {{ model_verbose_name }}{% endif %}. +{% if model_verbose_name != "label" %} without deleting the {{ model_verbose_name }}{% endif %}. {% endif %} {% endif %} From 9d5693ff2de8e50f62ec3b5496ac377944c9c9fa Mon Sep 17 00:00:00 2001 From: Ken Hoes Date: Wed, 13 Jul 2016 10:42:09 -0400 Subject: [PATCH 094/123] Sanitizing name in popup --- awx/ui/client/src/inventories/list/inventory-list.controller.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/ui/client/src/inventories/list/inventory-list.controller.js b/awx/ui/client/src/inventories/list/inventory-list.controller.js index e57a7beb32..142e7eff53 100644 --- a/awx/ui/client/src/inventories/list/inventory-list.controller.js +++ b/awx/ui/client/src/inventories/list/inventory-list.controller.js @@ -197,7 +197,7 @@ function InventoriesList($scope, $rootScope, $location, $log, ". Click for details\" aw-tip-placement=\"top\">\n"; html += "
"; html += ""; + ". Click for details\" aw-tip-placement=\"top\">" + $filter('sanitize')(ellipsis(row.name)) + ""; html += "\n"; }); html += "\n"; From e9a0f41152c052c41aae29ab28d013d8d5e09510 Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Wed, 13 Jul 2016 10:45:17 -0400 Subject: [PATCH 095/123] Fix an ommitted fields 500 error If organization or notification_template is omitted entirely from the POST for a new item then a 500 error would be raised --- awx/api/serializers.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/awx/api/serializers.py b/awx/api/serializers.py index dbe74f9272..3680b1819c 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -2427,10 +2427,14 @@ class NotificationTemplateSerializer(BaseSerializer): notification_type = attrs['notification_type'] elif self.instance: notification_type = self.instance.notification_type + else: + notification_type = None if 'organization' in attrs: organization = attrs['organization'] elif self.instance: organization = self.instance.organization + else: + organization = None if not notification_type: raise serializers.ValidationError('Missing required fields for Notification Configuration: notification_type') if not organization: From 624225e81f3f68ae5de13188fdf14b0f73e46a86 Mon Sep 17 00:00:00 2001 From: Michael Abashian Date: Wed, 13 Jul 2016 10:53:41 -0400 Subject: [PATCH 096/123] Credential owners are no longer searchable --- awx/ui/client/src/lists/Credentials.js | 1 + 1 file changed, 1 insertion(+) diff --git a/awx/ui/client/src/lists/Credentials.js b/awx/ui/client/src/lists/Credentials.js index 8aff2db5e4..580b4b9f5b 100644 --- a/awx/ui/client/src/lists/Credentials.js +++ b/awx/ui/client/src/lists/Credentials.js @@ -45,6 +45,7 @@ export default owners: { label: 'Owners', type: 'owners', + searchable: false, nosort: true, excludeModal: true, columnClass: 'col-md-2 hidden-sm hidden-xs' From e9afee6149381b7605c1fe82a1f9fd37b371b171 Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Wed, 13 Jul 2016 11:10:18 -0400 Subject: [PATCH 097/123] Revert required org on notification templates This reverts the validation of a required Organization on the Notification Template. After discussion with jlaska and jladd, I think this can behave fine in this situation. It's more like a Credential then where without an Organization the NT only becomes available to Super Users and the creator of the NT. --- awx/api/serializers.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/awx/api/serializers.py b/awx/api/serializers.py index 3680b1819c..2f58b776f4 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -2429,16 +2429,8 @@ class NotificationTemplateSerializer(BaseSerializer): notification_type = self.instance.notification_type else: notification_type = None - if 'organization' in attrs: - organization = attrs['organization'] - elif self.instance: - organization = self.instance.organization - else: - organization = None if not notification_type: raise serializers.ValidationError('Missing required fields for Notification Configuration: notification_type') - if not organization: - raise serializers.ValidationError("Missing 'organization' from required fields") notification_class = NotificationTemplate.CLASS_FOR_NOTIFICATION_TYPE[notification_type] missing_fields = [] From 9965ec01be97e03eeb9221c6d36e055c836a04c1 Mon Sep 17 00:00:00 2001 From: Leigh Johnson Date: Wed, 13 Jul 2016 12:19:49 -0400 Subject: [PATCH 098/123] supply correct params to ParseTypeChange call, resolves #2966 (#2977) --- awx/ui/client/src/job-submission/job-submission.controller.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/awx/ui/client/src/job-submission/job-submission.controller.js b/awx/ui/client/src/job-submission/job-submission.controller.js index 010fbb7af8..16fc77324e 100644 --- a/awx/ui/client/src/job-submission/job-submission.controller.js +++ b/awx/ui/client/src/job-submission/job-submission.controller.js @@ -537,8 +537,9 @@ export default // It shares the same scope with this directive and will // pull the new value of parseType out to determine which // direction to convert the extra vars + $scope.parseType = $scope.other_prompt_data.parseType; - $scope.parseTypeChange(); + $scope.parseTypeChange('parseType', 'jobLaunchVariables'); }; } From 8c234dc915e2f7f31da81bc0c6a1bb4700079d41 Mon Sep 17 00:00:00 2001 From: Michael Abashian Date: Wed, 13 Jul 2016 13:31:10 -0400 Subject: [PATCH 099/123] Added onExit functions to routes where the job launch and survey maker modals may be displayed. This will handle cleaning up the modal properly so that it can be re-opened --- awx/ui/client/src/app.js | 20 +++++++++++++++++++ .../host-summary/host-summary.route.js | 10 ++++++++++ .../client/src/job-detail/job-detail.route.js | 12 ++++++++++- .../add/job-templates-add.route.js | 14 +++++++++++++ .../edit/job-templates-edit.route.js | 14 +++++++++++++ .../list/job-templates-list.route.js | 10 ++++++++++ .../linkout/organizations-linkout.route.js | 10 ++++++++++ .../src/portal-mode/portal-mode.route.js | 10 ++++++++++ 8 files changed, 99 insertions(+), 1 deletion(-) diff --git a/awx/ui/client/src/app.js b/awx/ui/client/src/app.js index e25d53ac51..0698a2fdbb 100644 --- a/awx/ui/client/src/app.js +++ b/awx/ui/client/src/app.js @@ -255,6 +255,16 @@ var tower = angular.module('Tower', [ }); }); }] + }, + onExit: function(){ + // close the job launch modal + // using an onExit event to handle cases where the user navs away using the url bar / back and not modal "X" + // Destroy the dialog + if($("#job-launch-modal").hasClass('ui-dialog-content')) { + $('#job-launch-modal').dialog('destroy'); + } + // Remove the directive from the page (if it's there) + $('#content-container').find('submit-job').remove(); } }). @@ -264,6 +274,16 @@ var tower = angular.module('Tower', [ controller: JobsListController, ncyBreadcrumb: { label: "JOBS" + }, + onExit: function(){ + // close the job launch modal + // using an onExit event to handle cases where the user navs away using the url bar / back and not modal "X" + // Destroy the dialog + if($("#job-launch-modal").hasClass('ui-dialog-content')) { + $('#job-launch-modal').dialog('destroy'); + } + // Remove the directive from the page (if it's there) + $('#content-container').find('submit-job').remove(); } }). diff --git a/awx/ui/client/src/job-detail/host-summary/host-summary.route.js b/awx/ui/client/src/job-detail/host-summary/host-summary.route.js index a2de70e5d4..f7722462a0 100644 --- a/awx/ui/client/src/job-detail/host-summary/host-summary.route.js +++ b/awx/ui/client/src/job-detail/host-summary/host-summary.route.js @@ -17,5 +17,15 @@ export default { }, ncyBreadcrumb: { skip: true // Never display this state in breadcrumb. + }, + onExit: function(){ + // close the job launch modal + // using an onExit event to handle cases where the user navs away using the url bar / back and not modal "X" + // Destroy the dialog + if($("#job-launch-modal").hasClass('ui-dialog-content')) { + $('#job-launch-modal').dialog('destroy'); + } + // Remove the directive from the page (if it's there) + $('#content-container').find('submit-job').remove(); } }; diff --git a/awx/ui/client/src/job-detail/job-detail.route.js b/awx/ui/client/src/job-detail/job-detail.route.js index dda2722511..bf21fb8e5a 100644 --- a/awx/ui/client/src/job-detail/job-detail.route.js +++ b/awx/ui/client/src/job-detail/job-detail.route.js @@ -30,5 +30,15 @@ export default { }] }, templateUrl: templateUrl('job-detail/job-detail'), - controller: 'JobDetailController' + controller: 'JobDetailController', + onExit: function(){ + // close the job launch modal + // using an onExit event to handle cases where the user navs away using the url bar / back and not modal "X" + // Destroy the dialog + if($("#job-launch-modal").hasClass('ui-dialog-content')) { + $('#job-launch-modal').dialog('destroy'); + } + // Remove the directive from the page (if it's there) + $('#content-container').find('submit-job').remove(); + } }; diff --git a/awx/ui/client/src/job-templates/add/job-templates-add.route.js b/awx/ui/client/src/job-templates/add/job-templates-add.route.js index d9f9a5c9d3..8218cabc32 100644 --- a/awx/ui/client/src/job-templates/add/job-templates-add.route.js +++ b/awx/ui/client/src/job-templates/add/job-templates-add.route.js @@ -14,5 +14,19 @@ export default { ncyBreadcrumb: { parent: "jobTemplates", label: "CREATE JOB TEMPLATE" + }, + onExit: function(){ + // close the job launch modal + // using an onExit event to handle cases where the user navs away using the url bar / back and not modal "X" + // Destroy the dialog + if($("#job-launch-modal").hasClass('ui-dialog-content')) { + $('#job-launch-modal').dialog('destroy'); + } + // Remove the directive from the page (if it's there) + $('#content-container').find('submit-job').remove(); + + if($("#survey-modal-dialog").hasClass('ui-dialog-content')) { + $('#survey-modal-dialog').dialog('destroy'); + } } }; diff --git a/awx/ui/client/src/job-templates/edit/job-templates-edit.route.js b/awx/ui/client/src/job-templates/edit/job-templates-edit.route.js index a9d5ff608f..78d383fa42 100644 --- a/awx/ui/client/src/job-templates/edit/job-templates-edit.route.js +++ b/awx/ui/client/src/job-templates/edit/job-templates-edit.route.js @@ -17,5 +17,19 @@ export default { ncyBreadcrumb: { parent: 'jobTemplates', label: "{{name}}" + }, + onExit: function(){ + // close the job launch modal + // using an onExit event to handle cases where the user navs away using the url bar / back and not modal "X" + // Destroy the dialog + if($("#job-launch-modal").hasClass('ui-dialog-content')) { + $('#job-launch-modal').dialog('destroy'); + } + // Remove the directive from the page (if it's there) + $('#content-container').find('submit-job').remove(); + + if($("#survey-modal-dialog").hasClass('ui-dialog-content')) { + $('#survey-modal-dialog').dialog('destroy'); + } } }; diff --git a/awx/ui/client/src/job-templates/list/job-templates-list.route.js b/awx/ui/client/src/job-templates/list/job-templates-list.route.js index deeb1982bd..6c646af729 100644 --- a/awx/ui/client/src/job-templates/list/job-templates-list.route.js +++ b/awx/ui/client/src/job-templates/list/job-templates-list.route.js @@ -17,5 +17,15 @@ export default { }, ncyBreadcrumb: { label: "JOB TEMPLATES" + }, + onExit: function(){ + // close the job launch modal + // using an onExit event to handle cases where the user navs away using the url bar / back and not modal "X" + // Destroy the dialog + if($("#job-launch-modal").hasClass('ui-dialog-content')) { + $('#job-launch-modal').dialog('destroy'); + } + // Remove the directive from the page (if it's there) + $('#content-container').find('submit-job').remove(); } }; diff --git a/awx/ui/client/src/organizations/linkout/organizations-linkout.route.js b/awx/ui/client/src/organizations/linkout/organizations-linkout.route.js index 13d51cc68f..caa8d7692f 100644 --- a/awx/ui/client/src/organizations/linkout/organizations-linkout.route.js +++ b/awx/ui/client/src/organizations/linkout/organizations-linkout.route.js @@ -121,6 +121,16 @@ export default [ features: ['FeaturesService', function(FeaturesService) { return FeaturesService.get(); }] + }, + onExit: function(){ + // close the job launch modal + // using an onExit event to handle cases where the user navs away using the url bar / back and not modal "X" + // Destroy the dialog + if($("#job-launch-modal").hasClass('ui-dialog-content')) { + $('#job-launch-modal').dialog('destroy'); + } + // Remove the directive from the page (if it's there) + $('#content-container').find('submit-job').remove(); } }, { diff --git a/awx/ui/client/src/portal-mode/portal-mode.route.js b/awx/ui/client/src/portal-mode/portal-mode.route.js index c0f7e5f3fb..6982f10620 100644 --- a/awx/ui/client/src/portal-mode/portal-mode.route.js +++ b/awx/ui/client/src/portal-mode/portal-mode.route.js @@ -24,5 +24,15 @@ export default { templateUrl: templateUrl('portal-mode/portal-mode-jobs'), controller: PortalModeJobsController } + }, + onExit: function(){ + // close the job launch modal + // using an onExit event to handle cases where the user navs away using the url bar / back and not modal "X" + // Destroy the dialog + if($("#job-launch-modal").hasClass('ui-dialog-content')) { + $('#job-launch-modal').dialog('destroy'); + } + // Remove the directive from the page (if it's there) + $('#content-container').find('submit-job').remove(); } }; From 5c32b17aeadde7719a0b94f459a2aa3ba1e8e5d7 Mon Sep 17 00:00:00 2001 From: Leigh Johnson Date: Wed, 13 Jul 2016 13:55:12 -0400 Subject: [PATCH 100/123] URI-encode text searches, resolves #2980 (#2982) --- awx/ui/client/src/search/tagSearch.service.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/ui/client/src/search/tagSearch.service.js b/awx/ui/client/src/search/tagSearch.service.js index 6b02b8fdf8..b340b5d8c3 100644 --- a/awx/ui/client/src/search/tagSearch.service.js +++ b/awx/ui/client/src/search/tagSearch.service.js @@ -182,7 +182,7 @@ export default ['Rest', '$q', 'GetBasePath', 'Wait', 'ProcessErrors', '$log', fu this.getTag = function(field, textVal, selectVal) { var tag = _.clone(field); if (tag.type === "text") { - tag.url = tag.value + "__icontains=" + textVal; + tag.url = tag.value + "__icontains=" + encodeURIComponent(textVal); tag.name = textVal; } else if (selectVal.value && typeof selectVal.value === 'string' && selectVal.value.indexOf("=") > 0) { tag.url = selectVal.value; From ab3a5c1a02902d441e16fef47cc098bbee3e5ece Mon Sep 17 00:00:00 2001 From: Michael Abashian Date: Wed, 13 Jul 2016 14:10:54 -0400 Subject: [PATCH 101/123] Fixed callback url prompt styling --- .../src/job-templates/add/job-templates-add.controller.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/ui/client/src/job-templates/add/job-templates-add.controller.js b/awx/ui/client/src/job-templates/add/job-templates-add.controller.js index 7abcf5dfca..848563196d 100644 --- a/awx/ui/client/src/job-templates/add/job-templates-add.controller.js +++ b/awx/ui/client/src/job-templates/add/job-templates-add.controller.js @@ -375,7 +375,7 @@

`, - 'alert-info', saveCompleted, null, null, + 'alert-danger', saveCompleted, null, null, null, true); } var orgDefer = $q.defer(); From 33bb3abd52ce6b7b6458d2766a5621c6dfd31428 Mon Sep 17 00:00:00 2001 From: Ken Hoes Date: Wed, 13 Jul 2016 15:14:12 -0400 Subject: [PATCH 102/123] Retrieved cloud credential from job template summary fields --- .../edit/job-templates-edit.controller.js | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/awx/ui/client/src/job-templates/edit/job-templates-edit.controller.js b/awx/ui/client/src/job-templates/edit/job-templates-edit.controller.js index ff9766ac55..4687840b70 100644 --- a/awx/ui/client/src/job-templates/edit/job-templates-edit.controller.js +++ b/awx/ui/client/src/job-templates/edit/job-templates-edit.controller.js @@ -347,20 +347,13 @@ export default ParseTypeChange({ scope: $scope, field_id: 'job_templates_variables', onChange: callback }); - if (related_cloud_credential) { - Rest.setUrl(related_cloud_credential); - Rest.get() - .success(function (data) { - $scope.$emit('cloudCredentialReady', data.name); - }) - .error(function (data, status) { - ProcessErrors($scope, data, status, null, {hdr: 'Error!', - msg: 'Failed to related cloud credential. GET returned status: ' + status }); - }); + if(related_cloud_credential) { + $scope.$emit('cloudCredentialReady', $scope.job_template_obj.summary_fields.cloud_credential.name); } else { // No existing cloud credential $scope.$emit('cloudCredentialReady', null); } + }); Wait('start'); From 53492f3fa1346f33b8f48a2b51c2ac587a68e4a2 Mon Sep 17 00:00:00 2001 From: Ken Hoes Date: Wed, 13 Jul 2016 15:33:24 -0400 Subject: [PATCH 103/123] Added sanitize filter to other name instances --- .../client/src/inventories/list/inventory-list.controller.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/awx/ui/client/src/inventories/list/inventory-list.controller.js b/awx/ui/client/src/inventories/list/inventory-list.controller.js index 142e7eff53..a5d2b00a01 100644 --- a/awx/ui/client/src/inventories/list/inventory-list.controller.js +++ b/awx/ui/client/src/inventories/list/inventory-list.controller.js @@ -232,14 +232,14 @@ function InventoriesList($scope, $rootScope, $location, $log, html += "
"; html += ``; html += ""; - html += ""; + html += ""; html += "\n"; } else { html += ""; html += ""; html += ""; - html += ""; + html += ""; html += "\n"; } }); From c60b8986de7dc879fecd07a368ff9a9ad4e34aa9 Mon Sep 17 00:00:00 2001 From: Leigh Johnson Date: Wed, 13 Jul 2016 16:36:21 -0400 Subject: [PATCH 104/123] remove type search field from single-type credential lookup modals, resolves #2976 (#2985) --- awx/ui/client/src/controllers/Projects.js | 7 +++++++ awx/ui/client/src/helpers/JobTemplates.js | 2 ++ .../src/inventories/manage/groups/groups-add.controller.js | 3 +++ .../inventories/manage/groups/groups-edit.controller.js | 4 ++++ .../src/job-templates/add/job-templates-add.controller.js | 4 ++++ .../job-templates/edit/job-templates-edit.controller.js | 2 ++ 6 files changed, 22 insertions(+) diff --git a/awx/ui/client/src/controllers/Projects.js b/awx/ui/client/src/controllers/Projects.js index 0fb826906a..4dbb111f79 100644 --- a/awx/ui/client/src/controllers/Projects.js +++ b/awx/ui/client/src/controllers/Projects.js @@ -391,6 +391,9 @@ export function ProjectsAdd(Refresh, $scope, $rootScope, $compile, $location, $l defaultUrl = GetBasePath('projects'), master = {}; + // remove "type" field from search options + CredentialList.fields.kind.noSearch = true; + generator.inject(form, { mode: 'add', related: false, scope: $scope }); generator.reset(); @@ -567,6 +570,10 @@ export function ProjectsEdit($scope, $rootScope, $compile, $location, $log, id = $stateParams.id, relatedSets = {}; + // remove "type" field from search options + CredentialList.fields.kind.noSearch = true; + + SchedulesList.well = false; generator.inject(form, { mode: 'edit', diff --git a/awx/ui/client/src/helpers/JobTemplates.js b/awx/ui/client/src/helpers/JobTemplates.js index 7db3e3cb2e..55b2419390 100644 --- a/awx/ui/client/src/helpers/JobTemplates.js +++ b/awx/ui/client/src/helpers/JobTemplates.js @@ -163,6 +163,8 @@ angular.module('JobTemplatesHelper', ['Utilities']) }); CredentialList.basePath = GetBasePath('credentials') + '?kind=ssh'; + // remove "type" field from search options + CredentialList.fields.kind.noSearch = true; LookUpInit({ url: GetBasePath('credentials') + '?kind=ssh', diff --git a/awx/ui/client/src/inventories/manage/groups/groups-add.controller.js b/awx/ui/client/src/inventories/manage/groups/groups-add.controller.js index 179f460d91..a405f2bb52 100644 --- a/awx/ui/client/src/inventories/manage/groups/groups-add.controller.js +++ b/awx/ui/client/src/inventories/manage/groups/groups-add.controller.js @@ -12,6 +12,9 @@ var generator = GenerateForm, form = GroupForm(); + // remove "type" field from search options + CredentialList.fields.kind.noSearch = true; + $scope.formCancel = function(){ $state.go('^'); }; diff --git a/awx/ui/client/src/inventories/manage/groups/groups-edit.controller.js b/awx/ui/client/src/inventories/manage/groups/groups-edit.controller.js index 4875e3abca..094b53399a 100644 --- a/awx/ui/client/src/inventories/manage/groups/groups-edit.controller.js +++ b/awx/ui/client/src/inventories/manage/groups/groups-edit.controller.js @@ -13,6 +13,10 @@ GroupManageService, GetChoices, GetBasePath, CreateSelect2, GetSourceTypeOptions, groupData, inventorySourceData){ var generator = GenerateForm, form = GroupForm(); + + // remove "type" field from search options + CredentialList.fields.kind.noSearch = true; + $scope.formCancel = function(){ $state.go('^'); }; diff --git a/awx/ui/client/src/job-templates/add/job-templates-add.controller.js b/awx/ui/client/src/job-templates/add/job-templates-add.controller.js index 7abcf5dfca..f3b957ed99 100644 --- a/awx/ui/client/src/job-templates/add/job-templates-add.controller.js +++ b/awx/ui/client/src/job-templates/add/job-templates-add.controller.js @@ -34,6 +34,9 @@ base = $location.path().replace(/^\//, '').split('/')[0], context = (base === 'job_templates') ? 'job_template' : 'inv'; + // remove "type" field from search options + CredentialList.fields.kind.noSearch = true; + CallbackHelpInit({ scope: $scope }); $scope.can_edit = true; generator.inject(form, { mode: 'add', related: false, scope: $scope }); @@ -82,6 +85,7 @@ NetworkCredentialList.iterator = 'networkcredential'; NetworkCredentialList.basePath = '/api/v1/credentials?kind=net'; + SurveyControllerInit({ scope: $scope, parent_scope: $scope diff --git a/awx/ui/client/src/job-templates/edit/job-templates-edit.controller.js b/awx/ui/client/src/job-templates/edit/job-templates-edit.controller.js index ff9766ac55..dc02f439b8 100644 --- a/awx/ui/client/src/job-templates/edit/job-templates-edit.controller.js +++ b/awx/ui/client/src/job-templates/edit/job-templates-edit.controller.js @@ -46,6 +46,8 @@ export default checkSCMStatus, getPlaybooks, callback, choicesCount = 0; + // remove "type" field from search options + CredentialList.fields.kind.noSearch = true; CallbackHelpInit({ scope: $scope }); From f9de8db89a0da9a239673a6b10b5a38393409581 Mon Sep 17 00:00:00 2001 From: Jared Tabor Date: Wed, 13 Jul 2016 15:38:38 -0700 Subject: [PATCH 105/123] Show proper error message for missing job template form when visiting the job template page for a job template that has been deleted or doesn't exist, we want to show the 404 error message, not an error message about tags or labels. --- .../edit/job-templates-edit.controller.js | 53 ++++++++------ .../client/src/search/tagSearch.controller.js | 18 ++--- awx/ui/client/src/search/tagSearch.service.js | 72 ++++++++++--------- 3 files changed, 79 insertions(+), 64 deletions(-) diff --git a/awx/ui/client/src/job-templates/edit/job-templates-edit.controller.js b/awx/ui/client/src/job-templates/edit/job-templates-edit.controller.js index dc02f439b8..f5ea8c7a41 100644 --- a/awx/ui/client/src/job-templates/edit/job-templates-edit.controller.js +++ b/awx/ui/client/src/job-templates/edit/job-templates-edit.controller.js @@ -448,31 +448,40 @@ export default } }); }; + if($state.params.id !== "null"){ + Rest.setUrl(defaultUrl + $state.params.id + + "/labels"); + Rest.get() + .success(function(data) { + if (data.next) { + getNext(data, data.results, seeMoreResolve); + } else { + seeMoreResolve.resolve(data.results); + } - Rest.setUrl(defaultUrl + $state.params.id + - "/labels"); - Rest.get() - .success(function(data) { - if (data.next) { - getNext(data, data.results, seeMoreResolve); - } else { - seeMoreResolve.resolve(data.results); - } - - seeMoreResolve.promise.then(function (labels) { - $scope.$emit("choicesReady"); - var opts = labels - .map(i => ({id: i.id + "", - test: i.name})); - CreateSelect2({ - element:'#job_templates_labels', - multiple: true, - addNew: true, - opts: opts + seeMoreResolve.promise.then(function (labels) { + $scope.$emit("choicesReady"); + var opts = labels + .map(i => ({id: i.id + "", + test: i.name})); + CreateSelect2({ + element:'#job_templates_labels', + multiple: true, + addNew: true, + opts: opts + }); + Wait("stop"); }); - Wait("stop"); + }).error(function(){ + // job template id is null in this case + $scope.$emit("choicesReady"); }); - }); + } + else { + // job template doesn't exist + $scope.$emit("choicesReady"); + } + }) .error(function (data, status) { ProcessErrors($scope, data, status, form, { diff --git a/awx/ui/client/src/search/tagSearch.controller.js b/awx/ui/client/src/search/tagSearch.controller.js index 2a1b8e3311..e50a25670e 100644 --- a/awx/ui/client/src/search/tagSearch.controller.js +++ b/awx/ui/client/src/search/tagSearch.controller.js @@ -1,17 +1,19 @@ -export default ['$scope', 'Refresh', 'tagSearchService', - function($scope, Refresh, tagSearchService) { +export default ['$scope', 'Refresh', 'tagSearchService', '$stateParams', + function($scope, Refresh, tagSearchService, $stateParams) { // JSONify passed field elements that can be searched $scope.list = angular.fromJson($scope.list); // Access config lines from list spec $scope.listConfig = $scope.$parent.list; // Grab options for the left-dropdown of the searchbar - tagSearchService.getSearchTypes($scope.list, $scope.endpoint) - .then(function(searchTypes) { - $scope.searchTypes = searchTypes; + if($stateParams.id !== "null"){ + tagSearchService.getSearchTypes($scope.list, $scope.endpoint) + .then(function(searchTypes) { + $scope.searchTypes = searchTypes; - // currently selected option of the left-dropdown - $scope.currentSearchType = $scope.searchTypes[0]; - }); + // currently selected option of the left-dropdown + $scope.currentSearchType = $scope.searchTypes[0]; + }); + } // shows/hide the search type dropdown $scope.toggleTypeDropdown = function() { diff --git a/awx/ui/client/src/search/tagSearch.service.js b/awx/ui/client/src/search/tagSearch.service.js index b340b5d8c3..fdd808d3ad 100644 --- a/awx/ui/client/src/search/tagSearch.service.js +++ b/awx/ui/client/src/search/tagSearch.service.js @@ -84,41 +84,45 @@ export default ['Rest', '$q', 'GetBasePath', 'Wait', 'ProcessErrors', '$log', fu if (needsRequest.length) { // make the options request to reutrn the typeOptions - Rest.setUrl(needsRequest[0].basePath ? GetBasePath(needsRequest[0].basePath) : basePath); - Rest.options() - .success(function (data) { - try { - var options = data.actions.GET; - needsRequest = needsRequest - .map(function (option) { - option.typeOptions = options[option - .value] - .choices - .map(function(i) { - return { - value: i[0], - label: i[1] - }; - }); - return option; - }); - } - catch(err){ - if (!basePath){ - $log.error('Cannot retrieve OPTIONS because the basePath parameter is not set on the list with the following fieldset: \n', list); + var url = needsRequest[0].basePath ? GetBasePath(needsRequest[0].basePath) : basePath; + if(url.indexOf('null') === 0 ){ + Rest.setUrl(url); + Rest.options() + .success(function (data) { + try { + var options = data.actions.GET; + needsRequest = needsRequest + .map(function (option) { + option.typeOptions = options[option + .value] + .choices + .map(function(i) { + return { + value: i[0], + label: i[1] + }; + }); + return option; + }); } - else { $log.error(err); } - } - Wait("stop"); - defer.resolve(joinOptions()); - }) - .error(function (data, status) { - Wait("stop"); - defer.reject("options request failed"); - ProcessErrors(null, data, status, null, { - hdr: 'Error!', - msg: 'Getting type options failed'}); - }); + catch(err){ + if (!basePath){ + $log.error('Cannot retrieve OPTIONS because the basePath parameter is not set on the list with the following fieldset: \n', list); + } + else { $log.error(err); } + } + Wait("stop"); + defer.resolve(joinOptions()); + }) + .error(function (data, status) { + Wait("stop"); + defer.reject("options request failed"); + ProcessErrors(null, data, status, null, { + hdr: 'Error!', + msg: 'Getting type options failed'}); + }); + } + } else { Wait("stop"); defer.resolve(joinOptions()); From 906db7d75e11afc8bb9fad012912598dbdcc0e69 Mon Sep 17 00:00:00 2001 From: Ken Hoes Date: Thu, 14 Jul 2016 08:50:42 -0400 Subject: [PATCH 106/123] Added condition based off PR feedback --- .../src/job-templates/edit/job-templates-edit.controller.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/ui/client/src/job-templates/edit/job-templates-edit.controller.js b/awx/ui/client/src/job-templates/edit/job-templates-edit.controller.js index 4687840b70..0eb0ac028b 100644 --- a/awx/ui/client/src/job-templates/edit/job-templates-edit.controller.js +++ b/awx/ui/client/src/job-templates/edit/job-templates-edit.controller.js @@ -347,7 +347,7 @@ export default ParseTypeChange({ scope: $scope, field_id: 'job_templates_variables', onChange: callback }); - if(related_cloud_credential) { + if($scope.job_template_obj.summary_fields.cloud_credential.name && related_cloud_credential) { $scope.$emit('cloudCredentialReady', $scope.job_template_obj.summary_fields.cloud_credential.name); } else { // No existing cloud credential From 7b8c2f5b7491ff3dd5fba16698c6d9fd332464bd Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Thu, 14 Jul 2016 09:53:20 -0400 Subject: [PATCH 107/123] Normalized CustomInventoryScriptAccess.can_admin --- awx/main/access.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/main/access.py b/awx/main/access.py index f47198e4b0..a4e8f466b5 100644 --- a/awx/main/access.py +++ b/awx/main/access.py @@ -1556,7 +1556,7 @@ class CustomInventoryScriptAccess(BaseAccess): return self.user in org.admin_role @check_superuser - def can_admin(self, obj): + def can_admin(self, obj, data=None): return self.user in obj.admin_role @check_superuser From bb14c9003debf5fbae29cf8219a5ea568daf98a7 Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Thu, 14 Jul 2016 09:54:06 -0400 Subject: [PATCH 108/123] Orphan handling in _old_access.py --- awx/main/migrations/_old_access.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/main/migrations/_old_access.py b/awx/main/migrations/_old_access.py index ce952461ac..da49723a9e 100644 --- a/awx/main/migrations/_old_access.py +++ b/awx/main/migrations/_old_access.py @@ -656,7 +656,7 @@ class TeamAccess(BaseAccess): raise PermissionDenied('Unable to change organization on a team') if self.user.is_superuser: return True - if self.user in obj.organization.deprecated_admins.all(): + if obj.organization and self.user in obj.organization.deprecated_admins.all(): return True return False From 353e6100b927cc2ceb7257ef57b03def24d60fa0 Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Thu, 14 Jul 2016 09:54:41 -0400 Subject: [PATCH 109/123] Fix team credential role access in rbac migration --- awx/main/migrations/_rbac.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/awx/main/migrations/_rbac.py b/awx/main/migrations/_rbac.py index ee4100431e..4a1115c4b3 100644 --- a/awx/main/migrations/_rbac.py +++ b/awx/main/migrations/_rbac.py @@ -198,7 +198,8 @@ def migrate_credential(apps, schema_editor): logger.info(smart_text(u"added Credential(name={}, kind={}, host={}) at organization level".format(cred.name, cred.kind, cred.host))) if cred.deprecated_team is not None: - cred.deprecated_team.member_role.children.add(cred.admin_role) + cred.deprecated_team.admin_role.children.add(cred.admin_role) + cred.deprecated_team.member_role.children.add(cred.use_role) cred.save() logger.info(smart_text(u"added Credential(name={}, kind={}, host={}) at user level".format(cred.name, cred.kind, cred.host))) elif cred.deprecated_user is not None: From 6f2e1f711c5c7b5e0739a205e43058977ed48dee Mon Sep 17 00:00:00 2001 From: Bill Nottingham Date: Thu, 14 Jul 2016 10:23:01 -0400 Subject: [PATCH 110/123] updated EULA text from rnalen --- awx/api/templates/eula.md | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/awx/api/templates/eula.md b/awx/api/templates/eula.md index f07453c04d..83fb3ccb6e 100644 --- a/awx/api/templates/eula.md +++ b/awx/api/templates/eula.md @@ -1,3 +1,19 @@ -TOWER SOFTWARE END USER LICENSE AGREEMENT +ANSIBLE TOWER BY RED HAT END USER LICENSE AGREEMENT -Unless otherwise agreed to, and executed in a definitive agreement, between Ansible, Inc. (“Ansible”) and the individual or entity (“Customer”) signing or electronically accepting these terms of use for the Tower Software (“EULA”), all Tower Software, including any and all versions released or made available by Ansible, shall be subject to the Ansible Software Subscription and Services Agreement found at www.ansible.com/subscription-agreement (“Agreement”). Ansible is not responsible for any additional obligations, conditions or warranties agreed to between Customer and an authorized distributor, or reseller, of the Tower Software. BY DOWNLOADING AND USING THE TOWER SOFTWARE, OR BY CLICKING ON THE “YES” BUTTON OR OTHER BUTTON OR MECHANISM DESIGNED TO ACKNOWLEDGE CONSENT TO THE TERMS OF AN ELECTRONIC COPY OF THIS EULA, THE CUSTOMER HEREBY ACKNOWLEDGES THAT CUSTOMER HAS READ, UNDERSTOOD, AND AGREES TO BE BOUND BY THE TERMS OF THIS EULA AND AGREEMENT, INCLUDING ALL TERMS INCORPORATED HEREIN BY REFERENCE, AND THAT THIS EULA AND AGREEMENT IS EQUIVALENT TO ANY WRITTEN NEGOTIATED AGREEMENT BETWEEN CUSTOMER AND ANSIBLE. THIS EULA AND AGREEMENT IS ENFORCEABLE AGAINST ANY PERSON OR ENTITY THAT USES OR AVAILS ITSELF OF THE TOWER SOFTWARE OR ANY PERSON OR ENTITY THAT USES THE OR AVAILS ITSELF OF THE TOWER SOFTWARE ON ANOTHER PERSON’S OR ENTITY’S BEHALF. +This end user license agreement (“EULA”) governs the use of the Ansible Tower software and any related updates, upgrades, versions, appearance, structure and organization (the “Ansible Tower Software”), regardless of the delivery mechanism. + +1. License Grant. Subject to the terms of this EULA, Red Hat, Inc. and its affiliates (“Red Hat”) grant to you (“You”) a non-transferable, non-exclusive, worldwide, non-sublicensable, limited, revocable license to use the Ansible Tower Software for the term of the associated Red Hat Software Subscription(s) and in a quantity equal to the number of Red Hat Software Subscriptions purchased from Red Hat for the Ansible Tower Software (“License”), each as set forth on the applicable Red Hat ordering document. You acquire only the right to use the Ansible Tower Software and do not acquire any rights of ownership. Red Hat reserves all rights to the Ansible Tower Software not expressly granted to You. This License grant pertains solely to Your use of the Ansible Tower Software and is not intended to limit Your rights under, or grant You rights that supersede, the license terms of any software packages which may be made available with the Ansible Tower Software that are subject to an open source software license. + +2. Intellectual Property Rights. Title to the Ansible Tower Software and each component, copy and modification, including all derivative works whether made by Red Hat, You or on Red Hat's behalf, including those made at Your suggestion and all associated intellectual property rights, are and shall remain the sole and exclusive property of Red Hat and/or it licensors. The License does not authorize You (nor may You allow any third party, specifically non-employees of Yours) to: (a) copy, distribute, reproduce, use or allow third party access to the Ansible Tower Software except as expressly authorized hereunder; (b) decompile, disassemble, reverse engineer, translate, modify, convert or apply any procedure or process to the Ansible Tower Software in order to ascertain, derive, and/or appropriate for any reason or purpose, including the Ansible Tower Software source code or source listings or any trade secret information or process contained in the Ansible Tower Software (except as permitted under applicable law); (c) execute or incorporate other software (except for approved software as appears in the Ansible Tower Software documentation or specifically approved by Red Hat in writing) into Ansible Tower Software, or create a derivative work of any part of the Ansible Tower Software; (d) remove any trademarks, trade names or titles, copyrights legends or any other proprietary marking on the Ansible Tower Software; (e) disclose the results of any benchmarking of the Ansible Tower Software (whether or not obtained with Red Hat’s assistance) to any third party; (f) attempt to circumvent any user limits or other license, timing or use restrictions that are built into, defined or agreed upon, regarding the Ansible Tower Software. You are hereby notified that the Ansible Tower Software may contain time-out devices, counter devices, and/or other devices intended to ensure the limits of the License will not be exceeded (“Limiting Devices”). If the Ansible Tower Software contains Limiting Devices, Red Hat will provide You materials necessary to use the Ansible Tower Software to the extent permitted. You may not tamper with or otherwise take any action to defeat or circumvent a Limiting Device or other control measure, including but not limited to, resetting the unit amount or using false host identification number for the purpose of extending any term of the License. + +3. Evaluation Licenses. Unless You have purchased Ansible Tower Software Subscriptions from Red Hat or an authorized reseller under the terms of a commercial agreement with Red Hat, all use of the Ansible Tower Software shall be limited to testing purposes and not for production use (“Evaluation”). Unless otherwise agreed by Red Hat, Evaluation of the Ansible Tower Software shall be limited to an evaluation environment and the Ansible Tower Software shall not be used to manage any systems or virtual machines on networks being used in the operation of Your business or any other non-evaluation purpose. Unless otherwise agreed by Red Hat, You shall limit all Evaluation use to a single 30 day evaluation period and shall not download or otherwise obtain additional copies of the Ansible Tower Software or license keys for Evaluation. + +4. Limited Warranty. Except as specifically stated in this Section 4, to the maximum extent permitted under applicable law, the Ansible Tower Software and the components are provided and licensed “as is” without warranty of any kind, expressed or implied, including the implied warranties of merchantability, non-infringement or fitness for a particular purpose. Red Hat warrants solely to You that the media on which the Ansible Tower Software may be furnished will be free from defects in materials and manufacture under normal use for a period of thirty (30) days from the date of delivery to You. Red Hat does not warrant that the functions contained in the Ansible Tower Software will meet Your requirements or that the operation of the Ansible Tower Software will be entirely error free, appear precisely as described in the accompanying documentation, or comply with regulatory requirements. + +5. Limitation of Remedies and Liability. To the maximum extent permitted by applicable law, Your exclusive remedy under this EULA is to return any defective media within thirty (30) days of delivery along with a copy of Your payment receipt and Red Hat, at its option, will replace it or refund the money paid by You for the media. To the maximum extent permitted under applicable law, neither Red Hat nor any Red Hat authorized distributor will be liable to You for any incidental or consequential damages, including lost profits or lost savings arising out of the use or inability to use the Ansible Tower Software or any component, even if Red Hat or the authorized distributor has been advised of the possibility of such damages. In no event shall Red Hat's liability or an authorized distributor’s liability exceed the amount that You paid to Red Hat for the Ansible Tower Software during the twelve months preceding the first event giving rise to liability. + +6. Export Control. In accordance with the laws of the United States and other countries, You represent and warrant that You: (a) understand that the Ansible Tower Software and its components may be subject to export controls under the U.S. Commerce Department’s Export Administration Regulations (“EAR”); (b) are not located in any country listed in Country Group E:1 in Supplement No. 1 to part 740 of the EAR; (c) will not export, re-export, or transfer the Ansible Tower Software to any prohibited destination or to any end user who has been prohibited from participating in US export transactions by any federal agency of the US government; (d) will not use or transfer the Ansible Tower Software for use in connection with the design, development or production of nuclear, chemical or biological weapons, or rocket systems, space launch vehicles, or sounding rockets or unmanned air vehicle systems; (e) understand and agree that if you are in the United States and you export or transfer the Ansible Tower Software to eligible end users, you will, to the extent required by EAR Section 740.17 obtain a license for such export or transfer and will submit semi-annual reports to the Commerce Department’s Bureau of Industry and Security, which include the name and address (including country) of each transferee; and (f) understand that countries including the United States may restrict the import, use, or export of encryption products (which may include the Ansible Tower Software) and agree that you shall be solely responsible for compliance with any such import, use, or export restrictions. + +7. General. If any provision of this EULA is held to be unenforceable, that shall not affect the enforceability of the remaining provisions. This agreement shall be governed by the laws of the State of New York and of the United States, without regard to any conflict of laws provisions. The rights and obligations of the parties to this EULA shall not be governed by the United Nations Convention on the International Sale of Goods. + +Copyright © 2015 Red Hat, Inc. All rights reserved. "Red Hat" and “Ansible Tower” are registered trademarks of Red Hat, Inc. All other trademarks are the property of their respective owners. From 4adb65596a7dc4a4744fd14f8ce5c79b234842de Mon Sep 17 00:00:00 2001 From: Michael Abashian Date: Thu, 14 Jul 2016 10:41:02 -0400 Subject: [PATCH 111/123] Remove socket binding when the scope is destroyed so that we don't make errant GET requests in future instances of this controller. --- awx/ui/client/src/standard-out/standard-out.controller.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/awx/ui/client/src/standard-out/standard-out.controller.js b/awx/ui/client/src/standard-out/standard-out.controller.js index 805200201e..f43d6c4ea5 100644 --- a/awx/ui/client/src/standard-out/standard-out.controller.js +++ b/awx/ui/client/src/standard-out/standard-out.controller.js @@ -39,6 +39,12 @@ export function JobStdoutController ($rootScope, $scope, $state, $stateParams, } }); + // Unbind $rootScope socket event binding(s) so that they don't get triggered + // in another instance of this controller + $scope.$on('$destroy', function() { + $scope.removeJobStatusChange(); + }); + // Set the parse type so that CodeMirror knows how to display extra params YAML/JSON $scope.parseType = 'yaml'; From ed5f07b151353f9191e50f1f0c0b696a5c27ff51 Mon Sep 17 00:00:00 2001 From: Ken Hoes Date: Thu, 14 Jul 2016 11:03:25 -0400 Subject: [PATCH 112/123] Changed conditonal to repair console error and page loade error --- .../src/job-templates/edit/job-templates-edit.controller.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/ui/client/src/job-templates/edit/job-templates-edit.controller.js b/awx/ui/client/src/job-templates/edit/job-templates-edit.controller.js index ff753d2077..d92d1ecfd5 100644 --- a/awx/ui/client/src/job-templates/edit/job-templates-edit.controller.js +++ b/awx/ui/client/src/job-templates/edit/job-templates-edit.controller.js @@ -349,7 +349,7 @@ export default ParseTypeChange({ scope: $scope, field_id: 'job_templates_variables', onChange: callback }); - if($scope.job_template_obj.summary_fields.cloud_credential.name && related_cloud_credential) { + if($scope.job_template_obj.summary_fields.cloud_credential && related_cloud_credential) { $scope.$emit('cloudCredentialReady', $scope.job_template_obj.summary_fields.cloud_credential.name); } else { // No existing cloud credential From 6da6f48521ad65e5444980f2b90fb456d88aff85 Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Thu, 14 Jul 2016 11:04:06 -0400 Subject: [PATCH 113/123] Updated tests to reflect credential access after migrations --- awx/main/tests/functional/test_rbac_credential.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/awx/main/tests/functional/test_rbac_credential.py b/awx/main/tests/functional/test_rbac_credential.py index 95a6610a23..3b154d6f42 100644 --- a/awx/main/tests/functional/test_rbac_credential.py +++ b/awx/main/tests/functional/test_rbac_credential.py @@ -28,8 +28,10 @@ def test_two_teams_same_cred_name(organization_factory): rbac.migrate_credential(apps, None) - assert objects.teams.team1.member_role in cred1.admin_role.parents.all() - assert objects.teams.team2.member_role in cred2.admin_role.parents.all() + assert objects.teams.team1.admin_role in cred1.admin_role.parents.all() + assert objects.teams.team2.admin_role in cred2.admin_role.parents.all() + assert objects.teams.team1.member_role in cred1.use_role.parents.all() + assert objects.teams.team2.member_role in cred2.use_role.parents.all() @pytest.mark.django_db def test_credential_use_role(credential, user, permissions): @@ -53,7 +55,7 @@ def test_credential_migration_team_member(credential, team, user, permissions): rbac.migrate_credential(apps, None) # Admin permissions post migration - assert u in credential.admin_role + assert u in credential.use_role @pytest.mark.django_db def test_credential_migration_team_admin(credential, team, user, permissions): From 994a72967dab5ef8d6f35159985ea44f3b335a15 Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Wed, 13 Jul 2016 17:23:00 -0400 Subject: [PATCH 114/123] Cascade delete teams when their organization is deleted --- .../migrations/0027_v300_team_migrations.py | 20 +++++++++++++ .../migrations/0028_v300_org_team_cascade.py | 20 +++++++++++++ awx/main/migrations/_team_cleanup.py | 30 +++++++++++++++++++ awx/main/models/organization.py | 2 +- 4 files changed, 71 insertions(+), 1 deletion(-) create mode 100644 awx/main/migrations/0027_v300_team_migrations.py create mode 100644 awx/main/migrations/0028_v300_org_team_cascade.py create mode 100644 awx/main/migrations/_team_cleanup.py diff --git a/awx/main/migrations/0027_v300_team_migrations.py b/awx/main/migrations/0027_v300_team_migrations.py new file mode 100644 index 0000000000..b53fd8a969 --- /dev/null +++ b/awx/main/migrations/0027_v300_team_migrations.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from awx.main.migrations import _rbac as rbac +from awx.main.migrations import _team_cleanup as team_cleanup +from awx.main.migrations import _migration_utils as migration_utils +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('main', '0026_v300_credential_unique'), + ] + + operations = [ + migrations.RunPython(migration_utils.set_current_apps_for_migrations), + migrations.RunPython(team_cleanup.migrate_team), + migrations.RunPython(rbac.rebuild_role_hierarchy), + ] diff --git a/awx/main/migrations/0028_v300_org_team_cascade.py b/awx/main/migrations/0028_v300_org_team_cascade.py new file mode 100644 index 0000000000..bf798df3c3 --- /dev/null +++ b/awx/main/migrations/0028_v300_org_team_cascade.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models +import awx.main.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('main', '0027_v300_team_migrations'), + ] + + operations = [ + migrations.AlterField( + model_name='team', + name='organization', + field=models.ForeignKey(related_name='teams', to='main.Organization', null=True), + ), + ] diff --git a/awx/main/migrations/_team_cleanup.py b/awx/main/migrations/_team_cleanup.py new file mode 100644 index 0000000000..1a937d1f88 --- /dev/null +++ b/awx/main/migrations/_team_cleanup.py @@ -0,0 +1,30 @@ +# Python +import logging +from django.utils.encoding import smart_text + +logger = logging.getLogger(__name__) + +def log_migration(wrapped): + '''setup the logging mechanism for each migration method + as it runs, Django resets this, so we use a decorator + to re-add the handler for each method. + ''' + handler = logging.FileHandler("/tmp/tower_rbac_migrations.log", mode="a", encoding="UTF-8") + formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') + handler.setLevel(logging.DEBUG) + handler.setFormatter(formatter) + + def wrapper(*args, **kwargs): + logger.handlers = [] + logger.addHandler(handler) + return wrapped(*args, **kwargs) + return wrapper + +@log_migration +def migrate_team(apps, schema_editor): + '''If an orphan team exists that is still active, delete it.''' + Team = apps.get_model('main', 'Team') + for team in Team.objects.iterator(): + if team.organization is None: + logger.info(smart_text(u"Deleting orphaned team: {}".format(team.name))) + team.delete() diff --git a/awx/main/models/organization.py b/awx/main/models/organization.py index 3717171411..79cb3facdc 100644 --- a/awx/main/models/organization.py +++ b/awx/main/models/organization.py @@ -93,7 +93,7 @@ class Team(CommonModelNameNotUnique, ResourceMixin): 'Organization', blank=False, null=True, - on_delete=models.SET_NULL, + on_delete=models.CASCADE, related_name='teams', ) deprecated_projects = models.ManyToManyField( From 00e55e38133049798631e539e546eb4700d62cfd Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Thu, 14 Jul 2016 12:22:33 -0400 Subject: [PATCH 115/123] Add delete protection from certain objects Certain objects can be sensitive to being deleted while jobs are running, this protects those objects --- awx/main/access.py | 51 +++++++++++++++++++++++++++++---- awx/main/models/unified_jobs.py | 1 + 2 files changed, 46 insertions(+), 6 deletions(-) diff --git a/awx/main/access.py b/awx/main/access.py index b3f2bc17e6..3b75a6cd98 100644 --- a/awx/main/access.py +++ b/awx/main/access.py @@ -12,11 +12,12 @@ from django.contrib.auth.models import User from django.contrib.contenttypes.models import ContentType # Django REST Framework -from rest_framework.exceptions import ParseError, PermissionDenied +from rest_framework.exceptions import ParseError, PermissionDenied, ValidationError # AWX from awx.main.utils import * # noqa from awx.main.models import * # noqa +from awx.main.models.unified_jobs import ACTIVE_STATES from awx.main.models.mixins import ResourceMixin from awx.api.license import LicenseForbids from awx.main.task_engine import TaskSerializer @@ -310,7 +311,16 @@ class OrganizationAccess(BaseAccess): def can_delete(self, obj): self.check_license(feature='multiple_organizations', check_expiration=False) - return self.can_change(obj, None) + is_change_possible = self.can_change(obj, None) + if not is_change_possible: + return False + active_jobs = [] + active_jobs.extend(Job.objects.filter(project__in=obj.projects.all(), status__in=ACTIVE_STATES)) + active_jobs.extend(ProjectUpdate.objects.filter(project__in=obj.projects.all(), status__in=ACTIVE_STATES)) + active_jobs.extend(InventoryUpdate.objects.filter(inventory_source__inventory__organization=obj, status__in=ACTIVE_STATES)) + if len(active_jobs) > 0: + raise ValidationError("Delete not allowed while there are jobs running. Number of jobs {}".format(len(active_jobs))) + return True class InventoryAccess(BaseAccess): ''' @@ -373,7 +383,15 @@ class InventoryAccess(BaseAccess): return self.user in obj.admin_role def can_delete(self, obj): - return self.can_admin(obj, None) + is_can_admin = self.can_admin(obj, None) + if not is_can_admin: + return False + active_jobs = [] + active_jobs.extend(Job.objects.filter(inventory=obj, status__in=ACTIVE_STATES)) + active_jobs.extend(InventoryUpdate.objects.filter(inventory_source__inventory=obj, status__in=ACTIVE_STATES)) + if len(active_jobs) > 0: + raise ValidationError("Delete not allowed while there are jobs running. Number of jobs {}".format(len(active_jobs))) + return True def can_run_ad_hoc_commands(self, obj): return self.user in obj.adhoc_role @@ -486,7 +504,14 @@ class GroupAccess(BaseAccess): return True def can_delete(self, obj): - return obj and self.user in obj.inventory.admin_role + is_delete_allowed = bool(obj and self.user in obj.inventory.admin_role) + if not is_delete_allowed: + return False + active_jobs = [] + active_jobs.extend(InventoryUpdate.objects.filter(inventory_source__in=obj.inventory_sources.all(), status__in=ACTIVE_STATES)) + if len(active_jobs) > 0: + raise ValidationError("Delete not allowed while there are jobs running. Number of jobs {}".format(len(active_jobs))) + return True class InventorySourceAccess(BaseAccess): ''' @@ -736,7 +761,15 @@ class ProjectAccess(BaseAccess): return self.user in obj.admin_role def can_delete(self, obj): - return self.can_change(obj, None) + is_change_allowed = self.can_change(obj, None) + if not is_change_allowed: + return False + active_jobs = [] + active_jobs.extend(Job.objects.filter(project=obj, status__in=ACTIVE_STATES)) + active_jobs.extend(ProjectUpdate.objects.filter(project=obj, status__in=ACTIVE_STATES)) + if len(active_jobs) > 0: + raise ValidationError("Delete not allowed while there are jobs running. Number of jobs {}".format(len(active_jobs))) + return True @check_superuser def can_start(self, obj): @@ -958,7 +991,13 @@ class JobTemplateAccess(BaseAccess): @check_superuser def can_delete(self, obj): - return self.user in obj.admin_role + is_delete_allowed = self.user in obj.admin_role + if not is_delete_allowed: + return False + active_jobs = obj.jobs.filter(status__in=ACTIVE_STATES) + if len(active_jobs) > 0: + raise ValidationError("Delete not allowed while there are jobs running. Number of jobs {}".format(len(active_jobs))) + return True class JobAccess(BaseAccess): ''' diff --git a/awx/main/models/unified_jobs.py b/awx/main/models/unified_jobs.py index 7084cef874..36b2dfe75e 100644 --- a/awx/main/models/unified_jobs.py +++ b/awx/main/models/unified_jobs.py @@ -39,6 +39,7 @@ __all__ = ['UnifiedJobTemplate', 'UnifiedJob'] logger = logging.getLogger('awx.main.models.unified_jobs') CAN_CANCEL = ('new', 'pending', 'waiting', 'running') +ACTIVE_STATES = CAN_CANCEL class UnifiedJobTemplate(PolymorphicModel, CommonModelNameNotUnique, NotificationFieldsModel): From c8525031632d03e965888e24308b7378dd96282d Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Thu, 14 Jul 2016 12:40:37 -0400 Subject: [PATCH 116/123] Fix up some flake8 issues --- awx/api/views.py | 3 --- awx/main/access.py | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/awx/api/views.py b/awx/api/views.py index 4af8a6561e..64ea980f66 100644 --- a/awx/api/views.py +++ b/awx/api/views.py @@ -2189,9 +2189,6 @@ class JobTemplateDetail(RetrieveUpdateDestroyAPIView): can_delete = request.user.can_access(JobTemplate, 'delete', obj) if not can_delete: raise PermissionDenied("Cannot delete job template.") - if obj.jobs.filter(status__in=['new', 'pending', 'waiting', 'running']).exists(): - return Response({"error": "Delete not allowed while there are jobs running"}, - status=status.HTTP_405_METHOD_NOT_ALLOWED) return super(JobTemplateDetail, self).destroy(request, *args, **kwargs) diff --git a/awx/main/access.py b/awx/main/access.py index 3b75a6cd98..af05c58be6 100644 --- a/awx/main/access.py +++ b/awx/main/access.py @@ -761,7 +761,7 @@ class ProjectAccess(BaseAccess): return self.user in obj.admin_role def can_delete(self, obj): - is_change_allowed = self.can_change(obj, None) + is_change_allowed = self.can_change(obj, None) if not is_change_allowed: return False active_jobs = [] From e18d1425335e773911a6792749e058baf90fa06b Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Thu, 14 Jul 2016 14:29:34 -0400 Subject: [PATCH 117/123] Don't let normal users create orgless projects #3006 --- awx/api/serializers.py | 13 +++++++++++++ awx/main/tests/functional/test_projects.py | 8 ++++++++ 2 files changed, 21 insertions(+) diff --git a/awx/api/serializers.py b/awx/api/serializers.py index 2f58b776f4..1487fe17e3 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -918,6 +918,19 @@ class ProjectSerializer(UnifiedJobTemplateSerializer, ProjectOptionsSerializer): args=(obj.last_update.pk,)) return res + def validate(self, attrs): + organization = None + if 'organization' in attrs: + organization = attrs['organization'] + elif self.instance: + organization = self.instance.organization + + view = self.context.get('view', None) + if not organization and not view.request.user.is_superuser: + # Only allow super users to create orgless projects + raise serializers.ValidationError('Organization is missing') + return super(ProjectSerializer, self).validate(attrs) + class ProjectPlaybooksSerializer(ProjectSerializer): diff --git a/awx/main/tests/functional/test_projects.py b/awx/main/tests/functional/test_projects.py index 4c32d1dd69..7ac1675341 100644 --- a/awx/main/tests/functional/test_projects.py +++ b/awx/main/tests/functional/test_projects.py @@ -114,3 +114,11 @@ def test_create_project(post, organization, org_admin, org_member, admin, rando, assert result.status_code == expected_status_code if expected_status_code == 201: assert Project.objects.filter(name='Project', organization=organization).exists() + +@pytest.mark.django_db() +def test_create_project_null_organization(post, organization, admin): + post(reverse('api:project_list'), { 'name': 't', 'organization': None}, admin, expect=201) + +@pytest.mark.django_db() +def test_create_project_null_organization_xfail(post, organization, org_admin): + post(reverse('api:project_list'), { 'name': 't', 'organization': None}, org_admin, expect=400) From 68d5a702af292d2bf958422fed742b39b4757b0f Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Thu, 14 Jul 2016 14:38:43 -0400 Subject: [PATCH 118/123] Team organization field made non-null --- awx/main/migrations/0028_v300_org_team_cascade.py | 3 ++- awx/main/models/organization.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/awx/main/migrations/0028_v300_org_team_cascade.py b/awx/main/migrations/0028_v300_org_team_cascade.py index bf798df3c3..80378c5729 100644 --- a/awx/main/migrations/0028_v300_org_team_cascade.py +++ b/awx/main/migrations/0028_v300_org_team_cascade.py @@ -15,6 +15,7 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='team', name='organization', - field=models.ForeignKey(related_name='teams', to='main.Organization', null=True), + field=models.ForeignKey(related_name='teams', to='main.Organization'), + preserve_default=False, ), ] diff --git a/awx/main/models/organization.py b/awx/main/models/organization.py index 79cb3facdc..5f3dc9d7c9 100644 --- a/awx/main/models/organization.py +++ b/awx/main/models/organization.py @@ -92,7 +92,7 @@ class Team(CommonModelNameNotUnique, ResourceMixin): organization = models.ForeignKey( 'Organization', blank=False, - null=True, + null=False, on_delete=models.CASCADE, related_name='teams', ) From 06c37d39ef7eb7b29919370f6c6a067d570e8fc2 Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Thu, 14 Jul 2016 14:43:25 -0400 Subject: [PATCH 119/123] Make development environment use ATOMIC_REQUESTS --- awx/settings/local_settings.py.docker_compose | 1 + 1 file changed, 1 insertion(+) diff --git a/awx/settings/local_settings.py.docker_compose b/awx/settings/local_settings.py.docker_compose index f7cf9f0c58..2e27a664ea 100644 --- a/awx/settings/local_settings.py.docker_compose +++ b/awx/settings/local_settings.py.docker_compose @@ -25,6 +25,7 @@ DATABASES = { 'NAME': 'awx-dev', 'USER': 'awx-dev', 'PASSWORD': 'AWXsome1', + 'ATOMIC_REQUESTS': True, 'HOST': 'postgres', 'PORT': '', } From 41d6d19bcb50d53a244cf0e39e5e00681cfb106e Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Thu, 14 Jul 2016 15:02:00 -0400 Subject: [PATCH 120/123] Added patch tests for updating project organizations --- awx/main/tests/functional/test_projects.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/awx/main/tests/functional/test_projects.py b/awx/main/tests/functional/test_projects.py index 7ac1675341..40ea659432 100644 --- a/awx/main/tests/functional/test_projects.py +++ b/awx/main/tests/functional/test_projects.py @@ -122,3 +122,11 @@ def test_create_project_null_organization(post, organization, admin): @pytest.mark.django_db() def test_create_project_null_organization_xfail(post, organization, org_admin): post(reverse('api:project_list'), { 'name': 't', 'organization': None}, org_admin, expect=400) + +@pytest.mark.django_db() +def test_patch_project_null_organization(patch, organization, project, admin): + patch(reverse('api:project_detail', args=(project.id,)), { 'name': 't', 'organization': organization.id}, admin, expect=200) + +@pytest.mark.django_db() +def test_patch_project_null_organization_xfail(patch, project, org_admin): + patch(reverse('api:project_detail', args=(project.id,)), { 'name': 't', 'organization': None}, org_admin, expect=400) From 8ed69d164117a56e3e0a9bd8148d61e22da930a7 Mon Sep 17 00:00:00 2001 From: Graham Mainwaring Date: Thu, 14 Jul 2016 15:34:16 -0400 Subject: [PATCH 121/123] Additional UI/JS licenses (#2974) * Additional UI/JS licenses --- docs/licenses/cowsay.txt | 26 ++++++++++++++++++++++++++ docs/licenses/font-awesome.txt | 13 +++++++++++++ 2 files changed, 39 insertions(+) create mode 100644 docs/licenses/cowsay.txt create mode 100644 docs/licenses/font-awesome.txt diff --git a/docs/licenses/cowsay.txt b/docs/licenses/cowsay.txt new file mode 100644 index 0000000000..a58a648c73 --- /dev/null +++ b/docs/licenses/cowsay.txt @@ -0,0 +1,26 @@ +cowsay is licensed under the following MIT license: + +==== +Copyright (c) 2012 Fabio Crisci + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +==== + +The original idea of cowsay come from [Tony Monroe](http://www.nog.net/~tony/) - [cowsay](https://github.com/schacon/cowsay) diff --git a/docs/licenses/font-awesome.txt b/docs/licenses/font-awesome.txt new file mode 100644 index 0000000000..0928d89347 --- /dev/null +++ b/docs/licenses/font-awesome.txt @@ -0,0 +1,13 @@ +Font License + +Applies to all desktop and webfont files in the following directory: font-awesome/fonts/. +License: SIL OFL 1.1 +URL: http://scripts.sil.org/OFL + + + +Code License + +Applies to all CSS and LESS files in the following directories: font-awesome/css/, font-awesome/less/, and font-awesome/scss/. +License: MIT License +URL: http://opensource.org/licenses/mit-license.html From 4ffc062d1963982eab7cb0a1736bcacb2414141c Mon Sep 17 00:00:00 2001 From: Leigh Johnson Date: Thu, 14 Jul 2016 15:35:46 -0400 Subject: [PATCH 122/123] resolves kickback on #2976 (#3009) --- awx/ui/client/src/controllers/Projects.js | 2 ++ awx/ui/client/src/helpers/JobTemplates.js | 1 + .../src/inventories/manage/groups/groups-add.controller.js | 1 + .../src/inventories/manage/groups/groups-edit.controller.js | 1 + .../src/job-templates/edit/job-templates-edit.controller.js | 3 ++- 5 files changed, 7 insertions(+), 1 deletion(-) diff --git a/awx/ui/client/src/controllers/Projects.js b/awx/ui/client/src/controllers/Projects.js index 4dbb111f79..1b641ab97a 100644 --- a/awx/ui/client/src/controllers/Projects.js +++ b/awx/ui/client/src/controllers/Projects.js @@ -392,6 +392,7 @@ export function ProjectsAdd(Refresh, $scope, $rootScope, $compile, $location, $l master = {}; // remove "type" field from search options + CredentialList = _.cloneDeep(CredentialList); CredentialList.fields.kind.noSearch = true; generator.inject(form, { mode: 'add', related: false, scope: $scope }); @@ -571,6 +572,7 @@ export function ProjectsEdit($scope, $rootScope, $compile, $location, $log, relatedSets = {}; // remove "type" field from search options + CredentialList = _.cloneDeep(CredentialList); CredentialList.fields.kind.noSearch = true; diff --git a/awx/ui/client/src/helpers/JobTemplates.js b/awx/ui/client/src/helpers/JobTemplates.js index 55b2419390..55a4a9aa5f 100644 --- a/awx/ui/client/src/helpers/JobTemplates.js +++ b/awx/ui/client/src/helpers/JobTemplates.js @@ -25,6 +25,7 @@ angular.module('JobTemplatesHelper', ['Utilities']) return function(params) { var scope = params.scope, + CredentialList = _.cloneDeep(CredentialList), defaultUrl = GetBasePath('job_templates'), // generator = GenerateForm, form = JobTemplateForm(), diff --git a/awx/ui/client/src/inventories/manage/groups/groups-add.controller.js b/awx/ui/client/src/inventories/manage/groups/groups-add.controller.js index a405f2bb52..a816cacd3a 100644 --- a/awx/ui/client/src/inventories/manage/groups/groups-add.controller.js +++ b/awx/ui/client/src/inventories/manage/groups/groups-add.controller.js @@ -13,6 +13,7 @@ form = GroupForm(); // remove "type" field from search options + CredentialList = _.cloneDeep(CredentialList); CredentialList.fields.kind.noSearch = true; $scope.formCancel = function(){ diff --git a/awx/ui/client/src/inventories/manage/groups/groups-edit.controller.js b/awx/ui/client/src/inventories/manage/groups/groups-edit.controller.js index 094b53399a..b008c0f888 100644 --- a/awx/ui/client/src/inventories/manage/groups/groups-edit.controller.js +++ b/awx/ui/client/src/inventories/manage/groups/groups-edit.controller.js @@ -15,6 +15,7 @@ form = GroupForm(); // remove "type" field from search options + CredentialList = _.cloneDeep(CredentialList); CredentialList.fields.kind.noSearch = true; $scope.formCancel = function(){ diff --git a/awx/ui/client/src/job-templates/edit/job-templates-edit.controller.js b/awx/ui/client/src/job-templates/edit/job-templates-edit.controller.js index c893463c28..8a2f433dbc 100644 --- a/awx/ui/client/src/job-templates/edit/job-templates-edit.controller.js +++ b/awx/ui/client/src/job-templates/edit/job-templates-edit.controller.js @@ -47,6 +47,7 @@ export default choicesCount = 0; // remove "type" field from search options + CredentialList = _.cloneDeep(CredentialList); CredentialList.fields.kind.noSearch = true; CallbackHelpInit({ scope: $scope }); @@ -471,7 +472,7 @@ export default }); } else { - // job template doesn't exist + // job template doesn't exist $scope.$emit("choicesReady"); } From 86afd6f758e00a21704121300f7e9ff0721f1e0e Mon Sep 17 00:00:00 2001 From: Leigh Johnson Date: Thu, 14 Jul 2016 15:36:11 -0400 Subject: [PATCH 123/123] Fix NaN / invalid end date value in schedule edit forms (#3007) * fix NAN on end date issue affecting schedule edit form, resolves #2817 * remove debug statement --- .../src/scheduler/schedulerEdit.controller.js | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/awx/ui/client/src/scheduler/schedulerEdit.controller.js b/awx/ui/client/src/scheduler/schedulerEdit.controller.js index 36e94151d3..2687f67ecf 100644 --- a/awx/ui/client/src/scheduler/schedulerEdit.controller.js +++ b/awx/ui/client/src/scheduler/schedulerEdit.controller.js @@ -1,4 +1,20 @@ -export default ['$compile', '$state', '$stateParams', 'EditSchedule', 'Wait', '$scope', '$rootScope', 'CreateSelect2', 'ParseTypeChange', function($compile, $state, $stateParams, EditSchedule, Wait, $scope, $rootScope, CreateSelect2, ParseTypeChange) { +export default ['$filter', '$compile', '$state', '$stateParams', 'EditSchedule', 'Wait', '$scope', '$rootScope', 'CreateSelect2', 'ParseTypeChange', +function($filter, $compile, $state, $stateParams, EditSchedule, Wait, $scope, $rootScope, CreateSelect2, ParseTypeChange) { + + $scope.processSchedulerEndDt = function(){ + // set the schedulerEndDt to be equal to schedulerStartDt + 1 day @ midnight + var dt = new Date($scope.schedulerUTCTime); + // increment date by 1 day + dt.setDate(dt.getDate() + 1); + var month = $filter('schZeroPad')(dt.getMonth() + 1, 2), + day = $filter('schZeroPad')(dt.getDate(), 2); + $scope.$parent.schedulerEndDt = month + '/' + day + '/' + dt.getFullYear(); + }; + // initial end @ midnight values + $scope.schedulerEndHour = "00"; + $scope.schedulerEndMinute = "00"; + $scope.schedulerEndSecond = "00"; + $scope.$on("ScheduleFormCreated", function(e, scope) { $scope.hideForm = false; $scope = angular.extend($scope, scope);
TitleName Time
" + ($filter('longDate')(row.last_updated)).replace(/ /,'
') + "
" + ellipsis(row.summary_fields.group.name) + "
" + ($filter('longDate')(row.created)).replace(/ /,'
') + "
" + ($filter('longDate')(row.finished)).replace(/ /,'
') + "
" + ellipsis(row.name) + "
" + ($filter('longDate')(row.last_updated)).replace(/ /,'
') + "
" + ellipsis(row.summary_fields.group.name) + "" + $filter('sanitize')(ellipsis(row.summary_fields.group.name)) + "
NA" + ellipsis(row.summary_fields.group.name) + "" + $filter('sanitize')(ellipsis(row.summary_fields.group.name)) + "