From ede9d961da5c8c7c318e23e8d59b51f841e78328 Mon Sep 17 00:00:00 2001 From: tongtie Date: Tue, 14 Sep 2021 22:20:52 +0800 Subject: [PATCH 01/26] fix: Internationalization causes the project to be unable to choose manual select --- awx/ui/src/screens/Project/shared/ProjectForm.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/ui/src/screens/Project/shared/ProjectForm.js b/awx/ui/src/screens/Project/shared/ProjectForm.js index af3b21f52d..91239784c3 100644 --- a/awx/ui/src/screens/Project/shared/ProjectForm.js +++ b/awx/ui/src/screens/Project/shared/ProjectForm.js @@ -224,7 +224,7 @@ function ProjectFormFields({ isDisabled: true, }, ...scmTypeOptions.map(([value, label]) => { - if (label === 'Manual') { + if (value === '') { value = 'manual'; } return { From 6d2c10ad02db12539e52219f4836e0c33dc47616 Mon Sep 17 00:00:00 2001 From: Martin Vician Date: Fri, 5 Aug 2022 14:13:12 +0100 Subject: [PATCH 02/26] Added domain item and authorizer for TSS --- awx/main/credential_plugins/tss.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/awx/main/credential_plugins/tss.py b/awx/main/credential_plugins/tss.py index 1803400e2f..44a35b7dbd 100644 --- a/awx/main/credential_plugins/tss.py +++ b/awx/main/credential_plugins/tss.py @@ -1,7 +1,7 @@ from .plugin import CredentialPlugin from django.utils.translation import gettext_lazy as _ -from thycotic.secrets.server import PasswordGrantAuthorizer, SecretServer, ServerSecret +from thycotic.secrets.server import DomainPasswordGrantAuthorizer, PasswordGrantAuthorizer, SecretServer, ServerSecret tss_inputs = { 'fields': [ @@ -17,6 +17,12 @@ tss_inputs = { 'help_text': _('The (Application) user username'), 'type': 'string', }, + { + 'id': 'domain', + 'label': _('Domain'), + 'help_text': _('The (Application) user domain'), + 'type': 'string', + }, { 'id': 'password', 'label': _('Password'), @@ -44,7 +50,10 @@ tss_inputs = { def tss_backend(**kwargs): - authorizer = PasswordGrantAuthorizer(kwargs['server_url'], kwargs['username'], kwargs['password']) + if 'domain' in kwargs: + authorizer = DomainPasswordGrantAuthorizer(kwargs['server_url'], kwargs['username'], kwargs['password'], kwargs['domain']) + else: + authorizer = PasswordGrantAuthorizer(kwargs['server_url'], kwargs['username'], kwargs['password']) secret_server = SecretServer(kwargs['server_url'], authorizer) secret_dict = secret_server.get_secret(kwargs['secret_id']) secret = ServerSecret(**secret_dict) From 3eb748ff1f2ef668339f2b07cb59a13ce221ab4e Mon Sep 17 00:00:00 2001 From: Alex Date: Sun, 25 Sep 2022 18:07:10 +0200 Subject: [PATCH 03/26] build: harden promote.yml permissions Signed-off-by: Alex --- .github/workflows/promote.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/promote.yml b/.github/workflows/promote.yml index 768cb8533b..b792cce14d 100644 --- a/.github/workflows/promote.yml +++ b/.github/workflows/promote.yml @@ -4,6 +4,9 @@ on: release: types: [published] +permissions: + contents: read # to fetch code (actions/checkout) + jobs: promote: runs-on: ubuntu-latest From 21291b53fda8d61f8a129f270059504b608f43d7 Mon Sep 17 00:00:00 2001 From: Alex Date: Sun, 25 Sep 2022 18:10:53 +0200 Subject: [PATCH 04/26] build: harden label_pr.yml permissions Signed-off-by: Alex --- .github/workflows/label_pr.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/label_pr.yml b/.github/workflows/label_pr.yml index 8e3f8b81a2..cd6036958f 100644 --- a/.github/workflows/label_pr.yml +++ b/.github/workflows/label_pr.yml @@ -7,6 +7,10 @@ on: - reopened - synchronize +permissions: + contents: read # to determine modified files (actions/labeler) + pull-requests: write # to add labels to PRs (actions/labeler) + jobs: triage: runs-on: ubuntu-latest From b3bda415da6b91865ee371bf27e97f7dc0992c04 Mon Sep 17 00:00:00 2001 From: Alex Date: Sun, 25 Sep 2022 18:12:14 +0200 Subject: [PATCH 05/26] build: harden label_issue.yml permissions Signed-off-by: Alex --- .github/workflows/label_issue.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/label_issue.yml b/.github/workflows/label_issue.yml index ead15724bb..d5b00d1d29 100644 --- a/.github/workflows/label_issue.yml +++ b/.github/workflows/label_issue.yml @@ -6,6 +6,10 @@ on: - opened - reopened + permissions: + contents: read # to fetch code + issues: write # to label issues + jobs: triage: runs-on: ubuntu-latest From ede185504c0257910da12d7c026e5c5059898d80 Mon Sep 17 00:00:00 2001 From: Lorenzo Tanganelli Date: Wed, 29 Mar 2023 16:49:32 +0200 Subject: [PATCH 06/26] fix js error in case of locale not exists --- awx/ui/src/App.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/awx/ui/src/App.js b/awx/ui/src/App.js index 21ca5a0b4f..dfe6f34e61 100644 --- a/awx/ui/src/App.js +++ b/awx/ui/src/App.js @@ -28,7 +28,7 @@ import { getLanguageWithoutRegionCode } from 'util/language'; import Metrics from 'screens/Metrics'; import SubscriptionEdit from 'screens/Setting/Subscription/SubscriptionEdit'; import useTitle from 'hooks/useTitle'; -import { dynamicActivate } from './i18nLoader'; +import { dynamicActivate, locales } from './i18nLoader'; import getRouteConfig from './routeConfig'; import { SESSION_REDIRECT_URL } from './constants'; @@ -142,9 +142,15 @@ function App() { const searchParams = Object.fromEntries(new URLSearchParams(search)); const pseudolocalization = searchParams.pseudolocalization === 'true' || false; - const language = + let language = searchParams.lang || getLanguageWithoutRegionCode(navigator) || 'en'; + if (!Object.keys(locales).includes(language)) { + // If there isn't a string catalog available for the browser's + // preferred language, default to one that has strings. + language = 'en'; + } + useEffect(() => { dynamicActivate(language, pseudolocalization); }, [language, pseudolocalization]); From 6c9e2502a524b235c99f9a54d23c384d5233a717 Mon Sep 17 00:00:00 2001 From: John Westcott IV Date: Mon, 10 Apr 2023 10:58:13 -0400 Subject: [PATCH 07/26] Unpinning future --- licenses/{python-future.txt => future.txt} | 0 requirements/requirements.txt | 5 ++--- requirements/requirements_git.txt | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) rename licenses/{python-future.txt => future.txt} (100%) diff --git a/licenses/python-future.txt b/licenses/future.txt similarity index 100% rename from licenses/python-future.txt rename to licenses/future.txt diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 217356b5ca..4b74fe3119 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -149,9 +149,8 @@ frozenlist==1.3.3 # via # aiohttp # aiosignal - # via - # -r /awx_devel/requirements/requirements_git.txt - # django-radius +future==0.18.3 + # via django-radius gitdb==4.0.10 # via gitpython gitpython==3.1.30 diff --git a/requirements/requirements_git.txt b/requirements/requirements_git.txt index df424cca95..a4ddd96586 100644 --- a/requirements/requirements_git.txt +++ b/requirements/requirements_git.txt @@ -2,6 +2,6 @@ git+https://github.com/ansible/system-certifi.git@devel#egg=certifi # Remove pbr from requirements.in when moving ansible-runner to requirements.in git+https://github.com/ansible/ansible-runner.git@devel#egg=ansible-runner # django-radius has an aggressive pin of future==0.16.0, see https://github.com/robgolding/django-radius/pull/25 +# There is a PR against django-radius that would fix this: https://github.com/robgolding/django-radius/pull/27 git+https://github.com/ansible/django-radius.git@develop#egg=django-radius -git+https://github.com/PythonCharmers/python-future@master#egg=future git+https://github.com/ansible/python3-saml.git@devel#egg=python3-saml From 4e5cce8d15c211b709e12eb12a1a4931c2c3464e Mon Sep 17 00:00:00 2001 From: Ladislav Smola Date: Wed, 29 Mar 2023 17:07:07 +0200 Subject: [PATCH 08/26] Analytics export other subs attrs We'll export also subscription_id since pool_id is not enough in certain cases. Then also export usage and account number --- awx/main/analytics/collectors.py | 5 ++++- awx/main/utils/licensing.py | 28 ++++++++++++++++++++++++++-- 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/awx/main/analytics/collectors.py b/awx/main/analytics/collectors.py index 1bc4c9044f..15577c9696 100644 --- a/awx/main/analytics/collectors.py +++ b/awx/main/analytics/collectors.py @@ -131,7 +131,7 @@ def _identify_lower(key, since, until, last_gather): return lower, last_entries -@register('config', '1.5', description=_('General platform configuration.')) +@register('config', '1.6', description=_('General platform configuration.')) def config(since, **kwargs): license_info = get_license() install_type = 'traditional' @@ -155,10 +155,13 @@ def config(since, **kwargs): 'subscription_name': license_info.get('subscription_name'), 'sku': license_info.get('sku'), 'support_level': license_info.get('support_level'), + 'usage': license_info.get('usage'), 'product_name': license_info.get('product_name'), 'valid_key': license_info.get('valid_key'), 'satellite': license_info.get('satellite'), 'pool_id': license_info.get('pool_id'), + 'subscription_id': license_info.get('subscription_id'), + 'account_number': license_info.get('account_number'), 'current_instances': license_info.get('current_instances'), 'automated_instances': license_info.get('automated_instances'), 'automated_since': license_info.get('automated_since'), diff --git a/awx/main/utils/licensing.py b/awx/main/utils/licensing.py index c692e3131a..b5e8957e32 100644 --- a/awx/main/utils/licensing.py +++ b/awx/main/utils/licensing.py @@ -170,6 +170,8 @@ class Licenser(object): license.setdefault('sku', sub['pool']['productId']) license.setdefault('subscription_name', sub['pool']['productName']) + license.setdefault('subscription_id', sub['pool']['subscriptionId']) + license.setdefault('account_number', sub['pool']['accountNumber']) license.setdefault('pool_id', sub['pool']['id']) license.setdefault('product_name', sub['pool']['productName']) license.setdefault('valid_key', True) @@ -185,6 +187,14 @@ class Licenser(object): license['instance_count'] = license.get('instance_count', 0) + instances license['subscription_name'] = re.sub(r'[\d]* Managed Nodes', '%d Managed Nodes' % license['instance_count'], license['subscription_name']) + license['support_level'] = '' + license['usage'] = '' + for attr in sub['pool'].get('productAttributes', []): + if attr.get('name') == 'support_level': + license['support_level'] = attr.get('value') + elif attr.get('name') == 'usage': + license['usage'] = attr.get('value') + if not license: logger.error("No valid subscriptions found in manifest") self._attrs.update(license) @@ -277,7 +287,10 @@ class Licenser(object): license['productId'] = sub['product_id'] license['quantity'] = int(sub['quantity']) license['support_level'] = sub['support_level'] + license['usage'] = sub['usage'] license['subscription_name'] = sub['name'] + license['subscriptionId'] = sub['subscription_id'] + license['accountNumber'] = sub['account_number'] license['id'] = sub['upstream_pool_id'] license['endDate'] = sub['end_date'] license['productName'] = "Red Hat Ansible Automation" @@ -304,7 +317,7 @@ class Licenser(object): def generate_license_options_from_entitlements(self, json): from dateutil.parser import parse - ValidSub = collections.namedtuple('ValidSub', 'sku name support_level end_date trial quantity pool_id satellite') + ValidSub = collections.namedtuple('ValidSub', 'sku name support_level end_date trial quantity pool_id satellite subscription_id account_number usage') valid_subs = [] for sub in json: satellite = sub.get('satellite') @@ -333,15 +346,23 @@ class Licenser(object): sku = sub['productId'] trial = sku.startswith('S') # i.e.,, SER/SVC support_level = '' + usage = '' pool_id = sub['id'] + subscription_id = sub['subscriptionId'] + account_number = sub['accountNumber'] if satellite: support_level = sub['support_level'] + usage = sub['usage'] else: for attr in sub.get('productAttributes', []): if attr.get('name') == 'support_level': support_level = attr.get('value') + elif attr.get('name') == 'usage': + usage = attr.get('value') - valid_subs.append(ValidSub(sku, sub['productName'], support_level, end_date, trial, quantity, pool_id, satellite)) + valid_subs.append( + ValidSub(sku, sub['productName'], support_level, end_date, trial, quantity, pool_id, satellite, subscription_id, account_number, usage) + ) if valid_subs: licenses = [] @@ -350,6 +371,7 @@ class Licenser(object): license._attrs['instance_count'] = int(sub.quantity) license._attrs['sku'] = sub.sku license._attrs['support_level'] = sub.support_level + license._attrs['usage'] = sub.usage license._attrs['license_type'] = 'enterprise' if sub.trial: license._attrs['trial'] = True @@ -364,6 +386,8 @@ class Licenser(object): license._attrs['valid_key'] = True license.update(license_date=int(sub.end_date.strftime('%s'))) license.update(pool_id=sub.pool_id) + license.update(subscription_id=sub.subscription_id) + license.update(account_number=sub.account_number) licenses.append(license._attrs.copy()) return licenses From ffa3cd1fffb88cf505e41b2a2ac7841f1a2d18b9 Mon Sep 17 00:00:00 2001 From: Seth Foster Date: Tue, 11 Apr 2023 10:58:11 -0400 Subject: [PATCH 09/26] Add troubleshooting to execution node docs (#13826) --- docs/execution_nodes.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/docs/execution_nodes.md b/docs/execution_nodes.md index fae93d4127..e0c318292e 100644 --- a/docs/execution_nodes.md +++ b/docs/execution_nodes.md @@ -76,3 +76,28 @@ Wait a few minutes for the periodic AWX task to do a health check against the ne ## Removing instances You can remove an instance by clicking "Remove" in the Instances page, or by setting the instance `node_state` to "deprovisioning" via the API. + +## Troubleshooting + +### Fact cache not working + +Make sure the system timezone on the execution node matches `settings.TIME_ZONE` (default is 'UTC') on AWX. +Fact caching relies on comparing modified times of artifact files, and these modified times are not timezone-aware. Therefore, it is critical that the timezones of the execution nodes match AWX's timezone setting. + +To set the system timezone to UTC + +`ln -s /usr/share/zoneinfo/Etc/UTC /etc/localtime` + +### Permission denied errors + +Jobs may fail with the following error +``` +"msg":"exec container process `/usr/local/bin/entrypoint`: Permission denied" +``` +or similar + +For RHEL based machines, this could due to SELinux that is enabled on the system. + +You can pass these `extra_settings` container options to override SELinux protections. + +`DEFAULT_CONTAINER_RUN_OPTIONS = ['--network', 'slirp4netns:enable_ipv6=true', '--security-opt', 'label=disable']` \ No newline at end of file From 1c51ef8a697480edf9d2fcbf24f4fe4b5d8e404e Mon Sep 17 00:00:00 2001 From: Seth Foster Date: Tue, 11 Apr 2023 15:06:48 -0400 Subject: [PATCH 10/26] Store serialized metrics locally (#13833) --- awx/main/analytics/subsystem_metrics.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/awx/main/analytics/subsystem_metrics.py b/awx/main/analytics/subsystem_metrics.py index d8836b332f..4e10ff98b8 100644 --- a/awx/main/analytics/subsystem_metrics.py +++ b/awx/main/analytics/subsystem_metrics.py @@ -298,11 +298,13 @@ class Metrics: try: current_time = time.time() if current_time - self.previous_send_metrics.decode(self.conn) > self.send_metrics_interval: + serialized_metrics = self.serialize_local_metrics() payload = { 'instance': self.instance_name, - 'metrics': self.serialize_local_metrics(), + 'metrics': serialized_metrics, } - + # store the serialized data locally as well, so that load_other_metrics will read it + self.conn.set(root_key + '_instance_' + self.instance_name, serialized_metrics) emit_channel_notification("metrics", payload) self.previous_send_metrics.set(current_time) From e53a5da91eca8966422222e3ec46b393b9b560e2 Mon Sep 17 00:00:00 2001 From: Alexander Komarov Date: Fri, 30 Jul 2021 14:21:41 +0500 Subject: [PATCH 11/26] Add more tests for different modules --- .../functional/api/test_notifications.py | 10 ++++ .../tests/functional/api/test_workflow_job.py | 54 +++++++++++++++++++ awx/main/tests/functional/conftest.py | 26 ++++++++- awx/main/tests/functional/test_copy.py | 18 +++++++ .../tests/functional/test_notifications.py | 29 ++++++++++ .../tests/functional/test_rbac_workflow.py | 24 --------- 6 files changed, 136 insertions(+), 25 deletions(-) create mode 100644 awx/main/tests/functional/api/test_workflow_job.py diff --git a/awx/main/tests/functional/api/test_notifications.py b/awx/main/tests/functional/api/test_notifications.py index 92f6045191..431065396d 100644 --- a/awx/main/tests/functional/api/test_notifications.py +++ b/awx/main/tests/functional/api/test_notifications.py @@ -153,3 +153,13 @@ def test_post_org_approval_notification(get, post, admin, notification_template, response = get(url, admin) assert response.status_code == 200 assert len(response.data['results']) == 1 + + +@pytest.mark.django_db +def test_post_wfj_notification(get, post, admin, workflow_job, notification): + workflow_job.notifications.add(notification) + workflow_job.save() + url = reverse("api:workflow_job_notifications_list", kwargs={'pk': workflow_job.pk}) + response = get(url, admin) + assert response.status_code == 200 + assert len(response.data['results']) == 1 diff --git a/awx/main/tests/functional/api/test_workflow_job.py b/awx/main/tests/functional/api/test_workflow_job.py new file mode 100644 index 0000000000..36553258db --- /dev/null +++ b/awx/main/tests/functional/api/test_workflow_job.py @@ -0,0 +1,54 @@ +import pytest + + +from awx.api.versioning import reverse + + +@pytest.mark.django_db +@pytest.mark.parametrize( + "is_admin, status", + [ + [True, 201], + [False, 403], + ], # if they're a WFJ admin, they get a 201 # if they're not a WFJ *nor* org admin, they get a 403 +) +def test_workflow_job_relaunch(workflow_job, post, admin_user, alice, is_admin, status): + url = reverse("api:workflow_job_relaunch", kwargs={'pk': workflow_job.pk}) + if is_admin: + post(url, user=admin_user, expect=status) + else: + post(url, user=alice, expect=status) + + +@pytest.mark.django_db +def test_workflow_job_relaunch_failure(workflow_job, post, admin_user): + workflow_job.is_sliced_job = True + workflow_job.job_template = None + workflow_job.save() + url = reverse("api:workflow_job_relaunch", kwargs={'pk': workflow_job.pk}) + post(url, user=admin_user, expect=400) + + +@pytest.mark.django_db +def test_workflow_job_relaunch_not_inventory_failure(workflow_job, post, admin_user): + workflow_job.is_sliced_job = True + workflow_job.inventory = None + workflow_job.save() + url = reverse("api:workflow_job_relaunch", kwargs={'pk': workflow_job.pk}) + post(url, user=admin_user, expect=400) + + +@pytest.mark.django_db +@pytest.mark.parametrize( + "is_admin, status", + [ + [True, 202], + [False, 403], + ], # if they're a WFJ admin, they get a 202 # if they're not a WFJ *nor* org admin, they get a 403 +) +def test_workflow_job_cancel(workflow_job, post, admin_user, alice, is_admin, status): + url = reverse("api:workflow_job_cancel", kwargs={'pk': workflow_job.pk}) + if is_admin: + post(url, user=admin_user, expect=status) + else: + post(url, user=alice, expect=status) diff --git a/awx/main/tests/functional/conftest.py b/awx/main/tests/functional/conftest.py index e1284ce87c..3ce22d9f96 100644 --- a/awx/main/tests/functional/conftest.py +++ b/awx/main/tests/functional/conftest.py @@ -39,7 +39,7 @@ from awx.main.models.events import ( InventoryUpdateEvent, SystemJobEvent, ) -from awx.main.models.workflow import WorkflowJobTemplate +from awx.main.models.workflow import WorkflowJobTemplate, WorkflowJob from awx.main.models.ad_hoc_commands import AdHocCommand from awx.main.models.oauth import OAuth2Application as Application from awx.main.models.execution_environments import ExecutionEnvironment @@ -743,6 +743,30 @@ def system_job_factory(system_job_template, admin): return factory +@pytest.fixture +def wfjt(workflow_job_template_factory, organization): + objects = workflow_job_template_factory('test_workflow', organization=organization, persisted=True) + return objects.workflow_job_template + + +@pytest.fixture +def wfjt_with_nodes(workflow_job_template_factory, organization, job_template): + objects = workflow_job_template_factory( + 'test_workflow', organization=organization, workflow_job_template_nodes=[{'unified_job_template': job_template}], persisted=True + ) + return objects.workflow_job_template + + +@pytest.fixture +def wfjt_node(wfjt_with_nodes): + return wfjt_with_nodes.workflow_job_template_nodes.all()[0] + + +@pytest.fixture +def workflow_job(wfjt): + return wfjt.workflow_jobs.create(name='test_workflow') + + def dumps(value): return DjangoJSONEncoder().encode(value) diff --git a/awx/main/tests/functional/test_copy.py b/awx/main/tests/functional/test_copy.py index 0574f9ccbd..001ebfbc74 100644 --- a/awx/main/tests/functional/test_copy.py +++ b/awx/main/tests/functional/test_copy.py @@ -123,6 +123,24 @@ def test_inventory_copy(inventory, group_factory, post, get, alice, organization assert set(group_2_2_copy.hosts.all()) == set() +@pytest.mark.django_db +@pytest.mark.parametrize( + "is_admin, can_copy, status", + [ + [True, True, 200], + [False, False, 200], + ], +) +def test_workflow_job_template_copy_access(get, admin_user, alice, workflow_job_template, is_admin, can_copy, status): + url = reverse('api:workflow_job_template_copy', kwargs={'pk': workflow_job_template.pk}) + if is_admin: + response = get(url, user=admin_user, expect=status) + else: + workflow_job_template.organization.auditor_role.members.add(alice) + response = get(url, user=alice, expect=status) + assert response.data['can_copy'] == can_copy + + @pytest.mark.django_db def test_workflow_job_template_copy(workflow_job_template, post, get, admin, organization): ''' diff --git a/awx/main/tests/functional/test_notifications.py b/awx/main/tests/functional/test_notifications.py index 08036db97c..4a5d2e8387 100644 --- a/awx/main/tests/functional/test_notifications.py +++ b/awx/main/tests/functional/test_notifications.py @@ -1,5 +1,6 @@ from unittest import mock import pytest +import json from requests.adapters import HTTPAdapter from requests.utils import select_proxy @@ -218,3 +219,31 @@ def test_webhook_notification_pointed_to_a_redirect_launch_endpoint(post, admin, ) assert n1.send("", n1.messages.get("success").get("body")) == 1 + + +@pytest.mark.django_db +def test_update_notification_template(admin, notification_template): + notification_template.messages['workflow_approval'] = { + "running": { + "message": None, + "body": None, + } + } + notification_template.save() + + workflow_approval_message = { + "approved": { + "message": None, + "body": None, + }, + "running": { + "message": "test-message", + "body": None, + }, + } + notification_template.messages['workflow_approval'] = workflow_approval_message + notification_template.save() + + subevents = sorted(notification_template.messages["workflow_approval"].keys()) + assert subevents == ["approved", "running"] + assert notification_template.messages['workflow_approval'] == workflow_approval_message diff --git a/awx/main/tests/functional/test_rbac_workflow.py b/awx/main/tests/functional/test_rbac_workflow.py index c2022783ac..0143399ba6 100644 --- a/awx/main/tests/functional/test_rbac_workflow.py +++ b/awx/main/tests/functional/test_rbac_workflow.py @@ -13,30 +13,6 @@ from rest_framework.exceptions import PermissionDenied from awx.main.models import InventorySource, JobLaunchConfig -@pytest.fixture -def wfjt(workflow_job_template_factory, organization): - objects = workflow_job_template_factory('test_workflow', organization=organization, persisted=True) - return objects.workflow_job_template - - -@pytest.fixture -def wfjt_with_nodes(workflow_job_template_factory, organization, job_template): - objects = workflow_job_template_factory( - 'test_workflow', organization=organization, workflow_job_template_nodes=[{'unified_job_template': job_template}], persisted=True - ) - return objects.workflow_job_template - - -@pytest.fixture -def wfjt_node(wfjt_with_nodes): - return wfjt_with_nodes.workflow_job_template_nodes.all()[0] - - -@pytest.fixture -def workflow_job(wfjt): - return wfjt.workflow_jobs.create(name='test_workflow') - - @pytest.mark.django_db class TestWorkflowJobTemplateAccess: def test_random_user_no_edit(self, wfjt, rando): From d32a5905e8edb4d1dd8def5bb9226b9ce9fcb060 Mon Sep 17 00:00:00 2001 From: Alexander Komarov Date: Fri, 30 Jul 2021 14:28:27 +0500 Subject: [PATCH 12/26] Remove unused imports --- awx/main/tests/functional/conftest.py | 2 +- awx/main/tests/functional/test_notifications.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/awx/main/tests/functional/conftest.py b/awx/main/tests/functional/conftest.py index 3ce22d9f96..c87f0a6c1a 100644 --- a/awx/main/tests/functional/conftest.py +++ b/awx/main/tests/functional/conftest.py @@ -39,7 +39,7 @@ from awx.main.models.events import ( InventoryUpdateEvent, SystemJobEvent, ) -from awx.main.models.workflow import WorkflowJobTemplate, WorkflowJob +from awx.main.models.workflow import WorkflowJobTemplate from awx.main.models.ad_hoc_commands import AdHocCommand from awx.main.models.oauth import OAuth2Application as Application from awx.main.models.execution_environments import ExecutionEnvironment diff --git a/awx/main/tests/functional/test_notifications.py b/awx/main/tests/functional/test_notifications.py index 4a5d2e8387..092c970539 100644 --- a/awx/main/tests/functional/test_notifications.py +++ b/awx/main/tests/functional/test_notifications.py @@ -1,6 +1,5 @@ from unittest import mock import pytest -import json from requests.adapters import HTTPAdapter from requests.utils import select_proxy From 3cd5d59d8710a5f082647fd8e8256748116bffa4 Mon Sep 17 00:00:00 2001 From: Alan Rominger Date: Tue, 11 Apr 2023 17:04:59 -0400 Subject: [PATCH 13/26] Get rid of 1 perpetually unused connection in our app --- awx/main/management/commands/run_wsrelay.py | 1 + 1 file changed, 1 insertion(+) diff --git a/awx/main/management/commands/run_wsrelay.py b/awx/main/management/commands/run_wsrelay.py index 8bdf6ea0a3..90edafdfc5 100644 --- a/awx/main/management/commands/run_wsrelay.py +++ b/awx/main/management/commands/run_wsrelay.py @@ -98,6 +98,7 @@ class Command(BaseCommand): try: executor = MigrationExecutor(connection) migrating = bool(executor.migration_plan(executor.loader.graph.leaf_nodes())) + connection.close() # Because of async nature, main loop will use new connection, so close this except Exception as exc: logger.warning(f'Error on startup of run_wsrelay (error: {exc}), retry in 10s...') time.sleep(10) From 1ea6d15ee31f521bd373c9512f9ed70173ae1ff7 Mon Sep 17 00:00:00 2001 From: Alan Rominger Date: Tue, 11 Apr 2023 17:05:41 -0400 Subject: [PATCH 14/26] Add run-clear-cache to tower-processes for auto-reload --- tools/docker-compose/supervisor.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/docker-compose/supervisor.conf b/tools/docker-compose/supervisor.conf index e8bb858910..341fe9fab7 100644 --- a/tools/docker-compose/supervisor.conf +++ b/tools/docker-compose/supervisor.conf @@ -101,7 +101,7 @@ stdout_events_enabled = true stderr_events_enabled = true [group:tower-processes] -programs=awx-dispatcher,awx-receiver,awx-uwsgi,awx-daphne,awx-nginx,awx-wsrelay,awx-rsyslogd,awx-heartbeet, awx-rsyslog-configurer +programs=awx-dispatcher,awx-receiver,awx-uwsgi,awx-daphne,awx-nginx,awx-wsrelay,awx-rsyslogd,awx-heartbeet,awx-rsyslog-configurer,awx-cache-clear priority=5 [program:awx-autoreload] From 0046ce5e694359796f78337bfa182f0ce88e5b8a Mon Sep 17 00:00:00 2001 From: Martin Slemr Date: Tue, 11 Apr 2023 13:39:54 +0200 Subject: [PATCH 15/26] Analytics API: Permissions for System Auditor --- awx/api/permissions.py | 14 ++++++++++++++ awx/api/views/analytics.py | 7 +++---- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/awx/api/permissions.py b/awx/api/permissions.py index c088f166d0..ff7a030c72 100644 --- a/awx/api/permissions.py +++ b/awx/api/permissions.py @@ -25,6 +25,7 @@ __all__ = [ 'UserPermission', 'IsSystemAdminOrAuditor', 'WorkflowApprovalPermission', + 'AnalyticsPermission', ] @@ -250,3 +251,16 @@ class IsSystemAdminOrAuditor(permissions.BasePermission): class WebhookKeyPermission(permissions.BasePermission): def has_object_permission(self, request, view, obj): return request.user.can_access(view.model, 'admin', obj, request.data) + + +class AnalyticsPermission(permissions.BasePermission): + """ + Allows GET/POST/OPTIONS to system admins and system auditors. + """ + + def has_permission(self, request, view): + if not (request.user and request.user.is_authenticated): + return False + if request.method in ["GET", "POST", "OPTIONS"]: + return request.user.is_superuser or request.user.is_system_auditor + return request.user.is_superuser diff --git a/awx/api/views/analytics.py b/awx/api/views/analytics.py index e7c50ad5b9..9f6066084f 100644 --- a/awx/api/views/analytics.py +++ b/awx/api/views/analytics.py @@ -7,10 +7,9 @@ from django.utils.translation import gettext_lazy as _ from django.utils import translation from awx.api.generics import APIView, Response -from awx.api.permissions import IsSystemAdminOrAuditor +from awx.api.permissions import AnalyticsPermission from awx.api.versioning import reverse from awx.main.utils import get_awx_version -from rest_framework.permissions import AllowAny from rest_framework import status from collections import OrderedDict @@ -43,7 +42,7 @@ class GetNotAllowedMixin(object): class AnalyticsRootView(APIView): - permission_classes = (AllowAny,) + permission_classes = (AnalyticsPermission,) name = _('Automation Analytics') swagger_topic = 'Automation Analytics' @@ -99,7 +98,7 @@ class AnalyticsGenericView(APIView): return Response(response.json(), status=response.status_code) """ - permission_classes = (IsSystemAdminOrAuditor,) + permission_classes = (AnalyticsPermission,) @staticmethod def _request_headers(request): From 9b716235a2898b8a255779006f8b2faf5687251c Mon Sep 17 00:00:00 2001 From: sean-m-sullivan Date: Thu, 6 Apr 2023 00:52:37 -0400 Subject: [PATCH 16/26] update credential list examples in awx collection --- awx_collection/plugins/modules/bulk_job_launch.py | 3 +++ awx_collection/plugins/modules/job_launch.py | 4 +++- awx_collection/plugins/modules/job_template.py | 1 + awx_collection/plugins/modules/workflow_job_template.py | 4 +++- 4 files changed, 10 insertions(+), 2 deletions(-) diff --git a/awx_collection/plugins/modules/bulk_job_launch.py b/awx_collection/plugins/modules/bulk_job_launch.py index 8aa5fca254..94b5415e33 100644 --- a/awx_collection/plugins/modules/bulk_job_launch.py +++ b/awx_collection/plugins/modules/bulk_job_launch.py @@ -186,6 +186,9 @@ EXAMPLES = ''' food: carrot color: orange limit: bar + credentials: + - "My Credential" + - "suplementary cred" extra_vars: # these override / extend extra_data at the job level food: grape animal: owl diff --git a/awx_collection/plugins/modules/job_launch.py b/awx_collection/plugins/modules/job_launch.py index 9a76f3a8b7..20f4fa7f73 100644 --- a/awx_collection/plugins/modules/job_launch.py +++ b/awx_collection/plugins/modules/job_launch.py @@ -151,7 +151,9 @@ EXAMPLES = ''' job_launch: job_template: "My Job Template" inventory: "My Inventory" - credential: "My Credential" + credentials: + - "My Credential" + - "suplementary cred" register: job - name: Wait for job max 120s job_wait: diff --git a/awx_collection/plugins/modules/job_template.py b/awx_collection/plugins/modules/job_template.py index 4508bc18d5..ef4b2d8aca 100644 --- a/awx_collection/plugins/modules/job_template.py +++ b/awx_collection/plugins/modules/job_template.py @@ -337,6 +337,7 @@ EXAMPLES = ''' playbook: "ping.yml" credentials: - "Local" + - "2nd credential" state: "present" controller_config_file: "~/tower_cli.cfg" survey_enabled: yes diff --git a/awx_collection/plugins/modules/workflow_job_template.py b/awx_collection/plugins/modules/workflow_job_template.py index 19954877b7..db9c646fda 100644 --- a/awx_collection/plugins/modules/workflow_job_template.py +++ b/awx_collection/plugins/modules/workflow_job_template.py @@ -461,7 +461,9 @@ EXAMPLES = ''' failure_nodes: - identifier: node201 always_nodes: [] - credentials: [] + credentials: + - local_cred + - suplementary cred - identifier: node201 unified_job_template: organization: From 0f4bac7aedce25f1cdf80acd2eaf85e679cd6863 Mon Sep 17 00:00:00 2001 From: Steffen Scheib Date: Mon, 20 Mar 2023 13:43:12 +0100 Subject: [PATCH 17/26] Add missing filtering mechanism for the Thycotic Devops Vault credential lookup --- awx/main/credential_plugins/dsv.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/awx/main/credential_plugins/dsv.py b/awx/main/credential_plugins/dsv.py index 9c89199710..78d5e6a4f0 100644 --- a/awx/main/credential_plugins/dsv.py +++ b/awx/main/credential_plugins/dsv.py @@ -35,8 +35,14 @@ dsv_inputs = { 'type': 'string', 'help_text': _('The secret path e.g. /test/secret1'), }, + { + 'id': 'secret_field', + 'label': _('Secret Field'), + 'help_text': _('The field to extract from the secret'), + 'type': 'string', + }, ], - 'required': ['tenant', 'client_id', 'client_secret', 'path'], + 'required': ['tenant', 'client_id', 'client_secret', 'path', 'secret_field'], } if settings.DEBUG: @@ -52,5 +58,5 @@ if settings.DEBUG: dsv_plugin = CredentialPlugin( 'Thycotic DevOps Secrets Vault', dsv_inputs, - lambda **kwargs: SecretsVault(**{k: v for (k, v) in kwargs.items() if k in [field['id'] for field in dsv_inputs['fields']]}).get_secret(kwargs['path']), + lambda **kwargs: SecretsVault(**{k: v for (k, v) in kwargs.items() if k in [field['id'] for field in dsv_inputs['fields']]}).get_secret(kwargs['path'])['data'][kwargs['secret_field']], ) From 2f68317e5fc32d331f11c04d6b84d1009a69c532 Mon Sep 17 00:00:00 2001 From: Steffen Scheib Date: Thu, 30 Mar 2023 17:03:55 +0200 Subject: [PATCH 18/26] Fixing api-lint error --- awx/main/credential_plugins/dsv.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/main/credential_plugins/dsv.py b/awx/main/credential_plugins/dsv.py index 78d5e6a4f0..7ebbe60401 100644 --- a/awx/main/credential_plugins/dsv.py +++ b/awx/main/credential_plugins/dsv.py @@ -58,5 +58,5 @@ if settings.DEBUG: dsv_plugin = CredentialPlugin( 'Thycotic DevOps Secrets Vault', dsv_inputs, - lambda **kwargs: SecretsVault(**{k: v for (k, v) in kwargs.items() if k in [field['id'] for field in dsv_inputs['fields']]}).get_secret(kwargs['path'])['data'][kwargs['secret_field']], + lambda **kwargs: SecretsVault(**{k: v for (k, v) in kwargs.items() if k in [field['id'] for field in dsv_inputs['fields']]}).get_secret(kwargs['path'])['data'][kwargs['secret_field']], # fmt: skip ) From fba4e06c50f995b48c9a6f03934cc02078efa92b Mon Sep 17 00:00:00 2001 From: John Westcott IV <32551173+john-westcott-iv@users.noreply.github.com> Date: Thu, 13 Apr 2023 09:02:52 -0400 Subject: [PATCH 19/26] Adding basic validation for local passwords (#13789) * Adding basic validation for local passwords * Adding edit screen * Fixing tests --- awx/api/serializers.py | 18 + .../tests/functional/api/test_serializers.py | 75 +++ awx/sso/conf.py | 44 ++ .../MiscAuthenticationEdit.js | 30 +- .../MiscAuthenticationEdit.test.js | 4 + .../shared/data.allSettingOptions.json | 515 +++++++++----- .../Setting/shared/data.allSettings.json | 630 ++++++++++-------- 7 files changed, 885 insertions(+), 431 deletions(-) create mode 100644 awx/main/tests/functional/api/test_serializers.py diff --git a/awx/api/serializers.py b/awx/api/serializers.py index 4b3a62c841..13228331e0 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -995,6 +995,24 @@ class UserSerializer(BaseSerializer): django_validate_password(value) if not self.instance and value in (None, ''): raise serializers.ValidationError(_('Password required for new User.')) + + # Check if a password is too long + password_max_length = User._meta.get_field('password').max_length + if len(value) > password_max_length: + raise serializers.ValidationError(_('Password max length is {}'.format(password_max_length))) + if getattr(settings, 'LOCAL_PASSWORD_MIN_LENGTH', 0) and len(value) < getattr(settings, 'LOCAL_PASSWORD_MIN_LENGTH'): + raise serializers.ValidationError(_('Password must be at least {} characters long.'.format(getattr(settings, 'LOCAL_PASSWORD_MIN_LENGTH')))) + if getattr(settings, 'LOCAL_PASSWORD_MIN_DIGITS', 0) and sum(c.isdigit() for c in value) < getattr(settings, 'LOCAL_PASSWORD_MIN_DIGITS'): + raise serializers.ValidationError(_('Password must contain at least {} digits.'.format(getattr(settings, 'LOCAL_PASSWORD_MIN_DIGITS')))) + if getattr(settings, 'LOCAL_PASSWORD_MIN_UPPER', 0) and sum(c.isupper() for c in value) < getattr(settings, 'LOCAL_PASSWORD_MIN_UPPER'): + raise serializers.ValidationError( + _('Password must contain at least {} uppercase characters.'.format(getattr(settings, 'LOCAL_PASSWORD_MIN_UPPER'))) + ) + if getattr(settings, 'LOCAL_PASSWORD_MIN_SPECIAL', 0) and sum(not c.isalnum() for c in value) < getattr(settings, 'LOCAL_PASSWORD_MIN_SPECIAL'): + raise serializers.ValidationError( + _('Password must contain at least {} special characters.'.format(getattr(settings, 'LOCAL_PASSWORD_MIN_SPECIAL'))) + ) + return value def _update_password(self, obj, new_password): diff --git a/awx/main/tests/functional/api/test_serializers.py b/awx/main/tests/functional/api/test_serializers.py new file mode 100644 index 0000000000..ab31e186e9 --- /dev/null +++ b/awx/main/tests/functional/api/test_serializers.py @@ -0,0 +1,75 @@ +import pytest + +from django.test.utils import override_settings + +from rest_framework.serializers import ValidationError + +from awx.api.serializers import UserSerializer +from django.contrib.auth.models import User + + +@pytest.mark.parametrize( + "password,min_length,min_digits,min_upper,min_special,expect_error", + [ + # Test length + ("a", 1, 0, 0, 0, False), + ("a", 2, 0, 0, 0, True), + ("aa", 2, 0, 0, 0, False), + ("aaabcDEF123$%^", 2, 0, 0, 0, False), + # Test digits + ("a", 0, 1, 0, 0, True), + ("1", 0, 1, 0, 0, False), + ("1", 0, 2, 0, 0, True), + ("12", 0, 2, 0, 0, False), + ("12abcDEF123$%^", 0, 2, 0, 0, False), + # Test upper + ("a", 0, 0, 1, 0, True), + ("A", 0, 0, 1, 0, False), + ("A", 0, 0, 2, 0, True), + ("AB", 0, 0, 2, 0, False), + ("ABabcDEF123$%^", 0, 0, 2, 0, False), + # Test special + ("a", 0, 0, 0, 1, True), + ("!", 0, 0, 0, 1, False), + ("!", 0, 0, 0, 2, True), + ("!@", 0, 0, 0, 2, False), + ("!@abcDEF123$%^", 0, 0, 0, 2, False), + ], +) +@pytest.mark.django_db +def test_validate_password_rules(password, min_length, min_digits, min_upper, min_special, expect_error): + user_serializer = UserSerializer() + + # First test password with no params, this should always pass + try: + user_serializer.validate_password(password) + except ValidationError: + assert False, f"Password {password} should not have validation issue if no params are used" + + with override_settings( + LOCAL_PASSWORD_MIN_LENGTH=min_length, LOCAL_PASSWORD_MIN_DIGITS=min_digits, LOCAL_PASSWORD_MIN_UPPER=min_upper, LOCAL_PASSWORD_MIN_SPECIAL=min_special + ): + if expect_error: + with pytest.raises(ValidationError): + user_serializer.validate_password(password) + else: + try: + user_serializer.validate_password(password) + except ValidationError: + assert False, "validate_password raised an unexpected exception" + + +@pytest.mark.django_db +def test_validate_password_too_long(): + password_max_length = User._meta.get_field('password').max_length + password = "x" * password_max_length + + user_serializer = UserSerializer() + try: + user_serializer.validate_password(password) + except ValidationError: + assert False, f"Password {password} should not have validation" + + password = f"{password}x" + with pytest.raises(ValidationError): + user_serializer.validate_password(password) diff --git a/awx/sso/conf.py b/awx/sso/conf.py index ddfd80fd13..3cae57311c 100644 --- a/awx/sso/conf.py +++ b/awx/sso/conf.py @@ -1603,6 +1603,50 @@ register( ], ) +register( + 'LOCAL_PASSWORD_MIN_LENGTH', + field_class=fields.IntegerField, + min_value=0, + default=0, + label=_('Minimum number of characters in local password'), + help_text=_('Minimum number of characters required in a local password. 0 means no minimum'), + category=_('Authentication'), + category_slug='authentication', +) + +register( + 'LOCAL_PASSWORD_MIN_DIGITS', + field_class=fields.IntegerField, + min_value=0, + default=0, + label=_('Minimum number of digit characters in local password'), + help_text=_('Minimum number of digit characters required in a local password. 0 means no minimum'), + category=_('Authentication'), + category_slug='authentication', +) + +register( + 'LOCAL_PASSWORD_MIN_UPPER', + field_class=fields.IntegerField, + min_value=0, + default=0, + label=_('Minimum number of uppercase characters in local password'), + help_text=_('Minimum number of uppercase characters required in a local password. 0 means no minimum'), + category=_('Authentication'), + category_slug='authentication', +) + +register( + 'LOCAL_PASSWORD_MIN_SPECIAL', + field_class=fields.IntegerField, + min_value=0, + default=0, + label=_('Minimum number of special characters in local password'), + help_text=_('Minimum number of special characters required in a local password. 0 means no minimum'), + category=_('Authentication'), + category_slug='authentication', +) + def tacacs_validate(serializer, attrs): if not serializer.instance or not hasattr(serializer.instance, 'TACACSPLUS_HOST') or not hasattr(serializer.instance, 'TACACSPLUS_SECRET'): diff --git a/awx/ui/src/screens/Setting/MiscAuthentication/MiscAuthenticationEdit/MiscAuthenticationEdit.js b/awx/ui/src/screens/Setting/MiscAuthentication/MiscAuthenticationEdit/MiscAuthenticationEdit.js index a8b7814543..97240efdbc 100644 --- a/awx/ui/src/screens/Setting/MiscAuthentication/MiscAuthenticationEdit/MiscAuthenticationEdit.js +++ b/awx/ui/src/screens/Setting/MiscAuthentication/MiscAuthenticationEdit/MiscAuthenticationEdit.js @@ -54,7 +54,11 @@ function MiscAuthenticationEdit() { 'SOCIAL_AUTH_ORGANIZATION_MAP', 'SOCIAL_AUTH_TEAM_MAP', 'SOCIAL_AUTH_USER_FIELDS', - 'SOCIAL_AUTH_USERNAME_IS_FULL_EMAIL' + 'SOCIAL_AUTH_USERNAME_IS_FULL_EMAIL', + 'LOCAL_PASSWORD_MIN_LENGTH', + 'LOCAL_PASSWORD_MIN_DIGITS', + 'LOCAL_PASSWORD_MIN_UPPER', + 'LOCAL_PASSWORD_MIN_SPECIAL' ); const authenticationData = { @@ -247,6 +251,30 @@ function MiscAuthenticationEdit() { name="SOCIAL_AUTH_USERNAME_IS_FULL_EMAIL" config={authentication.SOCIAL_AUTH_USERNAME_IS_FULL_EMAIL} /> + + + + {submitError && } {revertError && } diff --git a/awx/ui/src/screens/Setting/MiscAuthentication/MiscAuthenticationEdit/MiscAuthenticationEdit.test.js b/awx/ui/src/screens/Setting/MiscAuthentication/MiscAuthenticationEdit/MiscAuthenticationEdit.test.js index d84b759113..3e790a7544 100644 --- a/awx/ui/src/screens/Setting/MiscAuthentication/MiscAuthenticationEdit/MiscAuthenticationEdit.test.js +++ b/awx/ui/src/screens/Setting/MiscAuthentication/MiscAuthenticationEdit/MiscAuthenticationEdit.test.js @@ -33,6 +33,10 @@ const authenticationData = { SOCIAL_AUTH_TEAM_MAP: null, SOCIAL_AUTH_USER_FIELDS: null, SOCIAL_AUTH_USERNAME_IS_FULL_EMAIL: false, + LOCAL_PASSWORD_MIN_LENGTH: 0, + LOCAL_PASSWORD_MIN_DIGITS: 0, + LOCAL_PASSWORD_MIN_UPPER: 0, + LOCAL_PASSWORD_MIN_SPECIAL: 0, }; describe('', () => { diff --git a/awx/ui/src/screens/Setting/shared/data.allSettingOptions.json b/awx/ui/src/screens/Setting/shared/data.allSettingOptions.json index c68b11474e..fa397c07f5 100644 --- a/awx/ui/src/screens/Setting/shared/data.allSettingOptions.json +++ b/awx/ui/src/screens/Setting/shared/data.allSettingOptions.json @@ -204,7 +204,7 @@ "type": "list", "required": false, "label": "Paths to expose to isolated jobs", - "help_text": "List of paths that would otherwise be hidden to expose to isolated jobs. Enter one path per line.", + "help_text": "List of paths that would otherwise be hidden to expose to isolated jobs. Enter one path per line. Volumes will be mounted from the execution node to the container. The supported format is HOST-DIR[:CONTAINER-DIR[:OPTIONS]]. ", "category": "Jobs", "category_slug": "jobs", "default": [], @@ -231,26 +231,36 @@ "read_only": false } }, + "AWX_RUNNER_KEEPALIVE_SECONDS": { + "type": "integer", + "required": true, + "label": "K8S Ansible Runner Keep-Alive Message Interval", + "help_text": "Only applies to jobs running in a Container Group. If not 0, send a message every so-many seconds to keep connection open.", + "category": "Jobs", + "category_slug": "jobs", + "placeholder": 240, + "default": 0 + }, "GALAXY_TASK_ENV": { "type": "nested object", - "required": true, + "required": true, "label": "Environment Variables for Galaxy Commands", - "help_text": "Additional environment variables set for invocations of ansible-galaxy within project updates. Useful if you must use a proxy server for ansible-galaxy but not git.", - "category": "Jobs", - "category_slug": "jobs", - "placeholder": { - "HTTP_PROXY": "myproxy.local:8080" - }, - "default": { - "ANSIBLE_FORCE_COLOR": "false", - "GIT_SSH_COMMAND": "ssh -o StrictHostKeyChecking=no" + "help_text": "Additional environment variables set for invocations of ansible-galaxy within project updates. Useful if you must use a proxy server for ansible-galaxy but not git.", + "category": "Jobs", + "category_slug": "jobs", + "placeholder": { + "HTTP_PROXY": "myproxy.local:8080" }, - "child": { - "type": "string", - "required": true, - "read_only": false + "default": { + "ANSIBLE_FORCE_COLOR": "false", + "GIT_SSH_COMMAND": "ssh -o StrictHostKeyChecking=no" + }, + "child": { + "type": "string", + "required": true, + "read_only": false } - }, + }, "INSIGHTS_TRACKING_STATE": { "type": "boolean", "required": false, @@ -334,6 +344,16 @@ "category_slug": "jobs", "default": 1024 }, + "MAX_WEBSOCKET_EVENT_RATE": { + "type": "integer", + "required": false, + "label": "Job Event Maximum Websocket Messages Per Second", + "help_text": "Maximum number of messages to update the UI live job output with per second. Value of 0 means no limit.", + "min_value": 0, + "category": "Jobs", + "category_slug": "jobs", + "default": 30 + }, "SCHEDULE_MAX_JOBS": { "type": "integer", "required": true, @@ -344,16 +364,6 @@ "category_slug": "jobs", "default": 10 }, - "AWX_RUNNER_KEEPALIVE_SECONDS": { - "type": "integer", - "required": true, - "label": "K8S Ansible Runner Keep-Alive Message Interval", - "help_text": "Only applies to K8S deployments and container_group jobs. If not 0, send a message every so-many seconds to keep connection open.", - "category": "Jobs", - "category_slug": "jobs", - "placeholder": 240, - "default": 0 - }, "AWX_ANSIBLE_CALLBACK_PLUGINS": { "type": "list", "required": false, @@ -383,7 +393,7 @@ "type": "integer", "required": false, "label": "Default Job Idle Timeout", - "help_text": "If no output is detected from ansible in this number of seconds the execution will be terminated. Use value of 0 to used default idle_timeout is 600s.", + "help_text": "If no output is detected from ansible in this number of seconds the execution will be terminated. Use value of 0 to indicate that no idle timeout should be imposed.", "min_value": 0, "category": "Jobs", "category_slug": "jobs", @@ -489,10 +499,16 @@ "type": "list", "required": false, "label": "Loggers Sending Data to Log Aggregator Form", - "help_text": "List of loggers that will send HTTP logs to the collector, these can include any or all of: \nawx - service logs\nactivity_stream - activity stream records\njob_events - callback data from Ansible job events\nsystem_tracking - facts gathered from scan jobs.", + "help_text": "List of loggers that will send HTTP logs to the collector, these can include any or all of: \nawx - service logs\nactivity_stream - activity stream records\njob_events - callback data from Ansible job events\nsystem_tracking - facts gathered from scan jobs\nbroadcast_websocket - errors pertaining to websockets broadcast metrics\n", "category": "Logging", "category_slug": "logging", - "default": ["awx", "activity_stream", "job_events", "system_tracking"], + "default": [ + "awx", + "activity_stream", + "job_events", + "system_tracking", + "broadcast_websocket" + ], "child": { "type": "string", "required": true, @@ -639,15 +655,51 @@ "unit": "seconds", "default": 14400 }, + "BULK_JOB_MAX_LAUNCH": { + "type": "integer", + "required": false, + "label": "Max jobs to allow bulk jobs to launch", + "help_text": "Max jobs to allow bulk jobs to launch", + "category": "Bulk Actions", + "category_slug": "bulk", + "default": 100 + }, + "BULK_HOST_MAX_CREATE": { + "type": "integer", + "required": false, + "label": "Max number of hosts to allow to be created in a single bulk action", + "help_text": "Max number of hosts to allow to be created in a single bulk action", + "category": "Bulk Actions", + "category_slug": "bulk", + "default": 100 + }, "UI_NEXT": { "type": "boolean", "required": false, "label": "Enable Preview of New User Interface", - "help_text": "'Enable preview of new user interface.", + "help_text": "Enable preview of new user interface.", "category": "System", "category_slug": "system", "default": true }, + "SUBSCRIPTION_USAGE_MODEL": { + "type": "choice", + "required": false, + "label": "Defines subscription usage model and shows Host Metrics", + "category": "System", + "category_slug": "system", + "default": "", + "choices": [ + [ + "", + "Default model for AWX - no subscription. Deletion of host_metrics will not be considered for purposes of managed host counting" + ], + [ + "unique_managed_hosts", + "Usage based on unique managed nodes in a large historical time frame and delete functionality for no longer used managed nodes" + ] + ] + }, "SESSION_COOKIE_AGE": { "type": "integer", "required": true, @@ -740,6 +792,15 @@ ["detailed", "Detailed"] ] }, + "ALLOW_METRICS_FOR_ANONYMOUS_USERS": { + "type": "boolean", + "required": false, + "label": "Allow anonymous users to poll metrics", + "help_text": "If true, anonymous users are allowed to poll metrics.", + "category": "Authentication", + "category_slug": "authentication", + "default": false + }, "CUSTOM_LOGIN_INFO": { "type": "string", "required": false, @@ -782,7 +843,7 @@ "type": "nested object", "required": false, "label": "Social Auth Organization Map", - "help_text": "Mapping to organization admins/users from social auth accounts. This setting\ncontrols which users are placed into which organizations based on their\nusername and email address. Configuration details are available in the \ndocumentation.", + "help_text": "Mapping to organization admins/users from social auth accounts. This setting\ncontrols which users are placed into which organizations based on their\nusername and email address. Configuration details are available in the\ndocumentation.", "category": "Authentication", "category_slug": "authentication", "placeholder": { @@ -868,39 +929,6 @@ "category_slug": "authentication", "default": false }, - "SOCIAL_AUTH_OIDC_KEY": { - "type": "string", - "label": "OIDC Key", - "help_text": "The OIDC key (Client ID) from your IDP.", - "category": "Generic OIDC", - "category_slug": "oidc", - "default": "" - }, - "SOCIAL_AUTH_OIDC_SECRET": { - "type": "string", - "label": "OIDC Secret", - "help_text": "The OIDC secret (Client Secret) from your IDP.", - "category": "Generic OIDC", - "category_slug": "oidc", - "default": "" - }, - "SOCIAL_AUTH_OIDC_OIDC_ENDPOINT": { - "type": "string", - "label": "OIDC Provider URL", - "help_text": "The URL for your OIDC provider, e.g.: http(s)://hostname/.", - "category": "Generic OIDC", - "category_slug": "oidc", - "default": "" - }, - "SOCIAL_AUTH_OIDC_VERIFY_SSL": { - "type": "boolean", - "required": false, - "label": "Verify OIDC Provider Certificate", - "help_text": "Verify the OIDC provider ssl certificate.", - "category": "Generic OIDC", - "category_slug": "oidc", - "default": true - }, "AUTH_LDAP_SERVER_URI": { "type": "string", "required": false, @@ -2726,7 +2754,7 @@ "type": "nested object", "required": false, "label": "Google OAuth2 Organization Map", - "help_text": "Mapping to organization admins/users from social auth accounts. This setting\ncontrols which users are placed into which organizations based on their\nusername and email address. Configuration details are available in the \ndocumentation.", + "help_text": "Mapping to organization admins/users from social auth accounts. This setting\ncontrols which users are placed into which organizations based on their\nusername and email address. Configuration details are available in the\ndocumentation.", "category": "Google OAuth2", "category_slug": "google-oauth2", "placeholder": { @@ -2810,7 +2838,7 @@ "type": "nested object", "required": false, "label": "GitHub OAuth2 Organization Map", - "help_text": "Mapping to organization admins/users from social auth accounts. This setting\ncontrols which users are placed into which organizations based on their\nusername and email address. Configuration details are available in the \ndocumentation.", + "help_text": "Mapping to organization admins/users from social auth accounts. This setting\ncontrols which users are placed into which organizations based on their\nusername and email address. Configuration details are available in the\ndocumentation.", "category": "GitHub OAuth2", "category_slug": "github", "placeholder": { @@ -2903,7 +2931,7 @@ "type": "nested object", "required": false, "label": "GitHub Organization OAuth2 Organization Map", - "help_text": "Mapping to organization admins/users from social auth accounts. This setting\ncontrols which users are placed into which organizations based on their\nusername and email address. Configuration details are available in the \ndocumentation.", + "help_text": "Mapping to organization admins/users from social auth accounts. This setting\ncontrols which users are placed into which organizations based on their\nusername and email address. Configuration details are available in the\ndocumentation.", "category": "GitHub Organization OAuth2", "category_slug": "github-org", "placeholder": { @@ -2996,7 +3024,7 @@ "type": "nested object", "required": false, "label": "GitHub Team OAuth2 Organization Map", - "help_text": "Mapping to organization admins/users from social auth accounts. This setting\ncontrols which users are placed into which organizations based on their\nusername and email address. Configuration details are available in the \ndocumentation.", + "help_text": "Mapping to organization admins/users from social auth accounts. This setting\ncontrols which users are placed into which organizations based on their\nusername and email address. Configuration details are available in the\ndocumentation.", "category": "GitHub Team OAuth2", "category_slug": "github-team", "placeholder": { @@ -3098,7 +3126,7 @@ "type": "nested object", "required": false, "label": "GitHub Enterprise OAuth2 Organization Map", - "help_text": "Mapping to organization admins/users from social auth accounts. This setting\ncontrols which users are placed into which organizations based on their\nusername and email address. Configuration details are available in the \ndocumentation.", + "help_text": "Mapping to organization admins/users from social auth accounts. This setting\ncontrols which users are placed into which organizations based on their\nusername and email address. Configuration details are available in the\ndocumentation.", "category": "GitHub Enterprise OAuth2", "category_slug": "github-enterprise", "placeholder": { @@ -3209,7 +3237,7 @@ "type": "nested object", "required": false, "label": "GitHub Enterprise Organization OAuth2 Organization Map", - "help_text": "Mapping to organization admins/users from social auth accounts. This setting\ncontrols which users are placed into which organizations based on their\nusername and email address. Configuration details are available in the \ndocumentation.", + "help_text": "Mapping to organization admins/users from social auth accounts. This setting\ncontrols which users are placed into which organizations based on their\nusername and email address. Configuration details are available in the\ndocumentation.", "category": "GitHub Enterprise Organization OAuth2", "category_slug": "github-enterprise-org", "placeholder": { @@ -3320,7 +3348,7 @@ "type": "nested object", "required": false, "label": "GitHub Enterprise Team OAuth2 Organization Map", - "help_text": "Mapping to organization admins/users from social auth accounts. This setting\ncontrols which users are placed into which organizations based on their\nusername and email address. Configuration details are available in the \ndocumentation.", + "help_text": "Mapping to organization admins/users from social auth accounts. This setting\ncontrols which users are placed into which organizations based on their\nusername and email address. Configuration details are available in the\ndocumentation.", "category": "GitHub Enterprise Team OAuth2", "category_slug": "github-enterprise-team", "placeholder": { @@ -3404,7 +3432,7 @@ "type": "nested object", "required": false, "label": "Azure AD OAuth2 Organization Map", - "help_text": "Mapping to organization admins/users from social auth accounts. This setting\ncontrols which users are placed into which organizations based on their\nusername and email address. Configuration details are available in the \ndocumentation.", + "help_text": "Mapping to organization admins/users from social auth accounts. This setting\ncontrols which users are placed into which organizations based on their\nusername and email address. Configuration details are available in the\ndocumentation.", "category": "Azure AD OAuth2", "category_slug": "azuread-oauth2", "placeholder": { @@ -3466,6 +3494,42 @@ } } }, + "SOCIAL_AUTH_OIDC_KEY": { + "type": "string", + "required": false, + "label": "OIDC Key", + "help_text": "The OIDC key (Client ID) from your IDP.", + "category": "Generic OIDC", + "category_slug": "oidc", + "default": null + }, + "SOCIAL_AUTH_OIDC_SECRET": { + "type": "string", + "required": false, + "label": "OIDC Secret", + "help_text": "The OIDC secret (Client Secret) from your IDP.", + "category": "Generic OIDC", + "category_slug": "oidc", + "default": "" + }, + "SOCIAL_AUTH_OIDC_OIDC_ENDPOINT": { + "type": "string", + "required": false, + "label": "OIDC Provider URL", + "help_text": "The URL for your OIDC provider including the path up to /.well-known/openid-configuration", + "category": "Generic OIDC", + "category_slug": "oidc", + "default": "" + }, + "SOCIAL_AUTH_OIDC_VERIFY_SSL": { + "type": "boolean", + "required": false, + "label": "Verify OIDC Provider Certificate", + "help_text": "Verify the OIDC provider ssl certificate.", + "category": "Generic OIDC", + "category_slug": "oidc", + "default": true + }, "SAML_AUTO_CREATE_OBJECTS": { "type": "boolean", "required": false, @@ -3678,7 +3742,7 @@ "type": "nested object", "required": false, "label": "SAML Organization Map", - "help_text": "Mapping to organization admins/users from social auth accounts. This setting\ncontrols which users are placed into which organizations based on their\nusername and email address. Configuration details are available in the \ndocumentation.", + "help_text": "Mapping to organization admins/users from social auth accounts. This setting\ncontrols which users are placed into which organizations based on their\nusername and email address. Configuration details are available in the\ndocumentation.", "category": "SAML", "category_slug": "saml", "placeholder": { @@ -3813,20 +3877,62 @@ "help_text": "Used to map super users and system auditors from SAML.", "category": "SAML", "category_slug": "saml", - "placeholder": { - "is_superuser_attr": "saml_attr", - "is_superuser_value": "value", - "is_superuser_role": "saml_role", - "is_system_auditor_attr": "saml_attr", - "is_system_auditor_value": "value", - "is_system_auditor_role": "saml_role" - }, + "placeholder": [ + ["is_superuser_attr", "saml_attr"], + ["is_superuser_value", ["value"]], + ["is_superuser_role", ["saml_role"]], + ["remove_superusers", true], + ["is_system_auditor_attr", "saml_attr"], + ["is_system_auditor_value", ["value"]], + ["is_system_auditor_role", ["saml_role"]], + ["remove_system_auditors", true] + ], "default": {}, "child": { "type": "field", "required": true, "read_only": false } + }, + "LOCAL_PASSWORD_MIN_LENGTH": { + "type": "integer", + "required": false, + "label": "Minimum number of characters in local password", + "help_text": "Minimum number of characters required in a local password. 0 means no minimum", + "min_value": 0, + "category": "Authentication", + "category_slug": "authentication", + "default": 0 + }, + "LOCAL_PASSWORD_MIN_DIGITS": { + "type": "integer", + "required": false, + "label": "Minimum number of digit characters in local password", + "help_text": "Minimum number of digit characters required in a local password. 0 means no minimum", + "min_value": 0, + "category": "Authentication", + "category_slug": "authentication", + "default": 0 + }, + "LOCAL_PASSWORD_MIN_UPPER": { + "type": "integer", + "required": false, + "label": "Minimum number of uppercase characters in local password", + "help_text": "Minimum number of uppercase characters required in a local password. 0 means no minimum", + "min_value": 0, + "category": "Authentication", + "category_slug": "authentication", + "default": 0 + }, + "LOCAL_PASSWORD_MIN_SPECIAL": { + "type": "integer", + "required": false, + "label": "Minimum number of special characters in local password", + "help_text": "Minimum number of special characters required in a local password. 0 means no minimum", + "min_value": 0, + "category": "Authentication", + "category_slug": "authentication", + "default": 0 } }, "GET": { @@ -3873,7 +3979,7 @@ "REMOTE_HOST_HEADERS": { "type": "list", "label": "Remote Host Headers", - "help_text": "HTTP headers and meta keys to search to determine remote host name or IP. Add additional items to this list, such as \"HTTP_X_FORWARDED_FOR\", if behind a reverse proxy. See the \"Proxy Support\" section of the Adminstrator guide for more details.", + "help_text": "HTTP headers and meta keys to search to determine remote host name or IP. Add additional items to this list, such as \"HTTP_X_FORWARDED_FOR\", if behind a reverse proxy. See the \"Proxy Support\" section of the AAP Installation guide for more details.", "category": "System", "category_slug": "system", "defined_in_file": false, @@ -3950,6 +4056,20 @@ "category_slug": "system", "defined_in_file": false }, + "DEFAULT_CONTROL_PLANE_QUEUE_NAME": { + "type": "string", + "label": "The instance group where control plane tasks run", + "category": "System", + "category_slug": "system", + "defined_in_file": false + }, + "DEFAULT_EXECUTION_QUEUE_NAME": { + "type": "string", + "label": "The instance group where user jobs run (currently only on non-VM installs)", + "category": "System", + "category_slug": "system", + "defined_in_file": false + }, "DEFAULT_EXECUTION_ENVIRONMENT": { "type": "field", "label": "Global default execution environment", @@ -4004,7 +4124,7 @@ "AWX_ISOLATION_SHOW_PATHS": { "type": "list", "label": "Paths to expose to isolated jobs", - "help_text": "List of paths that would otherwise be hidden to expose to isolated jobs. Enter one path per line.", + "help_text": "List of paths that would otherwise be hidden to expose to isolated jobs. Enter one path per line. Volumes will be mounted from the execution node to the container. The supported format is HOST-DIR[:CONTAINER-DIR[:OPTIONS]]. ", "category": "Jobs", "category_slug": "jobs", "defined_in_file": false, @@ -4023,26 +4143,25 @@ "type": "string" } }, + "AWX_RUNNER_KEEPALIVE_SECONDS": { + "type": "integer", + "label": "K8S Ansible Runner Keep-Alive Message Interval", + "help_text": "Only applies to jobs running in a Container Group. If not 0, send a message every so-many seconds to keep connection open.", + "category": "Jobs", + "category_slug": "jobs", + "defined_in_file": false + }, "GALAXY_TASK_ENV": { "type": "nested object", - "required": true, "label": "Environment Variables for Galaxy Commands", - "help_text": "Additional environment variables set for invocations of ansible-galaxy within project updates. Useful if you must use a proxy server for ansible-galaxy but not git.", - "category": "Jobs", - "category_slug": "jobs", - "placeholder": { - "HTTP_PROXY": "myproxy.local:8080" - }, - "default": { - "ANSIBLE_FORCE_COLOR": "false", - "GIT_SSH_COMMAND": "ssh -o StrictHostKeyChecking=no" - }, - "child": { - "type": "string", - "required": true, - "read_only": false + "help_text": "Additional environment variables set for invocations of ansible-galaxy within project updates. Useful if you must use a proxy server for ansible-galaxy but not git.", + "category": "Jobs", + "category_slug": "jobs", + "defined_in_file": false, + "child": { + "type": "string" } - }, + }, "INSIGHTS_TRACKING_STATE": { "type": "boolean", "label": "Gather data for Automation Analytics", @@ -4117,6 +4236,15 @@ "category_slug": "jobs", "defined_in_file": false }, + "MAX_WEBSOCKET_EVENT_RATE": { + "type": "integer", + "label": "Job Event Maximum Websocket Messages Per Second", + "help_text": "Maximum number of messages to update the UI live job output with per second. Value of 0 means no limit.", + "min_value": 0, + "category": "Jobs", + "category_slug": "jobs", + "defined_in_file": false + }, "SCHEDULE_MAX_JOBS": { "type": "integer", "label": "Maximum Scheduled Jobs", @@ -4126,15 +4254,6 @@ "category_slug": "jobs", "defined_in_file": false }, - "AWX_RUNNER_KEEPALIVE_SECONDS": { - "type": "integer", - "label": "K8S Ansible Runner Keep-Alive Message Interval", - "help_text": "Only applies to K8S deployments and container_group jobs. If not 0, send a message every so-many seconds to keep connection open.", - "category": "Jobs", - "category_slug": "jobs", - "placeholder": 240, - "default": 0 - }, "AWX_ANSIBLE_CALLBACK_PLUGINS": { "type": "list", "label": "Ansible Callback Plugins", @@ -4159,7 +4278,7 @@ "DEFAULT_JOB_IDLE_TIMEOUT": { "type": "integer", "label": "Default Job Idle Timeout", - "help_text": "If no output is detected from ansible in this number of seconds the execution will be terminated. Use value of 0 to used default idle_timeout is 600s.", + "help_text": "If no output is detected from ansible in this number of seconds the execution will be terminated. Use value of 0 to indicate that no idle timeout should be imposed.", "min_value": 0, "category": "Jobs", "category_slug": "jobs", @@ -4255,7 +4374,7 @@ "LOG_AGGREGATOR_LOGGERS": { "type": "list", "label": "Loggers Sending Data to Log Aggregator Form", - "help_text": "List of loggers that will send HTTP logs to the collector, these can include any or all of: \nawx - service logs\nactivity_stream - activity stream records\njob_events - callback data from Ansible job events\nsystem_tracking - facts gathered from scan jobs.", + "help_text": "List of loggers that will send HTTP logs to the collector, these can include any or all of: \nawx - service logs\nactivity_stream - activity stream records\njob_events - callback data from Ansible job events\nsystem_tracking - facts gathered from scan jobs\nbroadcast_websocket - errors pertaining to websockets broadcast metrics\n", "category": "Logging", "category_slug": "logging", "defined_in_file": false, @@ -4359,12 +4478,11 @@ }, "API_400_ERROR_LOG_FORMAT": { "type": "string", - "required": false, "label": "Log Format For API 4XX Errors", "help_text": "The format of logged messages when an API 4XX error occurs, the following variables will be substituted: \nstatus_code - The HTTP status code of the error\nuser_name - The user name attempting to use the API\nurl_path - The URL path to the API endpoint called\nremote_addr - The remote address seen for the user\nerror - The error set by the api endpoint\nVariables need to be in the format {}.", "category": "Logging", "category_slug": "logging", - "default": "status {status_code} received by user {user_name} attempting to access {url_path} from {remote_addr}" + "defined_in_file": false }, "AUTOMATION_ANALYTICS_LAST_GATHER": { "type": "datetime", @@ -4390,6 +4508,30 @@ "defined_in_file": false, "unit": "seconds" }, + "IS_K8S": { + "type": "boolean", + "label": "Is k8s", + "help_text": "Indicates whether the instance is part of a kubernetes-based deployment.", + "category": "System", + "category_slug": "system", + "defined_in_file": false + }, + "BULK_JOB_MAX_LAUNCH": { + "type": "integer", + "label": "Max jobs to allow bulk jobs to launch", + "help_text": "Max jobs to allow bulk jobs to launch", + "category": "Bulk Actions", + "category_slug": "bulk", + "defined_in_file": false + }, + "BULK_HOST_MAX_CREATE": { + "type": "integer", + "label": "Max number of hosts to allow to be created in a single bulk action", + "help_text": "Max number of hosts to allow to be created in a single bulk action", + "category": "Bulk Actions", + "category_slug": "bulk", + "defined_in_file": false + }, "UI_NEXT": { "type": "boolean", "label": "Enable Preview of New User Interface", @@ -4398,6 +4540,23 @@ "category_slug": "system", "defined_in_file": false }, + "SUBSCRIPTION_USAGE_MODEL": { + "type": "choice", + "label": "Defines subscription usage model and shows Host Metrics", + "category": "System", + "category_slug": "system", + "defined_in_file": false, + "choices": [ + [ + "", + "Default model for AWX - no subscription. Deletion of host_metrics will not be considered for purposes of managed host counting" + ], + [ + "unique_managed_hosts", + "Usage based on unique managed nodes in a large historical time frame and delete functionality for no longer used managed nodes" + ] + ] + }, "SESSION_COOKIE_AGE": { "type": "integer", "label": "Idle Time Force Log Out", @@ -4463,6 +4622,14 @@ "category_slug": "authentication", "defined_in_file": false }, + "ALLOW_METRICS_FOR_ANONYMOUS_USERS": { + "type": "boolean", + "label": "Allow anonymous users to poll metrics", + "help_text": "If true, anonymous users are allowed to poll metrics.", + "category": "Authentication", + "category_slug": "authentication", + "defined_in_file": false + }, "PENDO_TRACKING_STATE": { "type": "choice", "label": "User Analytics Tracking State", @@ -4523,7 +4690,7 @@ "SOCIAL_AUTH_ORGANIZATION_MAP": { "type": "nested object", "label": "Social Auth Organization Map", - "help_text": "Mapping to organization admins/users from social auth accounts. This setting\ncontrols which users are placed into which organizations based on their\nusername and email address. Configuration details are available in the \ndocumentation.", + "help_text": "Mapping to organization admins/users from social auth accounts. This setting\ncontrols which users are placed into which organizations based on their\nusername and email address. Configuration details are available in the\ndocumentation.", "category": "Authentication", "category_slug": "authentication", "defined_in_file": false, @@ -4569,39 +4736,7 @@ "help_text": "Enabling this setting will tell social auth to use the full Email as username instead of the full name", "category": "Authentication", "category_slug": "authentication", - "default": false - }, - "SOCIAL_AUTH_OIDC_KEY": { - "type": "string", - "label": "OIDC Key", - "help_text": "The OIDC key (Client ID) from your IDP.", - "category": "Generic OIDC", - "category_slug": "oidc", - "default": "" - }, - "SOCIAL_AUTH_OIDC_SECRET": { - "type": "string", - "label": "OIDC Secret", - "help_text": "The OIDC secret (Client Secret) from your IDP.", - "category": "Generic OIDC", - "category_slug": "oidc", - "default": "" - }, - "SOCIAL_AUTH_OIDC_OIDC_ENDPOINT": { - "type": "string", - "label": "OIDC Provider URL", - "help_text": "The URL for your OIDC provider, e.g.: http(s)://hostname/.", - "category": "Generic OIDC", - "category_slug": "oidc", - "default": "" - }, - "SOCIAL_AUTH_OIDC_VERIFY_SSL": { - "type": "boolean", - "label": "Verify OIDC Provider Certificate", - "help_text": "Verify the OIDC provider ssl certificate.", - "category": "Generic OIDC", - "category_slug": "oidc", - "default": true + "defined_in_file": false }, "AUTH_LDAP_SERVER_URI": { "type": "string", @@ -5830,7 +5965,7 @@ "SOCIAL_AUTH_GOOGLE_OAUTH2_ORGANIZATION_MAP": { "type": "nested object", "label": "Google OAuth2 Organization Map", - "help_text": "Mapping to organization admins/users from social auth accounts. This setting\ncontrols which users are placed into which organizations based on their\nusername and email address. Configuration details are available in the \ndocumentation.", + "help_text": "Mapping to organization admins/users from social auth accounts. This setting\ncontrols which users are placed into which organizations based on their\nusername and email address. Configuration details are available in the\ndocumentation.", "category": "Google OAuth2", "category_slug": "google-oauth2", "defined_in_file": false, @@ -5886,7 +6021,7 @@ "SOCIAL_AUTH_GITHUB_ORGANIZATION_MAP": { "type": "nested object", "label": "GitHub OAuth2 Organization Map", - "help_text": "Mapping to organization admins/users from social auth accounts. This setting\ncontrols which users are placed into which organizations based on their\nusername and email address. Configuration details are available in the \ndocumentation.", + "help_text": "Mapping to organization admins/users from social auth accounts. This setting\ncontrols which users are placed into which organizations based on their\nusername and email address. Configuration details are available in the\ndocumentation.", "category": "GitHub OAuth2", "category_slug": "github", "defined_in_file": false, @@ -5950,7 +6085,7 @@ "SOCIAL_AUTH_GITHUB_ORG_ORGANIZATION_MAP": { "type": "nested object", "label": "GitHub Organization OAuth2 Organization Map", - "help_text": "Mapping to organization admins/users from social auth accounts. This setting\ncontrols which users are placed into which organizations based on their\nusername and email address. Configuration details are available in the \ndocumentation.", + "help_text": "Mapping to organization admins/users from social auth accounts. This setting\ncontrols which users are placed into which organizations based on their\nusername and email address. Configuration details are available in the\ndocumentation.", "category": "GitHub Organization OAuth2", "category_slug": "github-org", "defined_in_file": false, @@ -6014,7 +6149,7 @@ "SOCIAL_AUTH_GITHUB_TEAM_ORGANIZATION_MAP": { "type": "nested object", "label": "GitHub Team OAuth2 Organization Map", - "help_text": "Mapping to organization admins/users from social auth accounts. This setting\ncontrols which users are placed into which organizations based on their\nusername and email address. Configuration details are available in the \ndocumentation.", + "help_text": "Mapping to organization admins/users from social auth accounts. This setting\ncontrols which users are placed into which organizations based on their\nusername and email address. Configuration details are available in the\ndocumentation.", "category": "GitHub Team OAuth2", "category_slug": "github-team", "defined_in_file": false, @@ -6086,7 +6221,7 @@ "SOCIAL_AUTH_GITHUB_ENTERPRISE_ORGANIZATION_MAP": { "type": "nested object", "label": "GitHub Enterprise OAuth2 Organization Map", - "help_text": "Mapping to organization admins/users from social auth accounts. This setting\ncontrols which users are placed into which organizations based on their\nusername and email address. Configuration details are available in the \ndocumentation.", + "help_text": "Mapping to organization admins/users from social auth accounts. This setting\ncontrols which users are placed into which organizations based on their\nusername and email address. Configuration details are available in the\ndocumentation.", "category": "GitHub Enterprise OAuth2", "category_slug": "github-enterprise", "defined_in_file": false, @@ -6166,7 +6301,7 @@ "SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_ORGANIZATION_MAP": { "type": "nested object", "label": "GitHub Enterprise Organization OAuth2 Organization Map", - "help_text": "Mapping to organization admins/users from social auth accounts. This setting\ncontrols which users are placed into which organizations based on their\nusername and email address. Configuration details are available in the \ndocumentation.", + "help_text": "Mapping to organization admins/users from social auth accounts. This setting\ncontrols which users are placed into which organizations based on their\nusername and email address. Configuration details are available in the\ndocumentation.", "category": "GitHub Enterprise Organization OAuth2", "category_slug": "github-enterprise-org", "defined_in_file": false, @@ -6246,7 +6381,7 @@ "SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_ORGANIZATION_MAP": { "type": "nested object", "label": "GitHub Enterprise Team OAuth2 Organization Map", - "help_text": "Mapping to organization admins/users from social auth accounts. This setting\ncontrols which users are placed into which organizations based on their\nusername and email address. Configuration details are available in the \ndocumentation.", + "help_text": "Mapping to organization admins/users from social auth accounts. This setting\ncontrols which users are placed into which organizations based on their\nusername and email address. Configuration details are available in the\ndocumentation.", "category": "GitHub Enterprise Team OAuth2", "category_slug": "github-enterprise-team", "defined_in_file": false, @@ -6302,7 +6437,7 @@ "SOCIAL_AUTH_AZUREAD_OAUTH2_ORGANIZATION_MAP": { "type": "nested object", "label": "Azure AD OAuth2 Organization Map", - "help_text": "Mapping to organization admins/users from social auth accounts. This setting\ncontrols which users are placed into which organizations based on their\nusername and email address. Configuration details are available in the \ndocumentation.", + "help_text": "Mapping to organization admins/users from social auth accounts. This setting\ncontrols which users are placed into which organizations based on their\nusername and email address. Configuration details are available in the\ndocumentation.", "category": "Azure AD OAuth2", "category_slug": "azuread-oauth2", "defined_in_file": false, @@ -6331,6 +6466,38 @@ } } }, + "SOCIAL_AUTH_OIDC_KEY": { + "type": "string", + "label": "OIDC Key", + "help_text": "The OIDC key (Client ID) from your IDP.", + "category": "Generic OIDC", + "category_slug": "oidc", + "defined_in_file": false + }, + "SOCIAL_AUTH_OIDC_SECRET": { + "type": "string", + "label": "OIDC Secret", + "help_text": "The OIDC secret (Client Secret) from your IDP.", + "category": "Generic OIDC", + "category_slug": "oidc", + "defined_in_file": false + }, + "SOCIAL_AUTH_OIDC_OIDC_ENDPOINT": { + "type": "string", + "label": "OIDC Provider URL", + "help_text": "The URL for your OIDC provider including the path up to /.well-known/openid-configuration", + "category": "Generic OIDC", + "category_slug": "oidc", + "defined_in_file": false + }, + "SOCIAL_AUTH_OIDC_VERIFY_SSL": { + "type": "boolean", + "label": "Verify OIDC Provider Certificate", + "help_text": "Verify the OIDC provider ssl certificate.", + "category": "Generic OIDC", + "category_slug": "oidc", + "defined_in_file": false + }, "SAML_AUTO_CREATE_OBJECTS": { "type": "boolean", "label": "Automatically Create Organizations and Teams on SAML Login", @@ -6469,7 +6636,7 @@ "SOCIAL_AUTH_SAML_ORGANIZATION_MAP": { "type": "nested object", "label": "SAML Organization Map", - "help_text": "Mapping to organization admins/users from social auth accounts. This setting\ncontrols which users are placed into which organizations based on their\nusername and email address. Configuration details are available in the \ndocumentation.", + "help_text": "Mapping to organization admins/users from social auth accounts. This setting\ncontrols which users are placed into which organizations based on their\nusername and email address. Configuration details are available in the\ndocumentation.", "category": "SAML", "category_slug": "saml", "defined_in_file": false, @@ -6522,7 +6689,7 @@ }, "SOCIAL_AUTH_SAML_USER_FLAGS_BY_ATTR": { "type": "nested object", - "label": "SAML User Flags Attribute Mapping", + "label": "SAML User Flags Attribute Mapping", "help_text": "Used to map super users and system auditors from SAML.", "category": "SAML", "category_slug": "saml", @@ -6531,6 +6698,42 @@ "type": "field" } }, + "LOCAL_PASSWORD_MIN_LENGTH": { + "type": "integer", + "label": "Minimum number of characters in local password", + "help_text": "Minimum number of characters required in a local password. 0 means no minimum", + "min_value": 0, + "category": "Authentication", + "category_slug": "authentication", + "defined_in_file": false + }, + "LOCAL_PASSWORD_MIN_DIGITS": { + "type": "integer", + "label": "Minimum number of digit characters in local password", + "help_text": "Minimum number of digit characters required in a local password. 0 means no minimum", + "min_value": 0, + "category": "Authentication", + "category_slug": "authentication", + "defined_in_file": false + }, + "LOCAL_PASSWORD_MIN_UPPER": { + "type": "integer", + "label": "Minimum number of uppercase characters in local password", + "help_text": "Minimum number of uppercase characters required in a local password. 0 means no minimum", + "min_value": 0, + "category": "Authentication", + "category_slug": "authentication", + "defined_in_file": false + }, + "LOCAL_PASSWORD_MIN_SPECIAL": { + "type": "integer", + "label": "Minimum number of special characters in local password", + "help_text": "Minimum number of special characters required in a local password. 0 means no minimum", + "min_value": 0, + "category": "Authentication", + "category_slug": "authentication", + "defined_in_file": false + }, "NAMED_URL_FORMATS": { "type": "nested object", "label": "Formats of all available named urls", diff --git a/awx/ui/src/screens/Setting/shared/data.allSettings.json b/awx/ui/src/screens/Setting/shared/data.allSettings.json index bf73ce0308..a23c1cfba3 100644 --- a/awx/ui/src/screens/Setting/shared/data.allSettings.json +++ b/awx/ui/src/screens/Setting/shared/data.allSettings.json @@ -1,19 +1,19 @@ { - "ACTIVITY_STREAM_ENABLED":true, - "ACTIVITY_STREAM_ENABLED_FOR_INVENTORY_SYNC":false, - "ORG_ADMINS_CAN_SEE_ALL_USERS":true, - "MANAGE_ORGANIZATION_AUTH":true, - "DISABLE_LOCAL_AUTH":false, - "TOWER_URL_BASE":"https://localhost:3000", - "REMOTE_HOST_HEADERS":["REMOTE_ADDR","REMOTE_HOST"], - "PROXY_IP_ALLOWED_LIST":[], - "LICENSE":{}, - "REDHAT_USERNAME":"", - "REDHAT_PASSWORD":"", - "AUTOMATION_ANALYTICS_URL":"https://example.com", - "INSTALL_UUID":"3f5a4d68-3a94-474c-a3c0-f23a33122ce6", - "CUSTOM_VENV_PATHS":[], - "AD_HOC_COMMANDS":[ + "ACTIVITY_STREAM_ENABLED": true, + "ACTIVITY_STREAM_ENABLED_FOR_INVENTORY_SYNC": false, + "ORG_ADMINS_CAN_SEE_ALL_USERS": true, + "MANAGE_ORGANIZATION_AUTH": true, + "DISABLE_LOCAL_AUTH": false, + "TOWER_URL_BASE": "https://localhost:3000", + "REMOTE_HOST_HEADERS": ["REMOTE_ADDR", "REMOTE_HOST"], + "PROXY_IP_ALLOWED_LIST": [], + "LICENSE": {}, + "REDHAT_USERNAME": "", + "REDHAT_PASSWORD": "", + "AUTOMATION_ANALYTICS_URL": "https://example.com", + "INSTALL_UUID": "3f5a4d68-3a94-474c-a3c0-f23a33122ce6", + "CUSTOM_VENV_PATHS": [], + "AD_HOC_COMMANDS": [ "command", "shell", "yum", @@ -34,278 +34,360 @@ "win_group", "win_user" ], - "ALLOW_JINJA_IN_EXTRA_VARS":"template", - "AWX_ISOLATION_BASE_PATH":"/tmp", - "AWX_ISOLATION_SHOW_PATHS":[], - "AWX_TASK_ENV":{}, + "ALLOW_JINJA_IN_EXTRA_VARS": "template", + "AWX_ISOLATION_BASE_PATH": "/tmp", + "AWX_ISOLATION_SHOW_PATHS": [], + "AWX_TASK_ENV": {}, "GALAXY_TASK_ENV": { "ANSIBLE_FORCE_COLOR": "false", "GIT_SSH_COMMAND": "ssh -o StrictHostKeyChecking=no" - }, - "INSIGHTS_TRACKING_STATE":false, - "PROJECT_UPDATE_VVV":false, - "AWX_ROLES_ENABLED":true, - "AWX_COLLECTIONS_ENABLED":true, - "AWX_SHOW_PLAYBOOK_LINKS":false, - "GALAXY_IGNORE_CERTS":false, - "STDOUT_MAX_BYTES_DISPLAY":1048576, - "EVENT_STDOUT_MAX_BYTES_DISPLAY":1024, - "SCHEDULE_MAX_JOBS":10, - "AWX_RUNNER_KEEPALIVE_SECONDS": 0, - "AWX_ANSIBLE_CALLBACK_PLUGINS":[], - "DEFAULT_JOB_TIMEOUT":0, - "DEFAULT_JOB_IDLE_TIMEOUT":0, - "DEFAULT_INVENTORY_UPDATE_TIMEOUT":0, - "DEFAULT_PROJECT_UPDATE_TIMEOUT":0, - "ANSIBLE_FACT_CACHE_TIMEOUT":0, - "MAX_FORKS":200, - "LOG_AGGREGATOR_HOST":null, - "LOG_AGGREGATOR_PORT":null, - "LOG_AGGREGATOR_TYPE":null, - "LOG_AGGREGATOR_USERNAME":"", - "LOG_AGGREGATOR_PASSWORD":"", - "LOG_AGGREGATOR_LOGGERS":["awx","activity_stream","job_events","system_tracking"], - "LOG_AGGREGATOR_INDIVIDUAL_FACTS":false, - "LOG_AGGREGATOR_ENABLED":true, - "LOG_AGGREGATOR_TOWER_UUID":"", - "LOG_AGGREGATOR_PROTOCOL":"https", - "LOG_AGGREGATOR_TCP_TIMEOUT":5, - "LOG_AGGREGATOR_VERIFY_CERT":true, - "LOG_AGGREGATOR_LEVEL":"INFO", - "LOG_AGGREGATOR_MAX_DISK_USAGE_GB":1, - "LOG_AGGREGATOR_MAX_DISK_USAGE_PATH":"/var/lib/awx", - "LOG_AGGREGATOR_RSYSLOGD_DEBUG":false, - "API_400_ERROR_LOG_FORMAT":"status {status_code} received by user {user_name} attempting to access {url_path} from {remote_addr}", - "AUTOMATION_ANALYTICS_LAST_GATHER":null, - "AUTOMATION_ANALYTICS_GATHER_INTERVAL":14400, - "SESSION_COOKIE_AGE":1800, - "SESSIONS_PER_USER":-1, - "AUTH_BASIC_ENABLED":true, - "OAUTH2_PROVIDER":{ - "ACCESS_TOKEN_EXPIRE_SECONDS":31536000000, - "REFRESH_TOKEN_EXPIRE_SECONDS":2628000, - "AUTHORIZATION_CODE_EXPIRE_SECONDS":600 }, - "ALLOW_OAUTH2_FOR_EXTERNAL_USERS":false, - "LOGIN_REDIRECT_OVERRIDE":"", - "PENDO_TRACKING_STATE":"off", - "CUSTOM_LOGIN_INFO":"", - "CUSTOM_LOGO":"", - "MAX_UI_JOB_EVENTS":4000, - "UI_LIVE_UPDATES_ENABLED":true, - "AUTHENTICATION_BACKENDS":[ + "INSIGHTS_TRACKING_STATE": false, + "PROJECT_UPDATE_VVV": false, + "AWX_ROLES_ENABLED": true, + "AWX_COLLECTIONS_ENABLED": true, + "AWX_SHOW_PLAYBOOK_LINKS": false, + "GALAXY_IGNORE_CERTS": false, + "STDOUT_MAX_BYTES_DISPLAY": 1048576, + "EVENT_STDOUT_MAX_BYTES_DISPLAY": 1024, + "SCHEDULE_MAX_JOBS": 10, + "AWX_RUNNER_KEEPALIVE_SECONDS": 0, + "AWX_ANSIBLE_CALLBACK_PLUGINS": [], + "DEFAULT_JOB_TIMEOUT": 0, + "DEFAULT_JOB_IDLE_TIMEOUT": 0, + "DEFAULT_INVENTORY_UPDATE_TIMEOUT": 0, + "DEFAULT_PROJECT_UPDATE_TIMEOUT": 0, + "ANSIBLE_FACT_CACHE_TIMEOUT": 0, + "MAX_FORKS": 200, + "LOG_AGGREGATOR_HOST": null, + "LOG_AGGREGATOR_PORT": null, + "LOG_AGGREGATOR_TYPE": null, + "LOG_AGGREGATOR_USERNAME": "", + "LOG_AGGREGATOR_PASSWORD": "", + "LOG_AGGREGATOR_LOGGERS": [ + "awx", + "activity_stream", + "job_events", + "system_tracking" + ], + "LOG_AGGREGATOR_INDIVIDUAL_FACTS": false, + "LOG_AGGREGATOR_ENABLED": true, + "LOG_AGGREGATOR_TOWER_UUID": "", + "LOG_AGGREGATOR_PROTOCOL": "https", + "LOG_AGGREGATOR_TCP_TIMEOUT": 5, + "LOG_AGGREGATOR_VERIFY_CERT": true, + "LOG_AGGREGATOR_LEVEL": "INFO", + "LOG_AGGREGATOR_MAX_DISK_USAGE_GB": 1, + "LOG_AGGREGATOR_MAX_DISK_USAGE_PATH": "/var/lib/awx", + "LOG_AGGREGATOR_RSYSLOGD_DEBUG": false, + "API_400_ERROR_LOG_FORMAT": "status {status_code} received by user {user_name} attempting to access {url_path} from {remote_addr}", + "AUTOMATION_ANALYTICS_LAST_GATHER": null, + "AUTOMATION_ANALYTICS_GATHER_INTERVAL": 14400, + "SESSION_COOKIE_AGE": 1800, + "SESSIONS_PER_USER": -1, + "AUTH_BASIC_ENABLED": true, + "OAUTH2_PROVIDER": { + "ACCESS_TOKEN_EXPIRE_SECONDS": 31536000000, + "REFRESH_TOKEN_EXPIRE_SECONDS": 2628000, + "AUTHORIZATION_CODE_EXPIRE_SECONDS": 600 + }, + "ALLOW_OAUTH2_FOR_EXTERNAL_USERS": false, + "LOGIN_REDIRECT_OVERRIDE": "", + "PENDO_TRACKING_STATE": "off", + "CUSTOM_LOGIN_INFO": "", + "CUSTOM_LOGO": "", + "MAX_UI_JOB_EVENTS": 4000, + "UI_LIVE_UPDATES_ENABLED": true, + "AUTHENTICATION_BACKENDS": [ "awx.sso.backends.LDAPBackend", "awx.sso.backends.RADIUSBackend", "awx.sso.backends.TACACSPlusBackend", "social_core.backends.github.GithubTeamOAuth2", "django.contrib.auth.backends.ModelBackend" ], - "SOCIAL_AUTH_ORGANIZATION_MAP":null, - "SOCIAL_AUTH_TEAM_MAP":null, - "SOCIAL_AUTH_USER_FIELDS":null, - "SOCIAL_AUTH_USERNAME_IS_FULL_EMAIL":false, - "AUTH_LDAP_SERVER_URI":"ldap://ldap.example.com", - "AUTH_LDAP_BIND_DN":"cn=eng_user1", - "AUTH_LDAP_BIND_PASSWORD":"$encrypted$", - "AUTH_LDAP_START_TLS":false, - "AUTH_LDAP_CONNECTION_OPTIONS":{"OPT_REFERRALS":0,"OPT_NETWORK_TIMEOUT":30}, - "AUTH_LDAP_USER_SEARCH":[], - "AUTH_LDAP_USER_DN_TEMPLATE":"uid=%(user)s,OU=Users,DC=example,DC=com", - "AUTH_LDAP_USER_ATTR_MAP":{}, - "AUTH_LDAP_GROUP_SEARCH":["DC=example,DC=com","SCOPE_SUBTREE","(objectClass=group)"], - "AUTH_LDAP_GROUP_TYPE":"MemberDNGroupType", - "AUTH_LDAP_GROUP_TYPE_PARAMS":{"name_attr":"cn","member_attr":"member"}, - "AUTH_LDAP_REQUIRE_GROUP":"CN=Service Users,OU=Users,DC=example,DC=com", - "AUTH_LDAP_DENY_GROUP":null, - "AUTH_LDAP_USER_FLAGS_BY_GROUP":{"is_superuser":["cn=superusers"]}, - "AUTH_LDAP_ORGANIZATION_MAP":{}, - "AUTH_LDAP_TEAM_MAP":{}, - "AUTH_LDAP_1_SERVER_URI":"", - "AUTH_LDAP_1_BIND_DN":"", - "AUTH_LDAP_1_BIND_PASSWORD":"", - "AUTH_LDAP_1_START_TLS":true, - "AUTH_LDAP_1_CONNECTION_OPTIONS":{"OPT_REFERRALS":0,"OPT_NETWORK_TIMEOUT":30}, - "AUTH_LDAP_1_USER_SEARCH":[], - "AUTH_LDAP_1_USER_DN_TEMPLATE":null, - "AUTH_LDAP_1_USER_ATTR_MAP":{}, - "AUTH_LDAP_1_GROUP_SEARCH":[], - "AUTH_LDAP_1_GROUP_TYPE":"MemberDNGroupType", - "AUTH_LDAP_1_GROUP_TYPE_PARAMS":{"member_attr":"member","name_attr":"cn"}, - "AUTH_LDAP_1_REQUIRE_GROUP":null, - "AUTH_LDAP_1_DENY_GROUP":"CN=Disabled1", - "AUTH_LDAP_1_USER_FLAGS_BY_GROUP":{}, - "AUTH_LDAP_1_ORGANIZATION_MAP":{}, - "AUTH_LDAP_1_TEAM_MAP":{}, - "AUTH_LDAP_2_SERVER_URI":"", - "AUTH_LDAP_2_BIND_DN":"", - "AUTH_LDAP_2_BIND_PASSWORD":"", - "AUTH_LDAP_2_START_TLS":false, - "AUTH_LDAP_2_CONNECTION_OPTIONS":{"OPT_REFERRALS":0,"OPT_NETWORK_TIMEOUT":30}, - "AUTH_LDAP_2_USER_SEARCH":[], - "AUTH_LDAP_2_USER_DN_TEMPLATE":null, - "AUTH_LDAP_2_USER_ATTR_MAP":{}, - "AUTH_LDAP_2_GROUP_SEARCH":[], - "AUTH_LDAP_2_GROUP_TYPE":"MemberDNGroupType", - "AUTH_LDAP_2_GROUP_TYPE_PARAMS":{"member_attr":"member","name_attr":"cn"}, - "AUTH_LDAP_2_REQUIRE_GROUP":null, - "AUTH_LDAP_2_DENY_GROUP":"CN=Disabled2", - "AUTH_LDAP_2_USER_FLAGS_BY_GROUP":{}, - "AUTH_LDAP_2_ORGANIZATION_MAP":{}, - "AUTH_LDAP_2_TEAM_MAP":{}, - "AUTH_LDAP_3_SERVER_URI":"", - "AUTH_LDAP_3_BIND_DN":"", - "AUTH_LDAP_3_BIND_PASSWORD":"", - "AUTH_LDAP_3_START_TLS":false, - "AUTH_LDAP_3_CONNECTION_OPTIONS":{"OPT_REFERRALS":0,"OPT_NETWORK_TIMEOUT":30}, - "AUTH_LDAP_3_USER_SEARCH":[], - "AUTH_LDAP_3_USER_DN_TEMPLATE":null, - "AUTH_LDAP_3_USER_ATTR_MAP":{}, - "AUTH_LDAP_3_GROUP_SEARCH":[], - "AUTH_LDAP_3_GROUP_TYPE":"MemberDNGroupType", - "AUTH_LDAP_3_GROUP_TYPE_PARAMS":{"member_attr":"member","name_attr":"cn"}, - "AUTH_LDAP_3_REQUIRE_GROUP":null, - "AUTH_LDAP_3_DENY_GROUP":null, - "AUTH_LDAP_3_USER_FLAGS_BY_GROUP":{}, - "AUTH_LDAP_3_ORGANIZATION_MAP":{}, - "AUTH_LDAP_3_TEAM_MAP":{}, - "AUTH_LDAP_4_SERVER_URI":"", - "AUTH_LDAP_4_BIND_DN":"", - "AUTH_LDAP_4_BIND_PASSWORD":"", - "AUTH_LDAP_4_START_TLS":false, - "AUTH_LDAP_4_CONNECTION_OPTIONS":{"OPT_REFERRALS":0,"OPT_NETWORK_TIMEOUT":30}, - "AUTH_LDAP_4_USER_SEARCH":[], - "AUTH_LDAP_4_USER_DN_TEMPLATE":null, - "AUTH_LDAP_4_USER_ATTR_MAP":{}, - "AUTH_LDAP_4_GROUP_SEARCH":[], - "AUTH_LDAP_4_GROUP_TYPE":"MemberDNGroupType", - "AUTH_LDAP_4_GROUP_TYPE_PARAMS":{"member_attr":"member","name_attr":"cn"}, - "AUTH_LDAP_4_REQUIRE_GROUP":null, - "AUTH_LDAP_4_DENY_GROUP":null, - "AUTH_LDAP_4_USER_FLAGS_BY_GROUP":{}, - "AUTH_LDAP_4_ORGANIZATION_MAP":{}, - "AUTH_LDAP_4_TEAM_MAP":{}, - "AUTH_LDAP_5_SERVER_URI":"", - "AUTH_LDAP_5_BIND_DN":"", - "AUTH_LDAP_5_BIND_PASSWORD":"", - "AUTH_LDAP_5_START_TLS":false, - "AUTH_LDAP_5_CONNECTION_OPTIONS":{"OPT_REFERRALS":0,"OPT_NETWORK_TIMEOUT":30}, - "AUTH_LDAP_5_USER_SEARCH":[], - "AUTH_LDAP_5_USER_DN_TEMPLATE":null, - "AUTH_LDAP_5_USER_ATTR_MAP":{}, - "AUTH_LDAP_5_GROUP_SEARCH":[], - "AUTH_LDAP_5_GROUP_TYPE":"MemberDNGroupType", - "AUTH_LDAP_5_GROUP_TYPE_PARAMS":{"member_attr":"member","name_attr":"cn"}, - "AUTH_LDAP_5_REQUIRE_GROUP":null, - "AUTH_LDAP_5_DENY_GROUP":null, - "AUTH_LDAP_5_USER_FLAGS_BY_GROUP":{}, - "AUTH_LDAP_5_ORGANIZATION_MAP":{}, - "AUTH_LDAP_5_TEAM_MAP":{}, - "RADIUS_SERVER":"example.org", - "RADIUS_PORT":1812, - "RADIUS_SECRET":"$encrypted$", - "TACACSPLUS_HOST":"", - "TACACSPLUS_PORT":49, - "TACACSPLUS_SECRET":"", - "TACACSPLUS_SESSION_TIMEOUT":5, - "TACACSPLUS_AUTH_PROTOCOL":"ascii", - "SOCIAL_AUTH_GOOGLE_OAUTH2_CALLBACK_URL":"https://localhost:3000/sso/complete/google-oauth2/", - "SOCIAL_AUTH_GOOGLE_OAUTH2_KEY":"", - "SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET":"", - "SOCIAL_AUTH_GOOGLE_OAUTH2_WHITELISTED_DOMAINS":[], - "SOCIAL_AUTH_GOOGLE_OAUTH2_AUTH_EXTRA_ARGUMENTS":{}, - "SOCIAL_AUTH_GOOGLE_OAUTH2_ORGANIZATION_MAP":null, - "SOCIAL_AUTH_GOOGLE_OAUTH2_TEAM_MAP":null, - "SOCIAL_AUTH_GITHUB_CALLBACK_URL":"https://localhost:3000/sso/complete/github/", - "SOCIAL_AUTH_GITHUB_KEY":"", - "SOCIAL_AUTH_GITHUB_SECRET":"", - "SOCIAL_AUTH_GITHUB_ORGANIZATION_MAP":null, - "SOCIAL_AUTH_GITHUB_TEAM_MAP":null, - "SOCIAL_AUTH_GITHUB_ORG_CALLBACK_URL":"https://localhost:3000/sso/complete/github-org/", - "SOCIAL_AUTH_GITHUB_ORG_KEY":"", - "SOCIAL_AUTH_GITHUB_ORG_SECRET":"", - "SOCIAL_AUTH_GITHUB_ORG_NAME":"", - "SOCIAL_AUTH_GITHUB_ORG_ORGANIZATION_MAP":null, - "SOCIAL_AUTH_GITHUB_ORG_TEAM_MAP":null, - "SOCIAL_AUTH_GITHUB_TEAM_CALLBACK_URL":"https://localhost:3000/sso/complete/github-team/", - "SOCIAL_AUTH_GITHUB_TEAM_KEY":"OAuth2 key (Client ID)", - "SOCIAL_AUTH_GITHUB_TEAM_SECRET":"$encrypted$", - "SOCIAL_AUTH_GITHUB_TEAM_ID":"team_id", - "SOCIAL_AUTH_GITHUB_TEAM_ORGANIZATION_MAP":{}, - "SOCIAL_AUTH_GITHUB_TEAM_TEAM_MAP":{}, - "SOCIAL_AUTH_AZUREAD_OAUTH2_CALLBACK_URL":"https://localhost:3000/sso/complete/azuread-oauth2/", - "SOCIAL_AUTH_AZUREAD_OAUTH2_KEY":"", - "SOCIAL_AUTH_AZUREAD_OAUTH2_SECRET":"", - "SOCIAL_AUTH_AZUREAD_OAUTH2_ORGANIZATION_MAP":null, - "SOCIAL_AUTH_AZUREAD_OAUTH2_TEAM_MAP":null, - "SAML_AUTO_CREATE_OBJECTS":true, - "SOCIAL_AUTH_SAML_CALLBACK_URL":"https://localhost:3000/sso/complete/saml/", - "SOCIAL_AUTH_SAML_METADATA_URL":"https://localhost:3000/sso/metadata/saml/", - "SOCIAL_AUTH_SAML_SP_ENTITY_ID":"", - "SOCIAL_AUTH_SAML_SP_PUBLIC_CERT":"", - "SOCIAL_AUTH_SAML_SP_PRIVATE_KEY":"", - "SOCIAL_AUTH_SAML_ORG_INFO":{}, - "SOCIAL_AUTH_SAML_TECHNICAL_CONTACT":{}, - "SOCIAL_AUTH_SAML_SUPPORT_CONTACT":{}, - "SOCIAL_AUTH_SAML_ENABLED_IDPS":{}, - "SOCIAL_AUTH_SAML_SECURITY_CONFIG":{"requestedAuthnContext":false}, - "SOCIAL_AUTH_SAML_SP_EXTRA":null, - "SOCIAL_AUTH_SAML_EXTRA_DATA":null, - "SOCIAL_AUTH_SAML_ORGANIZATION_MAP":null, - "SOCIAL_AUTH_SAML_TEAM_MAP":null, - "SOCIAL_AUTH_SAML_ORGANIZATION_ATTR":{}, - "SOCIAL_AUTH_SAML_TEAM_ATTR":{}, - "SOCIAL_AUTH_SAML_USER_FLAGS_BY_ATTR":{}, - "SOCIAL_AUTH_OIDC_KEY":"", - "SOCIAL_AUTH_OIDC_SECRET":"", - "SOCIAL_AUTH_OIDC_OIDC_ENDPOINT":"", - "SOCIAL_AUTH_OIDC_VERIFY_SSL":true, - "NAMED_URL_FORMATS":{ - "organizations":"", - "teams":"++", - "credential_types":"+", - "credentials":"+++++", - "notification_templates":"++", - "job_templates":"++", - "projects":"++", - "inventories":"++", - "hosts":"++++", - "groups":"++++", - "inventory_sources":"++++", - "inventory_scripts":"++", - "instance_groups":"", - "labels":"++", - "workflow_job_templates":"++", - "workflow_job_template_nodes":"++++", - "applications":"++", - "users":"", - "instances":"" + "SOCIAL_AUTH_ORGANIZATION_MAP": null, + "SOCIAL_AUTH_TEAM_MAP": null, + "SOCIAL_AUTH_USER_FIELDS": null, + "SOCIAL_AUTH_USERNAME_IS_FULL_EMAIL": false, + "AUTH_LDAP_SERVER_URI": "ldap://ldap.example.com", + "AUTH_LDAP_BIND_DN": "cn=eng_user1", + "AUTH_LDAP_BIND_PASSWORD": "$encrypted$", + "AUTH_LDAP_START_TLS": false, + "AUTH_LDAP_CONNECTION_OPTIONS": { + "OPT_REFERRALS": 0, + "OPT_NETWORK_TIMEOUT": 30 }, - "NAMED_URL_GRAPH_NODES":{ - "organizations":{"fields":["name"],"adj_list":[]}, - "teams":{"fields":["name"],"adj_list":[["organization","organizations"]]}, - "credential_types":{"fields":["name","kind"],"adj_list":[]}, - "credentials":{ - "fields":["name"], - "adj_list":[["credential_type","credential_types"],["organization","organizations"]] + "AUTH_LDAP_USER_SEARCH": [], + "AUTH_LDAP_USER_DN_TEMPLATE": "uid=%(user)s,OU=Users,DC=example,DC=com", + "AUTH_LDAP_USER_ATTR_MAP": {}, + "AUTH_LDAP_GROUP_SEARCH": [ + "DC=example,DC=com", + "SCOPE_SUBTREE", + "(objectClass=group)" + ], + "AUTH_LDAP_GROUP_TYPE": "MemberDNGroupType", + "AUTH_LDAP_GROUP_TYPE_PARAMS": { "name_attr": "cn", "member_attr": "member" }, + "AUTH_LDAP_REQUIRE_GROUP": "CN=Service Users,OU=Users,DC=example,DC=com", + "AUTH_LDAP_DENY_GROUP": null, + "AUTH_LDAP_USER_FLAGS_BY_GROUP": { "is_superuser": ["cn=superusers"] }, + "AUTH_LDAP_ORGANIZATION_MAP": {}, + "AUTH_LDAP_TEAM_MAP": {}, + "AUTH_LDAP_1_SERVER_URI": "", + "AUTH_LDAP_1_BIND_DN": "", + "AUTH_LDAP_1_BIND_PASSWORD": "", + "AUTH_LDAP_1_START_TLS": true, + "AUTH_LDAP_1_CONNECTION_OPTIONS": { + "OPT_REFERRALS": 0, + "OPT_NETWORK_TIMEOUT": 30 + }, + "AUTH_LDAP_1_USER_SEARCH": [], + "AUTH_LDAP_1_USER_DN_TEMPLATE": null, + "AUTH_LDAP_1_USER_ATTR_MAP": {}, + "AUTH_LDAP_1_GROUP_SEARCH": [], + "AUTH_LDAP_1_GROUP_TYPE": "MemberDNGroupType", + "AUTH_LDAP_1_GROUP_TYPE_PARAMS": { + "member_attr": "member", + "name_attr": "cn" + }, + "AUTH_LDAP_1_REQUIRE_GROUP": null, + "AUTH_LDAP_1_DENY_GROUP": "CN=Disabled1", + "AUTH_LDAP_1_USER_FLAGS_BY_GROUP": {}, + "AUTH_LDAP_1_ORGANIZATION_MAP": {}, + "AUTH_LDAP_1_TEAM_MAP": {}, + "AUTH_LDAP_2_SERVER_URI": "", + "AUTH_LDAP_2_BIND_DN": "", + "AUTH_LDAP_2_BIND_PASSWORD": "", + "AUTH_LDAP_2_START_TLS": false, + "AUTH_LDAP_2_CONNECTION_OPTIONS": { + "OPT_REFERRALS": 0, + "OPT_NETWORK_TIMEOUT": 30 + }, + "AUTH_LDAP_2_USER_SEARCH": [], + "AUTH_LDAP_2_USER_DN_TEMPLATE": null, + "AUTH_LDAP_2_USER_ATTR_MAP": {}, + "AUTH_LDAP_2_GROUP_SEARCH": [], + "AUTH_LDAP_2_GROUP_TYPE": "MemberDNGroupType", + "AUTH_LDAP_2_GROUP_TYPE_PARAMS": { + "member_attr": "member", + "name_attr": "cn" + }, + "AUTH_LDAP_2_REQUIRE_GROUP": null, + "AUTH_LDAP_2_DENY_GROUP": "CN=Disabled2", + "AUTH_LDAP_2_USER_FLAGS_BY_GROUP": {}, + "AUTH_LDAP_2_ORGANIZATION_MAP": {}, + "AUTH_LDAP_2_TEAM_MAP": {}, + "AUTH_LDAP_3_SERVER_URI": "", + "AUTH_LDAP_3_BIND_DN": "", + "AUTH_LDAP_3_BIND_PASSWORD": "", + "AUTH_LDAP_3_START_TLS": false, + "AUTH_LDAP_3_CONNECTION_OPTIONS": { + "OPT_REFERRALS": 0, + "OPT_NETWORK_TIMEOUT": 30 + }, + "AUTH_LDAP_3_USER_SEARCH": [], + "AUTH_LDAP_3_USER_DN_TEMPLATE": null, + "AUTH_LDAP_3_USER_ATTR_MAP": {}, + "AUTH_LDAP_3_GROUP_SEARCH": [], + "AUTH_LDAP_3_GROUP_TYPE": "MemberDNGroupType", + "AUTH_LDAP_3_GROUP_TYPE_PARAMS": { + "member_attr": "member", + "name_attr": "cn" + }, + "AUTH_LDAP_3_REQUIRE_GROUP": null, + "AUTH_LDAP_3_DENY_GROUP": null, + "AUTH_LDAP_3_USER_FLAGS_BY_GROUP": {}, + "AUTH_LDAP_3_ORGANIZATION_MAP": {}, + "AUTH_LDAP_3_TEAM_MAP": {}, + "AUTH_LDAP_4_SERVER_URI": "", + "AUTH_LDAP_4_BIND_DN": "", + "AUTH_LDAP_4_BIND_PASSWORD": "", + "AUTH_LDAP_4_START_TLS": false, + "AUTH_LDAP_4_CONNECTION_OPTIONS": { + "OPT_REFERRALS": 0, + "OPT_NETWORK_TIMEOUT": 30 + }, + "AUTH_LDAP_4_USER_SEARCH": [], + "AUTH_LDAP_4_USER_DN_TEMPLATE": null, + "AUTH_LDAP_4_USER_ATTR_MAP": {}, + "AUTH_LDAP_4_GROUP_SEARCH": [], + "AUTH_LDAP_4_GROUP_TYPE": "MemberDNGroupType", + "AUTH_LDAP_4_GROUP_TYPE_PARAMS": { + "member_attr": "member", + "name_attr": "cn" + }, + "AUTH_LDAP_4_REQUIRE_GROUP": null, + "AUTH_LDAP_4_DENY_GROUP": null, + "AUTH_LDAP_4_USER_FLAGS_BY_GROUP": {}, + "AUTH_LDAP_4_ORGANIZATION_MAP": {}, + "AUTH_LDAP_4_TEAM_MAP": {}, + "AUTH_LDAP_5_SERVER_URI": "", + "AUTH_LDAP_5_BIND_DN": "", + "AUTH_LDAP_5_BIND_PASSWORD": "", + "AUTH_LDAP_5_START_TLS": false, + "AUTH_LDAP_5_CONNECTION_OPTIONS": { + "OPT_REFERRALS": 0, + "OPT_NETWORK_TIMEOUT": 30 + }, + "AUTH_LDAP_5_USER_SEARCH": [], + "AUTH_LDAP_5_USER_DN_TEMPLATE": null, + "AUTH_LDAP_5_USER_ATTR_MAP": {}, + "AUTH_LDAP_5_GROUP_SEARCH": [], + "AUTH_LDAP_5_GROUP_TYPE": "MemberDNGroupType", + "AUTH_LDAP_5_GROUP_TYPE_PARAMS": { + "member_attr": "member", + "name_attr": "cn" + }, + "AUTH_LDAP_5_REQUIRE_GROUP": null, + "AUTH_LDAP_5_DENY_GROUP": null, + "AUTH_LDAP_5_USER_FLAGS_BY_GROUP": {}, + "AUTH_LDAP_5_ORGANIZATION_MAP": {}, + "AUTH_LDAP_5_TEAM_MAP": {}, + "RADIUS_SERVER": "example.org", + "RADIUS_PORT": 1812, + "RADIUS_SECRET": "$encrypted$", + "TACACSPLUS_HOST": "", + "TACACSPLUS_PORT": 49, + "TACACSPLUS_SECRET": "", + "TACACSPLUS_SESSION_TIMEOUT": 5, + "TACACSPLUS_AUTH_PROTOCOL": "ascii", + "SOCIAL_AUTH_GOOGLE_OAUTH2_CALLBACK_URL": "https://localhost:3000/sso/complete/google-oauth2/", + "SOCIAL_AUTH_GOOGLE_OAUTH2_KEY": "", + "SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET": "", + "SOCIAL_AUTH_GOOGLE_OAUTH2_WHITELISTED_DOMAINS": [], + "SOCIAL_AUTH_GOOGLE_OAUTH2_AUTH_EXTRA_ARGUMENTS": {}, + "SOCIAL_AUTH_GOOGLE_OAUTH2_ORGANIZATION_MAP": null, + "SOCIAL_AUTH_GOOGLE_OAUTH2_TEAM_MAP": null, + "SOCIAL_AUTH_GITHUB_CALLBACK_URL": "https://localhost:3000/sso/complete/github/", + "SOCIAL_AUTH_GITHUB_KEY": "", + "SOCIAL_AUTH_GITHUB_SECRET": "", + "SOCIAL_AUTH_GITHUB_ORGANIZATION_MAP": null, + "SOCIAL_AUTH_GITHUB_TEAM_MAP": null, + "SOCIAL_AUTH_GITHUB_ORG_CALLBACK_URL": "https://localhost:3000/sso/complete/github-org/", + "SOCIAL_AUTH_GITHUB_ORG_KEY": "", + "SOCIAL_AUTH_GITHUB_ORG_SECRET": "", + "SOCIAL_AUTH_GITHUB_ORG_NAME": "", + "SOCIAL_AUTH_GITHUB_ORG_ORGANIZATION_MAP": null, + "SOCIAL_AUTH_GITHUB_ORG_TEAM_MAP": null, + "SOCIAL_AUTH_GITHUB_TEAM_CALLBACK_URL": "https://localhost:3000/sso/complete/github-team/", + "SOCIAL_AUTH_GITHUB_TEAM_KEY": "OAuth2 key (Client ID)", + "SOCIAL_AUTH_GITHUB_TEAM_SECRET": "$encrypted$", + "SOCIAL_AUTH_GITHUB_TEAM_ID": "team_id", + "SOCIAL_AUTH_GITHUB_TEAM_ORGANIZATION_MAP": {}, + "SOCIAL_AUTH_GITHUB_TEAM_TEAM_MAP": {}, + "SOCIAL_AUTH_AZUREAD_OAUTH2_CALLBACK_URL": "https://localhost:3000/sso/complete/azuread-oauth2/", + "SOCIAL_AUTH_AZUREAD_OAUTH2_KEY": "", + "SOCIAL_AUTH_AZUREAD_OAUTH2_SECRET": "", + "SOCIAL_AUTH_AZUREAD_OAUTH2_ORGANIZATION_MAP": null, + "SOCIAL_AUTH_AZUREAD_OAUTH2_TEAM_MAP": null, + "SAML_AUTO_CREATE_OBJECTS": true, + "SOCIAL_AUTH_SAML_CALLBACK_URL": "https://localhost:3000/sso/complete/saml/", + "SOCIAL_AUTH_SAML_METADATA_URL": "https://localhost:3000/sso/metadata/saml/", + "SOCIAL_AUTH_SAML_SP_ENTITY_ID": "", + "SOCIAL_AUTH_SAML_SP_PUBLIC_CERT": "", + "SOCIAL_AUTH_SAML_SP_PRIVATE_KEY": "", + "SOCIAL_AUTH_SAML_ORG_INFO": {}, + "SOCIAL_AUTH_SAML_TECHNICAL_CONTACT": {}, + "SOCIAL_AUTH_SAML_SUPPORT_CONTACT": {}, + "SOCIAL_AUTH_SAML_ENABLED_IDPS": {}, + "SOCIAL_AUTH_SAML_SECURITY_CONFIG": { "requestedAuthnContext": false }, + "SOCIAL_AUTH_SAML_SP_EXTRA": null, + "SOCIAL_AUTH_SAML_EXTRA_DATA": null, + "SOCIAL_AUTH_SAML_ORGANIZATION_MAP": null, + "SOCIAL_AUTH_SAML_TEAM_MAP": null, + "SOCIAL_AUTH_SAML_ORGANIZATION_ATTR": {}, + "SOCIAL_AUTH_SAML_TEAM_ATTR": {}, + "SOCIAL_AUTH_SAML_USER_FLAGS_BY_ATTR": {}, + "SOCIAL_AUTH_OIDC_KEY": "", + "SOCIAL_AUTH_OIDC_SECRET": "", + "SOCIAL_AUTH_OIDC_OIDC_ENDPOINT": "", + "SOCIAL_AUTH_OIDC_VERIFY_SSL": true, + "NAMED_URL_FORMATS": { + "organizations": "", + "teams": "++", + "credential_types": "+", + "credentials": "+++++", + "notification_templates": "++", + "job_templates": "++", + "projects": "++", + "inventories": "++", + "hosts": "++++", + "groups": "++++", + "inventory_sources": "++++", + "inventory_scripts": "++", + "instance_groups": "", + "labels": "++", + "workflow_job_templates": "++", + "workflow_job_template_nodes": "++++", + "applications": "++", + "users": "", + "instances": "" + }, + "LOCAL_PASSWORD_MIN_LENGTH": 0, + "LOCAL_PASSWORD_MIN_DIGITS": 0, + "LOCAL_PASSWORD_MIN_UPPER": 0, + "LOCAL_PASSWORD_MIN_SPECIAL": 0, + "NAMED_URL_GRAPH_NODES": { + "organizations": { "fields": ["name"], "adj_list": [] }, + "teams": { + "fields": ["name"], + "adj_list": [["organization", "organizations"]] }, - "notification_templates":{"fields":["name"],"adj_list":[["organization","organizations"]]}, - "job_templates":{"fields":["name"],"adj_list":[["organization","organizations"]]}, - "projects":{"fields":["name"],"adj_list":[["organization","organizations"]]}, - "inventories":{"fields":["name"],"adj_list":[["organization","organizations"]]}, - "hosts":{"fields":["name"],"adj_list":[["inventory","inventories"]]}, - "groups":{"fields":["name"],"adj_list":[["inventory","inventories"]]}, - "inventory_sources":{"fields":["name"],"adj_list":[["inventory","inventories"]]}, - "inventory_scripts":{"fields":["name"],"adj_list":[["organization","organizations"]]}, - "instance_groups":{"fields":["name"],"adj_list":[]}, - "labels":{"fields":["name"],"adj_list":[["organization","organizations"]]}, - "workflow_job_templates":{"fields":["name"],"adj_list":[["organization","organizations"]]}, - "workflow_job_template_nodes":{ - "fields":["identifier"], - "adj_list":[["workflow_job_template","workflow_job_templates"]] + "credential_types": { "fields": ["name", "kind"], "adj_list": [] }, + "credentials": { + "fields": ["name"], + "adj_list": [ + ["credential_type", "credential_types"], + ["organization", "organizations"] + ] }, - "applications":{"fields":["name"],"adj_list":[["organization","organizations"]]}, - "users":{"fields":["username"],"adj_list":[]}, - "instances":{"fields":["hostname"],"adj_list":[]} + "notification_templates": { + "fields": ["name"], + "adj_list": [["organization", "organizations"]] + }, + "job_templates": { + "fields": ["name"], + "adj_list": [["organization", "organizations"]] + }, + "projects": { + "fields": ["name"], + "adj_list": [["organization", "organizations"]] + }, + "inventories": { + "fields": ["name"], + "adj_list": [["organization", "organizations"]] + }, + "hosts": { "fields": ["name"], "adj_list": [["inventory", "inventories"]] }, + "groups": { + "fields": ["name"], + "adj_list": [["inventory", "inventories"]] + }, + "inventory_sources": { + "fields": ["name"], + "adj_list": [["inventory", "inventories"]] + }, + "inventory_scripts": { + "fields": ["name"], + "adj_list": [["organization", "organizations"]] + }, + "instance_groups": { "fields": ["name"], "adj_list": [] }, + "labels": { + "fields": ["name"], + "adj_list": [["organization", "organizations"]] + }, + "workflow_job_templates": { + "fields": ["name"], + "adj_list": [["organization", "organizations"]] + }, + "workflow_job_template_nodes": { + "fields": ["identifier"], + "adj_list": [["workflow_job_template", "workflow_job_templates"]] + }, + "applications": { + "fields": ["name"], + "adj_list": [["organization", "organizations"]] + }, + "users": { "fields": ["username"], "adj_list": [] }, + "instances": { "fields": ["hostname"], "adj_list": [] } }, "DEFAULT_EXECUTION_ENVIRONMENT": 1, "AWX_MOUNT_ISOLATED_PATHS_ON_K8S": false, From 11d5e5c7d49b1aa997f84c7b4599dc690c19f41d Mon Sep 17 00:00:00 2001 From: Joe Garcia Date: Thu, 13 Apr 2023 13:11:37 -0400 Subject: [PATCH 20/26] Fixes #13402 allow user defined key retrieval from CYBR (#13411) * Fixed #13402 allow user defined key retrieval from CYBR * Add default value to object_property * Raise ValueError if object_property not in response * Raise KeyError instead of ValueError --- awx/main/credential_plugins/aim.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/awx/main/credential_plugins/aim.py b/awx/main/credential_plugins/aim.py index e72fe285f4..048bd1b324 100644 --- a/awx/main/credential_plugins/aim.py +++ b/awx/main/credential_plugins/aim.py @@ -54,6 +54,12 @@ aim_inputs = { 'help_text': _('Lookup query for the object. Ex: Safe=TestSafe;Object=testAccountName123'), }, {'id': 'object_query_format', 'label': _('Object Query Format'), 'type': 'string', 'default': 'Exact', 'choices': ['Exact', 'Regexp']}, + { + 'id': 'object_property', + 'label': _('Object Property'), + 'type': 'string', + 'help_text': _('The property of the object to return. Default: Content Ex: Username, Address, etc.'), + }, { 'id': 'reason', 'label': _('Reason'), @@ -74,6 +80,7 @@ def aim_backend(**kwargs): app_id = kwargs['app_id'] object_query = kwargs['object_query'] object_query_format = kwargs['object_query_format'] + object_property = kwargs.get('object_property', '') reason = kwargs.get('reason', None) if webservice_id == '': webservice_id = 'AIMWebService' @@ -98,7 +105,18 @@ def aim_backend(**kwargs): allow_redirects=False, ) raise_for_status(res) - return res.json()['Content'] + # CCP returns the property name capitalized, username is camel case + # so we need to handle that case + if object_property == '': + object_property = 'Content' + elif object_property.lower() == 'username': + object_property = 'UserName' + elif object_property not in res: + raise KeyError('Property {} not found in object'.format(object_property)) + else: + object_property = object_property.capitalize() + + return res.json()[object_property] aim_plugin = CredentialPlugin('CyberArk Central Credential Provider Lookup', inputs=aim_inputs, backend=aim_backend) From c1455ee12501e3e5a5942450f3cfa0e9aaa56a19 Mon Sep 17 00:00:00 2001 From: Dien Nguyen Date: Thu, 13 Apr 2023 14:36:38 -0400 Subject: [PATCH 21/26] bugfix: add scm_branch to optional_args for workflow_launch (#13254) * add scm_branch to optional_args * add in limits * Update workflow_launch.py remove json from import to pass linting. --------- Co-authored-by: dien nguyen Co-authored-by: Jessica Steurer <70719005+jay-steurer@users.noreply.github.com> --- awx_collection/plugins/modules/workflow_launch.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/awx_collection/plugins/modules/workflow_launch.py b/awx_collection/plugins/modules/workflow_launch.py index 1613e4fa8b..afbf9ca8d6 100644 --- a/awx_collection/plugins/modules/workflow_launch.py +++ b/awx_collection/plugins/modules/workflow_launch.py @@ -91,7 +91,6 @@ EXAMPLES = ''' ''' from ..module_utils.controller_api import ControllerAPIModule -import json def main(): @@ -116,15 +115,18 @@ def main(): name = module.params.get('name') organization = module.params.get('organization') inventory = module.params.get('inventory') - optional_args['limit'] = module.params.get('limit') wait = module.params.get('wait') interval = module.params.get('interval') timeout = module.params.get('timeout') - # Special treatment of extra_vars parameter - extra_vars = module.params.get('extra_vars') - if extra_vars is not None: - optional_args['extra_vars'] = json.dumps(extra_vars) + for field_name in ( + 'limit', + 'extra_vars', + 'scm_branch', + ): + field_val = module.params.get(field_name) + if field_val is not None: + optional_args[field_name] = field_val # Create a datastructure to pass into our job launch post_data = {} From 8719648ff56e06a1cbd6eee14d06f04323e8b787 Mon Sep 17 00:00:00 2001 From: John Westcott IV Date: Thu, 13 Apr 2023 15:02:08 -0400 Subject: [PATCH 22/26] Adding tacacs+ container for testing --- Makefile | 6 +++- tools/docker-compose/README.md | 24 ++++++++++++++ tools/docker-compose/ansible/plumb_tacacs.yml | 32 +++++++++++++++++++ .../sources/templates/docker-compose.yml.j2 | 8 +++++ .../templates/tacacsplus_settings.json.j2 | 7 ++++ 5 files changed, 76 insertions(+), 1 deletion(-) create mode 100644 tools/docker-compose/ansible/plumb_tacacs.yml create mode 100644 tools/docker-compose/ansible/templates/tacacsplus_settings.json.j2 diff --git a/Makefile b/Makefile index 9afc7c7440..7664a32e24 100644 --- a/Makefile +++ b/Makefile @@ -37,6 +37,8 @@ SPLUNK ?= false PROMETHEUS ?= false # If set to true docker-compose will also start a grafana instance GRAFANA ?= false +# If set to true docker-compose will also start a tacacs+ instance +TACACS ?= false VENV_BASE ?= /var/lib/awx/venv @@ -519,7 +521,9 @@ docker-compose-sources: .git/hooks/pre-commit -e enable_ldap=$(LDAP) \ -e enable_splunk=$(SPLUNK) \ -e enable_prometheus=$(PROMETHEUS) \ - -e enable_grafana=$(GRAFANA) $(EXTRA_SOURCES_ANSIBLE_OPTS) + -e enable_grafana=$(GRAFANA) \ + -e enable_tacacs=$(TACACS) \ + $(EXTRA_SOURCES_ANSIBLE_OPTS) docker-compose: awx/projects docker-compose-sources $(DOCKER_COMPOSE) -f tools/docker-compose/_sources/docker-compose.yml $(COMPOSE_OPTS) up $(COMPOSE_UP_OPTS) --remove-orphans diff --git a/tools/docker-compose/README.md b/tools/docker-compose/README.md index b450398ee0..e071f33923 100644 --- a/tools/docker-compose/README.md +++ b/tools/docker-compose/README.md @@ -244,6 +244,7 @@ $ make docker-compose - [SAML and OIDC Integration](#saml-and-oidc-integration) - [OpenLDAP Integration](#openldap-integration) - [Splunk Integration](#splunk-integration) +- [tacacs+ Integration](#tacacs+-integration) ### Start a Shell @@ -472,6 +473,29 @@ ansible-playbook tools/docker-compose/ansible/plumb_splunk.yml Once the playbook is done running Splunk should now be setup in your development environment. You can log into the admin console (see above for username/password) and click on "Searching and Reporting" in the left hand navigation. In the search box enter `source="http:tower_logging_collections"` and click search. +### - tacacs+ Integration + +tacacs+ is an networking protocol that provides external authentication which can be used with AWX. This section describes how to build a reference tacacs+ instance and plumb it with your AWX for testing purposes. + +First, be sure that you have the awx.awx collection installed by running `make install_collection`. + +Anytime you want to run a tacacs+ instance alongside AWX we can start docker-compose with the TACACS option to get a containerized instance with the command: +```bash +TACACS=true make docker-compose +``` + +Once the containers come up a new port (49) should be exposed and the tacacs+ server should be running on those ports. + +Now we are ready to configure and plumb tacacs+ with AWX. To do this we have provided a playbook which will: +* Backup and configure the tacacsplus adapter in AWX. NOTE: this will back up your existing settings but the password fields can not be backed up through the API, you need a DB backup to recover this. + +```bash +export CONTROLLER_USERNAME= +export CONTROLLER_PASSWORD= +ansible-playbook tools/docker-compose/ansible/plumb_tacacs.yml +``` + +Once the playbook is done running tacacs+ should now be setup in your development environment. This server has the accounts listed on https://hub.docker.com/r/dchidell/docker-tacacs ### Prometheus and Grafana integration diff --git a/tools/docker-compose/ansible/plumb_tacacs.yml b/tools/docker-compose/ansible/plumb_tacacs.yml new file mode 100644 index 0000000000..c7dcbe5e22 --- /dev/null +++ b/tools/docker-compose/ansible/plumb_tacacs.yml @@ -0,0 +1,32 @@ +--- +- name: Plumb a tacacs+ instance + hosts: localhost + connection: local + gather_facts: False + vars: + awx_host: "https://localhost:8043" + tasks: + - name: Load existing and new tacacs+ settings + set_fact: + existing_tacacs: "{{ lookup('awx.awx.controller_api', 'settings/tacacsplus', host=awx_host, verify_ssl=false) }}" + new_tacacs: "{{ lookup('template', 'tacacsplus_settings.json.j2') }}" + + - name: Display existing tacacs+ configuration + debug: + msg: + - "Here is your existing tacacsplus configuration for reference:" + - "{{ existing_tacacs }}" + + - pause: + prompt: "Continuing to run this will replace your existing tacacs settings (displayed above). They will all be captured. Be sure that is backed up before continuing" + + - name: Write out the existing content + copy: + dest: "../_sources/existing_tacacsplus_adapter_settings.json" + content: "{{ existing_tacacs }}" + + - name: Configure AWX tacacs+ adapter + awx.awx.settings: + settings: "{{ new_tacacs }}" + controller_host: "{{ awx_host }}" + validate_certs: False diff --git a/tools/docker-compose/ansible/roles/sources/templates/docker-compose.yml.j2 b/tools/docker-compose/ansible/roles/sources/templates/docker-compose.yml.j2 index 7badd37181..6bc49347b2 100644 --- a/tools/docker-compose/ansible/roles/sources/templates/docker-compose.yml.j2 +++ b/tools/docker-compose/ansible/roles/sources/templates/docker-compose.yml.j2 @@ -174,6 +174,14 @@ services: - prometheus depends_on: - prometheus +{% endif %} +{% if enable_tacacs|bool %} + tacacs: + image: dchidell/docker-tacacs + container_name: tools_tacacs_1 + hostname: tacacs + ports: + - "49:49" {% endif %} # A useful container that simply passes through log messages to the console # helpful for testing awx/tower logging diff --git a/tools/docker-compose/ansible/templates/tacacsplus_settings.json.j2 b/tools/docker-compose/ansible/templates/tacacsplus_settings.json.j2 new file mode 100644 index 0000000000..fe9dd8c391 --- /dev/null +++ b/tools/docker-compose/ansible/templates/tacacsplus_settings.json.j2 @@ -0,0 +1,7 @@ +{ + "TACACSPLUS_HOST": "tacacs", + "TACACSPLUS_PORT": 49, + "TACACSPLUS_SECRET": "ciscotacacskey", + "TACACSPLUS_SESSION_TIMEOUT": 5, + "TACACSPLUS_AUTH_PROTOCOL": "ascii" +} From 7cdf471894925e67740eca75901d56f49e1c9d40 Mon Sep 17 00:00:00 2001 From: John Westcott IV <32551173+john-westcott-iv@users.noreply.github.com> Date: Thu, 13 Apr 2023 16:14:06 -0400 Subject: [PATCH 23/26] Fix sat instance var (#13851) * add the fallback satellite_instance_var_id * Removing unnecessary whitespace --------- Co-authored-by: Nikhil Jain --- awx/settings/defaults.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/awx/settings/defaults.py b/awx/settings/defaults.py index 7e1cba1c64..b72147c91f 100644 --- a/awx/settings/defaults.py +++ b/awx/settings/defaults.py @@ -734,10 +734,10 @@ CONTROLLER_INSTANCE_ID_VAR = 'remote_tower_id' # --------------------- # ----- Foreman ----- # --------------------- -SATELLITE6_ENABLED_VAR = 'foreman_enabled' +SATELLITE6_ENABLED_VAR = 'foreman_enabled,foreman.enabled' SATELLITE6_ENABLED_VALUE = 'True' SATELLITE6_EXCLUDE_EMPTY_GROUPS = True -SATELLITE6_INSTANCE_ID_VAR = 'foreman_id' +SATELLITE6_INSTANCE_ID_VAR = 'foreman_id,foreman.id' # SATELLITE6_GROUP_PREFIX and SATELLITE6_GROUP_PATTERNS defined in source vars # ---------------- From 342e9197b8934050452279a8414def0d4ace1409 Mon Sep 17 00:00:00 2001 From: Alan Rominger Date: Thu, 13 Apr 2023 22:36:36 -0400 Subject: [PATCH 24/26] Customize application_name for different connections in dispatcher service (#13074) * Introduce new method in settings, import in-line w NOQA mark * Further refine the app_name to use shorter service names like dispatcher * Clean up listener logic, change some names --- awx/main/dispatch/__init__.py | 11 +++++++---- awx/main/dispatch/periodic.py | 4 ++++ awx/main/dispatch/worker/base.py | 2 ++ awx/main/utils/db.py | 7 +++++++ awx/settings/application_name.py | 31 +++++++++++++++++++++++++++++++ awx/settings/development.py | 7 +++++-- awx/settings/production.py | 8 +++++--- 7 files changed, 61 insertions(+), 9 deletions(-) create mode 100644 awx/settings/application_name.py diff --git a/awx/main/dispatch/__init__.py b/awx/main/dispatch/__init__.py index 2029f540c2..e3cd64ce6d 100644 --- a/awx/main/dispatch/__init__.py +++ b/awx/main/dispatch/__init__.py @@ -4,6 +4,8 @@ import select from contextlib import contextmanager +from awx.settings.application_name import get_application_name + from django.conf import settings from django.db import connection as pg_connection @@ -83,10 +85,11 @@ def pg_bus_conn(new_connection=False): ''' if new_connection: - conf = settings.DATABASES['default'] - conn = psycopg2.connect( - dbname=conf['NAME'], host=conf['HOST'], user=conf['USER'], password=conf['PASSWORD'], port=conf['PORT'], **conf.get("OPTIONS", {}) - ) + conf = settings.DATABASES['default'].copy() + conf['OPTIONS'] = conf.get('OPTIONS', {}).copy() + # Modify the application name to distinguish from other connections the process might use + conf['OPTIONS']['application_name'] = get_application_name(settings.CLUSTER_HOST_ID, function='listener') + conn = psycopg2.connect(dbname=conf['NAME'], host=conf['HOST'], user=conf['USER'], password=conf['PASSWORD'], port=conf['PORT'], **conf['OPTIONS']) # Django connection.cursor().connection doesn't have autocommit=True on by default conn.set_session(autocommit=True) else: diff --git a/awx/main/dispatch/periodic.py b/awx/main/dispatch/periodic.py index e3e7da5db9..aac8427b5a 100644 --- a/awx/main/dispatch/periodic.py +++ b/awx/main/dispatch/periodic.py @@ -10,6 +10,7 @@ from django_guid import set_guid from django_guid.utils import generate_guid from awx.main.dispatch.worker import TaskWorker +from awx.main.utils.db import set_connection_name logger = logging.getLogger('awx.main.dispatch.periodic') @@ -21,6 +22,9 @@ class Scheduler(Scheduler): def run(): ppid = os.getppid() logger.warning('periodic beat started') + + set_connection_name('periodic') # set application_name to distinguish from other dispatcher processes + while True: if os.getppid() != ppid: # if the parent PID changes, this process has been orphaned diff --git a/awx/main/dispatch/worker/base.py b/awx/main/dispatch/worker/base.py index a7b0d83e95..9a9d4c803c 100644 --- a/awx/main/dispatch/worker/base.py +++ b/awx/main/dispatch/worker/base.py @@ -18,6 +18,7 @@ from django.conf import settings from awx.main.dispatch.pool import WorkerPool from awx.main.dispatch import pg_bus_conn from awx.main.utils.common import log_excess_runtime +from awx.main.utils.db import set_connection_name if 'run_callback_receiver' in sys.argv: logger = logging.getLogger('awx.main.commands.run_callback_receiver') @@ -219,6 +220,7 @@ class BaseWorker(object): def work_loop(self, queue, finished, idx, *args): ppid = os.getppid() signal_handler = WorkerSignalHandler() + set_connection_name('worker') # set application_name to distinguish from other dispatcher processes while not signal_handler.kill_now: # if the parent PID changes, this process has been orphaned # via e.g., segfault or sigkill, we should exit too diff --git a/awx/main/utils/db.py b/awx/main/utils/db.py index 5574d4ea91..4117c5274c 100644 --- a/awx/main/utils/db.py +++ b/awx/main/utils/db.py @@ -3,6 +3,9 @@ from itertools import chain +from awx.settings.application_name import set_application_name +from django.conf import settings + def get_all_field_names(model): # Implements compatibility with _meta.get_all_field_names @@ -18,3 +21,7 @@ def get_all_field_names(model): ) ) ) + + +def set_connection_name(function): + set_application_name(settings.DATABASES, settings.CLUSTER_HOST_ID, function=function) diff --git a/awx/settings/application_name.py b/awx/settings/application_name.py new file mode 100644 index 0000000000..25c68acfd3 --- /dev/null +++ b/awx/settings/application_name.py @@ -0,0 +1,31 @@ +import os +import sys + + +def get_service_name(argv): + ''' + Return best-effort guess as to the name of this service + ''' + for arg in argv: + if arg == '-m': + continue + if 'python' in arg: + continue + if 'manage' in arg: + continue + if arg.startswith('run_'): + return arg[len('run_') :] + return arg + + +def get_application_name(CLUSTER_HOST_ID, function=''): + if function: + function = f'_{function}' + return f'awx-{os.getpid()}-{get_service_name(sys.argv)}{function}-{CLUSTER_HOST_ID}'[:63] + + +def set_application_name(DATABASES, CLUSTER_HOST_ID, function=''): + if 'sqlite3' in DATABASES['default']['ENGINE']: + return + options_dict = DATABASES['default'].setdefault('OPTIONS', dict()) + options_dict['application_name'] = get_application_name(CLUSTER_HOST_ID, function) diff --git a/awx/settings/development.py b/awx/settings/development.py index 1be4b72956..b8b911b07c 100644 --- a/awx/settings/development.py +++ b/awx/settings/development.py @@ -105,8 +105,11 @@ AWX_CALLBACK_PROFILE = True AWX_DISABLE_TASK_MANAGERS = False # ======================!!!!!!! FOR DEVELOPMENT ONLY !!!!!!!================================= -if 'sqlite3' not in DATABASES['default']['ENGINE']: # noqa - DATABASES['default'].setdefault('OPTIONS', dict()).setdefault('application_name', f'{CLUSTER_HOST_ID}-{os.getpid()}-{" ".join(sys.argv)}'[:63]) # noqa +from .application_name import set_application_name + +set_application_name(DATABASES, CLUSTER_HOST_ID) + +del set_application_name # If any local_*.py files are present in awx/settings/, use them to override # default settings for development. If not present, we can still run using diff --git a/awx/settings/production.py b/awx/settings/production.py index 3dce95deb0..4f25d274b1 100644 --- a/awx/settings/production.py +++ b/awx/settings/production.py @@ -100,6 +100,8 @@ except IOError: # The below runs AFTER all of the custom settings are imported. -DATABASES.setdefault('default', dict()).setdefault('OPTIONS', dict()).setdefault( - 'application_name', f'{CLUSTER_HOST_ID}-{os.getpid()}-{" ".join(sys.argv)}'[:63] # NOQA -) # noqa +from .application_name import set_application_name + +set_application_name(DATABASES, CLUSTER_HOST_ID) # NOQA + +del set_application_name From 36c9c9cdc476ebc8d968cec2560c1e73b5d0ab4a Mon Sep 17 00:00:00 2001 From: Alan Rominger Date: Fri, 14 Apr 2023 09:51:53 -0400 Subject: [PATCH 25/26] Move integration tests to be consistent with the rest --- .../integration/targets/bulk_host_create/{ => tasks}/main.yml | 0 .../integration/targets/bulk_job_launch/{ => tasks}/main.yml | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename awx_collection/tests/integration/targets/bulk_host_create/{ => tasks}/main.yml (100%) rename awx_collection/tests/integration/targets/bulk_job_launch/{ => tasks}/main.yml (100%) diff --git a/awx_collection/tests/integration/targets/bulk_host_create/main.yml b/awx_collection/tests/integration/targets/bulk_host_create/tasks/main.yml similarity index 100% rename from awx_collection/tests/integration/targets/bulk_host_create/main.yml rename to awx_collection/tests/integration/targets/bulk_host_create/tasks/main.yml diff --git a/awx_collection/tests/integration/targets/bulk_job_launch/main.yml b/awx_collection/tests/integration/targets/bulk_job_launch/tasks/main.yml similarity index 100% rename from awx_collection/tests/integration/targets/bulk_job_launch/main.yml rename to awx_collection/tests/integration/targets/bulk_job_launch/tasks/main.yml From 32f7dfece1205580a66bc772ed47ac6ef25de229 Mon Sep 17 00:00:00 2001 From: John Westcott IV <32551173+john-westcott-iv@users.noreply.github.com> Date: Tue, 18 Apr 2023 09:29:25 -0400 Subject: [PATCH 26/26] Changing check for all in awx.awx.export (#13854) --- awx_collection/plugins/modules/export.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx_collection/plugins/modules/export.py b/awx_collection/plugins/modules/export.py index d04e996056..871c89d90e 100644 --- a/awx_collection/plugins/modules/export.py +++ b/awx_collection/plugins/modules/export.py @@ -159,7 +159,7 @@ def main(): # Here we are going to setup a dict of values to export export_args = {} for resource in EXPORTABLE_RESOURCES: - if module.params.get('all') or module.params.get(resource) == 'all': + if module.params.get('all') or module.params.get(resource) == ['all']: # If we are exporting everything or we got the keyword "all" we pass in an empty string for this asset type export_args[resource] = '' else: