From e4c11561cc9b845974ba972aedc2825efc94e4ef Mon Sep 17 00:00:00 2001 From: Djebran Lezzoum Date: Wed, 2 Oct 2024 15:50:17 +0200 Subject: [PATCH] Remove TACACS+ authentication (#15547) Remove TACACS+ authentication from AWX. Co-authored-by: Hao Liu <44379968+TheRealHaoLiu@users.noreply.github.com> --- Makefile | 3 - .../0011_remove_tacacs_plus_auth_conf.py | 26 ++++ .../tests/functional/api/test_settings.py | 15 --- awx/settings/defaults.py | 10 -- awx/sso/backends.py | 51 -------- awx/sso/common.py | 2 - awx/sso/conf.py | 97 +-------------- awx/sso/fields.py | 1 - awx/sso/models.py | 1 + awx/sso/tests/conftest.py | 34 ----- awx/sso/tests/functional/test_common.py | 3 +- .../test_get_or_set_enterprise_user.py | 37 ------ awx/sso/tests/unit/test_tacacsplus.py | 116 ------------------ awx/sso/validators.py | 11 +- awxkit/awxkit/api/pages/settings.py | 1 - awxkit/awxkit/api/resources.py | 1 - docs/auth/README.md | 5 +- docs/auth/tacacsplus.md | 51 -------- .../configure_awx_authentication.rst | 2 - docs/docsite/rst/administration/ent_auth.rst | 36 ------ licenses/tacacs-plus.txt | 24 ---- requirements/requirements.in | 1 - requirements/requirements.txt | 2 - tools/docker-compose/README.md | 25 ---- tools/docker-compose/ansible/plumb_tacacs.yml | 32 ----- .../sources/templates/docker-compose.yml.j2 | 8 -- .../templates/tacacsplus_settings.json.j2 | 7 -- 27 files changed, 31 insertions(+), 571 deletions(-) create mode 100644 awx/conf/migrations/0011_remove_tacacs_plus_auth_conf.py delete mode 100644 awx/sso/tests/conftest.py delete mode 100644 awx/sso/tests/functional/test_get_or_set_enterprise_user.py delete mode 100644 awx/sso/tests/unit/test_tacacsplus.py delete mode 100644 docs/auth/tacacsplus.md delete mode 100644 licenses/tacacs-plus.txt delete mode 100644 tools/docker-compose/ansible/plumb_tacacs.yml delete mode 100644 tools/docker-compose/ansible/templates/tacacsplus_settings.json.j2 diff --git a/Makefile b/Makefile index 257590f091c8..686a9eb4ff2c 100644 --- a/Makefile +++ b/Makefile @@ -43,8 +43,6 @@ GRAFANA ?= false VAULT ?= false # If set to true docker-compose will also start a hashicorp vault instance with TLS enabled VAULT_TLS ?= false -# If set to true docker-compose will also start a tacacs+ instance -TACACS ?= false # If set to true docker-compose will also start an OpenTelemetry Collector instance OTEL ?= false # If set to true docker-compose will also start a Loki instance @@ -511,7 +509,6 @@ docker-compose-sources: .git/hooks/pre-commit -e enable_grafana=$(GRAFANA) \ -e enable_vault=$(VAULT) \ -e vault_tls=$(VAULT_TLS) \ - -e enable_tacacs=$(TACACS) \ -e enable_otel=$(OTEL) \ -e enable_loki=$(LOKI) \ -e install_editable_dependencies=$(EDITABLE_DEPENDENCIES) \ diff --git a/awx/conf/migrations/0011_remove_tacacs_plus_auth_conf.py b/awx/conf/migrations/0011_remove_tacacs_plus_auth_conf.py new file mode 100644 index 000000000000..229c40bcc138 --- /dev/null +++ b/awx/conf/migrations/0011_remove_tacacs_plus_auth_conf.py @@ -0,0 +1,26 @@ +from django.db import migrations + +TACACS_PLUS_AUTH_CONF_KEYS = [ + 'TACACSPLUS_HOST', + 'TACACSPLUS_PORT', + 'TACACSPLUS_SECRET', + 'TACACSPLUS_SESSION_TIMEOUT', + 'TACACSPLUS_AUTH_PROTOCOL', + 'TACACSPLUS_REM_ADDR', +] + + +def remove_tacacs_plus_auth_conf(apps, scheme_editor): + setting = apps.get_model('conf', 'Setting') + setting.objects.filter(key__in=TACACS_PLUS_AUTH_CONF_KEYS).delete() + + +class Migration(migrations.Migration): + + dependencies = [ + ('conf', '0010_change_to_JSONField'), + ] + + operations = [ + migrations.RunPython(remove_tacacs_plus_auth_conf), + ] diff --git a/awx/main/tests/functional/api/test_settings.py b/awx/main/tests/functional/api/test_settings.py index 6f6c2a5e09af..67c92d8b9f0e 100644 --- a/awx/main/tests/functional/api/test_settings.py +++ b/awx/main/tests/functional/api/test_settings.py @@ -98,21 +98,6 @@ def test_radius_settings(get, put, patch, delete, admin, settings): assert settings.RADIUS_SECRET == '' -@pytest.mark.django_db -def test_tacacsplus_settings(get, put, patch, admin): - url = reverse('api:setting_singleton_detail', kwargs={'category_slug': 'tacacsplus'}) - response = get(url, user=admin, expect=200) - put(url, user=admin, data=response.data, expect=200) - patch(url, user=admin, data={'TACACSPLUS_SECRET': 'mysecret'}, expect=200) - patch(url, user=admin, data={'TACACSPLUS_SECRET': ''}, expect=200) - patch(url, user=admin, data={'TACACSPLUS_HOST': 'localhost'}, expect=400) - patch(url, user=admin, data={'TACACSPLUS_SECRET': 'mysecret'}, expect=200) - patch(url, user=admin, data={'TACACSPLUS_HOST': 'localhost'}, expect=200) - patch(url, user=admin, data={'TACACSPLUS_HOST': '', 'TACACSPLUS_SECRET': ''}, expect=200) - patch(url, user=admin, data={'TACACSPLUS_HOST': 'localhost', 'TACACSPLUS_SECRET': ''}, expect=400) - patch(url, user=admin, data={'TACACSPLUS_HOST': 'localhost', 'TACACSPLUS_SECRET': 'mysecret'}, expect=200) - - @pytest.mark.django_db def test_ui_settings(get, put, patch, delete, admin): url = reverse('api:setting_singleton_detail', kwargs={'category_slug': 'ui'}) diff --git a/awx/settings/defaults.py b/awx/settings/defaults.py index 28203989077b..b99c2af7b170 100644 --- a/awx/settings/defaults.py +++ b/awx/settings/defaults.py @@ -393,7 +393,6 @@ AUTHENTICATION_BACKENDS = ( 'awx.sso.backends.RADIUSBackend', - 'awx.sso.backends.TACACSPlusBackend', 'social_core.backends.google.GoogleOAuth2', 'social_core.backends.github.GithubOAuth2', 'social_core.backends.github.GithubOrganizationOAuth2', @@ -424,15 +423,6 @@ RADIUS_PORT = 1812 RADIUS_SECRET = '' -# TACACS+ settings (default host to empty string to skip using TACACS+ auth). -# Note: These settings may be overridden by database settings. -TACACSPLUS_HOST = '' -TACACSPLUS_PORT = 49 -TACACSPLUS_SECRET = '' -TACACSPLUS_SESSION_TIMEOUT = 5 -TACACSPLUS_AUTH_PROTOCOL = 'ascii' -TACACSPLUS_REM_ADDR = False - # Enable / Disable HTTP Basic Authentication used in the API browser # Note: Session limits are not enforced when using HTTP Basic Authentication. # Note: This setting may be overridden by database settings. diff --git a/awx/sso/backends.py b/awx/sso/backends.py index 3b60cb223e67..7b29df23766d 100644 --- a/awx/sso/backends.py +++ b/awx/sso/backends.py @@ -13,9 +13,6 @@ # radiusauth from radiusauth.backends import RADIUSBackend as BaseRADIUSBackend -# tacacs+ auth -import tacacs_plus - # social from social_core.backends.saml import OID_USERID from social_core.backends.saml import SAMLAuth as BaseSAMLAuth @@ -69,54 +66,6 @@ def get_django_user(self, username, password=None, groups=[], is_staff=False, is return _get_or_set_enterprise_user(force_str(username), force_str(password), 'radius') -class TACACSPlusBackend(object): - """ - Custom TACACS+ auth backend for AWX - """ - - def authenticate(self, request, username, password): - if not django_settings.TACACSPLUS_HOST: - return None - try: - # Upstream TACACS+ client does not accept non-string, so convert if needed. - tacacs_client = tacacs_plus.TACACSClient( - django_settings.TACACSPLUS_HOST, - django_settings.TACACSPLUS_PORT, - django_settings.TACACSPLUS_SECRET, - timeout=django_settings.TACACSPLUS_SESSION_TIMEOUT, - ) - auth_kwargs = {'authen_type': tacacs_plus.TAC_PLUS_AUTHEN_TYPES[django_settings.TACACSPLUS_AUTH_PROTOCOL]} - if django_settings.TACACSPLUS_AUTH_PROTOCOL: - client_ip = self._get_client_ip(request) - if client_ip: - auth_kwargs['rem_addr'] = client_ip - auth = tacacs_client.authenticate(username, password, **auth_kwargs) - except Exception as e: - logger.exception("TACACS+ Authentication Error: %s" % str(e)) - return None - if auth.valid: - return _get_or_set_enterprise_user(username, password, 'tacacs+') - - def get_user(self, user_id): - if not django_settings.TACACSPLUS_HOST: - return None - try: - return User.objects.get(pk=user_id) - except User.DoesNotExist: - return None - - def _get_client_ip(self, request): - if not request or not hasattr(request, 'META'): - return None - - x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR') - if x_forwarded_for: - ip = x_forwarded_for.split(',')[0] - else: - ip = request.META.get('REMOTE_ADDR') - return ip - - class TowerSAMLIdentityProvider(BaseSAMLIdentityProvider): """ Custom Identity Provider to make attributes to what we expect. diff --git a/awx/sso/common.py b/awx/sso/common.py index b57506f67dac..8f5b3a8b43b0 100644 --- a/awx/sso/common.py +++ b/awx/sso/common.py @@ -186,11 +186,9 @@ def get_external_account(user): def is_remote_auth_enabled(): from django.conf import settings - # Append Radius, TACACS+ and SAML options settings_that_turn_on_remote_auth = [ 'SOCIAL_AUTH_SAML_ENABLED_IDPS', 'RADIUS_SERVER', - 'TACACSPLUS_HOST', ] # Also include any SOCAIL_AUTH_*KEY (except SAML) for social_auth_key in dir(settings): diff --git a/awx/sso/conf.py b/awx/sso/conf.py index 332815deb6e7..44f7ec26b190 100644 --- a/awx/sso/conf.py +++ b/awx/sso/conf.py @@ -7,11 +7,8 @@ from django.urls import reverse from django.utils.translation import gettext_lazy as _ -# Django REST Framework -from rest_framework import serializers - # AWX -from awx.conf import register, register_validate, fields +from awx.conf import register, fields from awx.sso.fields import ( AuthenticationBackendsField, SAMLContactField, @@ -25,7 +22,6 @@ SocialTeamMapField, ) from awx.main.validators import validate_private_key, validate_certificate -from awx.sso.validators import validate_tacacsplus_disallow_nonascii # noqa class SocialAuthCallbackURL(object): @@ -187,79 +183,6 @@ def __call__(self): encrypted=True, ) - ############################################################################### - # TACACSPLUS AUTHENTICATION SETTINGS - ############################################################################### - - register( - 'TACACSPLUS_HOST', - field_class=fields.CharField, - allow_blank=True, - default='', - label=_('TACACS+ Server'), - help_text=_('Hostname of TACACS+ server.'), - category=_('TACACS+'), - category_slug='tacacsplus', - ) - - register( - 'TACACSPLUS_PORT', - field_class=fields.IntegerField, - min_value=1, - max_value=65535, - default=49, - label=_('TACACS+ Port'), - help_text=_('Port number of TACACS+ server.'), - category=_('TACACS+'), - category_slug='tacacsplus', - ) - - register( - 'TACACSPLUS_SECRET', - field_class=fields.CharField, - allow_blank=True, - default='', - validators=[validate_tacacsplus_disallow_nonascii], - label=_('TACACS+ Secret'), - help_text=_('Shared secret for authenticating to TACACS+ server.'), - category=_('TACACS+'), - category_slug='tacacsplus', - encrypted=True, - ) - - register( - 'TACACSPLUS_SESSION_TIMEOUT', - field_class=fields.IntegerField, - min_value=0, - default=5, - label=_('TACACS+ Auth Session Timeout'), - help_text=_('TACACS+ session timeout value in seconds, 0 disables timeout.'), - category=_('TACACS+'), - category_slug='tacacsplus', - unit=_('seconds'), - ) - - register( - 'TACACSPLUS_AUTH_PROTOCOL', - field_class=fields.ChoiceField, - choices=['ascii', 'pap'], - default='ascii', - label=_('TACACS+ Authentication Protocol'), - help_text=_('Choose the authentication protocol used by TACACS+ client.'), - category=_('TACACS+'), - category_slug='tacacsplus', - ) - - register( - 'TACACSPLUS_REM_ADDR', - field_class=fields.BooleanField, - default=True, - label=_('TACACS+ client address sending enabled'), - help_text=_('Enable the client address sending by TACACS+ client.'), - category=_('TACACS+'), - category_slug='tacacsplus', - ) - ############################################################################### # GOOGLE OAUTH2 AUTHENTICATION SETTINGS ############################################################################### @@ -1344,21 +1267,3 @@ def get_saml_entity_id(): 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'): - return attrs - errors = [] - host = serializer.instance.TACACSPLUS_HOST - if 'TACACSPLUS_HOST' in attrs: - host = attrs['TACACSPLUS_HOST'] - secret = serializer.instance.TACACSPLUS_SECRET - if 'TACACSPLUS_SECRET' in attrs: - secret = attrs['TACACSPLUS_SECRET'] - if host and not secret: - errors.append('TACACSPLUS_SECRET is required when TACACSPLUS_HOST is provided.') - if errors: - raise serializers.ValidationError(_('\n'.join(errors))) - return attrs - - register_validate('tacacsplus', tacacs_validate) diff --git a/awx/sso/fields.py b/awx/sso/fields.py index d0ee30316992..872d18e69acf 100644 --- a/awx/sso/fields.py +++ b/awx/sso/fields.py @@ -14,7 +14,6 @@ # AWX from awx.conf import fields from awx.main.validators import validate_certificate -from awx.sso.validators import validate_tacacsplus_disallow_nonascii # noqa def get_subclasses(cls): diff --git a/awx/sso/models.py b/awx/sso/models.py index 28eb23857f4b..5973aa3c1090 100644 --- a/awx/sso/models.py +++ b/awx/sso/models.py @@ -7,6 +7,7 @@ from django.utils.translation import gettext_lazy as _ +# todo: this model to be removed as part of sso removal issue AAP-28380 class UserEnterpriseAuth(models.Model): """Enterprise Auth association model""" diff --git a/awx/sso/tests/conftest.py b/awx/sso/tests/conftest.py deleted file mode 100644 index f94b1c528f6d..000000000000 --- a/awx/sso/tests/conftest.py +++ /dev/null @@ -1,34 +0,0 @@ -import pytest - -from django.contrib.auth.models import User - -from awx.sso.backends import TACACSPlusBackend -from awx.sso.models import UserEnterpriseAuth - - -@pytest.fixture -def tacacsplus_backend(): - return TACACSPlusBackend() - - -@pytest.fixture -def existing_normal_user(): - try: - user = User.objects.get(username="alice") - except User.DoesNotExist: - user = User(username="alice", password="password") - user.save() - return user - - -@pytest.fixture -def existing_tacacsplus_user(): - try: - user = User.objects.get(username="foo") - except User.DoesNotExist: - user = User(username="foo") - user.set_unusable_password() - user.save() - enterprise_auth = UserEnterpriseAuth(user=user, provider='tacacs+') - enterprise_auth.save() - return user diff --git a/awx/sso/tests/functional/test_common.py b/awx/sso/tests/functional/test_common.py index 18e485c6a10c..430ea55a0426 100644 --- a/awx/sso/tests/functional/test_common.py +++ b/awx/sso/tests/functional/test_common.py @@ -324,7 +324,7 @@ def test_get_external_account(self, enable_social, enable_enterprise, expected_r if enable_enterprise: from awx.sso.models import UserEnterpriseAuth - enterprise_auth = UserEnterpriseAuth(user=user, provider='tacacs+') + enterprise_auth = UserEnterpriseAuth(user=user, provider='saml') enterprise_auth.save() assert get_external_account(user) == expected_results @@ -336,7 +336,6 @@ def test_get_external_account(self, enable_social, enable_enterprise, expected_r ('JUNK_SETTING', False), ('SOCIAL_AUTH_SAML_ENABLED_IDPS', True), ('RADIUS_SERVER', True), - ('TACACSPLUS_HOST', True), # Set some SOCIAL_SOCIAL_AUTH_OIDC_KEYAUTH_*_KEY settings ('SOCIAL_AUTH_AZUREAD_OAUTH2_KEY', True), ('SOCIAL_AUTH_GITHUB_ENTERPRISE_KEY', True), diff --git a/awx/sso/tests/functional/test_get_or_set_enterprise_user.py b/awx/sso/tests/functional/test_get_or_set_enterprise_user.py deleted file mode 100644 index 3f37b41df319..000000000000 --- a/awx/sso/tests/functional/test_get_or_set_enterprise_user.py +++ /dev/null @@ -1,37 +0,0 @@ -# Python -import pytest -from unittest import mock - -# AWX -from awx.sso.backends import _get_or_set_enterprise_user - - -@pytest.mark.django_db -def test_fetch_user_if_exist(existing_tacacsplus_user): - with mock.patch('awx.sso.backends.logger') as mocked_logger: - new_user = _get_or_set_enterprise_user("foo", "password", "tacacs+") - mocked_logger.debug.assert_not_called() - mocked_logger.warning.assert_not_called() - assert new_user == existing_tacacsplus_user - - -@pytest.mark.django_db -def test_create_user_if_not_exist(existing_tacacsplus_user): - with mock.patch('awx.sso.backends.logger') as mocked_logger: - new_user = _get_or_set_enterprise_user("bar", "password", "tacacs+") - mocked_logger.debug.assert_called_once_with(u'Created enterprise user bar via TACACS+ backend.') - assert new_user != existing_tacacsplus_user - - -@pytest.mark.django_db -def test_created_user_has_no_usable_password(): - new_user = _get_or_set_enterprise_user("bar", "password", "tacacs+") - assert not new_user.has_usable_password() - - -@pytest.mark.django_db -def test_non_enterprise_user_does_not_get_pass(existing_normal_user): - with mock.patch('awx.sso.backends.logger') as mocked_logger: - new_user = _get_or_set_enterprise_user("alice", "password", "tacacs+") - mocked_logger.warning.assert_called_once_with(u'Enterprise user alice already defined in Tower.') - assert new_user is None diff --git a/awx/sso/tests/unit/test_tacacsplus.py b/awx/sso/tests/unit/test_tacacsplus.py deleted file mode 100644 index 49315a96432c..000000000000 --- a/awx/sso/tests/unit/test_tacacsplus.py +++ /dev/null @@ -1,116 +0,0 @@ -from unittest import mock -import pytest - - -def test_empty_host_fails_auth(tacacsplus_backend): - with mock.patch('awx.sso.backends.django_settings') as settings: - settings.TACACSPLUS_HOST = '' - ret_user = tacacsplus_backend.authenticate(None, u"user", u"pass") - assert ret_user is None - - -def test_client_raises_exception(tacacsplus_backend): - client = mock.MagicMock() - client.authenticate.side_effect = Exception("foo") - with mock.patch('awx.sso.backends.django_settings') as settings, mock.patch('awx.sso.backends.logger') as logger, mock.patch( - 'tacacs_plus.TACACSClient', return_value=client - ): - settings.TACACSPLUS_HOST = 'localhost' - settings.TACACSPLUS_AUTH_PROTOCOL = 'ascii' - ret_user = tacacsplus_backend.authenticate(None, u"user", u"pass") - assert ret_user is None - logger.exception.assert_called_once_with("TACACS+ Authentication Error: foo") - - -def test_client_return_invalid_fails_auth(tacacsplus_backend): - auth = mock.MagicMock() - auth.valid = False - client = mock.MagicMock() - client.authenticate.return_value = auth - with mock.patch('awx.sso.backends.django_settings') as settings, mock.patch('tacacs_plus.TACACSClient', return_value=client): - settings.TACACSPLUS_HOST = 'localhost' - settings.TACACSPLUS_AUTH_PROTOCOL = 'ascii' - ret_user = tacacsplus_backend.authenticate(None, u"user", u"pass") - assert ret_user is None - - -def test_client_return_valid_passes_auth(tacacsplus_backend): - auth = mock.MagicMock() - auth.valid = True - client = mock.MagicMock() - client.authenticate.return_value = auth - user = mock.MagicMock() - user.has_usable_password = mock.MagicMock(return_value=False) - with mock.patch('awx.sso.backends.django_settings') as settings, mock.patch('tacacs_plus.TACACSClient', return_value=client), mock.patch( - 'awx.sso.backends._get_or_set_enterprise_user', return_value=user - ): - settings.TACACSPLUS_HOST = 'localhost' - settings.TACACSPLUS_AUTH_PROTOCOL = 'ascii' - ret_user = tacacsplus_backend.authenticate(None, u"user", u"pass") - assert ret_user == user - - -@pytest.mark.parametrize( - "client_ip_header,client_ip_header_value,expected_client_ip", - [('HTTP_X_FORWARDED_FOR', '12.34.56.78, 23.45.67.89', '12.34.56.78'), ('REMOTE_ADDR', '12.34.56.78', '12.34.56.78')], -) -def test_remote_addr_is_passed_to_client_if_available_and_setting_enabled(tacacsplus_backend, client_ip_header, client_ip_header_value, expected_client_ip): - auth = mock.MagicMock() - auth.valid = True - client = mock.MagicMock() - client.authenticate.return_value = auth - user = mock.MagicMock() - user.has_usable_password = mock.MagicMock(return_value=False) - request = mock.MagicMock() - request.META = { - client_ip_header: client_ip_header_value, - } - with mock.patch('awx.sso.backends.django_settings') as settings, mock.patch('tacacs_plus.TACACSClient', return_value=client), mock.patch( - 'awx.sso.backends._get_or_set_enterprise_user', return_value=user - ): - settings.TACACSPLUS_HOST = 'localhost' - settings.TACACSPLUS_AUTH_PROTOCOL = 'ascii' - settings.TACACSPLUS_REM_ADDR = True - tacacsplus_backend.authenticate(request, u"user", u"pass") - - client.authenticate.assert_called_once_with('user', 'pass', authen_type=1, rem_addr=expected_client_ip) - - -def test_remote_addr_is_completely_ignored_in_client_call_if_setting_is_disabled(tacacsplus_backend): - auth = mock.MagicMock() - auth.valid = True - client = mock.MagicMock() - client.authenticate.return_value = auth - user = mock.MagicMock() - user.has_usable_password = mock.MagicMock(return_value=False) - request = mock.MagicMock() - request.META = {} - with mock.patch('awx.sso.backends.django_settings') as settings, mock.patch('tacacs_plus.TACACSClient', return_value=client), mock.patch( - 'awx.sso.backends._get_or_set_enterprise_user', return_value=user - ): - settings.TACACSPLUS_HOST = 'localhost' - settings.TACACSPLUS_AUTH_PROTOCOL = 'ascii' - settings.TACACSPLUS_REM_ADDR = False - tacacsplus_backend.authenticate(request, u"user", u"pass") - - client.authenticate.assert_called_once_with('user', 'pass', authen_type=1) - - -def test_remote_addr_is_completely_ignored_in_client_call_if_unavailable_and_setting_enabled(tacacsplus_backend): - auth = mock.MagicMock() - auth.valid = True - client = mock.MagicMock() - client.authenticate.return_value = auth - user = mock.MagicMock() - user.has_usable_password = mock.MagicMock(return_value=False) - request = mock.MagicMock() - request.META = {} - with mock.patch('awx.sso.backends.django_settings') as settings, mock.patch('tacacs_plus.TACACSClient', return_value=client), mock.patch( - 'awx.sso.backends._get_or_set_enterprise_user', return_value=user - ): - settings.TACACSPLUS_HOST = 'localhost' - settings.TACACSPLUS_AUTH_PROTOCOL = 'ascii' - settings.TACACSPLUS_REM_ADDR = True - tacacsplus_backend.authenticate(request, u"user", u"pass") - - client.authenticate.assert_called_once_with('user', 'pass', authen_type=1) diff --git a/awx/sso/validators.py b/awx/sso/validators.py index a93f22efb8f3..07a582532a78 100644 --- a/awx/sso/validators.py +++ b/awx/sso/validators.py @@ -2,13 +2,4 @@ from django.core.exceptions import ValidationError from django.utils.translation import gettext_lazy as _ -__all__ = [ - 'validate_tacacsplus_disallow_nonascii', -] - - -def validate_tacacsplus_disallow_nonascii(value): - try: - value.encode('ascii') - except (UnicodeEncodeError, UnicodeDecodeError): - raise ValidationError(_('TACACS+ secret does not allow non-ascii characters')) +__all__ = [] diff --git a/awxkit/awxkit/api/pages/settings.py b/awxkit/awxkit/api/pages/settings.py index bb29612ee81c..74807bcf5c4b 100644 --- a/awxkit/awxkit/api/pages/settings.py +++ b/awxkit/awxkit/api/pages/settings.py @@ -21,7 +21,6 @@ class Setting(base.Base): resources.settings_radius, resources.settings_saml, resources.settings_system, - resources.settings_tacacsplus, resources.settings_ui, resources.settings_user, resources.settings_user_defaults, diff --git a/awxkit/awxkit/api/resources.py b/awxkit/awxkit/api/resources.py index 7b73734e2a97..a14ec8730cd1 100644 --- a/awxkit/awxkit/api/resources.py +++ b/awxkit/awxkit/api/resources.py @@ -220,7 +220,6 @@ class Resources(object): _settings_radius = 'settings/radius/' _settings_saml = 'settings/saml/' _settings_system = 'settings/system/' - _settings_tacacsplus = 'settings/tacacsplus/' _settings_ui = 'settings/ui/' _settings_user = 'settings/user/' _settings_user_defaults = 'settings/user-defaults/' diff --git a/docs/auth/README.md b/docs/auth/README.md index 62be30a69358..92946746f06c 100644 --- a/docs/auth/README.md +++ b/docs/auth/README.md @@ -12,13 +12,10 @@ When a user wants to log into AWX, she can explicitly choose some of the support On the other hand, the other authentication methods use the same types of login info (username and password), but authenticate using external auth systems rather than AWX's own database. If some of these methods are enabled, AWX will try authenticating using the enabled methods *before AWX's own authentication method*. The order of precedence is: * RADIUS -* TACACS+ * SAML -AWX will try authenticating against each enabled authentication method *in the specified order*, meaning if the same username and password is valid in multiple enabled auth methods (*e.g.*, both RADIUS and TACACS+), AWX will only use the first positive match (in the above example, log a user in via RADIUS and skip TACACS+). - ## Notes: -SAML users, RADIUS users and TACACS+ users are categorized as 'Enterprise' users. The following rules apply to Enterprise users: +SAML users and RADIUS users are categorized as 'Enterprise' users. The following rules apply to Enterprise users: * Enterprise users can only be created via the first successful login attempt from remote authentication backend. * Enterprise users cannot be created/authenticated if non-enterprise users with the same name has already been created in AWX. diff --git a/docs/auth/tacacsplus.md b/docs/auth/tacacsplus.md deleted file mode 100644 index f895ed4aeb35..000000000000 --- a/docs/auth/tacacsplus.md +++ /dev/null @@ -1,51 +0,0 @@ -# TACACS+ -[Terminal Access Controller Access-Control System Plus (TACACS+)](https://en.wikipedia.org/wiki/TACACS) is a protocol developed by Cisco to handle remote authentication and related services for networked access control through a centralized server. In specific, TACACS+ provides authentication, authorization and accounting (AAA) services. AWX currently utilizes its authentication service. - -TACACS+ is configured by settings configuration and is available under `/api/v2/settings/tacacsplus/`. Here is a typical configuration with every configurable field included: -``` -{ - "TACACSPLUS_HOST": "127.0.0.1", - "TACACSPLUS_PORT": 49, - "TACACSPLUS_SECRET": "secret", - "TACACSPLUS_SESSION_TIMEOUT": 5, - "TACACSPLUS_AUTH_PROTOCOL": "ascii", - "TACACSPLUS_REM_ADDR": "false" -} -``` -Each field is explained below: - -| Field Name | Field Value Type | Field Value Default | Description | -|------------------------------|---------------------|---------------------|--------------------------------------------------------------------| -| `TACACSPLUS_HOST` | String | '' (empty string) | Hostname of TACACS+ server. Empty string disables TACACS+ service. | -| `TACACSPLUS_PORT` | Integer | 49 | Port number of TACACS+ server. | -| `TACACSPLUS_SECRET` | String | '' (empty string) | Shared secret for authenticating to TACACS+ server. | -| `TACACSPLUS_SESSION_TIMEOUT` | Integer | 5 | TACACS+ session timeout value in seconds. | -| `TACACSPLUS_AUTH_PROTOCOL` | String with choices | 'ascii' | The authentication protocol used by TACACS+ client (choices are `ascii` and `pap`). | -| `TACACSPLUS_REM_ADDR` | Boolean | false | Enable the client address sending by TACACS+ client. | - -Under the hood, AWX uses [open-source TACACS+ python client](https://github.com/ansible/tacacs_plus) to communicate with the remote TACACS+ server. During authentication, AWX passes username and password to TACACS+ client, which packs up auth information and sends it to the TACACS+ server. Based on what the server returns, AWX will invalidate login attempt if authentication fails. If authentication passes, AWX will create a user if she does not exist in database, and log the user in. - -## Test Environment Setup - -The suggested TACACS+ server for testing is [shrubbery TACACS+ daemon](http://www.shrubbery.net/tac_plus/). It is supposed to run on a CentOS machine. A verified candidate is CentOS 6.3 AMI in AWS EC2 Community AMIs (search for `CentOS 6.3 x86_64 HVM - Minimal with cloud-init aws-cfn-bootstrap and ec2-api-tools`). Note that it is required to keep TCP port 49 open, since it's the default port used by the TACACS+ daemon. - -We provide [a playbook](https://github.com/jangsutsr/ansible-role-tacacs) to install a working TACACS+ server. Here is a typical test setup using the provided playbook: - -1. In AWS EC2, spawn the CentOS 6 machine. -2. In Tower, create a test project using the stand-alone playbook inventory. -3. In Tower, create a test inventory with the only host to be the spawned CentOS machine. -4. In Tower, create and run a job template using the created project and inventory with parameters setup as below: - -![Example tacacs+ setup jt parameters](../img/auth_tacacsplus_1.png?raw=true) - -The playbook creates a user named 'tower' with ascii password default to 'login' and modifiable by `extra_var` `ascii_password` and pap password default to 'papme' and modifiable by `extra_var` `pap_password`. In order to configure TACACS+ server to meet custom test needs, we need to modify server-side file `/etc/tac_plus.conf` and `sudo service tac_plus restart` to restart the daemon. Details on how to modify config file can be found [here](http://manpages.ubuntu.com/manpages/xenial/man5/tac_plus.conf.5.html). - - -## Acceptance Criteria - -* All specified in configuration fields should be shown and configurable as documented. -* A user defined by the TACACS+ server should be able to log into AWX. -* User not defined by TACACS+ server should not be able to log into AWX via TACACS+. -* A user existing in TACACS+ server but not in AWX should be created after the first successful log in. -* TACACS+ backend should stop an authentication attempt after configured timeout and should not block the authentication pipeline in any case. -* If exceptions occur on TACACS+ server side, the exception details should be logged in AWX, and AWX should not authenticate that user via TACACS+. diff --git a/docs/docsite/rst/administration/configure_awx_authentication.rst b/docs/docsite/rst/administration/configure_awx_authentication.rst index c56dfc5937f5..01e610273fac 100644 --- a/docs/docsite/rst/administration/configure_awx_authentication.rst +++ b/docs/docsite/rst/administration/configure_awx_authentication.rst @@ -7,8 +7,6 @@ Through the AWX user interface, you can set up a simplified login through variou - :ref:`ag_auth_azure` - :ref:`ag_auth_github` - :ref:`ag_auth_google_oauth2` -- :ref:`ag_auth_radius` -- :ref:`ag_auth_tacacs` Different authentication types require you to enter different information. Be sure to include all the information as required. diff --git a/docs/docsite/rst/administration/ent_auth.rst b/docs/docsite/rst/administration/ent_auth.rst index 238893ecee3e..a31f4d1cadb0 100644 --- a/docs/docsite/rst/administration/ent_auth.rst +++ b/docs/docsite/rst/administration/ent_auth.rst @@ -13,8 +13,6 @@ This section describes setting up authentication for the following enterprise sy .. contents:: :local: -Azure, RADIUS, and TACACS+ users are categorized as 'Enterprise' users. The following rules apply to Enterprise users: - - Enterprise users can only be created via the first successful login attempt from remote authentication backend. - Enterprise users cannot be created/authenticated if non-enterprise users with the same name has already been created in AWX. - AWX passwords of enterprise users should always be empty and cannot be set by any user if there are enterprise backend-enabled. @@ -78,37 +76,3 @@ AWX can be configured to centrally use RADIUS as a source for authentication inf 4. Enter the port and secret information in the next two fields. 5. Click **Save** when done. - - -.. _ag_auth_tacacs: - -TACACS+ settings ------------------ - -.. index:: - pair: authentication; TACACS+ Authentication Settings - - -Terminal Access Controller Access-Control System Plus (TACACS+) is a protocol that handles remote authentication and related services for networked access control through a centralized server. In particular, TACACS+ provides authentication, authorization and accounting (AAA) services, in which you can configure AWX to use as a source for authentication. - -.. note:: - - This feature is deprecated and will be removed in a future release. - -1. Click **Settings** from the left navigation bar. - -2. On the left side of the Settings window, click **TACACs+ settings** from the list of Authentication options. - -3. Click **Edit** and enter information in the following fields: - -- **TACACS+ Server**: Provide the hostname or IP address of the TACACS+ server with which to authenticate. If this field is left blank, TACACS+ authentication is disabled. -- **TACACS+ Port**: TACACS+ uses port 49 by default, which is already pre-populated. -- **TACACS+ Secret**: Secret key for TACACS+ authentication server. -- **TACACS+ Auth Session Timeout**: Session timeout value in seconds. The default is 5 seconds. -- **TACACS+ Authentication Protocol**: The protocol used by TACACS+ client. Options are **ascii** or **pap**. - -.. image:: ../common/images/configure-awx-auth-tacacs.png - :alt: TACACS+ configuration details in AWX settings. - -4. Click **Save** when done. - diff --git a/licenses/tacacs-plus.txt b/licenses/tacacs-plus.txt deleted file mode 100644 index 56b2d91f18ad..000000000000 --- a/licenses/tacacs-plus.txt +++ /dev/null @@ -1,24 +0,0 @@ -# Copyright (c) 2017 Ansible by Red Hat -# All Rights Reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - * Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution. - * Neither the name of the nor the - names of its contributors may be used to endorse or promote products - derived from this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY -DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND -ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/requirements/requirements.in b/requirements/requirements.in index a162b671e2d5..1814ef8df789 100644 --- a/requirements/requirements.in +++ b/requirements/requirements.in @@ -60,7 +60,6 @@ sqlparse>=0.4.4 # Required by django https://github.com/ansible/awx/security/d redis[hiredis] requests slack-sdk -tacacs_plus==1.0 # UPGRADE BLOCKER: auth does not work with later versions twilio twisted[tls]>=23.10.0 # CVE-2023-46137 uWSGI diff --git a/requirements/requirements.txt b/requirements/requirements.txt index b23b18880480..44e395fd75e3 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -495,7 +495,6 @@ six==1.16.0 # pygerduty # pyrad # python-dateutil - # tacacs-plus slack-sdk==3.27.0 # via -r /awx_devel/requirements/requirements.in smmap==5.0.1 @@ -510,7 +509,6 @@ sqlparse==0.4.4 # via # -r /awx_devel/requirements/requirements.in # django -tacacs-plus==1.0 # via -r /awx_devel/requirements/requirements.in tempora==5.5.1 # via diff --git a/tools/docker-compose/README.md b/tools/docker-compose/README.md index 77e10233bcdb..df9187762e3b 100644 --- a/tools/docker-compose/README.md +++ b/tools/docker-compose/README.md @@ -273,7 +273,6 @@ $ make docker-compose - [Start with Minikube](#start-with-minikube) - [SAML and OIDC Integration](#saml-and-oidc-integration) - [Splunk Integration](#splunk-integration) -- [tacacs+ Integration](#tacacs+-integration) ### Start a Shell @@ -465,30 +464,6 @@ 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 - ### HashiVault Integration Run a HashiVault container alongside of AWX. diff --git a/tools/docker-compose/ansible/plumb_tacacs.yml b/tools/docker-compose/ansible/plumb_tacacs.yml deleted file mode 100644 index b18a72284a3e..000000000000 --- a/tools/docker-compose/ansible/plumb_tacacs.yml +++ /dev/null @@ -1,32 +0,0 @@ ---- -- 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 - ansible.builtin.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 - ansible.builtin.debug: - msg: - - "Here is your existing tacacsplus configuration for reference:" - - "{{ existing_tacacs }}" - - - ansible.builtin.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 - ansible.builtin.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 80f075ab4140..e0db3a5c6393 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 @@ -188,14 +188,6 @@ services: - "grafana_storage:/var/lib/grafana:rw" 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 deleted file mode 100644 index fe9dd8c39112..000000000000 --- a/tools/docker-compose/ansible/templates/tacacsplus_settings.json.j2 +++ /dev/null @@ -1,7 +0,0 @@ -{ - "TACACSPLUS_HOST": "tacacs", - "TACACSPLUS_PORT": 49, - "TACACSPLUS_SECRET": "ciscotacacskey", - "TACACSPLUS_SESSION_TIMEOUT": 5, - "TACACSPLUS_AUTH_PROTOCOL": "ascii" -}