Remove RADIUS authentication (#15548)

Remove RADIUS authentication from AWX

Do not remove models fields and tables let it for a stage where all the work of removing external auth finished AAP-27707

Co-authored-by: Hao Liu <44379968+TheRealHaoLiu@users.noreply.github.com>
This commit is contained in:
Djebran Lezzoum
2024-10-02 15:57:26 +02:00
committed by jessicamack
parent e4c11561cc
commit 2c2694ce89
21 changed files with 26 additions and 252 deletions

View File

@@ -78,7 +78,7 @@ register(
label=_('Allow External Users to Create OAuth2 Tokens'),
help_text=_(
'For security reasons, users from external auth providers (LDAP, SAML, '
'SSO, Radius, and others) are not allowed to create OAuth2 tokens. '
'SSO, and others) are not allowed to create OAuth2 tokens. '
'To change this behavior, enable this setting. Existing tokens will '
'not be deleted when this setting is toggled off.'
),

View File

@@ -0,0 +1,23 @@
from django.db import migrations
RADIUS_AUTH_CONF_KEYS = [
'RADIUS_SERVER',
'RADIUS_PORT',
'RADIUS_SECRET',
]
def remove_radius_auth_conf(apps, scheme_editor):
setting = apps.get_model('conf', 'Setting')
setting.objects.filter(key__in=RADIUS_AUTH_CONF_KEYS).delete()
class Migration(migrations.Migration):
dependencies = [
('conf', '0010_change_to_JSONField'),
]
operations = [
migrations.RunPython(remove_radius_auth_conf),
]

View File

@@ -246,11 +246,9 @@ User.add_to_class('is_system_auditor', user_is_system_auditor)
def user_is_in_enterprise_category(user, category):
ret = (category,) in user.enterprise_auth.values_list('provider') and not user.has_usable_password()
# NOTE: this if-else block ensures existing enterprise users are still able to
# NOTE: this if block ensures existing enterprise users are still able to
# log in. Remove it in a future release
if category == 'radius':
ret = ret or not user.has_usable_password()
elif category == 'saml':
if category == 'saml':
ret = ret or user.social_auth.all()
return ret

View File

@@ -13,8 +13,6 @@ from rest_framework.reverse import reverse as drf_reverse
from awx.main.utils.encryption import decrypt_value, get_encryption_key
from awx.api.versioning import reverse
from awx.main.models.oauth import OAuth2Application as Application, OAuth2AccessToken as AccessToken
from awx.main.tests.functional import immediate_on_commit
from awx.sso.models import UserEnterpriseAuth
from oauth2_provider.models import RefreshToken
@@ -33,52 +31,6 @@ def test_personal_access_token_creation(oauth_application, post, alice):
assert 'refresh_token' in resp_json
@pytest.mark.django_db
@pytest.mark.parametrize('allow_oauth, status', [(True, 201), (False, 403)])
def test_token_creation_disabled_for_external_accounts(oauth_application, post, alice, allow_oauth, status):
UserEnterpriseAuth(user=alice, provider='radius').save()
url = drf_reverse('api:oauth_authorization_root_view') + 'token/'
with override_settings(RADIUS_SERVER='example.org', ALLOW_OAUTH2_FOR_EXTERNAL_USERS=allow_oauth):
resp = post(
url,
data='grant_type=password&username=alice&password=alice&scope=read',
content_type='application/x-www-form-urlencoded',
HTTP_AUTHORIZATION='Basic ' + smart_str(base64.b64encode(smart_bytes(':'.join([oauth_application.client_id, oauth_application.client_secret])))),
status=status,
)
if allow_oauth:
assert AccessToken.objects.count() == 1
else:
assert 'OAuth2 Tokens cannot be created by users associated with an external authentication provider' in smart_str(resp.content) # noqa
assert AccessToken.objects.count() == 0
@pytest.mark.django_db
def test_existing_token_enabled_for_external_accounts(oauth_application, get, post, admin):
UserEnterpriseAuth(user=admin, provider='radius').save()
url = drf_reverse('api:oauth_authorization_root_view') + 'token/'
with override_settings(RADIUS_SERVER='example.org', ALLOW_OAUTH2_FOR_EXTERNAL_USERS=True):
resp = post(
url,
data='grant_type=password&username=admin&password=admin&scope=read',
content_type='application/x-www-form-urlencoded',
HTTP_AUTHORIZATION='Basic ' + smart_str(base64.b64encode(smart_bytes(':'.join([oauth_application.client_id, oauth_application.client_secret])))),
status=201,
)
token = json.loads(resp.content)['access_token']
assert AccessToken.objects.count() == 1
with immediate_on_commit():
resp = get(drf_reverse('api:user_me_list', kwargs={'version': 'v2'}), HTTP_AUTHORIZATION='Bearer ' + token, status=200)
assert json.loads(resp.content)['results'][0]['username'] == 'admin'
with override_settings(RADIUS_SERVER='example.org', ALLOW_OAUTH2_FOR_EXTERNAL_USER=False):
with immediate_on_commit():
resp = get(drf_reverse('api:user_me_list', kwargs={'version': 'v2'}), HTTP_AUTHORIZATION='Bearer ' + token, status=200)
assert json.loads(resp.content)['results'][0]['username'] == 'admin'
@pytest.mark.django_db
def test_pat_creation_no_default_scope(oauth_application, post, admin):
# tests that the default scope is overriden

View File

@@ -7,7 +7,6 @@ import pytest
# AWX
from awx.api.versioning import reverse
from awx.conf.models import Setting
from awx.conf.registry import settings_registry
TEST_GIF_LOGO = 'data:image/gif;base64,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' # NOQA
@@ -66,38 +65,6 @@ def test_awx_task_env_validity(get, patch, admin, value, expected):
assert resp.data['AWX_TASK_ENV'] == dict()
@pytest.mark.django_db
def test_radius_settings(get, put, patch, delete, admin, settings):
url = reverse('api:setting_singleton_detail', kwargs={'category_slug': 'radius'})
response = get(url, user=admin, expect=200)
put(url, user=admin, data=response.data, expect=200)
# Set secret via the API.
patch(url, user=admin, data={'RADIUS_SECRET': 'mysecret'}, expect=200)
response = get(url, user=admin, expect=200)
assert response.data['RADIUS_SECRET'] == '$encrypted$'
assert Setting.objects.filter(key='RADIUS_SECRET').first().value.startswith('$encrypted$')
assert settings.RADIUS_SECRET == 'mysecret'
# Set secret via settings wrapper.
settings_wrapper = settings._awx_conf_settings
settings_wrapper.RADIUS_SECRET = 'mysecret2'
response = get(url, user=admin, expect=200)
assert response.data['RADIUS_SECRET'] == '$encrypted$'
assert Setting.objects.filter(key='RADIUS_SECRET').first().value.startswith('$encrypted$')
assert settings.RADIUS_SECRET == 'mysecret2'
# If we send back $encrypted$, the setting is not updated.
patch(url, user=admin, data={'RADIUS_SECRET': '$encrypted$'}, expect=200)
response = get(url, user=admin, expect=200)
assert response.data['RADIUS_SECRET'] == '$encrypted$'
assert Setting.objects.filter(key='RADIUS_SECRET').first().value.startswith('$encrypted$')
assert settings.RADIUS_SECRET == 'mysecret2'
# If we send an empty string, the setting is also set to an empty string.
patch(url, user=admin, data={'RADIUS_SECRET': ''}, expect=200)
response = get(url, user=admin, expect=200)
assert response.data['RADIUS_SECRET'] == ''
assert Setting.objects.filter(key='RADIUS_SECRET').first().value == ''
assert settings.RADIUS_SECRET == ''
@pytest.mark.django_db
def test_ui_settings(get, put, patch, delete, admin):
url = reverse('api:setting_singleton_detail', kwargs={'category_slug': 'ui'})

View File

@@ -392,7 +392,6 @@ REST_FRAMEWORK = {
}
AUTHENTICATION_BACKENDS = (
'awx.sso.backends.RADIUSBackend',
'social_core.backends.google.GoogleOAuth2',
'social_core.backends.github.GithubOAuth2',
'social_core.backends.github.GithubOrganizationOAuth2',
@@ -417,12 +416,6 @@ OAUTH2_PROVIDER = {'ACCESS_TOKEN_EXPIRE_SECONDS': 31536000000, 'AUTHORIZATION_CO
ALLOW_OAUTH2_FOR_EXTERNAL_USERS = False
# Radius server settings (default to empty string to skip using Radius auth).
# Note: These settings may be overridden by database settings.
RADIUS_SERVER = ''
RADIUS_PORT = 1812
RADIUS_SECRET = ''
# 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.

View File

@@ -7,11 +7,6 @@ import logging
# Django
from django.contrib.auth.models import User
from django.conf import settings as django_settings
from django.utils.encoding import force_str
from django.http import HttpResponse
# radiusauth
from radiusauth.backends import RADIUSBackend as BaseRADIUSBackend
# social
from social_core.backends.saml import OID_USERID
@@ -45,27 +40,6 @@ def _get_or_set_enterprise_user(username, password, provider):
logger.warning("Enterprise user %s already defined in Tower." % username)
class RADIUSBackend(BaseRADIUSBackend):
"""
Custom Radius backend to verify license status
"""
def authenticate(self, request, username, password):
if not django_settings.RADIUS_SERVER:
return None
return super(RADIUSBackend, self).authenticate(request, username, password)
def get_user(self, user_id):
if not django_settings.RADIUS_SERVER:
return None
user = super(RADIUSBackend, self).get_user(user_id)
if not user.has_usable_password():
return user
def get_django_user(self, username, password=None, groups=[], is_staff=False, is_superuser=False):
return _get_or_set_enterprise_user(force_str(username), force_str(password), 'radius')
class TowerSAMLIdentityProvider(BaseSAMLIdentityProvider):
"""
Custom Identity Provider to make attributes to what we expect.

View File

@@ -188,7 +188,6 @@ def is_remote_auth_enabled():
settings_that_turn_on_remote_auth = [
'SOCIAL_AUTH_SAML_ENABLED_IDPS',
'RADIUS_SERVER',
]
# Also include any SOCAIL_AUTH_*KEY (except SAML)
for social_auth_key in dir(settings):

View File

@@ -143,46 +143,6 @@ if settings.ALLOW_LOCAL_RESOURCE_MANAGEMENT:
category_slug='authentication',
)
###############################################################################
# RADIUS AUTHENTICATION SETTINGS
###############################################################################
register(
'RADIUS_SERVER',
field_class=fields.CharField,
allow_blank=True,
default='',
label=_('RADIUS Server'),
help_text=_('Hostname/IP of RADIUS server. RADIUS authentication is disabled if this setting is empty.'),
category=_('RADIUS'),
category_slug='radius',
placeholder='radius.example.com',
)
register(
'RADIUS_PORT',
field_class=fields.IntegerField,
min_value=1,
max_value=65535,
default=1812,
label=_('RADIUS Port'),
help_text=_('Port of RADIUS server.'),
category=_('RADIUS'),
category_slug='radius',
)
register(
'RADIUS_SECRET',
field_class=fields.CharField,
allow_blank=True,
default='',
label=_('RADIUS Secret'),
help_text=_('Shared secret for authenticating to RADIUS server.'),
category=_('RADIUS'),
category_slug='radius',
encrypted=True,
)
###############################################################################
# GOOGLE OAUTH2 AUTHENTICATION SETTINGS
###############################################################################

View File

@@ -107,7 +107,6 @@ class AuthenticationBackendsField(fields.StringListField):
# authentication backend.
REQUIRED_BACKEND_SETTINGS = collections.OrderedDict(
[
('awx.sso.backends.RADIUSBackend', ['RADIUS_SERVER']),
('social_core.backends.google.GoogleOAuth2', ['SOCIAL_AUTH_GOOGLE_OAUTH2_KEY', 'SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET']),
('social_core.backends.github.GithubOAuth2', ['SOCIAL_AUTH_GITHUB_KEY', 'SOCIAL_AUTH_GITHUB_SECRET']),
('social_core.backends.open_id_connect.OpenIdConnectAuth', ['SOCIAL_AUTH_OIDC_KEY', 'SOCIAL_AUTH_OIDC_SECRET', 'SOCIAL_AUTH_OIDC_OIDC_ENDPOINT']),

View File

@@ -335,7 +335,6 @@ class TestCommonFunctions:
# Set none of the social auth settings
('JUNK_SETTING', False),
('SOCIAL_AUTH_SAML_ENABLED_IDPS', True),
('RADIUS_SERVER', True),
# Set some SOCIAL_SOCIAL_AUTH_OIDC_KEYAUTH_*_KEY settings
('SOCIAL_AUTH_AZUREAD_OAUTH2_KEY', True),
('SOCIAL_AUTH_GITHUB_ENTERPRISE_KEY', True),