From 3b0efe44c6ecb99c256e82d8b82d503feebd5f02 Mon Sep 17 00:00:00 2001 From: Dolph Mathews Date: Tue, 5 Jul 2011 11:53:06 -0500 Subject: Initialized LDAP backend --- keystone/backends/ldap/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 keystone/backends/ldap/__init__.py diff --git a/keystone/backends/ldap/__init__.py b/keystone/backends/ldap/__init__.py new file mode 100644 index 00000000..e69de29b -- cgit From 44313df0c49352ebfea19b88c72ff9d4717f6846 Mon Sep 17 00:00:00 2001 From: Dolph Mathews Date: Tue, 5 Jul 2011 12:00:18 -0500 Subject: Added python-ldap to pip-requires --- tools/pip-requires | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tools/pip-requires b/tools/pip-requires index 2b994af3..b9951cdf 100644 --- a/tools/pip-requires +++ b/tools/pip-requires @@ -7,4 +7,5 @@ pysqlite sqlalchemy webob Routes -httplib2 \ No newline at end of file +httplib2 +python-ldap # optional authentication backend \ No newline at end of file -- cgit From ef7689cf6dd4029240116515253182c0738a857e Mon Sep 17 00:00:00 2001 From: Dolph Mathews Date: Thu, 7 Jul 2011 14:07:45 -0500 Subject: Rewrote .json/.xml extension handler with additional unit test --- keystone/queryext/exthandler.py | 45 ++++++++++++++--------------------- keystone/test/unit/test_exthandler.py | 11 +++++++-- 2 files changed, 27 insertions(+), 29 deletions(-) diff --git a/keystone/queryext/exthandler.py b/keystone/queryext/exthandler.py index cedf26fa..e0fae135 100644 --- a/keystone/queryext/exthandler.py +++ b/keystone/queryext/exthandler.py @@ -27,19 +27,8 @@ overwrites the Accept header in the request, if present. """ - -# Does this need to be configurable? -DEFAULT_EXTS = {'xml': 'application/xml', 'json': 'application/json'} - - -def scrub(uri, ext): - urisegs = uri.split('?') - first = urisegs[0][0: -(len(ext) + 1)] - if len(urisegs) > 1: - return '?'.join((first, urisegs[1], )) - else: - return first - +CONTENT_TYPES = {'json': 'application/json', 'xml': 'application/xml'} +DEFAULT_CONTENT_TYPE = CONTENT_TYPES['json'] class UrlExtensionFilter(object): @@ -48,20 +37,22 @@ class UrlExtensionFilter(object): self.app = app self.conf = conf - print 'Starting extension handler middleware' - def __call__(self, env, start_response): uri = env['PATH_INFO'] - querysegs = uri.split('?') - ressegs = querysegs[0].split('.') - if len(ressegs) > 1: # (Maybe) has an extension - ext = ressegs[-1] - if ext in DEFAULT_EXTS: - env['HTTP_ACCEPT'] = DEFAULT_EXTS[ext] - scrubbed = querysegs[0][0: -(len(ext) + 1)] # Remove extension - if len(querysegs) > 1: # Has query string - env['PATH_INFO'] = '?'.join((scrubbed, querysegs[1], )) - else: - env['PATH_INFO'] = scrubbed - + (path, ext) = uri.rsplit('.', 1) + if ext in CONTENT_TYPES: + env['HTTP_ACCEPT'] = CONTENT_TYPES[ext] + env['PATH_INFO'] = path + elif 'HTTP_ACCEPT' not in env: + env['HTTP_ACCEPT'] = DEFAULT_CONTENT_TYPE + return self.app(env, start_response) + +def filter_factory(global_conf, **local_conf): + """Returns a WSGI filter app for use with paste.deploy.""" + conf = global_conf.copy() + conf.update(local_conf) + + def ext_filter(app): + return UrlExtensionFilter(app, conf) + return ext_filter diff --git a/keystone/test/unit/test_exthandler.py b/keystone/test/unit/test_exthandler.py index 30a61f98..1e881a54 100644 --- a/keystone/test/unit/test_exthandler.py +++ b/keystone/test/unit/test_exthandler.py @@ -40,6 +40,12 @@ class UrlExtensionFilterTest(unittest.TestCase): def setUp(self): self.filter = UrlExtensionFilter(MockWsgiApp(), {}) + + def test_no_extension(self): + env = {'PATH_INFO': '/v2.0/someresource'} + self.filter(env, _start_response) + self.assertEqual('/v2.0/someresource', env['PATH_INFO']) + self.assertEqual('application/json', env['HTTP_ACCEPT']) def test_xml_extension(self): env = {'PATH_INFO': '/v2.0/someresource.xml'} @@ -54,8 +60,9 @@ class UrlExtensionFilterTest(unittest.TestCase): self.assertEqual('application/json', env['HTTP_ACCEPT']) def test_extension_overrides_header(self): - env = {'PATH_INFO': '/v2.0/someresource.json', - 'HTTP_ACCEPT': 'application/xml'} + env = { + 'PATH_INFO': '/v2.0/someresource.json', + 'HTTP_ACCEPT': 'application/xml'} self.filter(env, _start_response) self.assertEqual('/v2.0/someresource', env['PATH_INFO']) self.assertEqual('application/json', env['HTTP_ACCEPT']) -- cgit From 183da0cc89529e88999914cffd365efe105c4478 Mon Sep 17 00:00:00 2001 From: Dolph Mathews Date: Thu, 7 Jul 2011 14:08:10 -0500 Subject: Removed unused function --- keystone/server.py | 23 +++++++---------------- 1 file changed, 7 insertions(+), 16 deletions(-) diff --git a/keystone/server.py b/keystone/server.py index 79ed98af..8b704aaf 100755 --- a/keystone/server.py +++ b/keystone/server.py @@ -509,27 +509,18 @@ def get_marker_limit_and_url(req): if "limit" in req.GET: limit = req.GET["limit"] + url = get_url(req) + return (marker, limit, url) -def get_marker_and_limit(req): - marker = None - limit = 10 - - if "marker" in req.GET: - marker = req.GET["marker"] - - if "limit" in req.GET: - limit = req.GET["limit"] - - def get_url(req): - url = '%s://%s:%s%s' % (req.environ['wsgi.url_scheme'], - req.environ.get("SERVER_NAME"), - req.environ.get("SERVER_PORT"), - req.environ['PATH_INFO']) - return url + return '%s://%s:%s%s' % ( + req.environ['wsgi.url_scheme'], + req.environ.get("SERVER_NAME"), + req.environ.get("SERVER_PORT"), + req.environ['PATH_INFO']) class KeystoneAPI(wsgi.Router): -- cgit From aa5fdac7b66c50b24a21a64cba7ee96e525546de Mon Sep 17 00:00:00 2001 From: Dolph Mathews Date: Thu, 7 Jul 2011 14:10:36 -0500 Subject: Fixed 'is_xml_response' function, which had no clear intention --- keystone/utils.py | 19 ++++--------------- 1 file changed, 4 insertions(+), 15 deletions(-) diff --git a/keystone/utils.py b/keystone/utils.py index ddd63760..96991364 100755 --- a/keystone/utils.py +++ b/keystone/utils.py @@ -16,19 +16,10 @@ import functools -import httplib -import json import logging import os -import routes import sys -import hashlib from webob import Response -from webob import Request -from webob import descriptors -from webob.exc import (HTTPNotFound, - HTTPConflict, - HTTPBadRequest) POSSIBLE_TOPDIR = os.path.normpath(os.path.join(os.path.abspath(sys.argv[0]), os.pardir, @@ -36,14 +27,12 @@ POSSIBLE_TOPDIR = os.path.normpath(os.path.join(os.path.abspath(sys.argv[0]), if os.path.exists(os.path.join(POSSIBLE_TOPDIR, 'keystone', '__init__.py')): sys.path.insert(0, POSSIBLE_TOPDIR) -from queryext import exthandler import keystone.logic.types.fault as fault def is_xml_response(req): - if not "Accept" in req.headers: - return False - return req.content_type == "application/xml" + """Returns True when the request wants an XML response, False otherwise""" + return "Accept" in req.headers and "application/xml" in req.accept def get_app_root(): @@ -180,5 +169,5 @@ def import_module(module_name, class_name=None): __import__(module_name) return getattr(sys.modules[module_name], class_name) except (ImportError, ValueError, AttributeError), exception: - raise ImportError(_('Class %s.%s cannot be found (%s)') % - (module_name, class_name, exception)) \ No newline at end of file + raise ImportError(_('Class %s.%s cannot be found (%s)') % + (module_name, class_name, exception)) -- cgit From 1b0ef39d51dcb5f3d21c5038cfbf142de108ad36 Mon Sep 17 00:00:00 2001 From: Dolph Mathews Date: Thu, 7 Jul 2011 14:13:02 -0500 Subject: ** keystone.conf refactoring ** Added support for exthandler Added admin pipeline Moved public/admin host/port configs in support of pipeline configuration --- bin/keystone | 22 +++++++++++----------- bin/keystone-admin | 7 ++++--- bin/keystone-auth | 10 ++++++---- etc/keystone.conf | 49 ++++++++++++++++++++++++++++++++----------------- setup.py | 1 + 5 files changed, 54 insertions(+), 35 deletions(-) diff --git a/bin/keystone b/bin/keystone index 4664f80d..1725ed25 100755 --- a/bin/keystone +++ b/bin/keystone @@ -69,20 +69,20 @@ if __name__ == '__main__': config_file = config.find_config_file(options, args) print "Using config file:", config_file - # Load API server + # Load Public API server server = wsgi.Server() - server.start(app, int(conf['server_bind_port']), - conf['server_bind_host']) - print "Service API listening on %s:%s" % (conf['server_bind_host'], - conf['server_bind_port']) - + server.start(app, int(conf['public_port']), conf['public_host']) + + print "Service API listening on %s:%s" % ( + conf['public_host'], conf['public_port']) + # Load Admin API server admin_server = wsgi.Server() - admin_bind = options.get('admin_port') or admin_conf.get('bind_port') - admin_server.start(admin_app, int(admin_bind), - admin_conf['bind_host']) - print "Admin API listening on %s:%s" % (admin_conf['bind_host'], - admin_bind) + admin_server.start(admin_app, + int(conf['admin_port']), conf['admin_host']) + + print "Admin API listening on %s:%s" % ( + conf['admin_host'], conf['admin_port']) # Wait until done server.wait() diff --git a/bin/keystone-admin b/bin/keystone-admin index bf40c9fd..388e22fe 100755 --- a/bin/keystone-admin +++ b/bin/keystone-admin @@ -63,9 +63,10 @@ if __name__ == '__main__': print "Using config file:", config_file server = wsgi.Server() - server.start(app, int(conf['bind_port']), conf['bind_host']) - print "Admin API listening on %s:%s" % (conf['bind_host'], - conf['bind_port']) + server.start(app, int(conf['admin_port']), conf['admin_host']) + + print "Admin API listening on %s:%s" % ( + conf['admin_host'], conf['admin_port']) server.wait() except RuntimeError, e: diff --git a/bin/keystone-auth b/bin/keystone-auth index 4ac26b33..368872b4 100755 --- a/bin/keystone-auth +++ b/bin/keystone-auth @@ -54,6 +54,7 @@ if __name__ == '__main__': try: # Load Service API server conf, app = config.load_paste_app('keystone-legacy-auth', options, args) + debug = options.get('debug') or conf.get('debug', False) debug = debug in [True, "True", "1"] verbose = options.get('verbose') or conf.get('verbose', False) @@ -63,10 +64,11 @@ if __name__ == '__main__': print "Using config file:", config_file server = wsgi.Server() - server.start(app, int(conf['server_bind_port']), - conf['server_bind_host']) - print "Service API listening on %s:%s" % (conf['server_bind_host'], - conf['server_bind_port']) + server.start(app, int(conf['public_port']), conf['public_host']) + + print "Service API listening on %s:%s" % ( + conf['public_host'], conf['public_port']) + server.wait() except RuntimeError, e: sys.exit("ERROR: %s" % e) diff --git a/etc/keystone.conf b/etc/keystone.conf index 6a983f30..f93e3b58 100755 --- a/etc/keystone.conf +++ b/etc/keystone.conf @@ -14,21 +14,31 @@ default_store = sqlite # file for both the API and registry servers! #log_file = /var/log/keystone.log log_file = keystone.log + #List of backends to be configured backends = keystone.backends.sqlalchemy,keystone.backends.alterdb #Dictionary Maps every service to a header.Missing services would get header X_(SERVICE_NAME) Key => Service Name, Value => Header Name -service-header-mappings = {'nova' : 'X-Server-Management-Url' , 'swift' : 'X-Storage-Url', 'cdn' : 'X-CDN-Management-Url'} +service-header-mappings = { + 'nova' : 'X-Server-Management-Url', + 'swift' : 'X-Storage-Url', + 'cdn' : 'X-CDN-Management-Url'} # Address to bind the API server -#TODO Properties defined within app not available via pipeline.Till then server props stay outside. -server_bind_host = 0.0.0.0 +# TODO Properties defined within app not available via pipeline. +public_host = 0.0.0.0 # Port the bind the API server to -server_bind_port = 5000 +public_port = 5000 + +# Address to bind the Admin API server +admin_host = 0.0.0.0 + +# Port the bind the Admin API server to +admin_port = 5001 #Role that allows to perform admin operations. -keystone-admin-role=Admin +keystone-admin-role = Admin [keystone.backends.sqlalchemy] # SQLAlchemy connection string for the reference implementation @@ -36,6 +46,7 @@ keystone-admin-role=Admin # See: http://www.sqlalchemy.org/docs/05/reference/sqlalchemy/connections.html#sqlalchemy.create_engine sql_connection = sqlite:///keystone.db backend_entities = ['UserGroupAssociation', 'UserRoleAssociation', 'Endpoints', 'Role', 'Tenant', 'User', 'Credentials', 'Group', 'EndpointTemplates'] + # Period in seconds after which SQLAlchemy should reestablish its connection # to the database. sql_idle_timeout = 30 @@ -46,26 +57,30 @@ sql_idle_timeout = 30 # See: http://www.sqlalchemy.org/docs/05/reference/sqlalchemy/connections.html#sqlalchemy.create_engine sql_connection = sqlite:///keystone.token.db backend_entities = ['Token'] + # Period in seconds after which SQLAlchemy should reestablish its connection # to the database. sql_idle_timeout = 30 -[app:admin] -paste.app_factory = keystone.server:admin_app_factory - -# Address to bind the Admin API server -bind_host = 0.0.0.0 - -# Port the bind the Admin API server to -bind_port = 5001 - -[app:server] -paste.app_factory = keystone.server:app_factory +[pipeline:admin] +pipeline = + exthandler + admin_service [pipeline:keystone-legacy-auth] pipeline = + exthandler legacy_auth - server + public_service + +[app:public_service] +paste.app_factory = keystone.server:app_factory + +[app:admin_service] +paste.app_factory = keystone.server:admin_app_factory + +[filter:exthandler] +paste.filter_factory = keystone.queryext.exthandler:filter_factory [filter:legacy_auth] paste.filter_factory = keystone.frontends.legacy_token_auth:filter_factory diff --git a/setup.py b/setup.py index f936b418..6ed7a418 100755 --- a/setup.py +++ b/setup.py @@ -57,6 +57,7 @@ setup( entry_points={ 'paste.app_factory': ['main=identity:app_factory'], 'paste.filter_factory': [ + 'extfilter=keystone.queryext.exthandler:filter_factory', 'remoteauth=keystone.middleware.remoteauth:remoteauth_factory', 'tokenauth=keystone.auth_protocols.auth_token:filter_factory', 'swiftauth=keystone.middleware.swift_auth:filter_factory', -- cgit From 55e1af6a1c0b34acc9dfdc73cbdd58efc09d3b3f Mon Sep 17 00:00:00 2001 From: Dolph Mathews Date: Thu, 7 Jul 2011 14:53:34 -0500 Subject: Moved exthandler to keystone.middleware --- etc/keystone.conf | 2 +- keystone/middleware/exthandler.py | 58 +++++++++++++++++++++++++++++++++++ keystone/queryext/__init__.py | 0 keystone/queryext/exthandler.py | 58 ----------------------------------- keystone/test/unit/test_exthandler.py | 2 +- setup.py | 2 +- 6 files changed, 61 insertions(+), 61 deletions(-) create mode 100644 keystone/middleware/exthandler.py delete mode 100644 keystone/queryext/__init__.py delete mode 100644 keystone/queryext/exthandler.py diff --git a/etc/keystone.conf b/etc/keystone.conf index f93e3b58..425b41ee 100755 --- a/etc/keystone.conf +++ b/etc/keystone.conf @@ -80,7 +80,7 @@ paste.app_factory = keystone.server:app_factory paste.app_factory = keystone.server:admin_app_factory [filter:exthandler] -paste.filter_factory = keystone.queryext.exthandler:filter_factory +paste.filter_factory = keystone.middleware.exthandler:filter_factory [filter:legacy_auth] paste.filter_factory = keystone.frontends.legacy_token_auth:filter_factory diff --git a/keystone/middleware/exthandler.py b/keystone/middleware/exthandler.py new file mode 100644 index 00000000..e0fae135 --- /dev/null +++ b/keystone/middleware/exthandler.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# +# Copyright (c) 2010 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. + + +""" +Auth Middleware that accepts URL query extension. + +This module can be installed as a filter in front of your service to +detect extension in the resource URI (e.g., foo/resource.xml) to +specify HTTP response body type. If an extension is specified, it +overwrites the Accept header in the request, if present. + +""" + +CONTENT_TYPES = {'json': 'application/json', 'xml': 'application/xml'} +DEFAULT_CONTENT_TYPE = CONTENT_TYPES['json'] + +class UrlExtensionFilter(object): + + def __init__(self, app, conf): + # app is the next app in WSGI chain - eventually the OpenStack service + self.app = app + self.conf = conf + + def __call__(self, env, start_response): + uri = env['PATH_INFO'] + (path, ext) = uri.rsplit('.', 1) + if ext in CONTENT_TYPES: + env['HTTP_ACCEPT'] = CONTENT_TYPES[ext] + env['PATH_INFO'] = path + elif 'HTTP_ACCEPT' not in env: + env['HTTP_ACCEPT'] = DEFAULT_CONTENT_TYPE + + return self.app(env, start_response) + +def filter_factory(global_conf, **local_conf): + """Returns a WSGI filter app for use with paste.deploy.""" + conf = global_conf.copy() + conf.update(local_conf) + + def ext_filter(app): + return UrlExtensionFilter(app, conf) + return ext_filter diff --git a/keystone/queryext/__init__.py b/keystone/queryext/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/keystone/queryext/exthandler.py b/keystone/queryext/exthandler.py deleted file mode 100644 index e0fae135..00000000 --- a/keystone/queryext/exthandler.py +++ /dev/null @@ -1,58 +0,0 @@ -#!/usr/bin/env python -# vim: tabstop=4 shiftwidth=4 softtabstop=4 -# -# Copyright (c) 2010 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. - - -""" -Auth Middleware that accepts URL query extension. - -This module can be installed as a filter in front of your service to -detect extension in the resource URI (e.g., foo/resource.xml) to -specify HTTP response body type. If an extension is specified, it -overwrites the Accept header in the request, if present. - -""" - -CONTENT_TYPES = {'json': 'application/json', 'xml': 'application/xml'} -DEFAULT_CONTENT_TYPE = CONTENT_TYPES['json'] - -class UrlExtensionFilter(object): - - def __init__(self, app, conf): - # app is the next app in WSGI chain - eventually the OpenStack service - self.app = app - self.conf = conf - - def __call__(self, env, start_response): - uri = env['PATH_INFO'] - (path, ext) = uri.rsplit('.', 1) - if ext in CONTENT_TYPES: - env['HTTP_ACCEPT'] = CONTENT_TYPES[ext] - env['PATH_INFO'] = path - elif 'HTTP_ACCEPT' not in env: - env['HTTP_ACCEPT'] = DEFAULT_CONTENT_TYPE - - return self.app(env, start_response) - -def filter_factory(global_conf, **local_conf): - """Returns a WSGI filter app for use with paste.deploy.""" - conf = global_conf.copy() - conf.update(local_conf) - - def ext_filter(app): - return UrlExtensionFilter(app, conf) - return ext_filter diff --git a/keystone/test/unit/test_exthandler.py b/keystone/test/unit/test_exthandler.py index 1e881a54..76614ab6 100644 --- a/keystone/test/unit/test_exthandler.py +++ b/keystone/test/unit/test_exthandler.py @@ -19,7 +19,7 @@ import sys # Need to access identity module sys.path.append(os.path.abspath(os.path.join( os.getcwd(), '..', '..', 'keystone'))) -from keystone.queryext.exthandler import UrlExtensionFilter +from keystone.middleware.exthandler import UrlExtensionFilter import unittest diff --git a/setup.py b/setup.py index 6ed7a418..6e7276a4 100755 --- a/setup.py +++ b/setup.py @@ -57,7 +57,7 @@ setup( entry_points={ 'paste.app_factory': ['main=identity:app_factory'], 'paste.filter_factory': [ - 'extfilter=keystone.queryext.exthandler:filter_factory', + 'extfilter=keystone.middleware.exthandler:filter_factory', 'remoteauth=keystone.middleware.remoteauth:remoteauth_factory', 'tokenauth=keystone.auth_protocols.auth_token:filter_factory', 'swiftauth=keystone.middleware.swift_auth:filter_factory', -- cgit From 2ff9f978504599faa053509c4cbd7769469faf4c Mon Sep 17 00:00:00 2001 From: Dolph Mathews Date: Thu, 7 Jul 2011 14:55:27 -0500 Subject: Removed unused imports --- keystone/test/unit/test_exthandler.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/keystone/test/unit/test_exthandler.py b/keystone/test/unit/test_exthandler.py index 76614ab6..65539325 100644 --- a/keystone/test/unit/test_exthandler.py +++ b/keystone/test/unit/test_exthandler.py @@ -14,13 +14,9 @@ # See the License for the specific language governing permissions and # limitations under the License. -import os -import sys -# Need to access identity module -sys.path.append(os.path.abspath(os.path.join( - os.getcwd(), '..', '..', 'keystone'))) -from keystone.middleware.exthandler import UrlExtensionFilter + import unittest +from keystone.middleware.exthandler import UrlExtensionFilter class MockWsgiApp(object): -- cgit From 73ee608acff551378a998b4dc60b4c21ff8701a1 Mon Sep 17 00:00:00 2001 From: Dolph Mathews Date: Thu, 7 Jul 2011 15:11:37 -0500 Subject: Added import, autoformatting --- keystone/test/run_tests.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/keystone/test/run_tests.py b/keystone/test/run_tests.py index 190f632d..15fb8ede 100644 --- a/keystone/test/run_tests.py +++ b/keystone/test/run_tests.py @@ -1,4 +1,5 @@ import os +import sys import subprocess import time @@ -20,7 +21,7 @@ if __name__ == '__main__': # blatent hack. time.sleep(3) if server.poll() is not None: - print >>sys.stderr, 'Failed to start server' + print >> sys.stderr, 'Failed to start server' sys.exit(-1) try: -- cgit From 7845e7223487cea955c94a77e6a018a2ab37b419 Mon Sep 17 00:00:00 2001 From: Dolph Mathews Date: Fri, 8 Jul 2011 14:06:54 -0500 Subject: Refactored URL extensions handling (for .json/.xml) Added universal support for optional trailing slashes --- keystone/middleware/exthandler.py | 38 ++++++++++++++++++++++++++++------- keystone/test/unit/test_exthandler.py | 12 ++++++++++- 2 files changed, 42 insertions(+), 8 deletions(-) diff --git a/keystone/middleware/exthandler.py b/keystone/middleware/exthandler.py index e0fae135..78adb421 100644 --- a/keystone/middleware/exthandler.py +++ b/keystone/middleware/exthandler.py @@ -38,15 +38,39 @@ class UrlExtensionFilter(object): self.conf = conf def __call__(self, env, start_response): - uri = env['PATH_INFO'] - (path, ext) = uri.rsplit('.', 1) - if ext in CONTENT_TYPES: - env['HTTP_ACCEPT'] = CONTENT_TYPES[ext] - env['PATH_INFO'] = path - elif 'HTTP_ACCEPT' not in env: - env['HTTP_ACCEPT'] = DEFAULT_CONTENT_TYPE + (env['PATH_INFO'], env['HTTP_ACCEPT']) = self.override_accept_header( + env.get('PATH_INFO'), env.get('HTTP_ACCEPT')) + + env['PATH_INFO'] = self.remove_trailing_slash(env.get('PATH_INFO')) return self.app(env, start_response) + + def override_accept_header(self, path_info, http_accept): + """Looks for an (.json/.xml) extension on the URL, removes it, and + overrides the Accept header if an extension was found""" + # try to split the extension from the rest of the path + parts = path_info.rsplit('.', 1) + if len(parts) > 1: + (path, ext) = parts + else: + (path, ext) = (parts[0], None) + + if ext in CONTENT_TYPES: + # Use the content type specified by the extension + return (path, CONTENT_TYPES[ext]) + elif http_accept is None: + # No extension or Accept specified, use default + return (path_info, DEFAULT_CONTENT_TYPE) + else: + # Return what we were given + return (path_info, http_accept) + + def remove_trailing_slash(self, path_info): + """Removes a trailing slash from the given path, if any""" + if path_info[-1] == '/': + return path_info[:-1] + else: + return path_info def filter_factory(global_conf, **local_conf): """Returns a WSGI filter app for use with paste.deploy.""" diff --git a/keystone/test/unit/test_exthandler.py b/keystone/test/unit/test_exthandler.py index 65539325..b3616875 100644 --- a/keystone/test/unit/test_exthandler.py +++ b/keystone/test/unit/test_exthandler.py @@ -36,7 +36,17 @@ class UrlExtensionFilterTest(unittest.TestCase): def setUp(self): self.filter = UrlExtensionFilter(MockWsgiApp(), {}) - + + def test_remove_trailing_slash(self): + env = {'PATH_INFO': '/v2.0/'} + self.filter(env, _start_response) + self.assertEqual('/v2.0', env['PATH_INFO']) + + def test_remove_trailing_slash_from_empty_path(self): + env = {'PATH_INFO': '/'} + self.filter(env, _start_response) + self.assertEqual('', env['PATH_INFO']) + def test_no_extension(self): env = {'PATH_INFO': '/v2.0/someresource'} self.filter(env, _start_response) -- cgit From eeeb55e4a29a24a24e9b9f793eb9d38568ec9cee Mon Sep 17 00:00:00 2001 From: Dolph Mathews Date: Fri, 8 Jul 2011 14:11:36 -0500 Subject: Minor comment change --- keystone/middleware/exthandler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/keystone/middleware/exthandler.py b/keystone/middleware/exthandler.py index 78adb421..e0cc7b39 100644 --- a/keystone/middleware/exthandler.py +++ b/keystone/middleware/exthandler.py @@ -59,7 +59,7 @@ class UrlExtensionFilter(object): # Use the content type specified by the extension return (path, CONTENT_TYPES[ext]) elif http_accept is None: - # No extension or Accept specified, use default + # No extension or Accept header specified, use default return (path_info, DEFAULT_CONTENT_TYPE) else: # Return what we were given -- cgit From f9c44f02b86d87f928a894ca4cf353202b01d3ff Mon Sep 17 00:00:00 2001 From: Dolph Mathews Date: Fri, 8 Jul 2011 14:22:14 -0500 Subject: Renamed exthandler to urlrewritefilter to better illustrate it's purpose --- etc/keystone.conf | 8 +-- keystone/middleware/exthandler.py | 82 ---------------------------- keystone/middleware/url.py | 83 +++++++++++++++++++++++++++++ keystone/test/unit/test_exthandler.py | 78 --------------------------- keystone/test/unit/test_keystone.py | 2 +- keystone/test/unit/test_urlrewritefilter.py | 78 +++++++++++++++++++++++++++ setup.py | 2 +- 7 files changed, 167 insertions(+), 166 deletions(-) delete mode 100644 keystone/middleware/exthandler.py create mode 100644 keystone/middleware/url.py delete mode 100644 keystone/test/unit/test_exthandler.py create mode 100644 keystone/test/unit/test_urlrewritefilter.py diff --git a/etc/keystone.conf b/etc/keystone.conf index 425b41ee..a404435e 100755 --- a/etc/keystone.conf +++ b/etc/keystone.conf @@ -64,12 +64,12 @@ sql_idle_timeout = 30 [pipeline:admin] pipeline = - exthandler + urlrewritefilter admin_service [pipeline:keystone-legacy-auth] pipeline = - exthandler + urlrewritefilter legacy_auth public_service @@ -79,8 +79,8 @@ paste.app_factory = keystone.server:app_factory [app:admin_service] paste.app_factory = keystone.server:admin_app_factory -[filter:exthandler] -paste.filter_factory = keystone.middleware.exthandler:filter_factory +[filter:urlrewritefilter] +paste.filter_factory = keystone.middleware.url:filter_factory [filter:legacy_auth] paste.filter_factory = keystone.frontends.legacy_token_auth:filter_factory diff --git a/keystone/middleware/exthandler.py b/keystone/middleware/exthandler.py deleted file mode 100644 index e0cc7b39..00000000 --- a/keystone/middleware/exthandler.py +++ /dev/null @@ -1,82 +0,0 @@ -#!/usr/bin/env python -# vim: tabstop=4 shiftwidth=4 softtabstop=4 -# -# Copyright (c) 2010 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. - - -""" -Auth Middleware that accepts URL query extension. - -This module can be installed as a filter in front of your service to -detect extension in the resource URI (e.g., foo/resource.xml) to -specify HTTP response body type. If an extension is specified, it -overwrites the Accept header in the request, if present. - -""" - -CONTENT_TYPES = {'json': 'application/json', 'xml': 'application/xml'} -DEFAULT_CONTENT_TYPE = CONTENT_TYPES['json'] - -class UrlExtensionFilter(object): - - def __init__(self, app, conf): - # app is the next app in WSGI chain - eventually the OpenStack service - self.app = app - self.conf = conf - - def __call__(self, env, start_response): - (env['PATH_INFO'], env['HTTP_ACCEPT']) = self.override_accept_header( - env.get('PATH_INFO'), env.get('HTTP_ACCEPT')) - - env['PATH_INFO'] = self.remove_trailing_slash(env.get('PATH_INFO')) - - return self.app(env, start_response) - - def override_accept_header(self, path_info, http_accept): - """Looks for an (.json/.xml) extension on the URL, removes it, and - overrides the Accept header if an extension was found""" - # try to split the extension from the rest of the path - parts = path_info.rsplit('.', 1) - if len(parts) > 1: - (path, ext) = parts - else: - (path, ext) = (parts[0], None) - - if ext in CONTENT_TYPES: - # Use the content type specified by the extension - return (path, CONTENT_TYPES[ext]) - elif http_accept is None: - # No extension or Accept header specified, use default - return (path_info, DEFAULT_CONTENT_TYPE) - else: - # Return what we were given - return (path_info, http_accept) - - def remove_trailing_slash(self, path_info): - """Removes a trailing slash from the given path, if any""" - if path_info[-1] == '/': - return path_info[:-1] - else: - return path_info - -def filter_factory(global_conf, **local_conf): - """Returns a WSGI filter app for use with paste.deploy.""" - conf = global_conf.copy() - conf.update(local_conf) - - def ext_filter(app): - return UrlExtensionFilter(app, conf) - return ext_filter diff --git a/keystone/middleware/url.py b/keystone/middleware/url.py new file mode 100644 index 00000000..fd97a6a7 --- /dev/null +++ b/keystone/middleware/url.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# +# Copyright (c) 2010 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. + + +""" +Auth Middleware that accepts URL query extension. + +This module can be installed as a filter in front of your service to +detect extension in the resource URI (e.g., foo/resource.xml) to +specify HTTP response body type. If an extension is specified, it +overwrites the Accept header in the request, if present. + +""" + +CONTENT_TYPES = {'json': 'application/json', 'xml': 'application/xml'} +DEFAULT_CONTENT_TYPE = CONTENT_TYPES['json'] + +class UrlRewriteFilter(object): + """Middleware filter to handle URL rewriting""" + + def __init__(self, app, conf): + # app is the next app in WSGI chain - eventually the OpenStack service + self.app = app + self.conf = conf + + def __call__(self, env, start_response): + (env['PATH_INFO'], env['HTTP_ACCEPT']) = self.override_accept_header( + env.get('PATH_INFO'), env.get('HTTP_ACCEPT')) + + env['PATH_INFO'] = self.remove_trailing_slash(env.get('PATH_INFO')) + + return self.app(env, start_response) + + def override_accept_header(self, path_info, http_accept): + """Looks for an (.json/.xml) extension on the URL, removes it, and + overrides the Accept header if an extension was found""" + # try to split the extension from the rest of the path + parts = path_info.rsplit('.', 1) + if len(parts) > 1: + (path, ext) = parts + else: + (path, ext) = (parts[0], None) + + if ext in CONTENT_TYPES: + # Use the content type specified by the extension + return (path, CONTENT_TYPES[ext]) + elif http_accept is None: + # No extension or Accept header specified, use default + return (path_info, DEFAULT_CONTENT_TYPE) + else: + # Return what we were given + return (path_info, http_accept) + + def remove_trailing_slash(self, path_info): + """Removes a trailing slash from the given path, if any""" + if path_info[-1] == '/': + return path_info[:-1] + else: + return path_info + +def filter_factory(global_conf, **local_conf): + """Returns a WSGI filter app for use with paste.deploy.""" + conf = global_conf.copy() + conf.update(local_conf) + + def ext_filter(app): + return UrlRewriteFilter(app, conf) + return ext_filter diff --git a/keystone/test/unit/test_exthandler.py b/keystone/test/unit/test_exthandler.py deleted file mode 100644 index b3616875..00000000 --- a/keystone/test/unit/test_exthandler.py +++ /dev/null @@ -1,78 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 -# Copyright (c) 2010-2011 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 unittest -from keystone.middleware.exthandler import UrlExtensionFilter - - -class MockWsgiApp(object): - - def __init__(self): - pass - - def __call__(self, env, start_response): - pass - - -def _start_response(): - pass - - -class UrlExtensionFilterTest(unittest.TestCase): - - def setUp(self): - self.filter = UrlExtensionFilter(MockWsgiApp(), {}) - - def test_remove_trailing_slash(self): - env = {'PATH_INFO': '/v2.0/'} - self.filter(env, _start_response) - self.assertEqual('/v2.0', env['PATH_INFO']) - - def test_remove_trailing_slash_from_empty_path(self): - env = {'PATH_INFO': '/'} - self.filter(env, _start_response) - self.assertEqual('', env['PATH_INFO']) - - def test_no_extension(self): - env = {'PATH_INFO': '/v2.0/someresource'} - self.filter(env, _start_response) - self.assertEqual('/v2.0/someresource', env['PATH_INFO']) - self.assertEqual('application/json', env['HTTP_ACCEPT']) - - def test_xml_extension(self): - env = {'PATH_INFO': '/v2.0/someresource.xml'} - self.filter(env, _start_response) - self.assertEqual('/v2.0/someresource', env['PATH_INFO']) - self.assertEqual('application/xml', env['HTTP_ACCEPT']) - - def test_json_extension(self): - env = {'PATH_INFO': '/v2.0/someresource.json'} - self.filter(env, _start_response) - self.assertEqual('/v2.0/someresource', env['PATH_INFO']) - self.assertEqual('application/json', env['HTTP_ACCEPT']) - - def test_extension_overrides_header(self): - env = { - 'PATH_INFO': '/v2.0/someresource.json', - 'HTTP_ACCEPT': 'application/xml'} - self.filter(env, _start_response) - self.assertEqual('/v2.0/someresource', env['PATH_INFO']) - self.assertEqual('application/json', env['HTTP_ACCEPT']) - - -if __name__ == '__main__': - unittest.main() diff --git a/keystone/test/unit/test_keystone.py b/keystone/test/unit/test_keystone.py index 05fe1864..5909c08f 100755 --- a/keystone/test/unit/test_keystone.py +++ b/keystone/test/unit/test_keystone.py @@ -9,7 +9,7 @@ TEST_FILES = [ #'test_authn_v2.py', # this is largely failing 'test_common.py', # this doesn't actually contain tests 'test_endpoints.py', - 'test_exthandler.py', + 'test_urlrewritefilter.py', 'test_groups.py', 'test_keystone.py', # not sure why this is referencing itself 'test_roles.py', diff --git a/keystone/test/unit/test_urlrewritefilter.py b/keystone/test/unit/test_urlrewritefilter.py new file mode 100644 index 00000000..e22c362f --- /dev/null +++ b/keystone/test/unit/test_urlrewritefilter.py @@ -0,0 +1,78 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# Copyright (c) 2010-2011 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 unittest +from keystone.middleware.url import UrlRewriteFilter + + +class MockWsgiApp(object): + + def __init__(self): + pass + + def __call__(self, env, start_response): + pass + + +def _start_response(): + pass + + +class UrlExtensionFilterTest(unittest.TestCase): + + def setUp(self): + self.filter = UrlRewriteFilter(MockWsgiApp(), {}) + + def test_remove_trailing_slash(self): + env = {'PATH_INFO': '/v2.0/'} + self.filter(env, _start_response) + self.assertEqual('/v2.0', env['PATH_INFO']) + + def test_remove_trailing_slash_from_empty_path(self): + env = {'PATH_INFO': '/'} + self.filter(env, _start_response) + self.assertEqual('', env['PATH_INFO']) + + def test_no_extension(self): + env = {'PATH_INFO': '/v2.0/someresource'} + self.filter(env, _start_response) + self.assertEqual('/v2.0/someresource', env['PATH_INFO']) + self.assertEqual('application/json', env['HTTP_ACCEPT']) + + def test_xml_extension(self): + env = {'PATH_INFO': '/v2.0/someresource.xml'} + self.filter(env, _start_response) + self.assertEqual('/v2.0/someresource', env['PATH_INFO']) + self.assertEqual('application/xml', env['HTTP_ACCEPT']) + + def test_json_extension(self): + env = {'PATH_INFO': '/v2.0/someresource.json'} + self.filter(env, _start_response) + self.assertEqual('/v2.0/someresource', env['PATH_INFO']) + self.assertEqual('application/json', env['HTTP_ACCEPT']) + + def test_extension_overrides_header(self): + env = { + 'PATH_INFO': '/v2.0/someresource.json', + 'HTTP_ACCEPT': 'application/xml'} + self.filter(env, _start_response) + self.assertEqual('/v2.0/someresource', env['PATH_INFO']) + self.assertEqual('application/json', env['HTTP_ACCEPT']) + + +if __name__ == '__main__': + unittest.main() diff --git a/setup.py b/setup.py index 6e7276a4..56444937 100755 --- a/setup.py +++ b/setup.py @@ -57,7 +57,7 @@ setup( entry_points={ 'paste.app_factory': ['main=identity:app_factory'], 'paste.filter_factory': [ - 'extfilter=keystone.middleware.exthandler:filter_factory', + 'extfilter=keystone.middleware.url:filter_factory', 'remoteauth=keystone.middleware.remoteauth:remoteauth_factory', 'tokenauth=keystone.auth_protocols.auth_token:filter_factory', 'swiftauth=keystone.middleware.swift_auth:filter_factory', -- cgit From 87f6faab6eeb1037de675b559436b53c35415675 Mon Sep 17 00:00:00 2001 From: Dolph Mathews Date: Fri, 8 Jul 2011 14:25:26 -0500 Subject: Removed redundant action mappings (for version controller) --- keystone/server.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/keystone/server.py b/keystone/server.py index 8b704aaf..56cb4482 100755 --- a/keystone/server.py +++ b/keystone/server.py @@ -543,9 +543,6 @@ class KeystoneAPI(wsgi.Router): # Miscellaneous Operations version_controller = VersionController(options) - mapper.connect("/v2.0/", controller=version_controller, - action="get_version_info", - conditions=dict(method=["GET"])) mapper.connect("/v2.0", controller=version_controller, action="get_version_info", conditions=dict(method=["GET"])) @@ -755,9 +752,6 @@ class KeystoneAdminAPI(wsgi.Router): # Miscellaneous Operations version_controller = VersionController(options) - mapper.connect("/v2.0/", controller=version_controller, - action="get_version_info", - conditions=dict(method=["GET"])) mapper.connect("/v2.0", controller=version_controller, action="get_version_info", conditions=dict(method=["GET"])) -- cgit From 418a9e5913fe25fb4c5e704655d2497da3bc5d41 Mon Sep 17 00:00:00 2001 From: Dolph Mathews Date: Fri, 8 Jul 2011 15:03:06 -0500 Subject: Fixed default content type behavior (was defaulting to XML) --- keystone/middleware/url.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/keystone/middleware/url.py b/keystone/middleware/url.py index fd97a6a7..3c876b8c 100644 --- a/keystone/middleware/url.py +++ b/keystone/middleware/url.py @@ -59,7 +59,8 @@ class UrlRewriteFilter(object): if ext in CONTENT_TYPES: # Use the content type specified by the extension return (path, CONTENT_TYPES[ext]) - elif http_accept is None: + elif http_accept is None or http_accept == '*/*': + # TODO: This probably isn't the best place to handle "Accept: */*" # No extension or Accept header specified, use default return (path_info, DEFAULT_CONTENT_TYPE) else: -- cgit