diff --git a/etc/keystone.conf b/etc/keystone.conf index fde1300487..dd78b4a957 100644 --- a/etc/keystone.conf +++ b/etc/keystone.conf @@ -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 diff --git a/keystone/common/sql/core.py b/keystone/common/sql/core.py index 2bd24f4835..cb62186598 100644 --- a/keystone/common/sql/core.py +++ b/keystone/common/sql/core.py @@ -26,6 +26,7 @@ ModelBase = declarative.declarative_base() Column = sql.Column String = sql.String ForeignKey = sql.ForeignKey +DateTime = sql.DateTime # Special Fields diff --git a/keystone/common/sql/migrate_repo/versions/001_add_initial_tables.py b/keystone/common/sql/migrate_repo/versions/001_add_initial_tables.py index 5875817e5b..ae54b476dc 100644 --- a/keystone/common/sql/migrate_repo/versions/001_add_initial_tables.py +++ b/keystone/common/sql/migrate_repo/versions/001_add_initial_tables.py @@ -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 diff --git a/keystone/common/utils.py b/keystone/common/utils.py index b5269bed2f..96e595bb0a 100644 --- a/keystone/common/utils.py +++ b/keystone/common/utils.py @@ -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()) diff --git a/keystone/contrib/ec2/core.py b/keystone/contrib/ec2/core.py index c8ad4425b6..08a286cc60 100644 --- a/keystone/contrib/ec2/core.py +++ b/keystone/contrib/ec2/core.py @@ -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)) diff --git a/keystone/identity/backends/sql.py b/keystone/identity/backends/sql.py index 8a3db42248..4918a94241 100644 --- a/keystone/identity/backends/sql.py +++ b/keystone/identity/backends/sql.py @@ -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 diff --git a/keystone/service.py b/keystone/service.py index 5be853c1bc..2c361e400b 100644 --- a/keystone/service.py +++ b/keystone/service.py @@ -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'], diff --git a/keystone/token/backends/kvs.py b/keystone/token/backends/kvs.py index 84604ac5e3..990e107bee 100644 --- a/keystone/token/backends/kvs.py +++ b/keystone/token/backends/kvs.py @@ -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: diff --git a/keystone/token/backends/memcache.py b/keystone/token/backends/memcache.py index 8dfb373197..b9c2cbe1dd 100644 --- a/keystone/token/backends/memcache.py +++ b/keystone/token/backends/memcache.py @@ -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 diff --git a/keystone/token/backends/sql.py b/keystone/token/backends/sql.py index 090a3600dd..b57f32a876 100644 --- a/keystone/token/backends/sql.py +++ b/keystone/token/backends/sql.py @@ -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() diff --git a/keystone/token/core.py b/keystone/token/core.py index 7183c1793e..8c816efe2b 100644 --- a/keystone/token/core.py +++ b/keystone/token/core.py @@ -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 diff --git a/tests/backend_sql.conf b/tests/backend_sql.conf index 0fec0488a4..cb246194b1 100644 --- a/tests/backend_sql.conf +++ b/tests/backend_sql.conf @@ -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 diff --git a/tests/test_backend.py b/tests/test_backend.py index 9dc949da57..bbc651ef9f 100644 --- a/tests/test_backend.py +++ b/tests/test_backend.py @@ -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) diff --git a/tests/test_backend_memcache.py b/tests/test_backend_memcache.py index b320b1f91a..05ef2107c8 100644 --- a/tests/test_backend_memcache.py +++ b/tests/test_backend_memcache.py @@ -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): diff --git a/tests/test_keystoneclient_sql.py b/tests/test_keystoneclient_sql.py index e365f83c5e..0d8ef52dfa 100644 --- a/tests/test_keystoneclient_sql.py +++ b/tests/test_keystoneclient_sql.py @@ -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 diff --git a/tests/test_utils.py b/tests/test_utils.py index 81cec7c918..7409e30c65 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -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)