mirror of
https://github.com/ansible/awx.git
synced 2026-05-17 22:37:41 -02:30
AC-654 Add API support for token auth using X-Auth-Token header in addition to AUthorization header, fixes issue where API requests from the UI are picking up the basic auth from an API session.
This commit is contained in:
@@ -4,6 +4,7 @@
|
|||||||
# Django REST Framework
|
# Django REST Framework
|
||||||
from rest_framework import authentication
|
from rest_framework import authentication
|
||||||
from rest_framework import exceptions
|
from rest_framework import exceptions
|
||||||
|
from rest_framework import HTTP_HEADER_ENCODING
|
||||||
|
|
||||||
# AWX
|
# AWX
|
||||||
from awx.main.models import Job, AuthToken
|
from awx.main.models import Job, AuthToken
|
||||||
@@ -16,9 +17,33 @@ class TokenAuthentication(authentication.TokenAuthentication):
|
|||||||
|
|
||||||
model = AuthToken
|
model = AuthToken
|
||||||
|
|
||||||
|
def _get_x_auth_token_header(self, request):
|
||||||
|
auth = request.META.get('HTTP_X_AUTH_TOKEN', '')
|
||||||
|
if type(auth) == type(''):
|
||||||
|
# Work around django test client oddness
|
||||||
|
auth = auth.encode(HTTP_HEADER_ENCODING)
|
||||||
|
return auth
|
||||||
|
|
||||||
def authenticate(self, request):
|
def authenticate(self, request):
|
||||||
self.request = request
|
self.request = request
|
||||||
return super(TokenAuthentication, self).authenticate(request)
|
|
||||||
|
# Prefer the custom X-Auth-Token header over the Authorization header,
|
||||||
|
# to handle cases where the browser submits saved Basic auth and
|
||||||
|
# overrides the UI's normal use of the Authorization header.
|
||||||
|
auth = self._get_x_auth_token_header(request).split()
|
||||||
|
if not auth or auth[0].lower() != 'token':
|
||||||
|
auth = authentication.get_authorization_header(request).split()
|
||||||
|
if not auth or auth[0].lower() != 'token':
|
||||||
|
return None
|
||||||
|
|
||||||
|
if len(auth) == 1:
|
||||||
|
msg = 'Invalid token header. No credentials provided.'
|
||||||
|
raise exceptions.AuthenticationFailed(msg)
|
||||||
|
elif len(auth) > 2:
|
||||||
|
msg = 'Invalid token header. Token string should not contain spaces.'
|
||||||
|
raise exceptions.AuthenticationFailed(msg)
|
||||||
|
|
||||||
|
return self.authenticate_credentials(auth[1])
|
||||||
|
|
||||||
def authenticate_credentials(self, key):
|
def authenticate_credentials(self, key):
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -85,7 +85,12 @@ class APIView(views.APIView):
|
|||||||
continue
|
continue
|
||||||
if resp_hdr.split()[0] and resp_hdr.split()[0] == req_hdr.split()[0]:
|
if resp_hdr.split()[0] and resp_hdr.split()[0] == req_hdr.split()[0]:
|
||||||
return resp_hdr
|
return resp_hdr
|
||||||
return super(APIView, self).get_authenticate_header(request)
|
# If it can't be determined from the request, use the last
|
||||||
|
# authenticator (should be Basic).
|
||||||
|
try:
|
||||||
|
return authenticator.authenticate_header(request)
|
||||||
|
except NameError:
|
||||||
|
pass
|
||||||
|
|
||||||
def get_description_context(self):
|
def get_description_context(self):
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -172,18 +172,29 @@ class BaseTestMixin(object):
|
|||||||
client_kwargs['HTTP_ACCEPT'] = accept
|
client_kwargs['HTTP_ACCEPT'] = accept
|
||||||
if remote_addr is not None:
|
if remote_addr is not None:
|
||||||
client_kwargs['REMOTE_ADDR'] = remote_addr
|
client_kwargs['REMOTE_ADDR'] = remote_addr
|
||||||
client = Client(**client_kwargs)
|
|
||||||
auth = auth or self._current_auth
|
auth = auth or self._current_auth
|
||||||
if auth:
|
if auth:
|
||||||
if isinstance(auth, (list, tuple)):
|
# Dict is only used to test case when both Authorization and
|
||||||
|
# X-Auth-Token headers are passed.
|
||||||
|
if isinstance(auth, dict):
|
||||||
|
basic = auth.get('basic', ())
|
||||||
|
if basic:
|
||||||
|
basic_auth = base64.b64encode('%s:%s' % (basic[0], basic[1]))
|
||||||
|
basic_auth = basic_auth.decode('ascii')
|
||||||
|
client_kwargs['HTTP_AUTHORIZATION'] = 'Basic %s' % basic_auth
|
||||||
|
token = auth.get('token', '')
|
||||||
|
if token and not basic:
|
||||||
|
client_kwargs['HTTP_AUTHORIZATION'] = 'Token %s' % token
|
||||||
|
elif token:
|
||||||
|
client_kwargs['HTTP_X_AUTH_TOKEN'] = 'Token %s' % token
|
||||||
|
elif isinstance(auth, (list, tuple)):
|
||||||
#client.login(username=auth[0], password=auth[1])
|
#client.login(username=auth[0], password=auth[1])
|
||||||
basic_auth = base64.b64encode('%s:%s' % (auth[0], auth[1]))
|
basic_auth = base64.b64encode('%s:%s' % (auth[0], auth[1]))
|
||||||
basic_auth = basic_auth.decode('ascii')
|
basic_auth = basic_auth.decode('ascii')
|
||||||
client_kwargs['HTTP_AUTHORIZATION'] = 'Basic %s' % basic_auth
|
client_kwargs['HTTP_AUTHORIZATION'] = 'Basic %s' % basic_auth
|
||||||
client = Client(**client_kwargs)
|
|
||||||
elif isinstance(auth, basestring):
|
elif isinstance(auth, basestring):
|
||||||
client_kwargs['HTTP_AUTHORIZATION'] = 'Token %s' % auth
|
client_kwargs['HTTP_AUTHORIZATION'] = 'Token %s' % auth
|
||||||
client = Client(**client_kwargs)
|
client = Client(**client_kwargs)
|
||||||
method = getattr(client, method_name)
|
method = getattr(client, method_name)
|
||||||
response = None
|
response = None
|
||||||
if data is not None:
|
if data is not None:
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ class UsersTest(BaseTest):
|
|||||||
|
|
||||||
def test_auth_token_login(self):
|
def test_auth_token_login(self):
|
||||||
auth_token_url = reverse('api:auth_token_view')
|
auth_token_url = reverse('api:auth_token_view')
|
||||||
|
user_me_url = reverse('api:user_me_list')
|
||||||
|
|
||||||
# Always returns a 405 for any GET request, regardless of credentials.
|
# Always returns a 405 for any GET request, regardless of credentials.
|
||||||
self.get(auth_token_url, expect=405, auth=None)
|
self.get(auth_token_url, expect=405, auth=None)
|
||||||
@@ -69,16 +70,26 @@ class UsersTest(BaseTest):
|
|||||||
auth_token = response['token']
|
auth_token = response['token']
|
||||||
|
|
||||||
# Verify we can access our own user information with the auth token.
|
# Verify we can access our own user information with the auth token.
|
||||||
response = self.get(reverse('api:user_me_list'), expect=200,
|
response = self.get(user_me_url, expect=200, auth=auth_token)
|
||||||
auth=auth_token)
|
self.assertEquals(response['results'][0]['username'], 'normal')
|
||||||
|
self.assertEquals(response['count'], 1)
|
||||||
|
|
||||||
|
# If basic auth is passed via the Authorization header and the UI also
|
||||||
|
# passes token auth via the X-Auth-Token header, the API should favor
|
||||||
|
# the X-Auth-Token value.
|
||||||
|
mixed_auth = {
|
||||||
|
'basic': self.get_super_credentials(),
|
||||||
|
'token': auth_token,
|
||||||
|
}
|
||||||
|
response = self.get(user_me_url, expect=200, auth=mixed_auth)
|
||||||
self.assertEquals(response['results'][0]['username'], 'normal')
|
self.assertEquals(response['results'][0]['username'], 'normal')
|
||||||
self.assertEquals(response['count'], 1)
|
self.assertEquals(response['count'], 1)
|
||||||
|
|
||||||
# If we simulate a different remote address, should not be able to use
|
# If we simulate a different remote address, should not be able to use
|
||||||
# the first auth token.
|
# the first auth token.
|
||||||
remote_addr = '127.0.0.2'
|
remote_addr = '127.0.0.2'
|
||||||
response = self.get(reverse('api:user_me_list'), expect=401,
|
response = self.get(user_me_url, expect=401, auth=auth_token,
|
||||||
auth=auth_token, remote_addr=remote_addr)
|
remote_addr=remote_addr)
|
||||||
self.assertEqual(response['detail'], 'Invalid token')
|
self.assertEqual(response['detail'], 'Invalid token')
|
||||||
|
|
||||||
# The WWW-Authenticate header should specify Token auth, since that
|
# The WWW-Authenticate header should specify Token auth, since that
|
||||||
@@ -97,45 +108,42 @@ class UsersTest(BaseTest):
|
|||||||
|
|
||||||
# Verify we can access our own user information with the second auth
|
# Verify we can access our own user information with the second auth
|
||||||
# token from the other remote address.
|
# token from the other remote address.
|
||||||
response = self.get(reverse('api:user_me_list'), expect=200,
|
response = self.get(user_me_url, expect=200, auth=auth_token2,
|
||||||
auth=auth_token2, remote_addr=remote_addr)
|
remote_addr=remote_addr)
|
||||||
self.assertEquals(response['results'][0]['username'], 'normal')
|
self.assertEquals(response['results'][0]['username'], 'normal')
|
||||||
self.assertEquals(response['count'], 1)
|
self.assertEquals(response['count'], 1)
|
||||||
|
|
||||||
# The second auth token also can't be used from the first address, but
|
# The second auth token also can't be used from the first address, but
|
||||||
# the first auth token is still valid from its address.
|
# the first auth token is still valid from its address.
|
||||||
response = self.get(reverse('api:user_me_list'), expect=401,
|
response = self.get(user_me_url, expect=401, auth=auth_token2)
|
||||||
auth=auth_token2)
|
|
||||||
self.assertEqual(response['detail'], 'Invalid token')
|
self.assertEqual(response['detail'], 'Invalid token')
|
||||||
response_header = response.response.get('WWW-Authenticate', '')
|
response_header = response.response.get('WWW-Authenticate', '')
|
||||||
self.assertEqual(response_header.split()[0], 'Token')
|
self.assertEqual(response_header.split()[0], 'Token')
|
||||||
response = self.get(reverse('api:user_me_list'), expect=200,
|
response = self.get(user_me_url, expect=200, auth=auth_token)
|
||||||
auth=auth_token)
|
|
||||||
|
|
||||||
# A request without authentication should ask for Basic by default.
|
# A request without authentication should ask for Basic by default.
|
||||||
response = self.get(reverse('api:user_me_list'), expect=401)
|
response = self.get(user_me_url, expect=401)
|
||||||
response_header = response.response.get('WWW-Authenticate', '')
|
response_header = response.response.get('WWW-Authenticate', '')
|
||||||
self.assertEqual(response_header.split()[0], 'Basic')
|
self.assertEqual(response_header.split()[0], 'Basic')
|
||||||
|
|
||||||
# A request that attempts Basic auth should request Basic auth again.
|
# A request that attempts Basic auth should request Basic auth again.
|
||||||
response = self.get(reverse('api:user_me_list'), expect=401,
|
response = self.get(user_me_url, expect=401,
|
||||||
auth=('invalid', 'password'))
|
auth=('invalid', 'password'))
|
||||||
response_header = response.response.get('WWW-Authenticate', '')
|
response_header = response.response.get('WWW-Authenticate', '')
|
||||||
self.assertEqual(response_header.split()[0], 'Basic')
|
self.assertEqual(response_header.split()[0], 'Basic')
|
||||||
|
|
||||||
# Invalidate a key (simulate expiration), now token auth should fail
|
# Invalidate a key (simulate expiration), now token auth should fail
|
||||||
# with the first token, but still work with the second.
|
# with the first token, but still work with the second.
|
||||||
self.normal_django_user.auth_tokens.get(key=auth_token).invalidate()
|
self.normal_django_user.auth_tokens.get(key=auth_token).invalidate()
|
||||||
response = self.get(reverse('api:user_me_list'), expect=401,
|
response = self.get(user_me_url, expect=401, auth=auth_token)
|
||||||
auth=auth_token)
|
|
||||||
self.assertEqual(response['detail'], 'Token is expired')
|
self.assertEqual(response['detail'], 'Token is expired')
|
||||||
response = self.get(reverse('api:user_me_list'), expect=200,
|
response = self.get(user_me_url, expect=200, auth=auth_token2,
|
||||||
auth=auth_token2, remote_addr=remote_addr)
|
remote_addr=remote_addr)
|
||||||
|
|
||||||
# Token auth should be denied if the user is inactive.
|
# Token auth should be denied if the user is inactive.
|
||||||
self.normal_django_user.mark_inactive()
|
self.normal_django_user.mark_inactive()
|
||||||
response = self.get(reverse('api:user_me_list'), expect=401,
|
response = self.get(user_me_url, expect=401, auth=auth_token2,
|
||||||
auth=auth_token2, remote_addr=remote_addr)
|
remote_addr=remote_addr)
|
||||||
self.assertEqual(response['detail'], 'User inactive or deleted')
|
self.assertEqual(response['detail'], 'User inactive or deleted')
|
||||||
|
|
||||||
def test_ordinary_user_can_modify_some_fields_about_himself_but_not_all_and_passwords_work(self):
|
def test_ordinary_user_can_modify_some_fields_about_himself_but_not_all_and_passwords_work(self):
|
||||||
|
|||||||
@@ -143,8 +143,8 @@ REST_FRAMEWORK = {
|
|||||||
'PAGINATE_BY': 25,
|
'PAGINATE_BY': 25,
|
||||||
'PAGINATE_BY_PARAM': 'page_size',
|
'PAGINATE_BY_PARAM': 'page_size',
|
||||||
'DEFAULT_AUTHENTICATION_CLASSES': (
|
'DEFAULT_AUTHENTICATION_CLASSES': (
|
||||||
'rest_framework.authentication.BasicAuthentication',
|
|
||||||
'awx.api.authentication.TokenAuthentication',
|
'awx.api.authentication.TokenAuthentication',
|
||||||
|
'rest_framework.authentication.BasicAuthentication',
|
||||||
#'rest_framework.authentication.SessionAuthentication',
|
#'rest_framework.authentication.SessionAuthentication',
|
||||||
),
|
),
|
||||||
'DEFAULT_PERMISSION_CLASSES': (
|
'DEFAULT_PERMISSION_CLASSES': (
|
||||||
|
|||||||
Reference in New Issue
Block a user