Fix conflict error when creating new token
Change-Id: I21fb50dd0d1e5e6be668539a55c067a86d22d994
This commit is contained in:
parent
ef649372c9
commit
2b91c54325
|
@ -67,6 +67,99 @@ SWIFT_MIN_VERSION = "2.2.0"
|
|||
CONTENT_TYPE_JSON = 'application/json'
|
||||
|
||||
|
||||
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.user_detail = None
|
||||
self.token = None
|
||||
self.error = None
|
||||
self.authenticated = False
|
||||
|
||||
def _fail(self, error):
|
||||
"""Update instance attributes after unsuccessful authorization attempt
|
||||
"""
|
||||
self.error = error
|
||||
self.user_detail = None
|
||||
self.token = None
|
||||
self.authenticated = False
|
||||
|
||||
def _success(self, user_detail, token):
|
||||
"""Update instance attributes after successful authorization attempt
|
||||
"""
|
||||
self.error = None
|
||||
self.user_detail = user_detail
|
||||
self.token = token
|
||||
self.authenticated = True
|
||||
|
||||
def _response_is_ok(self, resp):
|
||||
"""Check whether request was successful
|
||||
|
||||
:param resp: Response from pre authed request
|
||||
:returns: True if returned status code is 2xx, False otherwise
|
||||
:raises: Exception if response status code is 404
|
||||
"""
|
||||
if resp.status_int == 404:
|
||||
self._fail(HTTPUnauthorized(request=self.req))
|
||||
return False
|
||||
elif resp.status_int // 100 != 2:
|
||||
raise Exception('Could not obtain user details: %s %s' %
|
||||
(self.path, resp.status))
|
||||
else:
|
||||
return True
|
||||
|
||||
def refresh_token(self):
|
||||
"""Get current token using HEAD request and actualize instance
|
||||
attributes
|
||||
|
||||
:returns: True if request was successful, False otherwise
|
||||
"""
|
||||
resp = self.swauth.make_pre_authed_request(
|
||||
self.req.environ, 'HEAD', self.path).get_response(self.swauth.app)
|
||||
|
||||
if self._response_is_ok(resp):
|
||||
token = resp.headers.get('x-object-meta-auth-token')
|
||||
self._success(self.user_detail, token)
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def authenticate(self):
|
||||
"""Authorize user and get user detail and current token
|
||||
|
||||
:returns: True if authorization was successful, False otherwise
|
||||
"""
|
||||
resp = self.swauth.make_pre_authed_request(
|
||||
self.req.environ, 'GET', self.path).get_response(self.swauth.app)
|
||||
|
||||
if not self._response_is_ok(resp):
|
||||
return False
|
||||
|
||||
user_detail = json.loads(resp.body)
|
||||
if not self.swauth.credentials_match(user_detail, self.key):
|
||||
self._fail(HTTPUnauthorized(request=self.req))
|
||||
return False
|
||||
|
||||
token = resp.headers.get('x-object-meta-auth-token')
|
||||
|
||||
self._success(user_detail, token)
|
||||
return True
|
||||
|
||||
|
||||
class Swauth(object):
|
||||
"""Scalable authentication and authorization system that uses Swift as its
|
||||
backing store.
|
||||
|
@ -1219,6 +1312,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 +1464,19 @@ 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)
|
||||
if authenticator.authenticate():
|
||||
user_detail = authenticator.user_detail
|
||||
candidate_token = authenticator.token
|
||||
else:
|
||||
return authenticator.error
|
||||
|
||||
# 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 +1517,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
|
||||
if authenticator.refresh_token():
|
||||
candidate_token = authenticator.token
|
||||
else:
|
||||
return authenticator.error
|
||||
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(
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue