From b3c4f16ca84e4d19caef1c40266310bf090c0927 Mon Sep 17 00:00:00 2001 From: Dolph Mathews Date: Mon, 1 Aug 2011 12:07:27 -0500 Subject: Added support for versioned openstack MIME types Additional MIME types are normalized down to 'application/xml' and 'application/json' to minimize impact on underlying layers. Also added normalized KEYSTONE_API_VERSION and KEYSTONE_RESPONSE_ENCODING (simplified prefferred Accept encoding) to the wsgi environment for underlying apps to optionally utilize. (note: the legacy layer now looks for the KEYSTONE_API_VERSION). Includes additional pylint fixes. Change-Id: I06b82e603799ed89a2e2d92005892ede0a295225 --- .../backends/sqlalchemy/api/endpoint_template.py | 64 ++++----- keystone/common/template.py | 68 ++++++---- keystone/frontends/legacy_token_auth.py | 5 +- keystone/middleware/url.py | 147 ++++++++++++++++----- keystone/routers/admin.py | 70 +++++----- keystone/routers/service.py | 14 +- keystone/test/functional/sample_test.py | 7 +- keystone/test/system/test_request_specs.py | 12 ++ 8 files changed, 246 insertions(+), 141 deletions(-) diff --git a/keystone/backends/sqlalchemy/api/endpoint_template.py b/keystone/backends/sqlalchemy/api/endpoint_template.py index 52231fec..f74afbec 100755 --- a/keystone/backends/sqlalchemy/api/endpoint_template.py +++ b/keystone/backends/sqlalchemy/api/endpoint_template.py @@ -63,31 +63,31 @@ class EndpointTemplateAPI(BaseEndpointTemplateAPI): return (None, None) if marker is None: marker = first.id - next = session.query(models.EndpointTemplates).filter("id > :marker").params(\ + next_page = session.query(models.EndpointTemplates).filter("id > :marker").params(\ marker='%s' % marker).order_by(\ models.EndpointTemplates.id).limit(limit).all() - prev = session.query(models.EndpointTemplates).filter("id < :marker").params(\ + prev_page = session.query(models.EndpointTemplates).filter("id < :marker").params(\ marker='%s' % marker).order_by(\ models.EndpointTemplates.id.desc()).limit(int(limit)).all() - if len(next) == 0: - next = last + if len(next_page) == 0: + next_page = last else: - for t in next: - next = t - if len(prev) == 0: - prev = first + for t in next_page: + next_page = t + if len(prev_page) == 0: + prev_page = first else: - for t in prev: - prev = t - if prev.id == marker: - prev = None + for t in prev_page: + prev_page = t + if prev_page.id == marker: + prev_page = None else: - prev = prev.id - if next.id == last.id: - next = None + prev_page = prev_page.id + if next_page.id == last.id: + next_page = None else: - next = next.id - return (prev, next) + next_page = next_page.id + return (prev_page, next_page) def endpoint_get_by_tenant_get_page(self, tenant_id, marker, limit, session=None): @@ -119,39 +119,39 @@ class EndpointTemplateAPI(BaseEndpointTemplateAPI): return (None, None) if marker is None: marker = first.id - next = session.query(tba).\ + next_page = session.query(tba).\ filter(tba.tenant_id == tenant_id).\ filter("id>=:marker").params( marker='%s' % marker).order_by( tba.id).limit(int(limit)).all() - prev = session.query(tba).\ + prev_page = session.query(tba).\ filter(tba.tenant_id == tenant_id).\ filter("id < :marker").params( marker='%s' % marker).order_by( tba.id).limit(int(limit) + 1).all() - next_len = len(next) - prev_len = len(prev) + next_len = len(next_page) + prev_len = len(prev_page) if next_len == 0: - next = last + next_page = last else: - for t in next: - next = t + for t in next_page: + next_page = t if prev_len == 0: - prev = first + prev_page = first else: - for t in prev: - prev = t + for t in prev_page: + prev_page = t if first.id == marker: - prev = None + prev_page = None else: - prev = prev.id + prev_page = prev_page.id if marker == last.id: - next = None + next_page = None else: - next = next.id - return (prev, next) + next_page = next_page.id + return (prev_page, next_page) def endpoint_add(self, values): endpoints = models.Endpoints() diff --git a/keystone/common/template.py b/keystone/common/template.py index 97e52658..308c8fd1 100644 --- a/keystone/common/template.py +++ b/keystone/common/template.py @@ -64,7 +64,7 @@ class BaseTemplate(object): settings = {} #used in prepare() defaults = {} #used in render() - def __init__(self, source=None, name=None, lookup=[], encoding='utf8', + def __init__(self, source=None, name=None, lookup=None, encoding='utf8', **settings): """ Create a new template. If the source parameter (str or buffer) is missing, the name argument @@ -76,10 +76,12 @@ class BaseTemplate(object): The encoding parameter should be used to decode byte strings or files. The settings parameter contains a dict for engine-specific settings. """ + lookup = lookup or [] + self.name = name self.source = source.read() if hasattr(source, 'read') else source self.filename = source.filename if hasattr(source, 'filename') else None - self.lookup = map(os.path.abspath, lookup) + self.lookup = [os.path.abspath(path) for path in lookup] self.encoding = encoding self.settings = self.settings.copy() # Copy from class variable self.settings.update(settings) # Apply @@ -92,9 +94,11 @@ class BaseTemplate(object): self.prepare(**self.settings) @classmethod - def search(cls, name, lookup=[]): + def search(cls, name, lookup=None): """ Search name in all directories specified in lookup. First without, then with common extensions. Return first hit. """ + lookup = lookup or [] + if os.path.isfile(name): return name for spath in lookup: @@ -129,17 +133,23 @@ class BaseTemplate(object): class SimpleTemplate(BaseTemplate): - blocks = ('if', 'elif', 'else', 'try', 'except', 'finally', 'for', 'while', 'with', 'def', 'class') + blocks = ('if', 'elif', 'else', 'try', 'except', 'finally', 'for', 'while', + 'with', 'def', 'class') dedent_blocks = ('elif', 'else', 'except', 'finally') - + cache = None + code = None + compiled = None + _str = None + _escape = None + def prepare(self, escape_func=cgi.escape, noescape=False): self.cache = {} if self.source: self.code = self.translate(self.source) - self.co = compile(self.code, '', 'exec') + self.compiled = compile(self.code, '', 'exec') else: self.code = self.translate(open(self.filename).read()) - self.co = compile(self.code, self.filename, 'exec') + self.compiled = compile(self.code, self.filename, 'exec') enc = self.encoding touni = functools.partial(unicode, encoding=self.encoding) self._str = lambda x: touni(x, enc) @@ -158,7 +168,8 @@ class SimpleTemplate(BaseTemplate): def yield_tokens(line): for i, part in enumerate(re.split(r'\{\{(.*?)\}\}', line)): if i % 2: - if part.startswith('!'): yield 'RAW', part[1:] + if part.startswith('!'): + yield 'RAW', part[1:] else: yield 'CMD', part else: yield 'TXT', part @@ -172,11 +183,14 @@ class SimpleTemplate(BaseTemplate): for token in tokens: if token[0] == tokenize.COMMENT: start, end = token[2][1], token[3][1] - return codeline[:start] + codeline[end:], codeline[start:end] + return ( + codeline[:start] + codeline[end:], + codeline[start:end]) return line, '' def flush(): # Flush the ptrbuffer - if not ptrbuffer: return + if not ptrbuffer: + return cline = '' for line in ptrbuffer: for token, value in line: @@ -205,10 +219,12 @@ class SimpleTemplate(BaseTemplate): else unicode(line, encoding=self.encoding) if lineno <= 2: m = re.search(r"%.*coding[:=]\s*([-\w\.]+)", line) - if m: self.encoding = m.group(1) - if m: line = line.replace('coding', 'coding (removed)') + if m: + self.encoding = m.group(1) + if m: + line = line.replace('coding', 'coding (removed)') if line.strip()[:2].count('%') == 1: - line = line.split('%', 1)[1].lstrip() # Full line following the % + line = line.split('%', 1)[1].lstrip() # Rest of line after % cline = split_comment(line)[0].strip() cmd = re.split(r'[^a-zA-Z0-9_]', cline)[0] flush() ##encodig (TODO: why?) @@ -235,7 +251,8 @@ class SimpleTemplate(BaseTemplate): elif cmd == 'rebase': p = cline.split(None, 2)[1:] if len(p) == 2: - code("globals()['_rebase']=(%s, dict(%s))" % (repr(p[0]), p[1])) + code("globals()['_rebase']=(%s, dict(%s))" % ( + repr(p[0]), p[1])) elif p: code("globals()['_rebase']=(%s, {})" % repr(p[0])) else: @@ -258,7 +275,7 @@ class SimpleTemplate(BaseTemplate): '_include': self.subtemplate, '_str': self._str, '_escape': self._escape}) env.update(args) - eval(self.co, env) + eval(self.compiled, env) if '_rebase' in env: subtpl, rargs = env['_rebase'] subtpl = self.__class__(name=subtpl, lookup=self.lookup) @@ -273,7 +290,8 @@ class SimpleTemplate(BaseTemplate): self.execute(stdout, **args) return ''.join(stdout) -def static_file(resp, req, filename, root, guessmime=True, mimetype=None, download=False): +def static_file(resp, req, filename, root, guessmime=True, mimetype=None, + download=False): """ Opens a file in a safe way and returns a HTTPError object with status code 200, 305, 401 or 404. Sets Content-Type, Content-Length and Last-Modified header. Obeys If-Modified-Since header and HEAD requests. @@ -281,13 +299,10 @@ def static_file(resp, req, filename, root, guessmime=True, mimetype=None, downlo root = os.path.abspath(root) + os.sep filename = os.path.abspath(os.path.join(root, filename.strip('/\\'))) if not filename.startswith(root): - #return HTTPError(403, "Access denied.") return ForbiddenFault("Access denied.") if not os.path.exists(filename) or not os.path.isfile(filename): - #return HTTPError(404, "File does not exist.") return fault.ItemNotFoundFault("File does not exist.") if not os.access(filename, os.R_OK): - #return HTTPError(403, "You do not have permission to access this file.") return ForbiddenFault("You do not have permission to access this file.") if not mimetype and guessmime: @@ -309,13 +324,15 @@ def static_file(resp, req, filename, root, guessmime=True, mimetype=None, downlo ims = parse_date(ims) if ims is not None and ims >= int(stats.st_mtime): - resp.date = time.strftime("%a, %d %b %Y %H:%M:%S GMT", time.gmtime()) + resp.date = time.strftime( + "%a, %d %b %Y %H:%M:%S GMT", time.gmtime()) return Response(body=None, status=304, headerlist=resp.headerlist) resp.content_length = stats.st_size if req.method == 'HEAD': return Response(body=None, status=200, headerlist=resp.headerlist) else: - return Response(body=open(filename).read(), status=200, headerlist=resp.headerlist) + return Response(body=open(filename).read(), status=200, + headerlist=resp.headerlist) def template(tpl, template_adapter=SimpleTemplate, **kwargs): @@ -328,11 +345,14 @@ def template(tpl, template_adapter=SimpleTemplate, **kwargs): lookup = kwargs.get('template_lookup', TEMPLATE_PATH) if isinstance(tpl, template_adapter): TEMPLATES[tpl] = tpl - if settings: TEMPLATES[tpl].prepare(**settings) + if settings: + TEMPLATES[tpl].prepare(**settings) elif "\n" in tpl or "{" in tpl or "%" in tpl or '$' in tpl: - TEMPLATES[tpl] = template_adapter(source=tpl, lookup=lookup, **settings) + TEMPLATES[tpl] = template_adapter(source=tpl, lookup=lookup, + **settings) else: - TEMPLATES[tpl] = template_adapter(name=tpl, lookup=lookup, **settings) + TEMPLATES[tpl] = template_adapter(name=tpl, lookup=lookup, + **settings) return TEMPLATES[tpl].render(**kwargs) diff --git a/keystone/frontends/legacy_token_auth.py b/keystone/frontends/legacy_token_auth.py index 1d7cfc64..ff1f67ce 100644 --- a/keystone/frontends/legacy_token_auth.py +++ b/keystone/frontends/legacy_token_auth.py @@ -65,13 +65,12 @@ class AuthProtocol(object): self.start_response = start_response self.env = env self.request = Request(env) - if self.request.path.startswith('/v1.0' - ) or self.request.path.startswith('/v1.1'): + if env['KEYSTONE_API_VERSION'] in ['1.0', '1.1']: params = {"passwordCredentials": {"username": utils.get_auth_user(self.request), "password": utils.get_auth_key(self.request)}} #Make request to keystone - new_request = Request.blank('/v2.0/tokens') + new_request = Request.blank('/tokens') new_request.method = 'POST' new_request.headers['Content-type'] = 'application/json' new_request.accept = 'text/json' diff --git a/keystone/middleware/url.py b/keystone/middleware/url.py index fdbcf794..d0643e77 100644 --- a/keystone/middleware/url.py +++ b/keystone/middleware/url.py @@ -27,52 +27,127 @@ overwrites the Accept header in the request, if present. """ -CONTENT_TYPES = {'json': 'application/json', 'xml': 'application/xml'} -DEFAULT_CONTENT_TYPE = CONTENT_TYPES['json'] +import webob.acceptparse -class UrlRewriteFilter(object): - """Middleware filter to handle URL rewriting""" +# Maps supported URL prefixes to API_VERSION +PATH_PREFIXES = { + '/v2.0': '2.0', + '/v1.1': '1.1', + '/v1.0': '1.0'} + +# Maps supported URL extensions to RESPONSE_ENCODING +PATH_SUFFIXES = { + '.json': 'json', + '.xml': 'xml'} + +# Maps supported Accept headers to RESPONSE_ENCODING and API_VERSION +ACCEPT_HEADERS = { + 'application/vnd.openstack.identity-v2.0+json': ('json', '2.0'), + 'application/vnd.openstack.identity-v2.0+xml': ('xml', '2.0'), + 'application/vnd.openstack.identity-v1.1+json': ('json', '1.1'), + 'application/vnd.openstack.identity-v1.1+xml': ('xml', '1.1'), + 'application/vnd.openstack.identity-v1.0+json': ('json', '1.0'), + 'application/vnd.openstack.identity-v1.0+xml': ('xml', '1.0'), + 'application/json': ('json', None), + 'application/xml': ('xml', None)} + +DEFAULT_RESPONSE_ENCODING = 'json' +DEFAULT_API_VERSION = '2.0' + +class NormalizingFilter(object): + """Middleware filter to handle URL and Accept header normalization""" 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')) + # Inspect the request for mime type and API version + env = normalize_accept_header(env) + env = normalize_path_prefix(env) + env = normalize_path_suffix(env) + env['PATH_INFO'] = normalize_starting_slash(env.get('PATH_INFO')) + env['PATH_INFO'] = normalize_trailing_slash(env['PATH_INFO']) + # Fall back on defaults, if necessary + env['KEYSTONE_API_VERSION'] = env.get( + 'KEYSTONE_API_VERSION') or DEFAULT_API_VERSION + env['KEYSTONE_RESPONSE_ENCODING'] = env.get( + 'KEYSTONE_RESPONSE_ENCODING') or DEFAULT_RESPONSE_ENCODING + env['HTTP_ACCEPT'] = 'application/' + (env.get( + 'KEYSTONE_RESPONSE_ENCODING') or DEFAULT_RESPONSE_ENCODING) + 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 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: - # Return what we were given - return (path_info, http_accept) +def normalize_accept_header(env): + """Matches the preferred Accept encoding to supported encodings. + + Sets KEYSTONE_RESPONSE_ENCODING and KEYSTONE_API_VERSION, if appropriate.""" + if env.get('HTTP_ACCEPT'): + accept = webob.acceptparse.Accept('Accept', env.get('HTTP_ACCEPT')) + best_accept = accept.best_match(ACCEPT_HEADERS.keys()) + if best_accept: + response_encoding, api_version = ACCEPT_HEADERS[best_accept] + + if response_encoding: + env['KEYSTONE_RESPONSE_ENCODING'] = response_encoding + + if api_version: + env['KEYSTONE_API_VERSION'] = api_version + + return env + +def normalize_path_prefix(env): + """Handles recognized PATH_INFO prefixes. + + Looks for a version prefix on the PATH_INFO, sets KEYSTONE_API_VERSION + accordingly, and removes the prefix to normalize the request.""" + for prefix in PATH_PREFIXES.keys(): + if env['PATH_INFO'].startswith(prefix): + env['KEYSTONE_API_VERSION'] = PATH_PREFIXES[prefix] + env['PATH_INFO'] = env['PATH_INFO'][len(prefix):] + break + + return env + +def normalize_path_suffix(env): + """Hnadles recognized PATH_INFO suffixes. + + Looks for a recognized suffix on the PATH_INFO, sets the + KEYSTONE_RESPONSE_ENCODING accordingly, and removes the suffix to normalize + the request.""" + for suffix in PATH_SUFFIXES.keys(): + if env['PATH_INFO'].endswith(suffix): + env['KEYSTONE_RESPONSE_ENCODING'] = PATH_SUFFIXES[suffix] + env['PATH_INFO'] = env['PATH_INFO'][:-len(suffix)] + break + + return env + +def normalize_starting_slash(path_info): + """Removes a trailing slash from the given path, if any.""" + # Ensure the path at least contains a slash + if not path_info: + return '/' + + # Ensure the path starts with a slash + elif path_info[0] != '/': + return '/' + path_info + + # No need to change anything + else: + return path_info + +def normalize_trailing_slash(path_info): + """Removes a trailing slash from the given path, if any.""" + # Remove trailing slash, unless it's the only char + if len(path_info) > 1 and path_info[-1] == '/': + return path_info[:-1] - def remove_trailing_slash(self, path_info): - """Removes a trailing slash from the given path, if any""" - if len(path_info) > 1 and path_info[-1] == '/': - return path_info[:-1] - else: - return path_info + # No need to change anything + else: + return path_info def filter_factory(global_conf, **local_conf): """Returns a WSGI filter app for use with paste.deploy.""" @@ -80,5 +155,5 @@ def filter_factory(global_conf, **local_conf): conf.update(local_conf) def ext_filter(app): - return UrlRewriteFilter(app, conf) + return NormalizingFilter(app, conf) return ext_filter diff --git a/keystone/routers/admin.py b/keystone/routers/admin.py index d23baeba..62c17ea5 100644 --- a/keystone/routers/admin.py +++ b/keystone/routers/admin.py @@ -19,136 +19,136 @@ class AdminApi(wsgi.Router): mapper = routes.Mapper() db.configure_backends(options) - + # Token Operations auth_controller = AuthController(options) - mapper.connect("/v2.0/tokens", controller=auth_controller, + mapper.connect("/tokens", controller=auth_controller, action="authenticate", conditions=dict(method=["POST"])) - mapper.connect("/v2.0/tokens/{token_id}", controller=auth_controller, + mapper.connect("/tokens/{token_id}", controller=auth_controller, action="validate_token", conditions=dict(method=["GET"])) - mapper.connect("/v2.0/tokens/{token_id}", controller=auth_controller, + mapper.connect("/tokens/{token_id}", controller=auth_controller, action="delete_token", conditions=dict(method=["DELETE"])) # Tenant Operations tenant_controller = TenantController(options) - mapper.connect("/v2.0/tenants", controller=tenant_controller, + mapper.connect("/tenants", controller=tenant_controller, action="create_tenant", conditions=dict(method=["PUT", "POST"])) - mapper.connect("/v2.0/tenants", controller=tenant_controller, + mapper.connect("/tenants", controller=tenant_controller, action="get_tenants", conditions=dict(method=["GET"])) - mapper.connect("/v2.0/tenants/{tenant_id}", + mapper.connect("/tenants/{tenant_id}", controller=tenant_controller, action="get_tenant", conditions=dict(method=["GET"])) - mapper.connect("/v2.0/tenants/{tenant_id}", + mapper.connect("/tenants/{tenant_id}", controller=tenant_controller, action="update_tenant", conditions=dict(method=["PUT"])) - mapper.connect("/v2.0/tenants/{tenant_id}", + mapper.connect("/tenants/{tenant_id}", controller=tenant_controller, action="delete_tenant", conditions=dict(method=["DELETE"])) - # User Operations + # User Operations user_controller = UserController(options) - mapper.connect("/v2.0/users", + mapper.connect("/users", controller=user_controller, action="create_user", conditions=dict(method=["PUT", "POST"])) - mapper.connect("/v2.0/users", + mapper.connect("/users", controller=user_controller, action="get_users", conditions=dict(method=["GET"])) - mapper.connect("/v2.0/users/{user_id}", + mapper.connect("/users/{user_id}", controller=user_controller, action="get_user", conditions=dict(method=["GET"])) - mapper.connect("/v2.0/users/{user_id}", + mapper.connect("/users/{user_id}", controller=user_controller, action="update_user", conditions=dict(method=["PUT"])) - mapper.connect("/v2.0/users/{user_id}", + mapper.connect("/users/{user_id}", controller=user_controller, action="delete_user", conditions=dict(method=["DELETE"])) - mapper.connect("/v2.0/users/{user_id}/password", + mapper.connect("/users/{user_id}/password", controller=user_controller, action="set_user_password", conditions=dict(method=["PUT"])) - mapper.connect("/v2.0/users/{user_id}/tenant", + mapper.connect("/users/{user_id}/tenant", controller=user_controller, action="update_user_tenant", conditions=dict(method=["PUT"])) - # Test this, test failed - mapper.connect("/v2.0/users/{user_id}/enabled", + # Test this, test failed + mapper.connect("/users/{user_id}/enabled", controller=user_controller, action="set_user_enabled", conditions=dict(method=["PUT"])) - mapper.connect("/v2.0/tenants/{tenant_id}/users", + mapper.connect("/tenants/{tenant_id}/users", controller=user_controller, action="get_tenant_users", conditions=dict(method=["GET"])) #Roles and RoleRefs roles_controller = RolesController(options) - mapper.connect("/v2.0/roles", controller=roles_controller, + mapper.connect("/roles", controller=roles_controller, action="get_roles", conditions=dict(method=["GET"])) - mapper.connect("/v2.0/roles/{role_id}", controller=roles_controller, + mapper.connect("/roles/{role_id}", controller=roles_controller, action="get_role", conditions=dict(method=["GET"])) - mapper.connect("/v2.0/users/{user_id}/roleRefs", + mapper.connect("/users/{user_id}/roleRefs", controller=roles_controller, action="get_role_refs", conditions=dict(method=["GET"])) - mapper.connect("/v2.0/users/{user_id}/roleRefs", + mapper.connect("/users/{user_id}/roleRefs", controller=roles_controller, action="create_role_ref", conditions=dict(method=["POST"])) - mapper.connect("/v2.0/users/{user_id}/roleRefs/{role_ref_id}", + mapper.connect("/users/{user_id}/roleRefs/{role_ref_id}", controller=roles_controller, action="delete_role_ref", conditions=dict(method=["DELETE"])) #EndpointTemplatesControllers and Endpoints endpoint_templates_controller = EndpointTemplatesController(options) - mapper.connect("/v2.0/endpointTemplates", + mapper.connect("/endpointTemplates", controller=endpoint_templates_controller, action="get_endpoint_templates", conditions=dict(method=["GET"])) - mapper.connect("/v2.0/endpointTemplates/{endpoint_templates_id}", + mapper.connect("/endpointTemplates/{endpoint_templates_id}", controller=endpoint_templates_controller, action="get_endpoint_template", conditions=dict(method=["GET"])) - mapper.connect("/v2.0/tenants/{tenant_id}/endpoints", + mapper.connect("/tenants/{tenant_id}/endpoints", controller=endpoint_templates_controller, action="get_endpoints_for_tenant", conditions=dict(method=["GET"])) - mapper.connect("/v2.0/tenants/{tenant_id}/endpoints", + mapper.connect("/tenants/{tenant_id}/endpoints", controller=endpoint_templates_controller, action="add_endpoint_to_tenant", conditions=dict(method=["POST"])) mapper.connect( - "/v2.0/tenants/{tenant_id}/endpoints/{endpoints_id}", + "/tenants/{tenant_id}/endpoints/{endpoints_id}", controller=endpoint_templates_controller, action="remove_endpoint_from_tenant", conditions=dict(method=["DELETE"])) # Miscellaneous Operations version_controller = VersionController(options) - mapper.connect("/v2.0", controller=version_controller, + mapper.connect("/", controller=version_controller, action="get_version_info", conditions=dict(method=["GET"])) # Static Files Controller static_files_controller = StaticFilesController(options) - mapper.connect("/v2.0/identitydevguide.pdf", + mapper.connect("/identitydevguide.pdf", controller=static_files_controller, action="get_pdf_contract", conditions=dict(method=["GET"])) - mapper.connect("/v2.0/identity.wadl", + mapper.connect("/identity.wadl", controller=static_files_controller, action="get_wadl_contract", conditions=dict(method=["GET"])) - mapper.connect("/v2.0/xsd/{xsd}", + mapper.connect("/xsd/{xsd}", controller=static_files_controller, action="get_xsd_contract", conditions=dict(method=["GET"])) - mapper.connect("/v2.0/xsd/atom/{xsd}", + mapper.connect("/xsd/atom/{xsd}", controller=static_files_controller, action="get_xsd_atom_contract", conditions=dict(method=["GET"])) diff --git a/keystone/routers/service.py b/keystone/routers/service.py index 6039274d..1219cc32 100644 --- a/keystone/routers/service.py +++ b/keystone/routers/service.py @@ -18,36 +18,36 @@ class ServiceApi(wsgi.Router): # Token Operations auth_controller = AuthController(options) - mapper.connect("/v2.0/tokens", controller=auth_controller, + mapper.connect("/tokens", controller=auth_controller, action="authenticate", conditions=dict(method=["POST"])) # Tenant Operations tenant_controller = TenantController(options) - mapper.connect("/v2.0/tenants", controller=tenant_controller, + mapper.connect("/tenants", controller=tenant_controller, action="get_tenants", conditions=dict(method=["GET"])) # Miscellaneous Operations version_controller = VersionController(options) - mapper.connect("/v2.0", controller=version_controller, + mapper.connect("/", controller=version_controller, action="get_version_info", conditions=dict(method=["GET"])) # Static Files Controller static_files_controller = StaticFilesController(options) - mapper.connect("/v2.0/identitydevguide.pdf", + mapper.connect("/identitydevguide.pdf", controller=static_files_controller, action="get_pdf_contract", conditions=dict(method=["GET"])) - mapper.connect("/v2.0/identity.wadl", + mapper.connect("/identity.wadl", controller=static_files_controller, action="get_wadl_contract", conditions=dict(method=["GET"])) - mapper.connect("/v2.0/xsd/{xsd}", + mapper.connect("/xsd/{xsd}", controller=static_files_controller, action="get_pdf_contract", conditions=dict(method=["GET"])) - mapper.connect("/v2.0/xsd/atom/{xsd}", + mapper.connect("/xsd/atom/{xsd}", controller=static_files_controller, action="get_pdf_contract", conditions=dict(method=["GET"])) diff --git a/keystone/test/functional/sample_test.py b/keystone/test/functional/sample_test.py index 10d24abe..6143829e 100644 --- a/keystone/test/functional/sample_test.py +++ b/keystone/test/functional/sample_test.py @@ -37,7 +37,6 @@ ## @dtest.istest decorator). Adhere to these rules, and DTest can ## discover and run the tests without you having to do anything other ## than create them. -import dtest from dtest import util ## The "base" module contains KeystoneTest, which ensures that there's @@ -60,8 +59,8 @@ class SampleTest(base.KeystoneTest): ## You don't *have* to declare a doc string, but it's good ## practice. - ## Here we're making a "sample_call()", passing self.token as - ## the authentication token. For available calls and the + ## Here we're making a sample call to "validate_token()", passing + ## self.token as the authentication token. For available calls and the ## order of arguments, check out ksapi.py. The return value ## will be an httplib.HTTPResponse object with additional ## 'body' (str) and 'obj' (dict) attributes. If a status code @@ -70,7 +69,7 @@ class SampleTest(base.KeystoneTest): ## attached to the 'response' attribute of the exception, and ## the status will be on the 'status' attribute of the ## exception. Note that redirects are followed. - resp = self.ks.sample_call(self.token, 'argument 1', 'argument 2') + resp = self.ks.validate_token(self.token, 'argument 1', 'argument 2') # Verify that resp is correct util.assert_equal(resp.status, 200) diff --git a/keystone/test/system/test_request_specs.py b/keystone/test/system/test_request_specs.py index 97b1cf48..cc8a24b7 100644 --- a/keystone/test/system/test_request_specs.py +++ b/keystone/test/system/test_request_specs.py @@ -38,6 +38,18 @@ class TestContentTypes(KeystoneTestCase): r = self.service_request(headers={'Accept': 'application/json'}) self.assertTrue('application/json' in r.getheader('Content-Type')) + def test_versioned_xml_accept_header(self): + """Service responds to versioned xml Accept header""" + r = self.service_request(headers={ + 'Accept': 'application/vnd.openstack.identity-v2.0+xml'}) + self.assertTrue('application/xml' in r.getheader('Content-Type')) + + def test_versioned_json_accept_header(self): + """Service responds to versioned json Accept header""" + r = self.service_request(headers={ + 'Accept': 'application/vnd.openstack.identity-v2.0+json'}) + self.assertTrue('application/json' in r.getheader('Content-Type')) + def test_xml_extension_overrides_conflicting_header(self): """Service returns XML when Accept header conflicts with extension""" r = self.service_request(path='.xml', -- cgit