Skip to content
Browse files

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.
  • Loading branch information...
1 parent ce1072b commit 36a156bc52ded0d713a09a70bec177b92fc4cd9b @gholt committed Mar 1, 2013
Showing with 98 additions and 10 deletions.
  1. +3 −1 etc/proxy-server.conf-sample
  2. +13 −8 swauth/middleware.py
  3. +82 −1 test_swauth/unit/test_middleware.py
View
4 etc/proxy-server.conf-sample
@@ -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
21 swauth/middleware.py
@@ -122,6 +122,7 @@ def __init__(self, app, conf):
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 @@ def handle_get_token(self, req):
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 @@ def handle_get_token(self, req):
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 @@ def handle_get_token(self, req):
# 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 @@ def handle_get_token(self, req):
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
83 test_swauth/unit/test_middleware.py
@@ -27,6 +27,10 @@
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 @@ def test_get_token_success_v1_0(self):
"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

0 comments on commit 36a156b

Please sign in to comment.
Something went wrong with that request. Please try again.