summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authormdietz <mdietz@openstack>2010-09-23 18:27:56 +0000
committermdietz <mdietz@openstack>2010-09-23 18:27:56 +0000
commit0b45eea603c85ad0bee77c9668c1cda41eeebaa2 (patch)
tree5812ed25b606d299f39a626c1c9c2fe1506cd694
parent4960342b47b3692314439f1a828e5739da1f0bcd (diff)
parent64dd3000c4a9b88719e86d1090097e35398d3838 (diff)
downloadnova-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__.py8
-rw-r--r--nova/api/rackspace/auth.py117
-rw-r--r--nova/db/api.py15
-rw-r--r--nova/db/sqlalchemy/api.py18
-rw-r--r--nova/db/sqlalchemy/models.py14
-rw-r--r--nova/tests/api/rackspace/auth.py84
-rw-r--r--nova/tests/api/rackspace/test_helper.py66
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)