mirror of
https://github.com/ansible/awx.git
synced 2026-05-19 23:07:42 -02:30
Send real client remote address in TACACS+ authentication packet (#14077)
Co-authored-by: ekougs <ekougs@gmail.com>
This commit is contained in:
@@ -419,6 +419,7 @@ TACACSPLUS_PORT = 49
|
|||||||
TACACSPLUS_SECRET = ''
|
TACACSPLUS_SECRET = ''
|
||||||
TACACSPLUS_SESSION_TIMEOUT = 5
|
TACACSPLUS_SESSION_TIMEOUT = 5
|
||||||
TACACSPLUS_AUTH_PROTOCOL = 'ascii'
|
TACACSPLUS_AUTH_PROTOCOL = 'ascii'
|
||||||
|
TACACSPLUS_REM_ADDR = False
|
||||||
|
|
||||||
# Enable / Disable HTTP Basic Authentication used in the API browser
|
# Enable / Disable HTTP Basic Authentication used in the API browser
|
||||||
# Note: Session limits are not enforced when using HTTP Basic Authentication.
|
# Note: Session limits are not enforced when using HTTP Basic Authentication.
|
||||||
|
|||||||
@@ -224,12 +224,18 @@ class TACACSPlusBackend(object):
|
|||||||
return None
|
return None
|
||||||
try:
|
try:
|
||||||
# Upstream TACACS+ client does not accept non-string, so convert if needed.
|
# 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_HOST,
|
||||||
django_settings.TACACSPLUS_PORT,
|
django_settings.TACACSPLUS_PORT,
|
||||||
django_settings.TACACSPLUS_SECRET,
|
django_settings.TACACSPLUS_SECRET,
|
||||||
timeout=django_settings.TACACSPLUS_SESSION_TIMEOUT,
|
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:
|
except Exception as e:
|
||||||
logger.exception("TACACS+ Authentication Error: %s" % str(e))
|
logger.exception("TACACS+ Authentication Error: %s" % str(e))
|
||||||
return None
|
return None
|
||||||
@@ -244,6 +250,17 @@ class TACACSPlusBackend(object):
|
|||||||
except User.DoesNotExist:
|
except User.DoesNotExist:
|
||||||
return None
|
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):
|
class TowerSAMLIdentityProvider(BaseSAMLIdentityProvider):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -554,6 +554,16 @@ register(
|
|||||||
category_slug='tacacsplus',
|
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
|
# GOOGLE OAUTH2 AUTHENTICATION SETTINGS
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
def test_empty_host_fails_auth(tacacsplus_backend):
|
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'
|
settings.TACACSPLUS_AUTH_PROTOCOL = 'ascii'
|
||||||
ret_user = tacacsplus_backend.authenticate(None, u"user", u"pass")
|
ret_user = tacacsplus_backend.authenticate(None, u"user", u"pass")
|
||||||
assert ret_user == user
|
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_SECRET: '$encrypted$',
|
||||||
TACACSPLUS_SESSION_TIMEOUT: 5,
|
TACACSPLUS_SESSION_TIMEOUT: 5,
|
||||||
TACACSPLUS_AUTH_PROTOCOL: 'ascii',
|
TACACSPLUS_AUTH_PROTOCOL: 'ascii',
|
||||||
|
TACACSPLUS_REM_ADDR: false,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ describe('<TACACSDetail />', () => {
|
|||||||
TACACSPLUS_SECRET: '$encrypted$',
|
TACACSPLUS_SECRET: '$encrypted$',
|
||||||
TACACSPLUS_SESSION_TIMEOUT: 5,
|
TACACSPLUS_SESSION_TIMEOUT: 5,
|
||||||
TACACSPLUS_AUTH_PROTOCOL: 'ascii',
|
TACACSPLUS_AUTH_PROTOCOL: 'ascii',
|
||||||
|
TACACSPLUS_REM_ADDR: false,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import useModal from 'hooks/useModal';
|
|||||||
import useRequest from 'hooks/useRequest';
|
import useRequest from 'hooks/useRequest';
|
||||||
import { SettingsAPI } from 'api';
|
import { SettingsAPI } from 'api';
|
||||||
import {
|
import {
|
||||||
|
BooleanField,
|
||||||
ChoiceField,
|
ChoiceField,
|
||||||
EncryptedField,
|
EncryptedField,
|
||||||
InputField,
|
InputField,
|
||||||
@@ -116,6 +117,10 @@ function TACACSEdit() {
|
|||||||
name="TACACSPLUS_AUTH_PROTOCOL"
|
name="TACACSPLUS_AUTH_PROTOCOL"
|
||||||
config={tacacs.TACACSPLUS_AUTH_PROTOCOL}
|
config={tacacs.TACACSPLUS_AUTH_PROTOCOL}
|
||||||
/>
|
/>
|
||||||
|
<BooleanField
|
||||||
|
name="TACACSPLUS_REM_ADDR"
|
||||||
|
config={tacacs.TACACSPLUS_REM_ADDR}
|
||||||
|
/>
|
||||||
{submitError && <FormSubmitError error={submitError} />}
|
{submitError && <FormSubmitError error={submitError} />}
|
||||||
{revertError && <FormSubmitError error={revertError} />}
|
{revertError && <FormSubmitError error={revertError} />}
|
||||||
</FormColumnLayout>
|
</FormColumnLayout>
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ describe('<TACACSEdit />', () => {
|
|||||||
TACACSPLUS_SECRET: '$encrypted$',
|
TACACSPLUS_SECRET: '$encrypted$',
|
||||||
TACACSPLUS_SESSION_TIMEOUT: 123,
|
TACACSPLUS_SESSION_TIMEOUT: 123,
|
||||||
TACACSPLUS_AUTH_PROTOCOL: 'ascii',
|
TACACSPLUS_AUTH_PROTOCOL: 'ascii',
|
||||||
|
TACACSPLUS_REM_ADDR: false,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -112,6 +113,7 @@ describe('<TACACSEdit />', () => {
|
|||||||
TACACSPLUS_SECRET: '',
|
TACACSPLUS_SECRET: '',
|
||||||
TACACSPLUS_SESSION_TIMEOUT: 123,
|
TACACSPLUS_SESSION_TIMEOUT: 123,
|
||||||
TACACSPLUS_AUTH_PROTOCOL: 'ascii',
|
TACACSPLUS_AUTH_PROTOCOL: 'ascii',
|
||||||
|
TACACSPLUS_REM_ADDR: false,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -2708,6 +2708,15 @@
|
|||||||
["pap", "pap"]
|
["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": {
|
"SOCIAL_AUTH_GOOGLE_OAUTH2_KEY": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"required": false,
|
"required": false,
|
||||||
@@ -5936,6 +5945,15 @@
|
|||||||
["pap", "pap"]
|
["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": {
|
"SOCIAL_AUTH_GOOGLE_OAUTH2_CALLBACK_URL": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"label": "Google OAuth2 Callback URL",
|
"label": "Google OAuth2 Callback URL",
|
||||||
|
|||||||
@@ -251,6 +251,7 @@
|
|||||||
"TACACSPLUS_SECRET": "",
|
"TACACSPLUS_SECRET": "",
|
||||||
"TACACSPLUS_SESSION_TIMEOUT": 5,
|
"TACACSPLUS_SESSION_TIMEOUT": 5,
|
||||||
"TACACSPLUS_AUTH_PROTOCOL": "ascii",
|
"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_CALLBACK_URL": "https://localhost:3000/sso/complete/google-oauth2/",
|
||||||
"SOCIAL_AUTH_GOOGLE_OAUTH2_KEY": "",
|
"SOCIAL_AUTH_GOOGLE_OAUTH2_KEY": "",
|
||||||
"SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET": "",
|
"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_PORT": 49,
|
||||||
"TACACSPLUS_SECRET": "secret",
|
"TACACSPLUS_SECRET": "secret",
|
||||||
"TACACSPLUS_SESSION_TIMEOUT": 5,
|
"TACACSPLUS_SESSION_TIMEOUT": 5,
|
||||||
"TACACSPLUS_AUTH_PROTOCOL": "ascii"
|
"TACACSPLUS_AUTH_PROTOCOL": "ascii",
|
||||||
|
"TACACSPLUS_REM_ADDR": "false"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
Each field is explained below:
|
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_SECRET` | String | '' (empty string) | Shared secret for authenticating to TACACS+ server. |
|
||||||
| `TACACSPLUS_SESSION_TIMEOUT` | Integer | 5 | TACACS+ session timeout value in seconds. |
|
| `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_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.
|
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.
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user