Fix conflict error when creating new token

Change-Id: I21fb50dd0d1e5e6be668539a55c067a86d22d994
This commit is contained in:
Daniel Melo 2019-05-07 16:58:26 +02:00
parent ef649372c9
commit 650b4380b9
2 changed files with 193 additions and 45 deletions

View File

@ -67,6 +67,77 @@ SWIFT_MIN_VERSION = "2.2.0"
CONTENT_TYPE_JSON = 'application/json'
class AuthorizationError(Exception):
def __init__(self, request=None):
self.request = request
class UserAuthenticator(object):
"""Authentication of user
:param swauth: Swauth instance
:param req: swob.Request object
:param account: Account user is part of
:param user: Username
:param key: User key
"""
def __init__(self, swauth, req, account, user, key):
self.swauth = swauth
self.req = req
self.key = key
self.path = quote('/v1/%s/%s/%s'
% (self.swauth.auth_account, account, user))
self.token = None
self.user_detail = None
def _check_response(self, resp):
"""Check whether request was successful
:param resp: Response from pre authed request
:raises: AuthorizationError if response status code is 404
:raises: Exception if response status code is not 2xx
"""
if resp.status_int == 404:
raise AuthorizationError(request=self.req)
elif resp.status_int // 100 != 2:
raise Exception('Could not obtain user details: %s %s' %
(self.path, resp.status))
def refresh_token(self):
"""Get current token using HEAD request and update instance
attributes
:returns: token string
"""
resp = self.swauth.make_pre_authed_request(
self.req.environ, 'HEAD', self.path).get_response(self.swauth.app)
self._check_response(resp)
self.token = resp.headers.get('x-object-meta-auth-token')
return self.token
def authenticate(self):
"""Authorize user and get user detail and current token
:returns: tuple of user_detail object and token string
"""
resp = self.swauth.make_pre_authed_request(
self.req.environ, 'GET', self.path).get_response(self.swauth.app)
self._check_response(resp)
self.user_detail = json.loads(resp.body)
if not self.swauth.credentials_match(self.user_detail, self.key):
raise AuthorizationError(request=self.req)
self.token = resp.headers.get('x-object-meta-auth-token')
return (self.user_detail, self.token)
class Swauth(object):
"""Scalable authentication and authorization system that uses Swift as its
backing store.
@ -1219,6 +1290,60 @@ class Swauth(object):
return '.reseller_admin' in (g['name'] for g in user_detail['groups'])
def check_candidate_token(self, req, candidate_token):
"""Return current token if it's not neither expired or marked
for deletion
:param req: The swob.Request to process.
:param candidate_token: Token string
:returns: Tuple (token, expires)
"""
token = None
expires = None
object_name = self._get_concealed_token(candidate_token)
path = quote('/v1/%s/.token_%s/%s' %
(self.auth_account, object_name[-1], object_name))
delete_token = False
try:
if req.headers.get('x-auth-new-token', 'false').lower() in \
TRUE_VALUES:
delete_token = True
else:
resp = self.make_pre_authed_request(
req.environ, 'GET', path).get_response(self.app)
if resp.status_int // 100 == 2:
token_detail = json.loads(resp.body)
if token_detail['expires'] > time():
token = candidate_token
expires = token_detail['expires']
else:
delete_token = True
elif resp.status_int != 404:
raise Exception(
'Could not detect whether a token already exists: '
'%s %s' % (path, resp.status))
finally:
if delete_token:
self.delete_token(req, path, token)
return token, expires
def delete_token(self, req, path, token):
"""Delete token
:param req: The swob.Request to process
:param path: Token URL path
:param token: Token string
"""
self.make_pre_authed_request(
req.environ, 'DELETE', path).get_response(self.app)
memcache_client = cache_from_env(req.environ)
if memcache_client:
memcache_key = '%s/auth/%s' % (self.reseller_prefix,
token)
memcache_client.delete(memcache_key)
def handle_get_token(self, req):
"""Handles the various `request for token and service end point(s)` calls.
There are various formats to support the various auth servers in the
@ -1317,54 +1442,18 @@ class Swauth(object):
'x-storage-token': token,
'x-storage-url': url})
# Authenticate user
path = quote('/v1/%s/%s/%s' % (self.auth_account, account, user))
resp = self.make_pre_authed_request(
req.environ, 'GET', path).get_response(self.app)
if resp.status_int == 404:
return HTTPUnauthorized(request=req)
if resp.status_int // 100 != 2:
raise Exception('Could not obtain user details: %s %s' %
(path, resp.status))
user_detail = json.loads(resp.body)
if not self.credentials_match(user_detail, key):
return HTTPUnauthorized(request=req)
authenticator = UserAuthenticator(self, req, account, user, key)
try:
user_detail, candidate_token = authenticator.authenticate()
except AuthorizationError as e:
return HTTPUnauthorized(request=e.request)
# See if a token already exists and hasn't expired
token = None
expires = None
candidate_token = resp.headers.get('x-object-meta-auth-token')
if candidate_token:
object_name = self._get_concealed_token(candidate_token)
path = quote('/v1/%s/.token_%s/%s' %
(self.auth_account, object_name[-1], object_name))
delete_token = False
try:
if req.headers.get('x-auth-new-token', 'false').lower() in \
TRUE_VALUES:
delete_token = True
else:
resp = self.make_pre_authed_request(
req.environ, 'GET', path).get_response(self.app)
if resp.status_int // 100 == 2:
token_detail = json.loads(resp.body)
if token_detail['expires'] > time():
token = candidate_token
expires = token_detail['expires']
else:
delete_token = True
elif resp.status_int != 404:
raise Exception(
'Could not detect whether a token already exists: '
'%s %s' % (path, resp.status))
finally:
if delete_token:
self.make_pre_authed_request(
req.environ, 'DELETE', path).get_response(self.app)
memcache_client = cache_from_env(req.environ)
if memcache_client:
memcache_key = '%s/auth/%s' % (self.reseller_prefix,
candidate_token)
memcache_client.delete(memcache_key)
token, expires = self.check_candidate_token(req, candidate_token)
# Create a new token if one didn't exist
if not token:
# Retrieve account id, we'll save this in the token
@ -1405,9 +1494,27 @@ class Swauth(object):
req.environ, 'POST', path,
headers={'X-Object-Meta-Auth-Token': token}
).get_response(self.app)
if resp.status_int // 100 != 2:
if resp.status_int == 409:
# Another process probably set a new token already
# Delete created token
self.delete_token(req, path, token)
token = None
# Try to obtain the new one
try:
candidate_token = authenticator.refresh_token()
except AuthorizationError as e:
return HTTPUnauthorized(request=e.request)
if candidate_token:
token, expires = \
self.check_candidate_token(req, candidate_token)
if not token:
raise Exception('Conflict on saving new token, '
'but existing token could not be loaded: '
'%s %s' % (path, resp.status))
elif resp.status_int // 100 != 2:
raise Exception('Could not save new token: %s %s' %
(path, resp.status))
# Get the services information
path = quote('/v1/%s/%s/.services' % (self.auth_account, account))
resp = self.make_pre_authed_request(

View File

@ -1096,6 +1096,47 @@ class TestAuth(unittest.TestCase):
"local": "http://127.0.0.1:8080/v1/AUTH_cfa"}})
self.assertEqual(self.test_auth.app.calls, 7)
def test_get_token_success_after_create_conflict(self):
self.test_auth.app = FakeApp(iter([
# GET of user object
('200 Ok', {},
json.dumps({"auth": "plaintext:key",
"groups": [{'name': "act:usr"}, {'name': "act"},
{'name': ".admin"}]})),
# GET of account
('204 Ok', {'X-Container-Meta-Account-Id': 'AUTH_cfa'}, ''),
# PUT of new token
('201 Created', {}, ''),
# POST of token to user object
('409 Conflict', {}, ''),
# DELETE of unused token
('204 No Content', {}, ''),
# HEAD of user object
('200 Ok', {'X-Object-Meta-Auth-Token': 'AUTH_tktest'}, ''),
# GET of token
('200 Ok', {}, json.dumps({"account": "act", "user": "usr",
"account_id": "AUTH_cfa", "groups": [{'name': "act:usr"},
{'name': "key"}, {'name': ".admin"}],
"expires": 9999999999.9999999})),
# GET of services object
('200 Ok', {}, json.dumps({"storage": {"default": "local",
"local": "http://127.0.0.1:8080/v1/AUTH_cfa"}}))]))
resp = Request.blank('/auth/v1.0',
headers={'X-Auth-User': 'act:usr',
'X-Auth-Key': 'key'}).get_response(self.test_auth)
self.assertEqual(resp.status_int, 200)
self.assertEqual(resp.content_type, CONTENT_TYPE_JSON)
self.assertTrue(resp.headers.get('x-auth-token',
'').startswith('AUTH_tk'), resp.headers.get('x-auth-token'))
self.assertEqual(resp.headers.get('x-auth-token'),
resp.headers.get('x-storage-token'))
self.assertEqual(resp.headers.get('x-storage-url'),
'http://127.0.0.1:8080/v1/AUTH_cfa')
self.assertEqual(json.loads(resp.body),
{"storage": {"default": "local",
"local": "http://127.0.0.1:8080/v1/AUTH_cfa"}})
self.assertEqual(self.test_auth.app.calls, 8)
def test_prep_success(self):
list_to_iter = [
# PUT of .auth account