Allow user set X-Auth-Token-Lifetime, with limits

* New conf value of max_token_life, existing token_life conf value is
  now just the default token life.

* When a user requests a token, they can send an
  X-Auth-Token-Lifetime header with the number of seconds they'd like
  the token to be valid for. This will be capped to max_token_life.

* Response to getting a token has new X-Auth-Token-Expires header
  that is the number of seconds the token is valid for.
This commit is contained in:
gholt 2013-03-01 06:22:10 +00:00
parent ce1072bc97
commit 36a156bc52
3 changed files with 98 additions and 10 deletions

View File

@ -41,8 +41,10 @@ use = egg:swauth#swauth
# useful when a load balancer url should be used by users, but swauth itself is
# behind the load balancer. Example:
# default_swift_cluster = local#https://public.com:8080/v1#http://private.com:8080/v1
# Number of seconds a newly issued token should be valid for.
# Number of seconds a newly issued token should be valid for, by default.
# token_life = 86400
# Maximum number of seconds a newly issued token can be valid for.
# max_token_life = <same as token_life>
# Specifies how the user key is stored. The default is 'plaintext', leaving the
# key unsecured but available for key-signing features if such are ever added.
# An alternative is 'sha1' which stores only a one-way hash of the key leaving

View File

@ -122,6 +122,7 @@ class Swauth(object):
except Exception:
pass
self.token_life = int(conf.get('token_life', 86400))
self.max_token_life = int(conf.get('max_token_life', self.token_life))
self.timeout = int(conf.get('node_timeout', 10))
self.itoken = None
self.itoken_expires = None
@ -1203,6 +1204,7 @@ class Swauth(object):
return HTTPUnauthorized(request=req)
# 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:
path = quote('/v1/%s/.token_%s/%s' %
@ -1219,6 +1221,7 @@ class Swauth(object):
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:
@ -1245,19 +1248,20 @@ class Swauth(object):
# Save token info
path = quote('/v1/%s/.token_%s/%s' %
(self.auth_account, token[-1], token))
if self.conf.get('user_set_tokenlifetime', False):
try:
self.token_life = int(req.headers.get('x-auth-token-lifetime'))
except (TypeError, ValueError):
pass
try:
token_life = min(
int(req.headers.get('x-auth-token-lifetime',
self.token_life)),
self.max_token_life)
except ValueError:
token_life = self.token_life
expires = int(time() + token_life)
resp = self.make_pre_authed_request(
req.environ, 'PUT', path,
json.dumps({'account': account, 'user': user,
'account_id': account_id,
'groups': user_detail['groups'],
'expires': time() + self.token_life})).get_response(self.app)
'expires': expires})).get_response(self.app)
if resp.status_int // 100 != 2:
raise Exception('Could not create new token: %s %s' %
(path, resp.status))
@ -1281,6 +1285,7 @@ class Swauth(object):
url = detail['storage'][detail['storage']['default']]
return Response(request=req, body=resp.body,
headers={'x-auth-token': token, 'x-storage-token': token,
'x-auth-token-expires': str(int(expires - time())),
'x-storage-url': url})
def handle_validate_token(self, req):

View File

@ -27,6 +27,10 @@ from swauth import middleware as auth
from swauth.authtypes import MAX_TOKEN_LENGTH
DEFAULT_TOKEN_LIFE = 86400
MAX_TOKEN_LIFE = 100000
class FakeMemcache(object):
def __init__(self):
@ -110,7 +114,10 @@ class TestAuth(unittest.TestCase):
def setUp(self):
self.test_auth = \
auth.filter_factory({'super_admin_key': 'supertest'})(FakeApp())
auth.filter_factory({
'super_admin_key': 'supertest',
'token_life': str(DEFAULT_TOKEN_LIFE),
'max_token_life': str(MAX_TOKEN_LIFE)})(FakeApp())
def test_super_admin_key_not_required(self):
auth.filter_factory({})(FakeApp())
@ -699,6 +706,80 @@ class TestAuth(unittest.TestCase):
"local": "http://127.0.0.1:8080/v1/AUTH_cfa"}})
self.assertEquals(self.test_auth.app.calls, 5)
def test_get_token_success_v1_0_with_user_token_life(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
('204 No Content', {}, ''),
# 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',
'X-Auth-Token-Lifetime': 10}).get_response(self.test_auth)
self.assertEquals(resp.status_int, 200)
left = int(resp.headers['x-auth-token-expires'])
self.assertTrue(left > 0, '%d > 0' % left)
self.assertTrue(left <= 10, '%d <= 10' % left)
self.assert_(resp.headers.get('x-auth-token',
'').startswith('AUTH_tk'), resp.headers.get('x-auth-token'))
self.assertEquals(resp.headers.get('x-auth-token'),
resp.headers.get('x-storage-token'))
self.assertEquals(resp.headers.get('x-storage-url'),
'http://127.0.0.1:8080/v1/AUTH_cfa')
self.assertEquals(json.loads(resp.body),
{"storage": {"default": "local",
"local": "http://127.0.0.1:8080/v1/AUTH_cfa"}})
self.assertEquals(self.test_auth.app.calls, 5)
def test_get_token_success_v1_0_with_user_token_life_past_max(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
('204 No Content', {}, ''),
# GET of services object
('200 Ok', {}, json.dumps({"storage": {"default": "local",
"local": "http://127.0.0.1:8080/v1/AUTH_cfa"}}))]))
req = Request.blank(
'/auth/v1.0',
headers={'X-Auth-User': 'act:usr',
'X-Auth-Key': 'key',
'X-Auth-Token-Lifetime': MAX_TOKEN_LIFE * 10})
resp = req.get_response(self.test_auth)
self.assertEquals(resp.status_int, 200)
left = int(resp.headers['x-auth-token-expires'])
self.assertTrue(left > DEFAULT_TOKEN_LIFE,
'%d > %d' % (left, DEFAULT_TOKEN_LIFE))
self.assertTrue(left <= MAX_TOKEN_LIFE,
'%d <= %d' % (left, MAX_TOKEN_LIFE))
self.assert_(resp.headers.get('x-auth-token',
'').startswith('AUTH_tk'), resp.headers.get('x-auth-token'))
self.assertEquals(resp.headers.get('x-auth-token'),
resp.headers.get('x-storage-token'))
self.assertEquals(resp.headers.get('x-storage-url'),
'http://127.0.0.1:8080/v1/AUTH_cfa')
self.assertEquals(json.loads(resp.body),
{"storage": {"default": "local",
"local": "http://127.0.0.1:8080/v1/AUTH_cfa"}})
self.assertEquals(self.test_auth.app.calls, 5)
def test_get_token_success_v1_act_auth(self):
self.test_auth.app = FakeApp(iter([
# GET of user object