Browse Source

Add token expiration

* Config option token.expiration defines amount of time tokens should be valid
* Fixes bug 928545

Change-Id: I3dff7a1ebf03bb44fc6e5247f976baea0581de08
changes/42/3942/12
Brian Waldon 10 years ago
parent
commit
71436dbf18
  1. 3
      etc/keystone.conf
  2. 1
      keystone/common/sql/core.py
  3. 1
      keystone/common/sql/migrate_repo/versions/001_add_initial_tables.py
  4. 24
      keystone/common/utils.py
  5. 3
      keystone/contrib/ec2/core.py
  6. 4
      keystone/identity/backends/sql.py
  7. 12
      keystone/service.py
  8. 18
      keystone/token/backends/kvs.py
  9. 13
      keystone/token/backends/memcache.py
  10. 33
      keystone/token/backends/sql.py
  11. 12
      keystone/token/core.py
  12. 3
      tests/backend_sql.conf
  13. 22
      tests/test_backend.py
  14. 14
      tests/test_backend_memcache.py
  15. 1
      tests/test_keystoneclient_sql.py
  16. 9
      tests/test_utils.py

3
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

1
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

1
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

24
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())

3
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))

4
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

12
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'],

18
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:

13
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

33
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()

12
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

3
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

22
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)

14
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):

1
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

9
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)
Loading…
Cancel
Save