Merge "proper handling of unsupported request methods"

This commit is contained in:
Jenkins 2015-01-27 01:57:25 +00:00 committed by Gerrit Code Review
commit 51a831f2cb
3 changed files with 404 additions and 215 deletions

View File

@ -12,6 +12,7 @@
# under the License. # under the License.
import routes import routes
import six
from heat.api.openstack.v1 import actions from heat.api.openstack.v1 import actions
from heat.api.openstack.v1 import build_info from heat.api.openstack.v1 import build_info
@ -32,248 +33,368 @@ class API(wsgi.Router):
def __init__(self, conf, **local_conf): def __init__(self, conf, **local_conf):
self.conf = conf self.conf = conf
mapper = routes.Mapper() mapper = routes.Mapper()
default_resource = wsgi.Resource(wsgi.DefaultMethodController(),
wsgi.JSONRequestDeserializer())
def connect(controller, path_prefix, routes):
"""
This function connects the list of routes to the given
controller, prepending the given path_prefix. Then for each URL it
finds which request methods aren't handled and configures those
to return a 405 error. Finally, it adds a handler for the
OPTIONS method to all URLs that returns the list of allowed
methods with 204 status code.
"""
# register the routes with the mapper, while keeping track of which
# methods are defined for each URL
urls = {}
for r in routes:
url = path_prefix + r['url']
methods = r['method']
if isinstance(methods, six.string_types):
methods = [methods]
methods_str = ','.join(methods)
mapper.connect(r['name'], url, controller=controller,
action=r['action'],
conditions={'method': methods_str})
if url not in urls:
urls[url] = methods
else:
urls[url] += methods
# now register the missing methods to return 405s, and register
# a handler for OPTIONS that returns the list of allowed methods
for url, methods in urls.items():
all_methods = ['HEAD', 'GET', 'POST', 'PUT', 'PATCH', 'DELETE']
missing_methods = [m for m in all_methods if m not in methods]
allowed_methods_str = ','.join(methods)
mapper.connect(url,
controller=default_resource,
action='reject',
allowed_methods=allowed_methods_str,
conditions={'method': missing_methods})
if 'OPTIONS' not in methods:
mapper.connect(url,
controller=default_resource,
action='options',
allowed_methods=allowed_methods_str,
conditions={'method': 'OPTIONS'})
# Stacks # Stacks
stacks_resource = stacks.create_resource(conf) stacks_resource = stacks.create_resource(conf)
with mapper.submapper(controller=stacks_resource, connect(controller=stacks_resource,
path_prefix="/{tenant_id}") as stack_mapper: path_prefix='/{tenant_id}',
# Template handling routes=[
stack_mapper.connect("template_validate", # Template handling
"/validate", {
action="validate_template", 'name': 'template_validate',
conditions={'method': 'POST'}) 'url': '/validate',
stack_mapper.connect("resource_types", 'action': 'validate_template',
"/resource_types", 'method': 'POST'
action="list_resource_types", },
conditions={'method': 'GET'}) {
stack_mapper.connect("resource_schema", 'name': 'resource_types',
"/resource_types/{type_name}", 'url': '/resource_types',
action="resource_schema", 'action': 'list_resource_types',
conditions={'method': 'GET'}) 'method': 'GET'
stack_mapper.connect("generate_template", },
"/resource_types/{type_name}/template", {
action="generate_template", 'name': 'resource_schema',
conditions={'method': 'GET'}) 'url': '/resource_types/{type_name}',
'action': 'resource_schema',
'method': 'GET'
},
{
'name': 'generate_template',
'url': '/resource_types/{type_name}/template',
'action': 'generate_template',
'method': 'GET'
},
# Stack collection # Stack collection
stack_mapper.connect("stack_index", {
"/stacks", 'name': 'stack_index',
action="index", 'url': '/stacks',
conditions={'method': 'GET'}) 'action': 'index',
stack_mapper.connect("stack_create", 'method': 'GET'
"/stacks", },
action="create", {
conditions={'method': 'POST'}) 'name': 'stack_create',
stack_mapper.connect("stack_preview", 'url': '/stacks',
"/stacks/preview", 'action': 'create',
action="preview", 'method': 'POST'
conditions={'method': 'POST'}) },
stack_mapper.connect("stack_detail", {
"/stacks/detail", 'name': 'stack_preview',
action="detail", 'url': '/stacks/preview',
conditions={'method': 'GET'}) 'action': 'preview',
'method': 'POST'
},
{
'name': 'stack_detail',
'url': '/stacks/detail',
'action': 'detail',
'method': 'GET'
},
# Stack data # Stack data
stack_mapper.connect("stack_lookup", {
"/stacks/{stack_name}", 'name': 'stack_lookup',
action="lookup") 'url': '/stacks/{stack_name}',
# \x3A matches on a colon. 'action': 'lookup',
# Routes treats : specially in its regexp 'method': ['GET', 'POST', 'PUT', 'PATCH', 'DELETE']
stack_mapper.connect("stack_lookup", },
r"/stacks/{stack_name:arn\x3A.*}", # \x3A matches on a colon.
action="lookup") # Routes treats : specially in its regexp
subpaths = ['resources', 'events', 'template', 'actions'] {
path = "{path:%s}" % '|'.join(subpaths) 'name': 'stack_lookup',
stack_mapper.connect("stack_lookup_subpath", 'url': r'/stacks/{stack_name:arn\x3A.*}',
"/stacks/{stack_name}/" + path, 'action': 'lookup',
action="lookup", 'method': ['GET', 'POST', 'PUT', 'PATCH', 'DELETE']
conditions={'method': 'GET'}) },
stack_mapper.connect("stack_lookup_subpath_post", {
"/stacks/{stack_name}/" + path, 'name': 'stack_lookup_subpath',
action="lookup", 'url': '/stacks/{stack_name}/'
conditions={'method': 'POST'}) '{path:resources|events|template|actions}',
stack_mapper.connect("stack_show", 'action': 'lookup',
"/stacks/{stack_name}/{stack_id}", 'method': 'GET'
action="show", },
conditions={'method': 'GET'}) {
stack_mapper.connect("stack_template", 'name': 'stack_lookup_subpath_post',
"/stacks/{stack_name}/{stack_id}/template", 'url': '/stacks/{stack_name}/'
action="template", '{path:resources|events|template|actions}',
conditions={'method': 'GET'}) 'action': 'lookup',
'method': 'POST'
},
{
'name': 'stack_show',
'url': '/stacks/{stack_name}/{stack_id}',
'action': 'show',
'method': 'GET'
},
{
'name': 'stack_lookup',
'url': '/stacks/{stack_name}/{stack_id}/template',
'action': 'template',
'method': 'GET'
},
# Stack update/delete # Stack update/delete
stack_mapper.connect("stack_update", {
"/stacks/{stack_name}/{stack_id}", 'name': 'stack_update',
action="update", 'url': '/stacks/{stack_name}/{stack_id}',
conditions={'method': 'PUT'}) 'action': 'update',
stack_mapper.connect("stack_update_patch", 'method': 'PUT'
"/stacks/{stack_name}/{stack_id}", },
action="update_patch", {
conditions={'method': 'PATCH'}) 'name': 'stack_update_patch',
stack_mapper.connect("stack_delete", 'url': '/stacks/{stack_name}/{stack_id}',
"/stacks/{stack_name}/{stack_id}", 'action': 'update_patch',
action="delete", 'method': 'PATCH'
conditions={'method': 'DELETE'}) },
{
'name': 'stack_delete',
'url': '/stacks/{stack_name}/{stack_id}',
'action': 'delete',
'method': 'DELETE'
},
# Stack abandon # Stack abandon
stack_mapper.connect("stack_abandon", {
"/stacks/{stack_name}/{stack_id}/abandon", 'name': 'stack_abandon',
action="abandon", 'url': '/stacks/{stack_name}/{stack_id}/abandon',
conditions={'method': 'DELETE'}) 'action': 'abandon',
'method': 'DELETE'
stack_mapper.connect("stack_snapshot", },
"/stacks/{stack_name}/{stack_id}/snapshots", {
action="snapshot", 'name': 'stack_snapshot',
conditions={'method': 'POST'}) 'url': '/stacks/{stack_name}/{stack_id}/snapshots',
'action': 'snapshot',
stack_mapper.connect("stack_snapshot_show", 'method': 'POST'
"/stacks/{stack_name}/{stack_id}/snapshots/" },
"{snapshot_id}", {
action="show_snapshot", 'name': 'stack_snapshot_show',
conditions={'method': 'GET'}) 'url': '/stacks/{stack_name}/{stack_id}/snapshots/'
'{snapshot_id}',
stack_mapper.connect("stack_snapshot_delete", 'action': 'show_snapshot',
"/stacks/{stack_name}/{stack_id}/snapshots/" 'method': 'GET'
"{snapshot_id}", },
action="delete_snapshot", {
conditions={'method': 'DELETE'}) 'name': 'stack_snapshot_delete',
'url': '/stacks/{stack_name}/{stack_id}/snapshots/'
stack_mapper.connect("stack_list_snapshots", '{snapshot_id}',
"/stacks/{stack_name}/{stack_id}/snapshots", 'action': 'delete_snapshot',
action="list_snapshots", 'method': 'DELETE'
conditions={'method': 'GET'}) },
{
stack_mapper.connect("stack_snapshot_restore", 'name': 'stack_list_snapshots',
"/stacks/{stack_name}/{stack_id}/snapshots/" 'url': '/stacks/{stack_name}/{stack_id}/snapshots',
"{snapshot_id}/restore", 'action': 'list_snapshots',
action="restore_snapshot", 'method': 'GET'
conditions={'method': 'POST'}) },
{
'name': 'stack_snapshot_restore',
'url': '/stacks/{stack_name}/{stack_id}/snapshots/'
'{snapshot_id}/restore',
'action': 'restore_snapshot',
'method': 'POST'
}
])
# Resources # Resources
resources_resource = resources.create_resource(conf) resources_resource = resources.create_resource(conf)
stack_path = "/{tenant_id}/stacks/{stack_name}/{stack_id}" stack_path = '/{tenant_id}/stacks/{stack_name}/{stack_id}'
with mapper.submapper(controller=resources_resource, connect(controller=resources_resource, path_prefix=stack_path,
path_prefix=stack_path) as res_mapper: routes=[
# Resource collection
{
'name': 'resource_index',
'url': '/resources',
'action': 'index',
'method': 'GET'
},
# Resource collection # Resource data
res_mapper.connect("resource_index", {
"/resources", 'name': 'resource_show',
action="index", 'url': '/resources/{resource_name}',
conditions={'method': 'GET'}) 'action': 'show',
'method': 'GET'
# Resource data },
res_mapper.connect("resource_show", {
"/resources/{resource_name}", 'name': 'resource_metadata_show',
action="show", 'url': '/resources/{resource_name}/metadata',
conditions={'method': 'GET'}) 'action': 'metadata',
res_mapper.connect("resource_metadata_show", 'method': 'GET'
"/resources/{resource_name}/metadata", },
action="metadata", {
conditions={'method': 'GET'}) 'name': 'resource_signal',
res_mapper.connect("resource_signal", 'url': '/resources/{resource_name}/signal',
"/resources/{resource_name}/signal", 'action': 'signal',
action="signal", 'method': 'POST'
conditions={'method': 'POST'}) }
])
# Events # Events
events_resource = events.create_resource(conf) events_resource = events.create_resource(conf)
with mapper.submapper(controller=events_resource, connect(controller=events_resource, path_prefix=stack_path,
path_prefix=stack_path) as ev_mapper: routes=[
# Stack event collection
{
'name': 'event_index_stack',
'url': '/events',
'action': 'index',
'method': 'GET'
},
# Stack event collection # Resource event collection
ev_mapper.connect("event_index_stack", {
"/events", 'name': 'event_index_resource',
action="index", 'url': '/resources/{resource_name}/events',
conditions={'method': 'GET'}) 'action': 'index',
# Resource event collection 'method': 'GET'
ev_mapper.connect("event_index_resource", },
"/resources/{resource_name}/events",
action="index",
conditions={'method': 'GET'})
# Event data # Event data
ev_mapper.connect("event_show", {
"/resources/{resource_name}/events/{event_id}", 'name': 'event_show',
action="show", 'url': '/resources/{resource_name}/events/{event_id}',
conditions={'method': 'GET'}) 'action': 'show',
'method': 'GET'
}
])
# Actions # Actions
actions_resource = actions.create_resource(conf) actions_resource = actions.create_resource(conf)
with mapper.submapper(controller=actions_resource, connect(controller=actions_resource, path_prefix=stack_path,
path_prefix=stack_path) as ac_mapper: routes=[
{
ac_mapper.connect("action_stack", 'name': 'action_stack',
"/actions", 'url': '/actions',
action="action", 'action': 'action',
conditions={'method': 'POST'}) 'method': 'POST'
}
])
# Info # Info
info_resource = build_info.create_resource(conf) info_resource = build_info.create_resource(conf)
with mapper.submapper(controller=info_resource, connect(controller=info_resource, path_prefix='/{tenant_id}',
path_prefix="/{tenant_id}") as info_mapper: routes=[
{
info_mapper.connect('build_info', 'name': 'build_info',
'/build_info', 'url': '/build_info',
action='build_info', 'action': 'build_info',
conditions={'method': 'GET'}) 'method': 'GET'
}
])
# Software configs # Software configs
software_config_resource = software_configs.create_resource(conf) software_config_resource = software_configs.create_resource(conf)
with mapper.submapper( connect(controller=software_config_resource,
controller=software_config_resource, path_prefix='/{tenant_id}/software_configs',
path_prefix="/{tenant_id}/software_configs" routes=[
) as sc_mapper: {
'name': 'software_config_create',
sc_mapper.connect("software_config_create", 'url': '',
"", 'action': 'create',
action="create", 'method': 'POST'
conditions={'method': 'POST'}) },
{
sc_mapper.connect("software_config_show", 'name': 'software_config_show',
"/{config_id}", 'url': '/{config_id}',
action="show", 'action': 'show',
conditions={'method': 'GET'}) 'method': 'GET'
},
sc_mapper.connect("software_config_delete", {
"/{config_id}", 'name': 'software_config_delete',
action="delete", 'url': '/{config_id}',
conditions={'method': 'DELETE'}) 'action': 'delete',
'method': 'DELETE'
}
])
# Software deployments # Software deployments
sd_resource = software_deployments.create_resource(conf) sd_resource = software_deployments.create_resource(conf)
with mapper.submapper( connect(controller=sd_resource,
controller=sd_resource, path_prefix='/{tenant_id}/software_deployments',
path_prefix='/{tenant_id}/software_deployments' routes=[
) as sa_mapper: {
'name': 'software_deployment_index',
sa_mapper.connect("software_deployment_index", 'url': '',
"", 'action': 'index',
action="index", 'method': 'GET'
conditions={'method': 'GET'}) },
{
sa_mapper.connect("software_deployment_metadata", 'name': 'software_deployment_metadata',
"/metadata/{server_id}", 'url': '/metadata/{server_id}',
action="metadata", 'action': 'metadata',
conditions={'method': 'GET'}) 'method': 'GET'
},
sa_mapper.connect("software_deployment_create", {
"", 'name': 'software_deployment_create',
action="create", 'url': '',
conditions={'method': 'POST'}) 'action': 'create',
'method': 'POST'
sa_mapper.connect("software_deployment_show", },
"/{deployment_id}", {
action="show", 'name': 'software_deployment_show',
conditions={'method': 'GET'}) 'url': '/{deployment_id}',
'action': 'show',
sa_mapper.connect("software_deployment_update", 'method': 'GET'
"/{deployment_id}", },
action="update", {
conditions={'method': 'PUT'}) 'name': 'software_deployment_update',
'url': '/{deployment_id}',
sa_mapper.connect("software_deployment_delete", 'action': 'update',
"/{deployment_id}", 'method': 'PUT'
action="delete", },
conditions={'method': 'DELETE'}) {
'name': 'software_deployment_delete',
'url': '/{deployment_id}',
'action': 'delete',
'method': 'DELETE'
}
])
# now that all the routes are defined, add a handler for
super(API, self).__init__(mapper) super(API, self).__init__(mapper)

View File

@ -440,6 +440,28 @@ def debug_filter(app, conf, **local_conf):
return Debug(app) return Debug(app)
class DefaultMethodController(object):
"""
This controller handles the OPTIONS request method and any of the
HTTP methods that are not explicitly implemented by the application.
"""
def options(self, req, allowed_methods, *args, **kwargs):
"""
Return a response that includes the 'Allow' header listing the methods
that are implemented. A 204 status code is used for this response.
"""
raise webob.exc.HTTPNoContent(headers=[('Allow', allowed_methods)])
def reject(self, req, allowed_methods, *args, **kwargs):
"""
Return a 405 method not allowed error. As a convenience, the 'Allow'
header with the list of implemented methods is included in the
response as well.
"""
raise webob.exc.HTTPMethodNotAllowed(
headers=[('Allow', allowed_methods)])
class Router(object): class Router(object):
""" """
WSGI middleware that maps incoming requests to WSGI apps. WSGI middleware that maps incoming requests to WSGI apps.

View File

@ -3617,6 +3617,52 @@ class RoutesTest(common.HeatTestCase):
{'tenant_id': 'fake_tenant'} {'tenant_id': 'fake_tenant'}
) )
def test_405(self):
self.assertRoute(
self.m,
'/fake_tenant/validate',
'GET',
'reject',
'DefaultMethodController',
{'tenant_id': 'fake_tenant', 'allowed_methods': 'POST'}
)
self.assertRoute(
self.m,
'/fake_tenant/stacks',
'PUT',
'reject',
'DefaultMethodController',
{'tenant_id': 'fake_tenant', 'allowed_methods': 'GET,POST'}
)
self.assertRoute(
self.m,
'/fake_tenant/stacks/fake_stack/stack_id',
'POST',
'reject',
'DefaultMethodController',
{'tenant_id': 'fake_tenant', 'stack_name': 'fake_stack',
'stack_id': 'stack_id', 'allowed_methods': 'GET,PUT,PATCH,DELETE'}
)
def test_options(self):
self.assertRoute(
self.m,
'/fake_tenant/validate',
'OPTIONS',
'options',
'DefaultMethodController',
{'tenant_id': 'fake_tenant', 'allowed_methods': 'POST'}
)
self.assertRoute(
self.m,
'/fake_tenant/stacks/fake_stack/stack_id',
'OPTIONS',
'options',
'DefaultMethodController',
{'tenant_id': 'fake_tenant', 'stack_name': 'fake_stack',
'stack_id': 'stack_id', 'allowed_methods': 'GET,PUT,PATCH,DELETE'}
)
@mock.patch.object(policy.Enforcer, 'enforce') @mock.patch.object(policy.Enforcer, 'enforce')
class ActionControllerTest(ControllerTest, common.HeatTestCase): class ActionControllerTest(ControllerTest, common.HeatTestCase):