mirror of
https://github.com/ansible/awx.git
synced 2026-01-23 15:38:06 -03:30
Send real client remote address in TACACS+ authentication packet (#14077)
Co-authored-by: ekougs <ekougs@gmail.com>
This commit is contained in:
parent
0ae720244c
commit
844666df4c
@ -419,6 +419,7 @@ 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.
|
||||
|
||||
@ -224,12 +224,18 @@ class TACACSPlusBackend(object):
|
||||
return None
|
||||
try:
|
||||
# Upstream TACACS+ client does not accept non-string, so convert if needed.
|
||||
auth = tacacs_plus.TACACSClient(
|
||||
tacacs_client = tacacs_plus.TACACSClient(
|
||||
django_settings.TACACSPLUS_HOST,
|
||||
django_settings.TACACSPLUS_PORT,
|
||||
django_settings.TACACSPLUS_SECRET,
|
||||
timeout=django_settings.TACACSPLUS_SESSION_TIMEOUT,
|
||||
).authenticate(username, password, authen_type=tacacs_plus.TAC_PLUS_AUTHEN_TYPES[django_settings.TACACSPLUS_AUTH_PROTOCOL])
|
||||
)
|
||||
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
|
||||
@ -244,6 +250,17 @@ class TACACSPlusBackend(object):
|
||||
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):
|
||||
"""
|
||||
|
||||
@ -554,6 +554,16 @@ register(
|
||||
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
|
||||
###############################################################################
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
from unittest import mock
|
||||
import pytest
|
||||
|
||||
|
||||
def test_empty_host_fails_auth(tacacsplus_backend):
|
||||
@ -47,3 +48,69 @@ def test_client_return_valid_passes_auth(tacacsplus_backend):
|
||||
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)
|
||||
|
||||
@ -15,6 +15,7 @@ SettingsAPI.readCategory.mockResolvedValue({
|
||||
TACACSPLUS_SECRET: '$encrypted$',
|
||||
TACACSPLUS_SESSION_TIMEOUT: 5,
|
||||
TACACSPLUS_AUTH_PROTOCOL: 'ascii',
|
||||
TACACSPLUS_REM_ADDR: false,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@ -23,6 +23,7 @@ describe('<TACACSDetail />', () => {
|
||||
TACACSPLUS_SECRET: '$encrypted$',
|
||||
TACACSPLUS_SESSION_TIMEOUT: 5,
|
||||
TACACSPLUS_AUTH_PROTOCOL: 'ascii',
|
||||
TACACSPLUS_REM_ADDR: false,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@ -12,6 +12,7 @@ import useModal from 'hooks/useModal';
|
||||
import useRequest from 'hooks/useRequest';
|
||||
import { SettingsAPI } from 'api';
|
||||
import {
|
||||
BooleanField,
|
||||
ChoiceField,
|
||||
EncryptedField,
|
||||
InputField,
|
||||
@ -116,6 +117,10 @@ function TACACSEdit() {
|
||||
name="TACACSPLUS_AUTH_PROTOCOL"
|
||||
config={tacacs.TACACSPLUS_AUTH_PROTOCOL}
|
||||
/>
|
||||
<BooleanField
|
||||
name="TACACSPLUS_REM_ADDR"
|
||||
config={tacacs.TACACSPLUS_REM_ADDR}
|
||||
/>
|
||||
{submitError && <FormSubmitError error={submitError} />}
|
||||
{revertError && <FormSubmitError error={revertError} />}
|
||||
</FormColumnLayout>
|
||||
|
||||
@ -26,6 +26,7 @@ describe('<TACACSEdit />', () => {
|
||||
TACACSPLUS_SECRET: '$encrypted$',
|
||||
TACACSPLUS_SESSION_TIMEOUT: 123,
|
||||
TACACSPLUS_AUTH_PROTOCOL: 'ascii',
|
||||
TACACSPLUS_REM_ADDR: false,
|
||||
},
|
||||
});
|
||||
});
|
||||
@ -112,6 +113,7 @@ describe('<TACACSEdit />', () => {
|
||||
TACACSPLUS_SECRET: '',
|
||||
TACACSPLUS_SESSION_TIMEOUT: 123,
|
||||
TACACSPLUS_AUTH_PROTOCOL: 'ascii',
|
||||
TACACSPLUS_REM_ADDR: false,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@ -2708,6 +2708,15 @@
|
||||
["pap", "pap"]
|
||||
]
|
||||
},
|
||||
"TACACSPLUS_REM_ADDR": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"label": "TACACS+ client address sending enabled",
|
||||
"help_text": "Enable the client address sending by TACACS+ client.",
|
||||
"category": "TACACS+",
|
||||
"category_slug": "tacacsplus",
|
||||
"default": false
|
||||
},
|
||||
"SOCIAL_AUTH_GOOGLE_OAUTH2_KEY": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
@ -5936,6 +5945,15 @@
|
||||
["pap", "pap"]
|
||||
]
|
||||
},
|
||||
"TACACSPLUS_REM_ADDR": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"label": "TACACS+ client address sending enabled",
|
||||
"help_text": "Enable the client address sending by TACACS+ client.",
|
||||
"category": "TACACS+",
|
||||
"category_slug": "tacacsplus",
|
||||
"defined_in_file": false
|
||||
},
|
||||
"SOCIAL_AUTH_GOOGLE_OAUTH2_CALLBACK_URL": {
|
||||
"type": "string",
|
||||
"label": "Google OAuth2 Callback URL",
|
||||
|
||||
@ -251,6 +251,7 @@
|
||||
"TACACSPLUS_SECRET": "",
|
||||
"TACACSPLUS_SESSION_TIMEOUT": 5,
|
||||
"TACACSPLUS_AUTH_PROTOCOL": "ascii",
|
||||
"TACACSPLUS_REM_ADDR": false,
|
||||
"SOCIAL_AUTH_GOOGLE_OAUTH2_CALLBACK_URL": "https://localhost:3000/sso/complete/google-oauth2/",
|
||||
"SOCIAL_AUTH_GOOGLE_OAUTH2_KEY": "",
|
||||
"SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET": "",
|
||||
|
||||
@ -8,7 +8,8 @@ TACACS+ is configured by settings configuration and is available under `/api/v2/
|
||||
"TACACSPLUS_PORT": 49,
|
||||
"TACACSPLUS_SECRET": "secret",
|
||||
"TACACSPLUS_SESSION_TIMEOUT": 5,
|
||||
"TACACSPLUS_AUTH_PROTOCOL": "ascii"
|
||||
"TACACSPLUS_AUTH_PROTOCOL": "ascii",
|
||||
"TACACSPLUS_REM_ADDR": "false"
|
||||
}
|
||||
```
|
||||
Each field is explained below:
|
||||
@ -20,6 +21,7 @@ Each field is explained below:
|
||||
| `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.
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user