diff --git a/etc/quantum.conf b/etc/quantum.conf index 9903f086bde..5162060d297 100644 --- a/etc/quantum.conf +++ b/etc/quantum.conf @@ -29,27 +29,26 @@ use = egg:Paste#urlmap # To enable Keystone integration comment out the # following line and uncomment the next one pipeline = extensions quantumapiapp_v1_0 -# pipeline = authtoken extensions quantumapiapp_v1_0 +# pipeline = authtoken keystonecontext extensions quantumapiapp_v1_0 [pipeline:quantumapi_v1_1] # By default, authentication is disabled. # To enable Keystone integration comment out the # following line and uncomment the next one pipeline = extensions quantumapiapp_v1_1 -# pipeline = authtoken extensions quantumapiapp_v1_1 +# pipeline = authtoken keystonecontext extensions quantumapiapp_v1_1 + +[filter:keystonecontext] +paste.filter_factory = quantum.auth:QuantumKeystoneContext.factory [filter:authtoken] paste.filter_factory = keystone.middleware.auth_token:filter_factory auth_host = 127.0.0.1 auth_port = 35357 auth_protocol = http -# auth_uri = http://127.0.0.1:5000/ -admin_tenant_name = service -admin_user = nova -admin_password = sp -# admin_token = 9a82c95a-99e9-4c3a-b5ee-199f6ba7ff04 -# memcache_servers = 127.0.0.1:11211 -# token_cache_time = 300 +admin_tenant_name = %SERVICE_TENANT_NAME% +admin_user = %SERVICE_USER% +admin_password = %SERVICE_PASSWORD% [filter:extensions] paste.filter_factory = quantum.extensions.extensions:plugin_aware_extension_middleware_factory diff --git a/quantum/auth.py b/quantum/auth.py new file mode 100644 index 00000000000..13dfa26840c --- /dev/null +++ b/quantum/auth.py @@ -0,0 +1,52 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012 OpenStack LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import logging + +import webob.dec +import webob.exc + +from quantum import context +from quantum import wsgi + + +LOG = logging.getLogger(__name__) + + +class QuantumKeystoneContext(wsgi.Middleware): + """Make a request context from keystone headers.""" + + @webob.dec.wsgify + def __call__(self, req): + # Determine the user ID + user_id = req.headers.get('X_USER_ID', req.headers.get('X_USER')) + if not user_id: + LOG.debug("Neither X_USER_ID nor X_USER found in request") + return webob.exc.HTTPUnauthorized() + + # Determine the tenant + tenant_id = req.headers.get('X_TENANT_ID', req.headers.get('X_TENANT')) + + # Suck out the roles + roles = [r.strip() for r in req.headers.get('X_ROLE', '').split(',')] + + # Create a context with the authentication data + ctx = context.Context(user_id, tenant_id, roles=roles) + + # Inject the context... + req.environ['quantum.context'] = ctx + + return self.application diff --git a/quantum/context.py b/quantum/context.py new file mode 100644 index 00000000000..b284d349e48 --- /dev/null +++ b/quantum/context.py @@ -0,0 +1,113 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012 OpenStack LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""Context: context for security/db session.""" + +import copy +import logging + +from datetime import datetime + +from quantum.db import api as db_api + +LOG = logging.getLogger(__name__) + + +class Context(object): + """Security context and request information. + + Represents the user taking a given action within the system. + + """ + + def __init__(self, user_id, tenant_id, is_admin=None, read_deleted="no", + roles=None, timestamp=None, **kwargs): + """ + :param read_deleted: 'no' indicates deleted records are hidden, 'yes' + indicates deleted records are visible, 'only' indicates that + *only* deleted records are visible. + """ + if kwargs: + LOG.warn(_('Arguments dropped when creating context: %s') % + str(kwargs)) + + self.user_id = user_id + self.tenant_id = tenant_id + self.roles = roles or [] + self.is_admin = is_admin + if self.is_admin is None: + self.is_admin = 'admin' in [x.lower() for x in self.roles] + elif self.is_admin and 'admin' not in [x.lower() for x in self.roles]: + self.roles.append('admin') + self.read_deleted = read_deleted + if not timestamp: + timestamp = datetime.utcnow() + self.timestamp = timestamp + self._session = None + + def _get_read_deleted(self): + return self._read_deleted + + def _set_read_deleted(self, read_deleted): + if read_deleted not in ('no', 'yes', 'only'): + raise ValueError(_("read_deleted can only be one of 'no', " + "'yes' or 'only', not %r") % read_deleted) + self._read_deleted = read_deleted + + def _del_read_deleted(self): + del self._read_deleted + + read_deleted = property(_get_read_deleted, _set_read_deleted, + _del_read_deleted) + + @property + def session(self): + if self._session is None: + self._session = db_api.get_session() + return self._session + + def to_dict(self): + return {'user_id': self.user_id, + 'tenant_id': self.tenant_id, + 'is_admin': self.is_admin, + 'read_deleted': self.read_deleted, + 'roles': self.roles, + 'timestamp': str(self.timestamp)} + + @classmethod + def from_dict(cls, values): + return cls(**values) + + def elevated(self, read_deleted=None): + """Return a version of this context with admin flag set.""" + context = copy.copy(self) + context.is_admin = True + + if 'admin' not in [x.lower() for x in context.roles]: + context.roles.append('admin') + + if read_deleted is not None: + context.read_deleted = read_deleted + + return context + + +def get_admin_context(read_deleted="no"): + return Context(user_id=None, + tenant_id=None, + is_admin=True, + read_deleted=read_deleted) diff --git a/quantum/tests/unit/test_auth.py b/quantum/tests/unit/test_auth.py new file mode 100644 index 00000000000..e2c55b97b6d --- /dev/null +++ b/quantum/tests/unit/test_auth.py @@ -0,0 +1,90 @@ +import unittest + +import webob + +from quantum import auth + + +class QuantumKeystoneContextTestCase(unittest.TestCase): + def setUp(self): + super(QuantumKeystoneContextTestCase, self).setUp() + + @webob.dec.wsgify + def fake_app(req): + self.context = req.environ['quantum.context'] + return webob.Response() + + self.context = None + self.middleware = auth.QuantumKeystoneContext(fake_app) + self.request = webob.Request.blank('/') + self.request.headers['X_AUTH_TOKEN'] = 'testauthtoken' + + def test_no_user_no_user_id(self): + self.request.headers['X_TENANT_ID'] = 'testtenantid' + response = self.request.get_response(self.middleware) + self.assertEqual(response.status, '401 Unauthorized') + + def test_with_user(self): + self.request.headers['X_TENANT_ID'] = 'testtenantid' + self.request.headers['X_USER_ID'] = 'testuserid' + response = self.request.get_response(self.middleware) + self.assertEqual(response.status, '200 OK') + self.assertEqual(self.context.user_id, 'testuserid') + + def test_with_user_id(self): + self.request.headers['X_TENANT_ID'] = 'testtenantid' + self.request.headers['X_USER'] = 'testuser' + response = self.request.get_response(self.middleware) + self.assertEqual(response.status, '200 OK') + self.assertEqual(self.context.user_id, 'testuser') + + def test_user_id_trumps_user(self): + self.request.headers['X_TENANT_ID'] = 'testtenantid' + self.request.headers['X_USER_ID'] = 'testuserid' + self.request.headers['X_USER'] = 'testuser' + response = self.request.get_response(self.middleware) + self.assertEqual(response.status, '200 OK') + self.assertEqual(self.context.user_id, 'testuserid') + + def test_with_tenant_id(self): + self.request.headers['X_TENANT_ID'] = 'testtenantid' + self.request.headers['X_USER_ID'] = 'test_user_id' + response = self.request.get_response(self.middleware) + self.assertEqual(response.status, '200 OK') + self.assertEqual(self.context.tenant_id, 'testtenantid') + + def test_with_tenant(self): + self.request.headers['X_TENANT'] = 'testtenant' + self.request.headers['X_USER_ID'] = 'test_user_id' + response = self.request.get_response(self.middleware) + self.assertEqual(response.status, '200 OK') + self.assertEqual(self.context.tenant_id, 'testtenant') + + def test_tenant_id_trumps_tenant(self): + self.request.headers['X_TENANT_ID'] = 'testtenantid' + self.request.headers['X_TENANT'] = 'testtenant' + self.request.headers['X_USER_ID'] = 'testuserid' + response = self.request.get_response(self.middleware) + self.assertEqual(response.status, '200 OK') + self.assertEqual(self.context.tenant_id, 'testtenantid') + + def test_roles_no_admin(self): + self.request.headers['X_TENANT_ID'] = 'testtenantid' + self.request.headers['X_USER_ID'] = 'testuserid' + self.request.headers['X_ROLE'] = 'role1, role2 , role3,role4,role5' + response = self.request.get_response(self.middleware) + self.assertEqual(response.status, '200 OK') + self.assertEqual(self.context.roles, ['role1', 'role2', 'role3', + 'role4', 'role5']) + self.assertEqual(self.context.is_admin, False) + + def test_roles_with_admin(self): + self.request.headers['X_TENANT_ID'] = 'testtenantid' + self.request.headers['X_USER_ID'] = 'testuserid' + self.request.headers['X_ROLE'] = ('role1, role2 , role3,role4,role5,' + 'AdMiN') + response = self.request.get_response(self.middleware) + self.assertEqual(response.status, '200 OK') + self.assertEqual(self.context.roles, ['role1', 'role2', 'role3', + 'role4', 'role5', 'AdMiN']) + self.assertEqual(self.context.is_admin, True)