AuthN support for Quantum
Adds authentication support for Quantum. Generates a context object and stuffs it into the 'quantum.context' variable in the WSGI environment. This will be used in conjunction with authZ, later. Partially implements blueprint authorization-support-for-quantum. Change-Id: I8af171c2f11a08db5ee41e609d60ad203548650d
This commit is contained in:
parent
76a8f573c1
commit
a6e8169df6
@ -29,27 +29,26 @@ use = egg:Paste#urlmap
|
|||||||
# To enable Keystone integration comment out the
|
# To enable Keystone integration comment out the
|
||||||
# following line and uncomment the next one
|
# following line and uncomment the next one
|
||||||
pipeline = extensions quantumapiapp_v1_0
|
pipeline = extensions quantumapiapp_v1_0
|
||||||
# pipeline = authtoken extensions quantumapiapp_v1_0
|
# pipeline = authtoken keystonecontext extensions quantumapiapp_v1_0
|
||||||
|
|
||||||
[pipeline:quantumapi_v1_1]
|
[pipeline:quantumapi_v1_1]
|
||||||
# By default, authentication is disabled.
|
# By default, authentication is disabled.
|
||||||
# To enable Keystone integration comment out the
|
# To enable Keystone integration comment out the
|
||||||
# following line and uncomment the next one
|
# following line and uncomment the next one
|
||||||
pipeline = extensions quantumapiapp_v1_1
|
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]
|
[filter:authtoken]
|
||||||
paste.filter_factory = keystone.middleware.auth_token:filter_factory
|
paste.filter_factory = keystone.middleware.auth_token:filter_factory
|
||||||
auth_host = 127.0.0.1
|
auth_host = 127.0.0.1
|
||||||
auth_port = 35357
|
auth_port = 35357
|
||||||
auth_protocol = http
|
auth_protocol = http
|
||||||
# auth_uri = http://127.0.0.1:5000/
|
admin_tenant_name = %SERVICE_TENANT_NAME%
|
||||||
admin_tenant_name = service
|
admin_user = %SERVICE_USER%
|
||||||
admin_user = nova
|
admin_password = %SERVICE_PASSWORD%
|
||||||
admin_password = sp
|
|
||||||
# admin_token = 9a82c95a-99e9-4c3a-b5ee-199f6ba7ff04
|
|
||||||
# memcache_servers = 127.0.0.1:11211
|
|
||||||
# token_cache_time = 300
|
|
||||||
|
|
||||||
[filter:extensions]
|
[filter:extensions]
|
||||||
paste.filter_factory = quantum.extensions.extensions:plugin_aware_extension_middleware_factory
|
paste.filter_factory = quantum.extensions.extensions:plugin_aware_extension_middleware_factory
|
||||||
|
52
quantum/auth.py
Normal file
52
quantum/auth.py
Normal file
@ -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
|
113
quantum/context.py
Normal file
113
quantum/context.py
Normal file
@ -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)
|
90
quantum/tests/unit/test_auth.py
Normal file
90
quantum/tests/unit/test_auth.py
Normal file
@ -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)
|
Loading…
Reference in New Issue
Block a user