Skip to content

Commit

Permalink
Remove RADIUS authentication (#15548)
Browse files Browse the repository at this point in the history
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 <[email protected]>
  • Loading branch information
2 people authored and jessicamack committed Oct 15, 2024
1 parent e4c1156 commit 2c2694c
Show file tree
Hide file tree
Showing 21 changed files with 26 additions and 252 deletions.
2 changes: 1 addition & 1 deletion awx/api/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@
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.'
),
Expand Down
23 changes: 23 additions & 0 deletions awx/conf/migrations/0011_remove_radius_auth_conf.py
Original file line number Diff line number Diff line change
@@ -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),
]
6 changes: 2 additions & 4 deletions awx/main/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -246,11 +246,9 @@ def user_is_system_auditor(user, tf):

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

Expand Down
48 changes: 0 additions & 48 deletions awx/main/tests/functional/api/test_oauth.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,6 @@
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


Expand All @@ -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
Expand Down
33 changes: 0 additions & 33 deletions awx/main/tests/functional/api/test_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@

# 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
Expand Down Expand Up @@ -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'})
Expand Down
7 changes: 0 additions & 7 deletions awx/settings/defaults.py
Original file line number Diff line number Diff line change
Expand Up @@ -392,7 +392,6 @@
}

AUTHENTICATION_BACKENDS = (
'awx.sso.backends.RADIUSBackend',
'social_core.backends.google.GoogleOAuth2',
'social_core.backends.github.GithubOAuth2',
'social_core.backends.github.GithubOrganizationOAuth2',
Expand All @@ -417,12 +416,6 @@
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.
Expand Down
26 changes: 0 additions & 26 deletions awx/sso/backends.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,6 @@
# 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
Expand Down Expand Up @@ -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.
Expand Down
1 change: 0 additions & 1 deletion awx/sso/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
40 changes: 0 additions & 40 deletions awx/sso/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,46 +143,6 @@ def __call__(self):
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
###############################################################################
Expand Down
1 change: 0 additions & 1 deletion awx/sso/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -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']),
Expand Down
1 change: 0 additions & 1 deletion awx/sso/tests/functional/test_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -335,7 +335,6 @@ def test_get_external_account(self, enable_social, enable_enterprise, expected_r
# 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),
Expand Down
1 change: 0 additions & 1 deletion awxkit/awxkit/api/pages/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ class Setting(base.Base):
resources.settings_github_team,
resources.settings_google_oauth2,
resources.settings_jobs,
resources.settings_radius,
resources.settings_saml,
resources.settings_system,
resources.settings_ui,
Expand Down
1 change: 0 additions & 1 deletion awxkit/awxkit/api/resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,6 @@ class Resources(object):
_settings_jobs = 'settings/jobs/'
_settings_logging = 'settings/logging/'
_settings_named_url = 'settings/named-url/'
_settings_radius = 'settings/radius/'
_settings_saml = 'settings/saml/'
_settings_system = 'settings/system/'
_settings_ui = 'settings/ui/'
Expand Down
3 changes: 0 additions & 3 deletions docs/auth/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,9 @@ When a user wants to log into AWX, she can explicitly choose some of the support
* Microsoft Azure Active Directory (AD) OAuth2

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
* SAML

## Notes:
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.
* AWX passwords of Enterprise users should always be empty and cannot be set by any user if there are enterprise backends enabled.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
Through the AWX user interface, you can set up a simplified login through various authentication types: GitHub, Google, and RADIUS. After you create and register your developer application with the appropriate service, you can set up authorizations for them.

1. From the left navigation bar, click **Settings**.

2. The left side of the Settings window is a set of configurable Authentication settings. Select from the following options:
Expand Down
20 changes: 0 additions & 20 deletions docs/docsite/rst/administration/ent_auth.rst
Original file line number Diff line number Diff line change
Expand Up @@ -56,23 +56,3 @@ For application registering basics in Azure AD, refer to the `Azure AD Identity

.. _`Azure AD Identity Platform (v2)`: https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-overview

.. _ag_auth_radius:

RADIUS settings
------------------

.. index::
pair: authentication; RADIUS Authentication Settings


AWX can be configured to centrally use RADIUS as a source for authentication information.

1. Click **Settings** from the left navigation bar.

2. On the left side of the Settings window, click **RADIUS settings** from the list of Authentication options.

3. Click **Edit** and enter the Host or IP of the Radius server in the **Radius Server** field. If this field is left blank, Radius authentication is disabled.

4. Enter the port and secret information in the next two fields.

5. Click **Save** when done.
6 changes: 0 additions & 6 deletions docs/docsite/rst/administration/social_auth.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,6 @@ Authentication methods help simplify logins for end users--offering single sign-

Account authentication can be configured in the AWX User Interface and saved to the PostgreSQL database. For instructions, refer to the :ref:`ag_configure_awx` section.

Account authentication in AWX can be configured to centrally use OAuth2, while enterprise-level account authentication can be configured for :ref:`Azure <ag_auth_azure>`, :ref:`RADIUS <ag_auth_radius>` as a source for authentication information. See :ref:`ag_ent_auth` for more detail.

For websites, such as Microsoft Azure, Google or GitHub, that provide account information, account information is often implemented using the OAuth standard. OAuth is a secure authorization protocol which is commonly used in conjunction with account authentication to grant 3rd party applications a "session token" allowing them to make API calls to providers on the user’s behalf.

The :ref:`RADIUS <ag_auth_radius>` distributed client/server system allows you to secure networks against unauthorized access and can be implemented in network environments requiring high levels of security while maintaining network access for remote users.


.. _ag_auth_github:

Expand Down
24 changes: 0 additions & 24 deletions licenses/django-radius.txt

This file was deleted.

Loading

0 comments on commit 2c2694c

Please sign in to comment.