diff options
| author | mdietz <mdietz@openstack> | 2010-09-23 18:27:56 +0000 |
|---|---|---|
| committer | mdietz <mdietz@openstack> | 2010-09-23 18:27:56 +0000 |
| commit | 0b45eea603c85ad0bee77c9668c1cda41eeebaa2 (patch) | |
| tree | 5812ed25b606d299f39a626c1c9c2fe1506cd694 | |
| parent | 4960342b47b3692314439f1a828e5739da1f0bcd (diff) | |
| parent | 64dd3000c4a9b88719e86d1090097e35398d3838 (diff) | |
| download | nova-0b45eea603c85ad0bee77c9668c1cda41eeebaa2.tar.gz nova-0b45eea603c85ad0bee77c9668c1cda41eeebaa2.tar.xz nova-0b45eea603c85ad0bee77c9668c1cda41eeebaa2.zip | |
Refactored the auth branch based on review feedback
| -rw-r--r-- | nova/api/rackspace/__init__.py | 8 | ||||
| -rw-r--r-- | nova/api/rackspace/auth.py | 117 | ||||
| -rw-r--r-- | nova/db/api.py | 15 | ||||
| -rw-r--r-- | nova/db/sqlalchemy/api.py | 18 | ||||
| -rw-r--r-- | nova/db/sqlalchemy/models.py | 14 | ||||
| -rw-r--r-- | nova/tests/api/rackspace/auth.py | 84 | ||||
| -rw-r--r-- | nova/tests/api/rackspace/test_helper.py | 66 |
7 files changed, 220 insertions, 102 deletions
diff --git a/nova/api/rackspace/__init__.py b/nova/api/rackspace/__init__.py index 0daa7da94..74bd1955f 100644 --- a/nova/api/rackspace/__init__.py +++ b/nova/api/rackspace/__init__.py @@ -40,7 +40,8 @@ from nova.auth import manager FLAGS = flags.FLAGS -flags.DEFINE_string('nova_api_auth', 'nova.api.rackspace.auth.FakeAuth', +flags.DEFINE_string('nova_api_auth', + 'nova.api.rackspace.auth.BasicApiAuthManager', 'The auth mechanism to use for the Rackspace API implemenation') class API(wsgi.Middleware): @@ -50,7 +51,6 @@ class API(wsgi.Middleware): app = AuthMiddleware(RateLimitingMiddleware(APIRouter())) super(API, self).__init__(app) - class AuthMiddleware(wsgi.Middleware): """Authorize the rackspace API request or return an HTTP Forbidden.""" @@ -61,13 +61,13 @@ class AuthMiddleware(wsgi.Middleware): @webob.dec.wsgify def __call__(self, req): if not req.headers.has_key("X-Auth-Token"): - return self.authenticate(req) + return self.auth_driver.authenticate(req) user = self.auth_driver.authorize_token(req.headers["X-Auth-Token"]) if not user: return webob.exc.HTTPUnauthorized() - context = {'user':user} + context = {'user': user} req.environ['nova.context'] = context return self.application diff --git a/nova/api/rackspace/auth.py b/nova/api/rackspace/auth.py index d2b5193c3..b29596880 100644 --- a/nova/api/rackspace/auth.py +++ b/nova/api/rackspace/auth.py @@ -1,37 +1,88 @@ +import datetime import json -from hashlib import sha1 -from nova import datastore - -class FakeAuth(object): - def __init__(self, store=datastore.Redis.instance): - self._store = store() - self.auth_hash = 'rs_fake_auth' - self._store.hsetnx(self.auth_hash, 'rs_last_id', 0) - - def authorize_token(self, token): - user = self._store.hget(self.auth_hash, token) - if user: - return json.loads(user) - return None +import time +import webob.exc +import webob.dec +import hashlib + +from nova import auth +from nova import manager +from nova import db + +class Context(object): + pass + +class BasicApiAuthManager(manager.Manager): + """ Implements a somewhat rudimentary version of Rackspace Auth""" + + def __init__(self): + self.auth = auth.manager.AuthManager() + self.context = Context() + super(BasicApiAuthManager, self).__init__() + + def authenticate(self, req): + # Unless the request is explicitly made against /<version>/ don't + # honor it + path_info = req.path_info + if len(path_info) > 1: + return webob.exc.HTTPUnauthorized() + + try: + username, key = req.headers['X-Auth-User'], \ + req.headers['X-Auth-Key'] + except KeyError: + return webob.exc.HTTPUnauthorized() - def authorize_user(self, user, key): - token = sha1("%s_%s" % (user, key)).hexdigest() - user = self._store.hget(self.auth_hash, token) - if not user: - return None, None + username, key = req.headers['X-Auth-User'], req.headers['X-Auth-Key'] + token, user = self._authorize_user(username, key) + if user and token: + res = webob.Response() + res.headers['X-Auth-Token'] = token['token_hash'] + res.headers['X-Server-Management-Url'] = \ + token['server_management_url'] + res.headers['X-Storage-Url'] = token['storage_url'] + res.headers['X-CDN-Management-Url'] = token['cdn_management_url'] + res.content_type = 'text/plain' + res.status = '204' + return res else: - return token, json.loads(user) - - def add_user(self, user, key): - last_id = self._store.hget(self.auth_hash, 'rs_last_id') - token = sha1("%s_%s" % (user, key)).hexdigest() - user = { - 'id':last_id, - 'cdn_management_url':'cdn_management_url', - 'storage_url':'storage_url', - 'server_management_url':'server_management_url' - } - new_user = self._store.hsetnx(self.auth_hash, token, json.dumps(user)) - if new_user: - self._store.hincrby(self.auth_hash, 'rs_last_id') + return webob.exc.HTTPUnauthorized() + + def authorize_token(self, token_hash): + """ retrieves user information from the datastore given a token + + If the token has expired, returns None + If the token is not found, returns None + Otherwise returns the token + + This method will also remove the token if the timestamp is older than + 2 days ago. + """ + token = self.db.auth_get_token(self.context, token_hash) + if token: + delta = datetime.datetime.now() - token['created_at'] + if delta.days >= 2: + self.db.auth_destroy_token(self.context, token) + else: + user = self.auth.get_user(self.context, token['user_id']) + return { 'id':user['id'] } + return None + + def _authorize_user(self, username, key): + """ Generates a new token and assigns it to a user """ + user = self.auth.get_user_from_access_key(key) + if user and user['name'] == username: + token_hash = hashlib.sha1('%s%s%f' % (username, key, + time.time())).hexdigest() + token = {} + token['token_hash'] = token_hash + token['cdn_management_url'] = '' + token['server_management_url'] = self._get_server_mgmt_url() + token['storage_url'] = '' + self.db.auth_create_token(self.context, token, user['id']) + return token, user + return None, None + + def _get_server_mgmt_url(self): + return 'https://%s/v1.0/' % self.host diff --git a/nova/db/api.py b/nova/db/api.py index 8e418f9f0..156b1d10c 100644 --- a/nova/db/api.py +++ b/nova/db/api.py @@ -448,6 +448,21 @@ def export_device_create(context, values): ################### +def auth_destroy_token(context, token): + """Destroy an auth token""" + return IMPL.auth_destroy_token(context, token) + +def auth_get_token(context, token_hash): + """Retrieves a token given the hash representing it""" + return IMPL.auth_get_token(context, token_hash) + +def auth_create_token(context, token, user_id): + """Creates a new token""" + return IMPL.auth_create_token(context, token_hash, token, user_id) + + +################### + def quota_create(context, values): """Create a quota from the values dictionary.""" diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index 30c550105..127d94787 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -708,7 +708,25 @@ def quota_destroy(_context, project_id): quota_ref = models.Quota.find_by_str(project_id, session=session) quota_ref.delete(session=session) +def auth_destroy_token(_context, token): + session = get_session() + session.delete(token) + +def auth_get_token(_context, token_hash): + session = get_session() + tk = session.query(models.AuthToken + ).filter_by(token_hash=token_hash) + if not tk: + raise exception.NotFound('Token %s does not exist' % token_hash) + return tk +def auth_create_token(_context, token, user_id): + tk = models.AuthToken() + for k,v in token.iteritems(): + tk[k] = v + tk.save() + return tk + ################### diff --git a/nova/db/sqlalchemy/models.py b/nova/db/sqlalchemy/models.py index f62b79af8..bd1e9164e 100644 --- a/nova/db/sqlalchemy/models.py +++ b/nova/db/sqlalchemy/models.py @@ -213,7 +213,7 @@ class Instance(BASE, NovaBase): image_id = Column(String(255)) kernel_id = Column(String(255)) - ramdisk_id = Column(String(255)) + # image_id = Column(Integer, ForeignKey('images.id'), nullable=True) # kernel_id = Column(Integer, ForeignKey('images.id'), nullable=True) # ramdisk_id = Column(Integer, ForeignKey('images.id'), nullable=True) @@ -396,6 +396,18 @@ class NetworkIndex(BASE, NovaBase): network = relationship(Network, backref=backref('network_index', uselist=False)) +class AuthToken(BASE, NovaBase): + """Represents an authorization token for all API transactions. Fields + are a string representing the actual token and a user id for mapping + to the actual user""" + __tablename__ = 'auth_tokens' + token_hash = Column(String(255)) + user_id = Column(Integer) + server_manageent_url = Column(String(255)) + storage_url = Column(String(255)) + cdn_management_url = Column(String(255)) + + # TODO(vish): can these both come from the same baseclass? class FixedIp(BASE, NovaBase): diff --git a/nova/tests/api/rackspace/auth.py b/nova/tests/api/rackspace/auth.py index 65264fae9..8ab10d94c 100644 --- a/nova/tests/api/rackspace/auth.py +++ b/nova/tests/api/rackspace/auth.py @@ -4,23 +4,24 @@ import unittest import stubout import nova.api import nova.api.rackspace.auth +from nova import auth from nova.tests.api.rackspace import test_helper +import datetime class Test(unittest.TestCase): def setUp(self): self.stubs = stubout.StubOutForTesting() - self.stubs.Set(nova.api.rackspace.auth.FakeAuth, '__init__', - test_helper.fake_auth_init) - ds = test_helper.FakeRedis() - ds.hset(test_helper.auth_hash, 'rs_last_id', 0) + self.stubs.Set(nova.api.rackspace.auth.BasicApiAuthManager, + '__init__', test_helper.fake_auth_init) + test_helper.auth_data = {} def tearDown(self): self.stubs.UnsetAll() test_helper.fake_data_store = {} def test_authorize_user(self): - auth = nova.api.rackspace.auth.FakeAuth() - auth.add_user('herp', 'derp') + f = test_helper.FakeAuthManager() + f.add_user('derp', { 'id': 1, 'name':'herp' } ) req = webob.Request.blank('/v1.0/') req.headers['X-Auth-User'] = 'herp' @@ -29,35 +30,58 @@ class Test(unittest.TestCase): self.assertEqual(result.status, '204 No Content') self.assertEqual(len(result.headers['X-Auth-Token']), 40) self.assertEqual(result.headers['X-Server-Management-Url'], - "server_management_url") + "https://foo/v1.0/") self.assertEqual(result.headers['X-CDN-Management-Url'], - "cdn_management_url") - self.assertEqual(result.headers['X-Storage-Url'], "storage_url") + "") + self.assertEqual(result.headers['X-Storage-Url'], "") - def test_authorize_token(self): - auth = nova.api.rackspace.auth.FakeAuth() - auth.add_user('herp', 'derp') + #def test_authorize_token(self): + # auth = nova.api.rackspace.auth.FakeAuth() + # auth.add_user('herp', 'derp') - req = webob.Request.blank('/v1.0/') - req.headers['X-Auth-User'] = 'herp' - req.headers['X-Auth-Key'] = 'derp' - result = req.get_response(nova.api.API()) - self.assertEqual(result.status, '204 No Content') - self.assertEqual(len(result.headers['X-Auth-Token']), 40) - self.assertEqual(result.headers['X-Server-Management-Url'], - "server_management_url") - self.assertEqual(result.headers['X-CDN-Management-Url'], - "cdn_management_url") - self.assertEqual(result.headers['X-Storage-Url'], "storage_url") + # req = webob.Request.blank('/v1.0/') + # req.headers['X-Auth-User'] = 'herp' + # req.headers['X-Auth-Key'] = 'derp' + # result = req.get_response(nova.api.API()) + # self.assertEqual(result.status, '204 No Content') + # self.assertEqual(len(result.headers['X-Auth-Token']), 40) + # self.assertEqual(result.headers['X-Server-Management-Url'], + # "server_management_url") + # self.assertEqual(result.headers['X-CDN-Management-Url'], + # "cdn_management_url") + # self.assertEqual(result.headers['X-Storage-Url'], "storage_url") + + # token = result.headers['X-Auth-Token'] + # self.stubs.Set(nova.api.rackspace, 'APIRouter', + # test_helper.FakeRouter) + # req = webob.Request.blank('/v1.0/fake') + # req.headers['X-Auth-Token'] = token + # result = req.get_response(nova.api.API()) + # self.assertEqual(result.status, '200 OK') + # self.assertEqual(result.headers['X-Test-Success'], 'True') + + def test_token_expiry(self): + self.destroy_called = False + token_hash = 'bacon' + + def destroy_token_mock(meh, context, token): + self.destroy_called = True + + def bad_token(meh, context, token_hash): + return { 'token_hash':token_hash, + 'created_at':datetime.datetime(1990, 1, 1) } + + self.stubs.Set(test_helper.FakeAuthDatabase, 'auth_destroy_token', + destroy_token_mock) - token = result.headers['X-Auth-Token'] - self.stubs.Set(nova.api.rackspace, 'APIRouter', - test_helper.FakeRouter) - req = webob.Request.blank('/v1.0/fake') - req.headers['X-Auth-Token'] = token + self.stubs.Set(test_helper.FakeAuthDatabase, 'auth_get_token', + bad_token) + + req = webob.Request.blank('/v1.0/') + req.headers['X-Auth-Token'] = 'bacon' result = req.get_response(nova.api.API()) - self.assertEqual(result.status, '200 OK') - self.assertEqual(result.headers['X-Test-Success'], 'True') + self.assertEqual(result.status, '401 Unauthorized') + self.assertEqual(self.destroy_called, True) def test_bad_user(self): req = webob.Request.blank('/v1.0/') diff --git a/nova/tests/api/rackspace/test_helper.py b/nova/tests/api/rackspace/test_helper.py index 578b1e841..8d784854f 100644 --- a/nova/tests/api/rackspace/test_helper.py +++ b/nova/tests/api/rackspace/test_helper.py @@ -1,38 +1,12 @@ import webob import webob.dec from nova.wsgi import Router +from nova import auth -fake_data_store = {} -auth_hash = 'dummy_hash' +auth_data = {} -class FakeRedis(object): - def __init__(self): - global fake_data_store - self.store = fake_data_store - - def hsetnx(self, hash_name, key, value): - if not self.store.has_key(hash_name): - self.store[hash_name] = {} - - if self.store[hash_name].has_key(key): - return 0 - self.store[hash_name][key] = value - return 1 - - def hset(self, hash_name, key, value): - if not self.store.has_key(hash_name): - self.store[hash_name] = {} - - self.store[hash_name][key] = value - return 1 - - def hget(self, hash_name, key): - if not self.store[hash_name].has_key(key): - return None - return self.store[hash_name][key] - - def hincrby(self, hash_name, key, amount=1): - self.store[hash_name][key] += amount +class Context(object): + pass class FakeRouter(Router): def __init__(self): @@ -45,8 +19,32 @@ class FakeRouter(Router): res.headers['X-Test-Success'] = 'True' return res -def fake_auth_init(self, store=FakeRedis): - global auth_hash - self._store = store() - self.auth_hash = auth_hash +def fake_auth_init(self): + self.db = FakeAuthDatabase() + self.context = Context() + self.auth = FakeAuthManager() + self.host = 'foo' + +class FakeAuthDatabase(object): + @staticmethod + def auth_get_token(context, token_hash): + pass + + @staticmethod + def auth_create_token(context, token, user_id): + pass + + @staticmethod + def auth_destroy_token(context, token): + pass + +class FakeAuthManager(object): + def __init__(self): + global auth_data + self.data = auth_data + + def add_user(self, key, user): + self.data[key] = user + def get_user_from_access_key(self, key): + return self.data.get(key, None) |
