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 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 diff --git a/.github/workflows/promote.yml b/.github/workflows/promote.yml index 31c61824b1..bf6ef17852 100644 --- a/.github/workflows/promote.yml +++ b/.github/workflows/promote.yml @@ -8,6 +8,9 @@ on: release: types: [published] +permissions: + contents: read # to fetch code (actions/checkout) + jobs: promote: if: endsWith(github.repository, '/awx') 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/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/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/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): 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/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) 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) diff --git a/awx/main/credential_plugins/dsv.py b/awx/main/credential_plugins/dsv.py index 9c89199710..7ebbe60401 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']], # fmt: skip ) diff --git a/awx/main/credential_plugins/tss.py b/awx/main/credential_plugins/tss.py index 887cb2d454..333596e85d 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) 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/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) 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_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/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..c87f0a6c1a 100644 --- a/awx/main/tests/functional/conftest.py +++ b/awx/main/tests/functional/conftest.py @@ -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..092c970539 100644 --- a/awx/main/tests/functional/test_notifications.py +++ b/awx/main/tests/functional/test_notifications.py @@ -218,3 +218,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): 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/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 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/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 # ---------------- 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 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/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]); 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, 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/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: 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: 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 = {} 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 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 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 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" +} 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]