Add token expiration
* Config option token.expiration defines amount of time tokens should be valid * Fixes bug 928545 Change-Id: I3dff7a1ebf03bb44fc6e5247f976baea0581de08
This commit is contained in:
parent
1ed067cb57
commit
71436dbf18
@ -32,6 +32,9 @@ template_file = ./etc/default_catalog.templates
|
||||
[token]
|
||||
driver = keystone.token.backends.kvs.Token
|
||||
|
||||
# Amount of time a token should remain valid (in seconds)
|
||||
expiration = 86400
|
||||
|
||||
[policy]
|
||||
driver = keystone.policy.backends.simple.SimpleMatch
|
||||
|
||||
|
@ -26,6 +26,7 @@ ModelBase = declarative.declarative_base()
|
||||
Column = sql.Column
|
||||
String = sql.String
|
||||
ForeignKey = sql.ForeignKey
|
||||
DateTime = sql.DateTime
|
||||
|
||||
|
||||
# Special Fields
|
||||
|
@ -5,6 +5,7 @@ from keystone.common import sql
|
||||
|
||||
# these are to make sure all the models we care about are defined
|
||||
import keystone.identity.backends.sql
|
||||
import keystone.token.backends.sql
|
||||
import keystone.contrib.ec2.backends.sql
|
||||
|
||||
|
||||
|
@ -23,6 +23,7 @@ import hmac
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
import urllib
|
||||
|
||||
import passlib.hash
|
||||
@ -35,6 +36,9 @@ CONF = config.CONF
|
||||
config.register_int('crypt_strength', default=40000)
|
||||
|
||||
|
||||
ISO_TIME_FORMAT = '%Y-%m-%dT%H:%M:%SZ'
|
||||
|
||||
|
||||
def import_class(import_str):
|
||||
"""Returns a class from a string including module and class."""
|
||||
mod_str, _sep, class_str = import_str.rpartition('.')
|
||||
@ -201,3 +205,23 @@ def check_output(*popenargs, **kwargs):
|
||||
|
||||
def git(*args):
|
||||
return check_output(['git'] + list(args))
|
||||
|
||||
|
||||
def isotime(dt_obj):
|
||||
"""Format datetime object as ISO compliant string.
|
||||
|
||||
:param dt_obj: datetime.datetime object
|
||||
:returns: string representation of datetime object
|
||||
|
||||
"""
|
||||
return dt_obj.strftime(ISO_TIME_FORMAT)
|
||||
|
||||
|
||||
def unixtime(dt_obj):
|
||||
"""Format datetime object as unix timestamp
|
||||
|
||||
:param dt_obj: datetime.datetime object
|
||||
:returns: float
|
||||
|
||||
"""
|
||||
return time.mktime(dt_obj.utctimetuple())
|
||||
|
@ -163,8 +163,7 @@ class Ec2Controller(wsgi.Application):
|
||||
metadata=metadata_ref)
|
||||
|
||||
token_ref = self.token_api.create_token(
|
||||
context, token_id, dict(expires='',
|
||||
id=token_id,
|
||||
context, token_id, dict(id=token_id,
|
||||
user=user_ref,
|
||||
tenant=tenant_ref,
|
||||
metadata=metadata_ref))
|
||||
|
@ -1,5 +1,7 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
import copy
|
||||
|
||||
from keystone import identity
|
||||
from keystone.common import sql
|
||||
from keystone.common import utils
|
||||
@ -64,7 +66,7 @@ class Tenant(sql.ModelBase, sql.DictBase):
|
||||
return cls(**tenant_dict)
|
||||
|
||||
def to_dict(self):
|
||||
extra_copy = self.extra.copy()
|
||||
extra_copy = copy.deepcopy(self.extra)
|
||||
extra_copy['id'] = self.id
|
||||
extra_copy['name'] = self.name
|
||||
return extra_copy
|
||||
|
@ -12,6 +12,7 @@ from keystone import identity
|
||||
from keystone import policy
|
||||
from keystone import token
|
||||
from keystone.common import logging
|
||||
from keystone.common import utils
|
||||
from keystone.common import wsgi
|
||||
|
||||
|
||||
@ -226,8 +227,7 @@ class TokenController(wsgi.Application):
|
||||
raise webob.exc.HTTPForbidden(e.message)
|
||||
|
||||
token_ref = self.token_api.create_token(
|
||||
context, token_id, dict(expires='',
|
||||
id=token_id,
|
||||
context, token_id, dict(id=token_id,
|
||||
user=user_ref,
|
||||
tenant=tenant_ref,
|
||||
metadata=metadata_ref))
|
||||
@ -283,8 +283,7 @@ class TokenController(wsgi.Application):
|
||||
catalog_ref = {}
|
||||
|
||||
token_ref = self.token_api.create_token(
|
||||
context, token_id, dict(expires='',
|
||||
id=token_id,
|
||||
context, token_id, dict(id=token_id,
|
||||
user=user_ref,
|
||||
tenant=tenant_ref,
|
||||
metadata=metadata_ref))
|
||||
@ -351,8 +350,11 @@ class TokenController(wsgi.Application):
|
||||
def _format_token(self, token_ref, roles_ref):
|
||||
user_ref = token_ref['user']
|
||||
metadata_ref = token_ref['metadata']
|
||||
expires = token_ref['expires']
|
||||
if expires is not None:
|
||||
expires = utils.isotime(expires)
|
||||
o = {'access': {'token': {'id': token_ref['id'],
|
||||
'expires': token_ref['expires']
|
||||
'expires': expires,
|
||||
},
|
||||
'user': {'id': user_ref['id'],
|
||||
'name': user_ref['name'],
|
||||
|
@ -1,5 +1,8 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
import copy
|
||||
import datetime
|
||||
|
||||
from keystone.common import kvs
|
||||
from keystone import exception
|
||||
from keystone import token
|
||||
@ -8,14 +11,19 @@ from keystone import token
|
||||
class Token(kvs.Base, token.Driver):
|
||||
# Public interface
|
||||
def get_token(self, token_id):
|
||||
try:
|
||||
return self.db['token-%s' % token_id]
|
||||
except KeyError:
|
||||
token = self.db.get('token-%s' % token_id)
|
||||
if (token and (token['expires'] is None
|
||||
or token['expires'] > datetime.datetime.now())):
|
||||
return token
|
||||
else:
|
||||
raise exception.TokenNotFound(token_id=token_id)
|
||||
|
||||
def create_token(self, token_id, data):
|
||||
self.db.set('token-%s' % token_id, data)
|
||||
return data
|
||||
data_copy = copy.deepcopy(data)
|
||||
if 'expires' not in data:
|
||||
data_copy['expires'] = self._get_default_expire_time()
|
||||
self.db.set('token-%s' % token_id, data_copy)
|
||||
return copy.deepcopy(data_copy)
|
||||
|
||||
def delete_token(self, token_id):
|
||||
try:
|
||||
|
@ -1,12 +1,14 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
from __future__ import absolute_import
|
||||
import copy
|
||||
|
||||
import memcache
|
||||
|
||||
from keystone import config
|
||||
from keystone import exception
|
||||
from keystone import token
|
||||
from keystone.common import utils
|
||||
|
||||
|
||||
CONF = config.CONF
|
||||
@ -38,9 +40,16 @@ class Token(token.Driver):
|
||||
return token
|
||||
|
||||
def create_token(self, token_id, data):
|
||||
data_copy = copy.deepcopy(data)
|
||||
ptk = self._prefix_token_id(token_id)
|
||||
self.client.set(ptk, data)
|
||||
return data
|
||||
if 'expires' not in data_copy:
|
||||
data_copy['expires'] = self._get_default_expire_time()
|
||||
kwargs = {}
|
||||
if data_copy['expires'] is not None:
|
||||
expires_ts = utils.unixtime(data_copy['expires'])
|
||||
kwargs['time'] = expires_ts
|
||||
self.client.set(ptk, data_copy, **kwargs)
|
||||
return copy.deepcopy(data_copy)
|
||||
|
||||
def delete_token(self, token_id):
|
||||
# Test for existence
|
||||
|
@ -1,5 +1,8 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
import copy
|
||||
import datetime
|
||||
|
||||
from keystone.common import sql
|
||||
from keystone import exception
|
||||
from keystone import token
|
||||
@ -8,21 +11,24 @@ from keystone import token
|
||||
class TokenModel(sql.ModelBase, sql.DictBase):
|
||||
__tablename__ = 'token'
|
||||
id = sql.Column(sql.String(64), primary_key=True)
|
||||
expires = sql.Column(sql.DateTime(), default=None)
|
||||
extra = sql.Column(sql.JsonBlob())
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, token_dict):
|
||||
# shove any non-indexed properties into extra
|
||||
extra = copy.deepcopy(token_dict)
|
||||
data = {}
|
||||
token_dict_copy = token_dict.copy()
|
||||
data['id'] = token_dict_copy.pop('id')
|
||||
data['extra'] = token_dict_copy
|
||||
for k in ('id', 'expires'):
|
||||
data[k] = extra.pop(k, None)
|
||||
data['extra'] = extra
|
||||
return cls(**data)
|
||||
|
||||
def to_dict(self):
|
||||
extra_copy = self.extra.copy()
|
||||
extra_copy['id'] = self.id
|
||||
return extra_copy
|
||||
out = copy.deepcopy(self.extra)
|
||||
out['id'] = self.id
|
||||
out['expires'] = self.expires
|
||||
return out
|
||||
|
||||
|
||||
class Token(sql.Base, token.Driver):
|
||||
@ -30,15 +36,22 @@ class Token(sql.Base, token.Driver):
|
||||
def get_token(self, token_id):
|
||||
session = self.get_session()
|
||||
token_ref = session.query(TokenModel).filter_by(id=token_id).first()
|
||||
if not token_ref:
|
||||
now = datetime.datetime.now()
|
||||
if token_ref and (not token_ref.expires or now < token_ref.expires):
|
||||
return token_ref.to_dict()
|
||||
else:
|
||||
raise exception.TokenNotFound(token_id=token_id)
|
||||
return token_ref.to_dict()
|
||||
|
||||
def create_token(self, token_id, data):
|
||||
data['id'] = token_id
|
||||
data_copy = copy.deepcopy(data)
|
||||
if 'expires' not in data_copy:
|
||||
data_copy['expires'] = self._get_default_expire_time()
|
||||
|
||||
token_ref = TokenModel.from_dict(data_copy)
|
||||
token_ref.id = token_id
|
||||
|
||||
session = self.get_session()
|
||||
with session.begin():
|
||||
token_ref = TokenModel.from_dict(data)
|
||||
session.add(token_ref)
|
||||
session.flush()
|
||||
return token_ref.to_dict()
|
||||
|
@ -2,11 +2,14 @@
|
||||
|
||||
"""Main entry point into the Token service."""
|
||||
|
||||
import datetime
|
||||
|
||||
from keystone import config
|
||||
from keystone.common import manager
|
||||
|
||||
|
||||
CONF = config.CONF
|
||||
config.register_int('expiration', group='token', default=86400)
|
||||
|
||||
|
||||
class Manager(manager.Manager):
|
||||
@ -68,3 +71,12 @@ class Driver(object):
|
||||
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def _get_default_expire_time(self):
|
||||
"""Determine when a token should expire based on the config.
|
||||
|
||||
:returns: datetime.datetime object
|
||||
|
||||
"""
|
||||
expire_delta = datetime.timedelta(seconds=CONF.token.expiration)
|
||||
return datetime.datetime.now() + expire_delta
|
||||
|
@ -8,5 +8,8 @@ pool_timeout = 200
|
||||
[identity]
|
||||
driver = keystone.identity.backends.sql.Identity
|
||||
|
||||
[token]
|
||||
driver = keystone.token.backends.sql.Token
|
||||
|
||||
[ec2]
|
||||
driver = keystone.contrib.ec2.backends.sql.Ec2
|
||||
|
@ -1,5 +1,6 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
import datetime
|
||||
import uuid
|
||||
|
||||
from keystone import exception
|
||||
@ -211,9 +212,13 @@ class TokenTests(object):
|
||||
token_id = uuid.uuid4().hex
|
||||
data = {'id': token_id, 'a': 'b'}
|
||||
data_ref = self.token_api.create_token(token_id, data)
|
||||
expires = data_ref.pop('expires')
|
||||
self.assertTrue(isinstance(expires, datetime.datetime))
|
||||
self.assertDictEquals(data_ref, data)
|
||||
|
||||
new_data_ref = self.token_api.get_token(token_id)
|
||||
expires = new_data_ref.pop('expires')
|
||||
self.assertTrue(isinstance(expires, datetime.datetime))
|
||||
self.assertEquals(new_data_ref, data)
|
||||
|
||||
self.token_api.delete_token(token_id)
|
||||
@ -221,3 +226,20 @@ class TokenTests(object):
|
||||
self.token_api.delete_token, token_id)
|
||||
self.assertRaises(exception.TokenNotFound,
|
||||
self.token_api.get_token, token_id)
|
||||
|
||||
def test_expired_token(self):
|
||||
token_id = uuid.uuid4().hex
|
||||
expire_time = datetime.datetime.now() - datetime.timedelta(minutes=1)
|
||||
data = {'id': token_id, 'a': 'b', 'expires': expire_time}
|
||||
data_ref = self.token_api.create_token(token_id, data)
|
||||
self.assertDictEquals(data_ref, data)
|
||||
self.assertRaises(exception.TokenNotFound,
|
||||
self.token_api.get_token, token_id)
|
||||
|
||||
def test_null_expires_token(self):
|
||||
token_id = uuid.uuid4().hex
|
||||
data = {'id': token_id, 'a': 'b', 'expires': None}
|
||||
data_ref = self.token_api.create_token(token_id, data)
|
||||
self.assertDictEquals(data_ref, data)
|
||||
new_data_ref = self.token_api.get_token(token_id)
|
||||
self.assertEqual(data_ref, new_data_ref)
|
||||
|
@ -1,5 +1,7 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
import datetime
|
||||
import time
|
||||
import uuid
|
||||
|
||||
import memcache
|
||||
@ -25,15 +27,17 @@ class MemcacheClient(object):
|
||||
def get(self, key):
|
||||
"""Retrieves the value for a key or None."""
|
||||
self.check_key(key)
|
||||
try:
|
||||
return self.cache[key]
|
||||
except KeyError:
|
||||
obj = self.cache.get(key)
|
||||
now = time.mktime(datetime.datetime.now().timetuple())
|
||||
if obj and (obj[1] == 0 or obj[1] > now):
|
||||
return obj[0]
|
||||
else:
|
||||
raise exception.TokenNotFound(token_id=key)
|
||||
|
||||
def set(self, key, value):
|
||||
def set(self, key, value, time=0):
|
||||
"""Sets the value for a key."""
|
||||
self.check_key(key)
|
||||
self.cache[key] = value
|
||||
self.cache[key] = (value, time)
|
||||
return True
|
||||
|
||||
def delete(self, key):
|
||||
|
@ -2,7 +2,6 @@
|
||||
from keystone import config
|
||||
from keystone import test
|
||||
from keystone.common.sql import util as sql_util
|
||||
from keystone.common.sql import migration
|
||||
|
||||
import test_keystoneclient
|
||||
|
||||
|
@ -15,6 +15,8 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import datetime
|
||||
|
||||
from keystone import test
|
||||
from keystone.common import utils
|
||||
|
||||
@ -38,3 +40,10 @@ class UtilsTestCase(test.TestCase):
|
||||
hashed = utils.hash_password(password)
|
||||
self.assertTrue(utils.check_password(password, hashed))
|
||||
self.assertFalse(utils.check_password(wrong, hashed))
|
||||
|
||||
def test_isotime(self):
|
||||
dt = datetime.datetime(year=1987, month=10, day=13,
|
||||
hour=1, minute=2, second=3)
|
||||
output = utils.isotime(dt)
|
||||
expected = '1987-10-13T01:02:03Z'
|
||||
self.assertEqual(output, expected)
|
||||
|
Loading…
x
Reference in New Issue
Block a user