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:
Brian Waldon 2012-02-08 16:08:08 -08:00
parent 1ed067cb57
commit 71436dbf18
16 changed files with 142 additions and 31 deletions

View File

@ -32,6 +32,9 @@ template_file = ./etc/default_catalog.templates
[token] [token]
driver = keystone.token.backends.kvs.Token driver = keystone.token.backends.kvs.Token
# Amount of time a token should remain valid (in seconds)
expiration = 86400
[policy] [policy]
driver = keystone.policy.backends.simple.SimpleMatch driver = keystone.policy.backends.simple.SimpleMatch

View File

@ -26,6 +26,7 @@ ModelBase = declarative.declarative_base()
Column = sql.Column Column = sql.Column
String = sql.String String = sql.String
ForeignKey = sql.ForeignKey ForeignKey = sql.ForeignKey
DateTime = sql.DateTime
# Special Fields # Special Fields

View File

@ -5,6 +5,7 @@ from keystone.common import sql
# these are to make sure all the models we care about are defined # these are to make sure all the models we care about are defined
import keystone.identity.backends.sql import keystone.identity.backends.sql
import keystone.token.backends.sql
import keystone.contrib.ec2.backends.sql import keystone.contrib.ec2.backends.sql

View File

@ -23,6 +23,7 @@ import hmac
import json import json
import subprocess import subprocess
import sys import sys
import time
import urllib import urllib
import passlib.hash import passlib.hash
@ -35,6 +36,9 @@ CONF = config.CONF
config.register_int('crypt_strength', default=40000) config.register_int('crypt_strength', default=40000)
ISO_TIME_FORMAT = '%Y-%m-%dT%H:%M:%SZ'
def import_class(import_str): def import_class(import_str):
"""Returns a class from a string including module and class.""" """Returns a class from a string including module and class."""
mod_str, _sep, class_str = import_str.rpartition('.') mod_str, _sep, class_str = import_str.rpartition('.')
@ -201,3 +205,23 @@ def check_output(*popenargs, **kwargs):
def git(*args): def git(*args):
return check_output(['git'] + list(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())

View File

@ -163,8 +163,7 @@ class Ec2Controller(wsgi.Application):
metadata=metadata_ref) metadata=metadata_ref)
token_ref = self.token_api.create_token( token_ref = self.token_api.create_token(
context, token_id, dict(expires='', context, token_id, dict(id=token_id,
id=token_id,
user=user_ref, user=user_ref,
tenant=tenant_ref, tenant=tenant_ref,
metadata=metadata_ref)) metadata=metadata_ref))

View File

@ -1,5 +1,7 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4 # vim: tabstop=4 shiftwidth=4 softtabstop=4
import copy
from keystone import identity from keystone import identity
from keystone.common import sql from keystone.common import sql
from keystone.common import utils from keystone.common import utils
@ -64,7 +66,7 @@ class Tenant(sql.ModelBase, sql.DictBase):
return cls(**tenant_dict) return cls(**tenant_dict)
def to_dict(self): def to_dict(self):
extra_copy = self.extra.copy() extra_copy = copy.deepcopy(self.extra)
extra_copy['id'] = self.id extra_copy['id'] = self.id
extra_copy['name'] = self.name extra_copy['name'] = self.name
return extra_copy return extra_copy

View File

@ -12,6 +12,7 @@ from keystone import identity
from keystone import policy from keystone import policy
from keystone import token from keystone import token
from keystone.common import logging from keystone.common import logging
from keystone.common import utils
from keystone.common import wsgi from keystone.common import wsgi
@ -226,8 +227,7 @@ class TokenController(wsgi.Application):
raise webob.exc.HTTPForbidden(e.message) raise webob.exc.HTTPForbidden(e.message)
token_ref = self.token_api.create_token( token_ref = self.token_api.create_token(
context, token_id, dict(expires='', context, token_id, dict(id=token_id,
id=token_id,
user=user_ref, user=user_ref,
tenant=tenant_ref, tenant=tenant_ref,
metadata=metadata_ref)) metadata=metadata_ref))
@ -283,8 +283,7 @@ class TokenController(wsgi.Application):
catalog_ref = {} catalog_ref = {}
token_ref = self.token_api.create_token( token_ref = self.token_api.create_token(
context, token_id, dict(expires='', context, token_id, dict(id=token_id,
id=token_id,
user=user_ref, user=user_ref,
tenant=tenant_ref, tenant=tenant_ref,
metadata=metadata_ref)) metadata=metadata_ref))
@ -351,8 +350,11 @@ class TokenController(wsgi.Application):
def _format_token(self, token_ref, roles_ref): def _format_token(self, token_ref, roles_ref):
user_ref = token_ref['user'] user_ref = token_ref['user']
metadata_ref = token_ref['metadata'] 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'], o = {'access': {'token': {'id': token_ref['id'],
'expires': token_ref['expires'] 'expires': expires,
}, },
'user': {'id': user_ref['id'], 'user': {'id': user_ref['id'],
'name': user_ref['name'], 'name': user_ref['name'],

View File

@ -1,5 +1,8 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4 # vim: tabstop=4 shiftwidth=4 softtabstop=4
import copy
import datetime
from keystone.common import kvs from keystone.common import kvs
from keystone import exception from keystone import exception
from keystone import token from keystone import token
@ -8,14 +11,19 @@ from keystone import token
class Token(kvs.Base, token.Driver): class Token(kvs.Base, token.Driver):
# Public interface # Public interface
def get_token(self, token_id): def get_token(self, token_id):
try: token = self.db.get('token-%s' % token_id)
return self.db['token-%s' % token_id] if (token and (token['expires'] is None
except KeyError: or token['expires'] > datetime.datetime.now())):
return token
else:
raise exception.TokenNotFound(token_id=token_id) raise exception.TokenNotFound(token_id=token_id)
def create_token(self, token_id, data): def create_token(self, token_id, data):
self.db.set('token-%s' % token_id, data) data_copy = copy.deepcopy(data)
return 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): def delete_token(self, token_id):
try: try:

View File

@ -1,12 +1,14 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4 # vim: tabstop=4 shiftwidth=4 softtabstop=4
from __future__ import absolute_import from __future__ import absolute_import
import copy
import memcache import memcache
from keystone import config from keystone import config
from keystone import exception from keystone import exception
from keystone import token from keystone import token
from keystone.common import utils
CONF = config.CONF CONF = config.CONF
@ -38,9 +40,16 @@ class Token(token.Driver):
return token return token
def create_token(self, token_id, data): def create_token(self, token_id, data):
data_copy = copy.deepcopy(data)
ptk = self._prefix_token_id(token_id) ptk = self._prefix_token_id(token_id)
self.client.set(ptk, data) if 'expires' not in data_copy:
return data 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): def delete_token(self, token_id):
# Test for existence # Test for existence

View File

@ -1,5 +1,8 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4 # vim: tabstop=4 shiftwidth=4 softtabstop=4
import copy
import datetime
from keystone.common import sql from keystone.common import sql
from keystone import exception from keystone import exception
from keystone import token from keystone import token
@ -8,21 +11,24 @@ from keystone import token
class TokenModel(sql.ModelBase, sql.DictBase): class TokenModel(sql.ModelBase, sql.DictBase):
__tablename__ = 'token' __tablename__ = 'token'
id = sql.Column(sql.String(64), primary_key=True) id = sql.Column(sql.String(64), primary_key=True)
expires = sql.Column(sql.DateTime(), default=None)
extra = sql.Column(sql.JsonBlob()) extra = sql.Column(sql.JsonBlob())
@classmethod @classmethod
def from_dict(cls, token_dict): def from_dict(cls, token_dict):
# shove any non-indexed properties into extra # shove any non-indexed properties into extra
extra = copy.deepcopy(token_dict)
data = {} data = {}
token_dict_copy = token_dict.copy() for k in ('id', 'expires'):
data['id'] = token_dict_copy.pop('id') data[k] = extra.pop(k, None)
data['extra'] = token_dict_copy data['extra'] = extra
return cls(**data) return cls(**data)
def to_dict(self): def to_dict(self):
extra_copy = self.extra.copy() out = copy.deepcopy(self.extra)
extra_copy['id'] = self.id out['id'] = self.id
return extra_copy out['expires'] = self.expires
return out
class Token(sql.Base, token.Driver): class Token(sql.Base, token.Driver):
@ -30,15 +36,22 @@ class Token(sql.Base, token.Driver):
def get_token(self, token_id): def get_token(self, token_id):
session = self.get_session() session = self.get_session()
token_ref = session.query(TokenModel).filter_by(id=token_id).first() 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) raise exception.TokenNotFound(token_id=token_id)
return token_ref.to_dict()
def create_token(self, token_id, data): 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() session = self.get_session()
with session.begin(): with session.begin():
token_ref = TokenModel.from_dict(data)
session.add(token_ref) session.add(token_ref)
session.flush() session.flush()
return token_ref.to_dict() return token_ref.to_dict()

View File

@ -2,11 +2,14 @@
"""Main entry point into the Token service.""" """Main entry point into the Token service."""
import datetime
from keystone import config from keystone import config
from keystone.common import manager from keystone.common import manager
CONF = config.CONF CONF = config.CONF
config.register_int('expiration', group='token', default=86400)
class Manager(manager.Manager): class Manager(manager.Manager):
@ -68,3 +71,12 @@ class Driver(object):
""" """
raise NotImplementedError() 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

View File

@ -8,5 +8,8 @@ pool_timeout = 200
[identity] [identity]
driver = keystone.identity.backends.sql.Identity driver = keystone.identity.backends.sql.Identity
[token]
driver = keystone.token.backends.sql.Token
[ec2] [ec2]
driver = keystone.contrib.ec2.backends.sql.Ec2 driver = keystone.contrib.ec2.backends.sql.Ec2

View File

@ -1,5 +1,6 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4 # vim: tabstop=4 shiftwidth=4 softtabstop=4
import datetime
import uuid import uuid
from keystone import exception from keystone import exception
@ -211,9 +212,13 @@ class TokenTests(object):
token_id = uuid.uuid4().hex token_id = uuid.uuid4().hex
data = {'id': token_id, 'a': 'b'} data = {'id': token_id, 'a': 'b'}
data_ref = self.token_api.create_token(token_id, data) 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) self.assertDictEquals(data_ref, data)
new_data_ref = self.token_api.get_token(token_id) 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.assertEquals(new_data_ref, data)
self.token_api.delete_token(token_id) self.token_api.delete_token(token_id)
@ -221,3 +226,20 @@ class TokenTests(object):
self.token_api.delete_token, token_id) self.token_api.delete_token, token_id)
self.assertRaises(exception.TokenNotFound, self.assertRaises(exception.TokenNotFound,
self.token_api.get_token, token_id) 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)

View File

@ -1,5 +1,7 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4 # vim: tabstop=4 shiftwidth=4 softtabstop=4
import datetime
import time
import uuid import uuid
import memcache import memcache
@ -25,15 +27,17 @@ class MemcacheClient(object):
def get(self, key): def get(self, key):
"""Retrieves the value for a key or None.""" """Retrieves the value for a key or None."""
self.check_key(key) self.check_key(key)
try: obj = self.cache.get(key)
return self.cache[key] now = time.mktime(datetime.datetime.now().timetuple())
except KeyError: if obj and (obj[1] == 0 or obj[1] > now):
return obj[0]
else:
raise exception.TokenNotFound(token_id=key) raise exception.TokenNotFound(token_id=key)
def set(self, key, value): def set(self, key, value, time=0):
"""Sets the value for a key.""" """Sets the value for a key."""
self.check_key(key) self.check_key(key)
self.cache[key] = value self.cache[key] = (value, time)
return True return True
def delete(self, key): def delete(self, key):

View File

@ -2,7 +2,6 @@
from keystone import config from keystone import config
from keystone import test from keystone import test
from keystone.common.sql import util as sql_util from keystone.common.sql import util as sql_util
from keystone.common.sql import migration
import test_keystoneclient import test_keystoneclient

View File

@ -15,6 +15,8 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
import datetime
from keystone import test from keystone import test
from keystone.common import utils from keystone.common import utils
@ -38,3 +40,10 @@ class UtilsTestCase(test.TestCase):
hashed = utils.hash_password(password) hashed = utils.hash_password(password)
self.assertTrue(utils.check_password(password, hashed)) self.assertTrue(utils.check_password(password, hashed))
self.assertFalse(utils.check_password(wrong, 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)