Files
manila/manila/api/openstack/__init__.py
T
Goutham Pacha Ravi 263d5438f0 Advertise v2 API routes without project_id
Manila APIs have had the requirement to include
project_id in the URLs since the very beginning.
This comes from an old assumption that our APIs
would be differentiated per-tenant on the cloud,
and we would allow different kinds of API endpoints
(public, admin, internal, etc). While it is possible
to set up different endpoints against the API
service, the same and complete API is exposed at
each of these endpoints.

We don't _need_ the project_id information that
we receive in the URL for any of our APIs to
function. We rather authorize tenants by gathering
information from the Identity service (Keystone)
and wrapping that into a RequestContext object
that we then rely on to ensure namespace isolation.

Removing the requirement for "project_id" simplifies
our API endpoint structure in the service catalog
as well as provides a way for system scoped users
to interact with manila without having to declare
their project.

In order to make project_id optional in urls, the
possible values of project_id have to be constrained.
This change introduces a new configuration option
so deployers may control that. This configuration
option defaults to accepting UUIDs with and without
dashes.

Since manila can be used in standalone deployments
without the need for Keystone, this change introduces
a noauth middleware that can work without project_id
in the URL paths.

The API version has been incremented to signal this
change to end users. When 2.60 is available, deployments
may drop "project_id" in the service catalog endpoint
for Manila and end users applications can stop needing
it as well (if they don't already rely on the service
catalog for this data).

APIImpact
Implements: bp remove-project-id-from-urls
Change-Id: I5127e150e8a71e621890f30dba6720b3932cf583
Signed-off-by: Goutham Pacha Ravi <gouthampravi@gmail.com>
2021-02-04 23:20:19 -08:00

170 lines
6.6 KiB
Python

# Copyright (c) 2013 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.
"""
WSGI middleware for OpenStack API controllers.
"""
from oslo_config import cfg
from oslo_log import log
from oslo_service import wsgi as base_wsgi
import routes
from manila.api.openstack import wsgi
from manila.i18n import _
openstack_api_opts = [
cfg.StrOpt('project_id_regex',
default=r"[0-9a-f\-]+",
help=r'The validation regex for project_ids used in urls. '
r'This defaults to [0-9a-f\\-]+ if not set, '
r'which matches normal uuids created by keystone.'),
]
CONF = cfg.CONF
CONF.register_opts(openstack_api_opts)
LOG = log.getLogger(__name__)
class APIMapper(routes.Mapper):
def routematch(self, url=None, environ=None):
if url == "":
result = self._match("", environ)
return result[0], result[1]
return routes.Mapper.routematch(self, url, environ)
def connect(self, *args, **kwargs):
# NOTE(inhye): Default the format part of a route to only accept json
# and xml so it doesn't eat all characters after a '.'
# in the url.
kwargs.setdefault('requirements', {})
if not kwargs['requirements'].get('format'):
kwargs['requirements']['format'] = 'json|xml'
return routes.Mapper.connect(self, *args, **kwargs)
class ProjectMapper(APIMapper):
def resource(self, member_name, collection_name, **kwargs):
"""Base resource path handler
This method is compatible with resource paths that include a
project_id and those that don't. Including project_id in the URLs
was a legacy API requirement; and making API requests against
such endpoints won't work for users that don't belong to a
particular project.
"""
# NOTE(gouthamr): project_id parameter is only valid if its hex
# or hex + dashes (note, integers are a subset of this). This
# is required to handle our overlapping routes issues.
project_id_regex = CONF.project_id_regex
project_id_token = '{project_id:%s}' % project_id_regex
if 'parent_resource' not in kwargs:
kwargs['path_prefix'] = '%s/' % project_id_token
else:
parent_resource = kwargs['parent_resource']
p_collection = parent_resource['collection_name']
p_member = parent_resource['member_name']
kwargs['path_prefix'] = '%s/%s/:%s_id' % (project_id_token,
p_collection,
p_member)
routes.Mapper.resource(self,
member_name,
collection_name,
**kwargs)
# NOTE(gouthamr): while we are in transition mode to not needing
# project_ids in URLs, we'll need additional routes without project_id.
if 'parent_resource' not in kwargs:
del kwargs['path_prefix']
else:
parent_resource = kwargs['parent_resource']
p_collection = parent_resource['collection_name']
p_member = parent_resource['member_name']
kwargs['path_prefix'] = '%s/:%s_id' % (p_collection,
p_member)
routes.Mapper.resource(self,
member_name,
collection_name,
**kwargs)
class APIRouter(base_wsgi.Router):
"""Routes requests on the API to the appropriate controller and method."""
ExtensionManager = None # override in subclasses
@classmethod
def factory(cls, global_config, **local_config):
"""Simple paste factory, :class:`manila.wsgi.Router` doesn't have."""
return cls()
def __init__(self, ext_mgr=None):
if ext_mgr is None:
if self.ExtensionManager:
# pylint: disable=not-callable
ext_mgr = self.ExtensionManager()
else:
raise Exception(_("Must specify an ExtensionManager class"))
mapper = ProjectMapper()
self.resources = {}
self._setup_routes(mapper)
self._setup_ext_routes(mapper, ext_mgr)
self._setup_extensions(ext_mgr)
super(APIRouter, self).__init__(mapper)
def _setup_ext_routes(self, mapper, ext_mgr):
for resource in ext_mgr.get_resources():
LOG.debug('Extended resource: %s',
resource.collection)
wsgi_resource = wsgi.Resource(resource.controller)
self.resources[resource.collection] = wsgi_resource
kargs = dict(
controller=wsgi_resource,
collection=resource.collection_actions,
member=resource.member_actions)
if resource.parent:
kargs['parent_resource'] = resource.parent
mapper.resource(resource.collection, resource.collection, **kargs)
if resource.custom_routes_fn:
resource.custom_routes_fn(mapper, wsgi_resource)
def _setup_extensions(self, ext_mgr):
for extension in ext_mgr.get_controller_extensions():
ext_name = extension.extension.name
collection = extension.collection
controller = extension.controller
if collection not in self.resources:
LOG.warning('Extension %(ext_name)s: Cannot extend '
'resource %(collection)s: No such resource',
{'ext_name': ext_name, 'collection': collection})
continue
LOG.debug('Extension %(ext_name)s extending resource: '
'%(collection)s',
{'ext_name': ext_name, 'collection': collection})
resource = self.resources[collection]
resource.register_actions(controller)
resource.register_extensions(controller)
def _setup_routes(self, mapper):
raise NotImplementedError