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]
|
[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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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())
|
||||||
|
|
|
@ -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))
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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'],
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
Loading…
Reference in New Issue