Merge "Remove pre-flask legacy code"
This commit is contained in:
commit
2af3707ace
@ -16,9 +16,9 @@ from oslo_serialization import jsonutils
|
|||||||
from six.moves import http_client
|
from six.moves import http_client
|
||||||
|
|
||||||
from keystone.common import json_home
|
from keystone.common import json_home
|
||||||
from keystone.common import wsgi
|
|
||||||
import keystone.conf
|
import keystone.conf
|
||||||
from keystone import exception
|
from keystone import exception
|
||||||
|
from keystone.server import flask as ks_flask
|
||||||
|
|
||||||
|
|
||||||
CONF = keystone.conf.CONF
|
CONF = keystone.conf.CONF
|
||||||
@ -87,11 +87,7 @@ def get_versions():
|
|||||||
return flask.Response(response=jsonutils.dumps(v3_json_home),
|
return flask.Response(response=jsonutils.dumps(v3_json_home),
|
||||||
mimetype=MimeTypes.JSON_HOME)
|
mimetype=MimeTypes.JSON_HOME)
|
||||||
else:
|
else:
|
||||||
# NOTE(morgan): wsgi.Application.base_url will eventually need to
|
identity_url = '%s/' % ks_flask.base_url()
|
||||||
# be moved to a better "common" location. For now, we'll just lean
|
|
||||||
# on it for the sake of leaning on common code where possible.
|
|
||||||
identity_url = '%s/v3/' % wsgi.Application.base_url(
|
|
||||||
context={'environment': request.environ})
|
|
||||||
versions = _get_versions_list(identity_url)
|
versions = _get_versions_list(identity_url)
|
||||||
return flask.Response(
|
return flask.Response(
|
||||||
response=jsonutils.dumps(
|
response=jsonutils.dumps(
|
||||||
@ -113,11 +109,7 @@ def get_version_v3():
|
|||||||
return flask.Response(response=jsonutils.dumps(content),
|
return flask.Response(response=jsonutils.dumps(content),
|
||||||
mimetype=MimeTypes.JSON_HOME)
|
mimetype=MimeTypes.JSON_HOME)
|
||||||
else:
|
else:
|
||||||
# NOTE(morgan): wsgi.Application.base_url will eventually need to
|
identity_url = '%s/' % ks_flask.base_url()
|
||||||
# be moved to a better "common" location. For now, we'll just lean
|
|
||||||
# on it for the sake of leaning on common code where possible.
|
|
||||||
identity_url = '%s/v3/' % wsgi.Application.base_url(
|
|
||||||
context={'environment': request.environ})
|
|
||||||
versions = _get_versions_list(identity_url)
|
versions = _get_versions_list(identity_url)
|
||||||
return flask.Response(
|
return flask.Response(
|
||||||
response=jsonutils.dumps({'version': versions['v3']}),
|
response=jsonutils.dumps({'version': versions['v3']}),
|
||||||
|
@ -15,163 +15,15 @@
|
|||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
from oslo_log import log
|
|
||||||
from oslo_utils import strutils
|
|
||||||
|
|
||||||
from keystone.common.policies import base as pol_base
|
|
||||||
from keystone.common import utils
|
|
||||||
from keystone import conf
|
|
||||||
from keystone import exception
|
|
||||||
|
|
||||||
|
# A couple common constants for Auth data
|
||||||
|
|
||||||
# Header used to transmit the auth token
|
# Header used to transmit the auth token
|
||||||
AUTH_TOKEN_HEADER = 'X-Auth-Token'
|
AUTH_TOKEN_HEADER = 'X-Auth-Token'
|
||||||
|
|
||||||
|
|
||||||
# Header used to transmit the subject token
|
# Header used to transmit the subject token
|
||||||
SUBJECT_TOKEN_HEADER = 'X-Subject-Token'
|
SUBJECT_TOKEN_HEADER = 'X-Subject-Token'
|
||||||
|
|
||||||
|
|
||||||
CONF = conf.CONF
|
|
||||||
|
|
||||||
# Environment variable used to convey the Keystone auth context,
|
# Environment variable used to convey the Keystone auth context,
|
||||||
# the user credential used for policy enforcement.
|
# the user credential used for policy enforcement.
|
||||||
AUTH_CONTEXT_ENV = 'KEYSTONE_AUTH_CONTEXT'
|
AUTH_CONTEXT_ENV = 'KEYSTONE_AUTH_CONTEXT'
|
||||||
|
|
||||||
LOG = log.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
def assert_admin(app, request):
|
|
||||||
"""Ensure the user is an admin.
|
|
||||||
|
|
||||||
:raises keystone.exception.Unauthorized: if a token could not be
|
|
||||||
found/authorized, a user is invalid, or a tenant is
|
|
||||||
invalid/not scoped.
|
|
||||||
:raises keystone.exception.Forbidden: if the user is not an admin and
|
|
||||||
does not have the admin role
|
|
||||||
|
|
||||||
"""
|
|
||||||
check_policy(app, request, 'admin_required', input_attr={})
|
|
||||||
|
|
||||||
|
|
||||||
def _build_policy_check_credentials(action, context, kwargs):
|
|
||||||
kwargs_str = ', '.join(['%s=%s' % (k, kwargs[k]) for k in kwargs])
|
|
||||||
kwargs_str = strutils.mask_password(kwargs_str)
|
|
||||||
msg = 'RBAC: Authorizing %(action)s(%(kwargs)s)'
|
|
||||||
LOG.debug(msg, {'action': action, 'kwargs': kwargs_str})
|
|
||||||
|
|
||||||
return context['environment'].get(AUTH_CONTEXT_ENV, {})
|
|
||||||
|
|
||||||
|
|
||||||
def _handle_member_from_driver(self, policy_dict, **kwargs):
|
|
||||||
# Check to see if we need to include the target entity in our
|
|
||||||
# policy checks. We deduce this by seeing if the class has
|
|
||||||
# specified a get_member() method and that kwargs contains the
|
|
||||||
# appropriate entity id.
|
|
||||||
if (hasattr(self, 'get_member_from_driver') and
|
|
||||||
self.get_member_from_driver is not None):
|
|
||||||
key = '%s_id' % self.member_name
|
|
||||||
if key in kwargs:
|
|
||||||
ref = self.get_member_from_driver(kwargs[key])
|
|
||||||
policy_dict['target'] = {self.member_name: ref}
|
|
||||||
|
|
||||||
|
|
||||||
def token_validation_window(request):
|
|
||||||
# NOTE(jamielennox): it's dumb that i have to put this here. We should
|
|
||||||
# only validate subject token in one place.
|
|
||||||
|
|
||||||
allow_expired = request.params.get('allow_expired')
|
|
||||||
allow_expired = strutils.bool_from_string(allow_expired, default=False)
|
|
||||||
return CONF.token.allow_expired_window if allow_expired else 0
|
|
||||||
|
|
||||||
|
|
||||||
def _handle_subject_token_id(self, request, policy_dict):
|
|
||||||
if request.subject_token is not None:
|
|
||||||
window_seconds = token_validation_window(request)
|
|
||||||
|
|
||||||
token = self.token_provider_api.validate_token(
|
|
||||||
request.subject_token, window_seconds=window_seconds
|
|
||||||
)
|
|
||||||
policy_dict.setdefault('target', {})
|
|
||||||
policy_dict['target'].setdefault(self.member_name, {})
|
|
||||||
policy_dict['target'][self.member_name]['user_id'] = (token.user_id)
|
|
||||||
try:
|
|
||||||
user_domain_id = token.user_domain['id']
|
|
||||||
except exception.UnexpectedError:
|
|
||||||
user_domain_id = None
|
|
||||||
if user_domain_id:
|
|
||||||
policy_dict['target'][self.member_name].setdefault(
|
|
||||||
'user', {})
|
|
||||||
policy_dict['target'][self.member_name][
|
|
||||||
'user'].setdefault('domain', {})
|
|
||||||
policy_dict['target'][self.member_name]['user'][
|
|
||||||
'domain']['id'] = (
|
|
||||||
user_domain_id)
|
|
||||||
|
|
||||||
|
|
||||||
def check_protection(controller, request, prep_info, target_attr=None,
|
|
||||||
*args, **kwargs):
|
|
||||||
"""Provide call protection for complex target attributes.
|
|
||||||
|
|
||||||
As well as including the standard parameters from the original API
|
|
||||||
call (which is passed in prep_info), this call will add in any
|
|
||||||
additional entities or attributes (passed in target_attr), so that
|
|
||||||
they can be referenced by policy rules.
|
|
||||||
|
|
||||||
"""
|
|
||||||
check_policy(controller, request,
|
|
||||||
pol_base.IDENTITY % prep_info['f_name'],
|
|
||||||
prep_info.get('filter_attr'),
|
|
||||||
prep_info.get('input_attr'),
|
|
||||||
target_attr,
|
|
||||||
*args, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
def check_policy(controller, request, action,
|
|
||||||
filter_attr=None, input_attr=None, target_attr=None,
|
|
||||||
*args, **kwargs):
|
|
||||||
# Makes the arguments from check protection explicit.
|
|
||||||
request.assert_authenticated()
|
|
||||||
if request.context.is_admin:
|
|
||||||
LOG.warning('RBAC: Bypassing authorization')
|
|
||||||
return
|
|
||||||
|
|
||||||
# TODO(henry-nash) need to log the target attributes as well
|
|
||||||
creds = _build_policy_check_credentials(
|
|
||||||
action, request.context_dict, input_attr)
|
|
||||||
# Build the dict the policy engine will check against from both the
|
|
||||||
# parameters passed into the call we are protecting plus the target
|
|
||||||
# attributes provided.
|
|
||||||
policy_dict = {}
|
|
||||||
_handle_member_from_driver(controller, policy_dict, **kwargs)
|
|
||||||
_handle_subject_token_id(controller, request, policy_dict)
|
|
||||||
|
|
||||||
if target_attr:
|
|
||||||
policy_dict = {'target': target_attr}
|
|
||||||
if input_attr:
|
|
||||||
policy_dict.update(input_attr)
|
|
||||||
if filter_attr:
|
|
||||||
policy_dict.update(filter_attr)
|
|
||||||
|
|
||||||
for key in kwargs:
|
|
||||||
policy_dict[key] = kwargs[key]
|
|
||||||
controller.policy_api.enforce(creds,
|
|
||||||
action,
|
|
||||||
utils.flatten_dict(policy_dict))
|
|
||||||
LOG.debug('RBAC: Authorization granted')
|
|
||||||
|
|
||||||
|
|
||||||
def get_token_ref(context):
|
|
||||||
"""Retrieve TokenModel object from the auth context and returns it.
|
|
||||||
|
|
||||||
:param dict context: The request context.
|
|
||||||
:raises keystone.exception.Unauthorized: If auth context cannot be found.
|
|
||||||
:returns: The TokenModel object.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
# Retrieve the auth context that was prepared by AuthContextMiddleware.
|
|
||||||
auth_context = (context['environment'][AUTH_CONTEXT_ENV])
|
|
||||||
return auth_context['token']
|
|
||||||
except KeyError:
|
|
||||||
LOG.warning("Couldn't find the auth context.")
|
|
||||||
raise exception.Unauthorized()
|
|
||||||
|
@ -1,623 +0,0 @@
|
|||||||
# Copyright 2013 OpenStack Foundation
|
|
||||||
#
|
|
||||||
# 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 functools
|
|
||||||
import uuid
|
|
||||||
|
|
||||||
from oslo_log import log
|
|
||||||
from oslo_log import versionutils
|
|
||||||
import six
|
|
||||||
|
|
||||||
from keystone.common import authorization
|
|
||||||
from keystone.common import driver_hints
|
|
||||||
from keystone.common import provider_api
|
|
||||||
from keystone.common import utils
|
|
||||||
from keystone.common import wsgi
|
|
||||||
import keystone.conf
|
|
||||||
from keystone import exception
|
|
||||||
from keystone.i18n import _
|
|
||||||
|
|
||||||
|
|
||||||
LOG = log.getLogger(__name__)
|
|
||||||
CONF = keystone.conf.CONF
|
|
||||||
PROVIDERS = provider_api.ProviderAPIs
|
|
||||||
|
|
||||||
|
|
||||||
def protected(callback=None):
|
|
||||||
"""Wrap API calls with role based access controls (RBAC).
|
|
||||||
|
|
||||||
This handles both the protection of the API parameters as well as any
|
|
||||||
target entities for single-entity API calls.
|
|
||||||
|
|
||||||
More complex API calls (for example that deal with several different
|
|
||||||
entities) should pass in a callback function, that will be subsequently
|
|
||||||
called to check protection for these multiple entities. This callback
|
|
||||||
function should gather the appropriate entities needed and then call
|
|
||||||
check_protection() in the V3Controller class.
|
|
||||||
|
|
||||||
"""
|
|
||||||
def wrapper(f):
|
|
||||||
@functools.wraps(f)
|
|
||||||
def inner(self, request, *args, **kwargs):
|
|
||||||
check_function = authorization.check_protection
|
|
||||||
if callback is not None:
|
|
||||||
check_function = callback
|
|
||||||
|
|
||||||
protected_wrapper(
|
|
||||||
self, f, check_function, request, None, *args, **kwargs)
|
|
||||||
return f(self, request, *args, **kwargs)
|
|
||||||
return inner
|
|
||||||
return wrapper
|
|
||||||
|
|
||||||
|
|
||||||
def filterprotected(*filters, **callback):
|
|
||||||
"""Wrap API list calls with role based access controls (RBAC).
|
|
||||||
|
|
||||||
This handles both the protection of the API parameters as well as any
|
|
||||||
filters supplied.
|
|
||||||
|
|
||||||
More complex API list calls (for example that need to examine the contents
|
|
||||||
of an entity referenced by one of the filters) should pass in a callback
|
|
||||||
function, that will be subsequently called to check protection for these
|
|
||||||
multiple entities. This callback function should gather the appropriate
|
|
||||||
entities needed and then call check_protection() in the V3Controller class.
|
|
||||||
|
|
||||||
"""
|
|
||||||
def _handle_filters(filters, request):
|
|
||||||
target = dict()
|
|
||||||
if filters:
|
|
||||||
for item in filters:
|
|
||||||
if item in request.params:
|
|
||||||
target[item] = request.params[item]
|
|
||||||
|
|
||||||
LOG.debug('RBAC: Adding query filter params (%s)', (
|
|
||||||
', '.join(['%s=%s' % (item, target[item])
|
|
||||||
for item in target])))
|
|
||||||
return target
|
|
||||||
|
|
||||||
def _filterprotected(f):
|
|
||||||
@functools.wraps(f)
|
|
||||||
def wrapper(self, request, **kwargs):
|
|
||||||
filter_attr = _handle_filters(filters, request)
|
|
||||||
check_function = authorization.check_protection
|
|
||||||
if 'callback' in callback and callback['callback'] is not None:
|
|
||||||
# A callback has been specified to load additional target
|
|
||||||
# data, so pass it the formal url params as well as the
|
|
||||||
# list of filters, so it can augment these and then call
|
|
||||||
# the check_protection() method.
|
|
||||||
check_function = callback['callback']
|
|
||||||
|
|
||||||
protected_wrapper(
|
|
||||||
self, f, check_function, request, filter_attr, **kwargs)
|
|
||||||
return f(self, request, filters, **kwargs)
|
|
||||||
return wrapper
|
|
||||||
return _filterprotected
|
|
||||||
|
|
||||||
|
|
||||||
# Unified calls for the decorators above.
|
|
||||||
# TODO(ayoung): Continue the refactoring. Always call check_protection
|
|
||||||
# explicitly, by removing the calls to check protection from the callbacks.
|
|
||||||
# Instead, have a call to the callbacks inserted prior to the call to
|
|
||||||
# `check_protection`.
|
|
||||||
def protected_wrapper(self, f, check_function, request, filter_attr,
|
|
||||||
*args, **kwargs):
|
|
||||||
request.assert_authenticated()
|
|
||||||
if request.context.is_admin:
|
|
||||||
LOG.warning('RBAC: Bypassing authorization')
|
|
||||||
return
|
|
||||||
prep_info = {'f_name': f.__name__,
|
|
||||||
'input_attr': kwargs}
|
|
||||||
if (filter_attr):
|
|
||||||
prep_info['filter_attr'] = filter_attr
|
|
||||||
check_function(self, request, prep_info, *args, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
# FIXME(lbragstad): Find a better home for this... I put there here since it's
|
|
||||||
# needed across a couple different controller (keystone/auth/controllers.py and
|
|
||||||
# keystone/contrib/ec2/controllers.py both need it). This is technically an
|
|
||||||
# opinion of how a token should look according to the V3 API contract. My
|
|
||||||
# thought was to try and work this into a view of a token associated to the V3
|
|
||||||
# controller logic somewhere.
|
|
||||||
def render_token_response_from_model(token, include_catalog=True):
|
|
||||||
token_reference = {
|
|
||||||
'token': {
|
|
||||||
'methods': token.methods,
|
|
||||||
'user': {
|
|
||||||
'domain': {
|
|
||||||
'id': token.user_domain['id'],
|
|
||||||
'name': token.user_domain['name']
|
|
||||||
},
|
|
||||||
'id': token.user_id,
|
|
||||||
'name': token.user['name'],
|
|
||||||
'password_expires_at': token.user[
|
|
||||||
'password_expires_at'
|
|
||||||
]
|
|
||||||
},
|
|
||||||
'audit_ids': token.audit_ids,
|
|
||||||
'expires_at': token.expires_at,
|
|
||||||
'issued_at': token.issued_at,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if token.system_scoped:
|
|
||||||
token_reference['token']['roles'] = token.roles
|
|
||||||
token_reference['token']['system'] = {'all': True}
|
|
||||||
elif token.domain_scoped:
|
|
||||||
token_reference['token']['domain'] = {
|
|
||||||
'id': token.domain['id'],
|
|
||||||
'name': token.domain['name']
|
|
||||||
}
|
|
||||||
token_reference['token']['roles'] = token.roles
|
|
||||||
elif token.trust_scoped:
|
|
||||||
token_reference['token']['OS-TRUST:trust'] = {
|
|
||||||
'id': token.trust_id,
|
|
||||||
'trustor_user': {'id': token.trustor['id']},
|
|
||||||
'trustee_user': {'id': token.trustee['id']},
|
|
||||||
'impersonation': token.trust['impersonation']
|
|
||||||
}
|
|
||||||
token_reference['token']['project'] = {
|
|
||||||
'domain': {
|
|
||||||
'id': token.project_domain['id'],
|
|
||||||
'name': token.project_domain['name']
|
|
||||||
},
|
|
||||||
'id': token.trust_project['id'],
|
|
||||||
'name': token.trust_project['name']
|
|
||||||
}
|
|
||||||
if token.trust.get('impersonation'):
|
|
||||||
trustor_domain = PROVIDERS.resource_api.get_domain(
|
|
||||||
token.trustor['domain_id']
|
|
||||||
)
|
|
||||||
token_reference['token']['user'] = {
|
|
||||||
'domain': {
|
|
||||||
'id': trustor_domain['id'],
|
|
||||||
'name': trustor_domain['name']
|
|
||||||
},
|
|
||||||
'id': token.trustor['id'],
|
|
||||||
'name': token.trustor['name'],
|
|
||||||
'password_expires_at': token.trustor[
|
|
||||||
'password_expires_at'
|
|
||||||
]
|
|
||||||
}
|
|
||||||
token_reference['token']['roles'] = token.roles
|
|
||||||
elif token.project_scoped:
|
|
||||||
token_reference['token']['project'] = {
|
|
||||||
'domain': {
|
|
||||||
'id': token.project_domain['id'],
|
|
||||||
'name': token.project_domain['name']
|
|
||||||
},
|
|
||||||
'id': token.project['id'],
|
|
||||||
'name': token.project['name']
|
|
||||||
}
|
|
||||||
token_reference['token']['is_domain'] = token.project.get(
|
|
||||||
'is_domain', False
|
|
||||||
)
|
|
||||||
token_reference['token']['roles'] = token.roles
|
|
||||||
ap_name = CONF.resource.admin_project_name
|
|
||||||
ap_domain_name = CONF.resource.admin_project_domain_name
|
|
||||||
if ap_name and ap_domain_name:
|
|
||||||
is_ap = (
|
|
||||||
token.project['name'] == ap_name and
|
|
||||||
ap_domain_name == token.project_domain['name']
|
|
||||||
)
|
|
||||||
token_reference['token']['is_admin_project'] = is_ap
|
|
||||||
if include_catalog and not token.unscoped:
|
|
||||||
user_id = token.user_id
|
|
||||||
if token.trust_id:
|
|
||||||
user_id = token.trust['trustor_user_id']
|
|
||||||
catalog = PROVIDERS.catalog_api.get_v3_catalog(
|
|
||||||
user_id, token.project_id
|
|
||||||
)
|
|
||||||
token_reference['token']['catalog'] = catalog
|
|
||||||
sps = PROVIDERS.federation_api.get_enabled_service_providers()
|
|
||||||
if sps:
|
|
||||||
token_reference['token']['service_providers'] = sps
|
|
||||||
if token.is_federated:
|
|
||||||
PROVIDERS.federation_api.get_idp(token.identity_provider_id)
|
|
||||||
federated_dict = {}
|
|
||||||
federated_dict['groups'] = token.federated_groups
|
|
||||||
federated_dict['identity_provider'] = {
|
|
||||||
'id': token.identity_provider_id
|
|
||||||
}
|
|
||||||
federated_dict['protocol'] = {'id': token.protocol_id}
|
|
||||||
token_reference['token']['user']['OS-FEDERATION'] = (
|
|
||||||
federated_dict
|
|
||||||
)
|
|
||||||
token_reference['token']['user']['domain'] = {
|
|
||||||
'id': 'Federated', 'name': 'Federated'
|
|
||||||
}
|
|
||||||
del token_reference['token']['user']['password_expires_at']
|
|
||||||
if token.access_token_id:
|
|
||||||
token_reference['token']['OS-OAUTH1'] = {
|
|
||||||
'access_token_id': token.access_token_id,
|
|
||||||
'consumer_id': token.access_token['consumer_id']
|
|
||||||
}
|
|
||||||
if token.application_credential_id:
|
|
||||||
key = 'application_credential'
|
|
||||||
token_reference['token'][key] = {}
|
|
||||||
token_reference['token'][key]['id'] = (
|
|
||||||
token.application_credential['id']
|
|
||||||
)
|
|
||||||
token_reference['token'][key]['name'] = (
|
|
||||||
token.application_credential['name']
|
|
||||||
)
|
|
||||||
restricted = not token.application_credential['unrestricted']
|
|
||||||
token_reference['token'][key]['restricted'] = restricted
|
|
||||||
|
|
||||||
return token_reference
|
|
||||||
|
|
||||||
|
|
||||||
class V3Controller(provider_api.ProviderAPIMixin, wsgi.Application):
|
|
||||||
"""Base controller class for Identity API v3.
|
|
||||||
|
|
||||||
Child classes should set the ``collection_name`` and ``member_name`` class
|
|
||||||
attributes, representing the collection of entities they are exposing to
|
|
||||||
the API. This is required for supporting self-referential links,
|
|
||||||
pagination, etc.
|
|
||||||
|
|
||||||
Class parameters:
|
|
||||||
|
|
||||||
* `_public_parameters` - set of parameters that are exposed to the user.
|
|
||||||
Usually used by cls.filter_params()
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
collection_name = 'entities'
|
|
||||||
member_name = 'entity'
|
|
||||||
get_member_from_driver = None
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def base_url(cls, context, path=None):
|
|
||||||
endpoint = super(V3Controller, cls).base_url(context, 'public')
|
|
||||||
if not path:
|
|
||||||
path = cls.collection_name
|
|
||||||
|
|
||||||
return '%s/%s/%s' % (endpoint, 'v3', path.lstrip('/'))
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def full_url(cls, context, path=None):
|
|
||||||
url = cls.base_url(context, path)
|
|
||||||
if context['environment'].get('QUERY_STRING'):
|
|
||||||
url = '%s?%s' % (url, context['environment']['QUERY_STRING'])
|
|
||||||
|
|
||||||
return url
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def query_filter_is_true(cls, filter_value):
|
|
||||||
"""Determine if bool query param is 'True'.
|
|
||||||
|
|
||||||
We treat this the same way as we do for policy
|
|
||||||
enforcement:
|
|
||||||
|
|
||||||
{bool_param}=0 is treated as False
|
|
||||||
|
|
||||||
Any other value is considered to be equivalent to
|
|
||||||
True, including the absence of a value
|
|
||||||
|
|
||||||
"""
|
|
||||||
if (isinstance(filter_value, six.string_types) and
|
|
||||||
filter_value == '0'):
|
|
||||||
val = False
|
|
||||||
else:
|
|
||||||
val = True
|
|
||||||
return val
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def _add_self_referential_link(cls, context, ref):
|
|
||||||
ref.setdefault('links', {})
|
|
||||||
ref['links']['self'] = cls.base_url(context) + '/' + ref['id']
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def wrap_member(cls, context, ref):
|
|
||||||
cls._add_self_referential_link(context, ref)
|
|
||||||
return {cls.member_name: ref}
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def wrap_collection(cls, context, refs, hints=None):
|
|
||||||
"""Wrap a collection, checking for filtering and pagination.
|
|
||||||
|
|
||||||
Returns the wrapped collection, which includes:
|
|
||||||
- Executing any filtering not already carried out
|
|
||||||
- Truncate to a set limit if necessary
|
|
||||||
- Adds 'self' links in every member
|
|
||||||
- Adds 'next', 'self' and 'prev' links for the whole collection.
|
|
||||||
|
|
||||||
:param context: the current context, containing the original url path
|
|
||||||
and query string
|
|
||||||
:param refs: the list of members of the collection
|
|
||||||
:param hints: list hints, containing any relevant filters and limit.
|
|
||||||
Any filters already satisfied by managers will have been
|
|
||||||
removed
|
|
||||||
"""
|
|
||||||
# Check if there are any filters in hints that were not
|
|
||||||
# handled by the drivers. The driver will not have paginated or
|
|
||||||
# limited the output if it found there were filters it was unable to
|
|
||||||
# handle.
|
|
||||||
|
|
||||||
if hints is not None:
|
|
||||||
refs = cls.filter_by_attributes(refs, hints)
|
|
||||||
|
|
||||||
list_limited, refs = cls.limit(refs, hints)
|
|
||||||
|
|
||||||
for ref in refs:
|
|
||||||
cls.wrap_member(context, ref)
|
|
||||||
|
|
||||||
container = {cls.collection_name: refs}
|
|
||||||
container['links'] = {
|
|
||||||
'next': None,
|
|
||||||
'self': cls.full_url(context, path=context['path']),
|
|
||||||
'previous': None}
|
|
||||||
|
|
||||||
if list_limited:
|
|
||||||
container['truncated'] = True
|
|
||||||
|
|
||||||
return container
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def limit(cls, refs, hints):
|
|
||||||
"""Limit a list of entities.
|
|
||||||
|
|
||||||
The underlying driver layer may have already truncated the collection
|
|
||||||
for us, but in case it was unable to handle truncation we check here.
|
|
||||||
|
|
||||||
:param refs: the list of members of the collection
|
|
||||||
:param hints: hints, containing, among other things, the limit
|
|
||||||
requested
|
|
||||||
|
|
||||||
:returns: boolean indicating whether the list was truncated, as well
|
|
||||||
as the list of (truncated if necessary) entities.
|
|
||||||
|
|
||||||
"""
|
|
||||||
NOT_LIMITED = False
|
|
||||||
LIMITED = True
|
|
||||||
|
|
||||||
if hints is None or hints.limit is None:
|
|
||||||
# No truncation was requested
|
|
||||||
return NOT_LIMITED, refs
|
|
||||||
|
|
||||||
if hints.limit.get('truncated', False):
|
|
||||||
# The driver did truncate the list
|
|
||||||
return LIMITED, refs
|
|
||||||
|
|
||||||
if len(refs) > hints.limit['limit']:
|
|
||||||
# The driver layer wasn't able to truncate it for us, so we must
|
|
||||||
# do it here
|
|
||||||
return LIMITED, refs[:hints.limit['limit']]
|
|
||||||
|
|
||||||
return NOT_LIMITED, refs
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def filter_by_attributes(cls, refs, hints):
|
|
||||||
"""Filter a list of references by filter values."""
|
|
||||||
def _attr_match(ref_attr, val_attr):
|
|
||||||
"""Matche attributes allowing for booleans as strings.
|
|
||||||
|
|
||||||
We test explicitly for a value that defines it as 'False',
|
|
||||||
which also means that the existence of the attribute with
|
|
||||||
no value implies 'True'
|
|
||||||
|
|
||||||
"""
|
|
||||||
if type(ref_attr) is bool:
|
|
||||||
return ref_attr == utils.attr_as_boolean(val_attr)
|
|
||||||
else:
|
|
||||||
return ref_attr == val_attr
|
|
||||||
|
|
||||||
def _inexact_attr_match(filter, ref):
|
|
||||||
"""Apply an inexact filter to a result dict.
|
|
||||||
|
|
||||||
:param filter: the filter in question
|
|
||||||
:param ref: the dict to check
|
|
||||||
|
|
||||||
:returns: True if there is a match
|
|
||||||
|
|
||||||
"""
|
|
||||||
comparator = filter['comparator']
|
|
||||||
key = filter['name']
|
|
||||||
|
|
||||||
if key in ref:
|
|
||||||
filter_value = filter['value']
|
|
||||||
target_value = ref[key]
|
|
||||||
if not filter['case_sensitive']:
|
|
||||||
# We only support inexact filters on strings so
|
|
||||||
# it's OK to use lower()
|
|
||||||
filter_value = filter_value.lower()
|
|
||||||
target_value = target_value.lower()
|
|
||||||
|
|
||||||
if comparator == 'contains':
|
|
||||||
return (filter_value in target_value)
|
|
||||||
elif comparator == 'startswith':
|
|
||||||
return target_value.startswith(filter_value)
|
|
||||||
elif comparator == 'endswith':
|
|
||||||
return target_value.endswith(filter_value)
|
|
||||||
else:
|
|
||||||
# We silently ignore unsupported filters
|
|
||||||
return True
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
for filter in hints.filters:
|
|
||||||
if filter['comparator'] == 'equals':
|
|
||||||
attr = filter['name']
|
|
||||||
value = filter['value']
|
|
||||||
refs = [r for r in refs if _attr_match(
|
|
||||||
utils.flatten_dict(r).get(attr), value)]
|
|
||||||
else:
|
|
||||||
# It might be an inexact filter
|
|
||||||
refs = [r for r in refs if _inexact_attr_match(
|
|
||||||
filter, r)]
|
|
||||||
|
|
||||||
return refs
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def build_driver_hints(cls, request, supported_filters):
|
|
||||||
"""Build list hints based on the context query string.
|
|
||||||
|
|
||||||
:param request: the current request
|
|
||||||
:param supported_filters: list of filters supported, so ignore any
|
|
||||||
keys in query_dict that are not in this list.
|
|
||||||
|
|
||||||
"""
|
|
||||||
hints = driver_hints.Hints()
|
|
||||||
|
|
||||||
if not request.params:
|
|
||||||
return hints
|
|
||||||
|
|
||||||
for key, value in request.params.items():
|
|
||||||
# Check if this is an exact filter
|
|
||||||
if supported_filters is None or key in supported_filters:
|
|
||||||
hints.add_filter(key, value)
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Check if it is an inexact filter
|
|
||||||
for valid_key in supported_filters:
|
|
||||||
# See if this entry in query_dict matches a known key with an
|
|
||||||
# inexact suffix added. If it doesn't match, then that just
|
|
||||||
# means that there is no inexact filter for that key in this
|
|
||||||
# query.
|
|
||||||
if not key.startswith(valid_key + '__'):
|
|
||||||
continue
|
|
||||||
|
|
||||||
base_key, comparator = key.split('__', 1)
|
|
||||||
|
|
||||||
# We map the query-style inexact of, for example:
|
|
||||||
#
|
|
||||||
# {'email__contains', 'myISP'}
|
|
||||||
#
|
|
||||||
# into a list directive add filter call parameters of:
|
|
||||||
#
|
|
||||||
# name = 'email'
|
|
||||||
# value = 'myISP'
|
|
||||||
# comparator = 'contains'
|
|
||||||
# case_sensitive = True
|
|
||||||
|
|
||||||
case_sensitive = True
|
|
||||||
if comparator.startswith('i'):
|
|
||||||
case_sensitive = False
|
|
||||||
comparator = comparator[1:]
|
|
||||||
hints.add_filter(base_key, value,
|
|
||||||
comparator=comparator,
|
|
||||||
case_sensitive=case_sensitive)
|
|
||||||
|
|
||||||
# NOTE(henry-nash): If we were to support pagination, we would pull any
|
|
||||||
# pagination directives out of the query_dict here, and add them into
|
|
||||||
# the hints list.
|
|
||||||
return hints
|
|
||||||
|
|
||||||
def _require_matching_id(self, value, ref):
|
|
||||||
"""Ensure the value matches the reference's ID, if any."""
|
|
||||||
if 'id' in ref and ref['id'] != value:
|
|
||||||
raise exception.ValidationError('Cannot change ID')
|
|
||||||
|
|
||||||
def _assign_unique_id(self, ref):
|
|
||||||
"""Generate and assigns a unique identifier to a reference."""
|
|
||||||
ref = ref.copy()
|
|
||||||
ref['id'] = uuid.uuid4().hex
|
|
||||||
return ref
|
|
||||||
|
|
||||||
def _get_domain_id_for_list_request(self, request):
|
|
||||||
"""Get the domain_id for a v3 list call.
|
|
||||||
|
|
||||||
If we running with multiple domain drivers, then the caller must
|
|
||||||
specify a domain_id either as a filter or as part of the token scope.
|
|
||||||
|
|
||||||
"""
|
|
||||||
if not CONF.identity.domain_specific_drivers_enabled:
|
|
||||||
# We don't need to specify a domain ID in this case
|
|
||||||
return
|
|
||||||
|
|
||||||
domain_id = request.params.get('domain_id')
|
|
||||||
if domain_id:
|
|
||||||
return domain_id
|
|
||||||
|
|
||||||
token = authorization.get_token_ref(request.context_dict)
|
|
||||||
|
|
||||||
if token.domain_scoped:
|
|
||||||
return token.domain_id
|
|
||||||
elif token.project_scoped:
|
|
||||||
return token.project_domain['id']
|
|
||||||
else:
|
|
||||||
msg = 'No domain information specified as part of list request'
|
|
||||||
tr_msg = _('No domain information specified as part of list '
|
|
||||||
'request')
|
|
||||||
LOG.warning(msg)
|
|
||||||
raise exception.Unauthorized(tr_msg)
|
|
||||||
|
|
||||||
def _get_domain_id_from_token(self, request):
|
|
||||||
"""Get the domain_id for a v3 create call.
|
|
||||||
|
|
||||||
In the case of a v3 create entity call that does not specify a domain
|
|
||||||
ID, the spec says that we should use the domain scoping from the token
|
|
||||||
being used.
|
|
||||||
|
|
||||||
"""
|
|
||||||
# return if domain scoped
|
|
||||||
if request.context.domain_id:
|
|
||||||
return request.context.domain_id
|
|
||||||
|
|
||||||
if request.context.is_admin:
|
|
||||||
raise exception.ValidationError(
|
|
||||||
_('You have tried to create a resource using the admin '
|
|
||||||
'token. As this token is not within a domain you must '
|
|
||||||
'explicitly include a domain for this resource to '
|
|
||||||
'belong to.'))
|
|
||||||
|
|
||||||
# TODO(henry-nash): We should issue an exception here since if
|
|
||||||
# a v3 call does not explicitly specify the domain_id in the
|
|
||||||
# entity, it should be using a domain scoped token. However,
|
|
||||||
# the current tempest heat tests issue a v3 call without this.
|
|
||||||
# This is raised as bug #1283539. Once this is fixed, we
|
|
||||||
# should remove the line below and replace it with an error.
|
|
||||||
#
|
|
||||||
# Ahead of actually changing the code to raise an exception, we
|
|
||||||
# issue a deprecation warning.
|
|
||||||
versionutils.report_deprecated_feature(
|
|
||||||
LOG,
|
|
||||||
'Not specifying a domain during a create user, group or '
|
|
||||||
'project call, and relying on falling back to the '
|
|
||||||
'default domain, is deprecated as of Liberty. There is no '
|
|
||||||
'plan to remove this compatibility, however, future API '
|
|
||||||
'versions may remove this, so please specify the domain '
|
|
||||||
'explicitly or use a domain-scoped token.')
|
|
||||||
return CONF.identity.default_domain_id
|
|
||||||
|
|
||||||
def _normalize_domain_id(self, request, ref):
|
|
||||||
"""Fill in domain_id if not specified in a v3 call."""
|
|
||||||
if not ref.get('domain_id'):
|
|
||||||
ref['domain_id'] = self._get_domain_id_from_token(request)
|
|
||||||
return ref
|
|
||||||
|
|
||||||
def check_protection(self, request, prep_info, target_attr=None):
|
|
||||||
"""Provide call protection for complex target attributes.
|
|
||||||
|
|
||||||
As well as including the standard parameters from the original API
|
|
||||||
call (which is passed in prep_info), this call will add in any
|
|
||||||
additional entities or attributes (passed in target_attr), so that
|
|
||||||
they can be referenced by policy rules.
|
|
||||||
|
|
||||||
"""
|
|
||||||
authorization.check_protection(self, request, prep_info, target_attr)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def filter_params(cls, ref):
|
|
||||||
"""Remove unspecified parameters from the dictionary.
|
|
||||||
|
|
||||||
This function removes unspecified parameters from the dictionary.
|
|
||||||
This method checks only root-level keys from a ref dictionary.
|
|
||||||
|
|
||||||
:param ref: a dictionary representing deserialized response to be
|
|
||||||
serialized
|
|
||||||
"""
|
|
||||||
ref_keys = set(ref.keys())
|
|
||||||
blocked_keys = ref_keys - cls._public_parameters
|
|
||||||
for blocked_param in blocked_keys:
|
|
||||||
del ref[blocked_param]
|
|
||||||
return ref
|
|
@ -1,59 +0,0 @@
|
|||||||
# Copyright 2012 OpenStack Foundation
|
|
||||||
#
|
|
||||||
# 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.
|
|
||||||
|
|
||||||
"""This module provides support for dependency injection.
|
|
||||||
|
|
||||||
WARNING: Use the ``keystone.common.provider_api`` module instead. This module
|
|
||||||
is going away in favor of an implementation that is better about following the
|
|
||||||
dependency injection model:
|
|
||||||
|
|
||||||
https://en.wikipedia.org/wiki/Dependency_injection
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
from keystone.common import provider_api
|
|
||||||
from keystone.i18n import _
|
|
||||||
|
|
||||||
|
|
||||||
REGISTRY = provider_api.ProviderAPIs
|
|
||||||
|
|
||||||
|
|
||||||
GET_REQUIRED = object()
|
|
||||||
GET_OPTIONAL = object()
|
|
||||||
|
|
||||||
|
|
||||||
def get_provider(name, optional=GET_REQUIRED):
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
class UnresolvableDependencyException(Exception):
|
|
||||||
"""Raised when a required dependency is not resolvable.
|
|
||||||
|
|
||||||
See ``resolve_future_dependencies()`` for more details.
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, name, targets):
|
|
||||||
msg = _('Unregistered dependency: %(name)s for %(targets)s') % {
|
|
||||||
'name': name, 'targets': targets}
|
|
||||||
super(UnresolvableDependencyException, self).__init__(msg)
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_future_dependencies(__provider_name=None):
|
|
||||||
"""Deprecated, does nothing."""
|
|
||||||
return {}
|
|
||||||
|
|
||||||
|
|
||||||
def reset():
|
|
||||||
"""Deprecated, does nothing."""
|
|
@ -1,44 +0,0 @@
|
|||||||
# Copyright 2013 OpenStack Foundation
|
|
||||||
#
|
|
||||||
# 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.
|
|
||||||
|
|
||||||
|
|
||||||
ADMIN_EXTENSIONS = {}
|
|
||||||
PUBLIC_EXTENSIONS = {}
|
|
||||||
|
|
||||||
|
|
||||||
def register_admin_extension(url_prefix, extension_data):
|
|
||||||
"""Register extension with collection of admin extensions.
|
|
||||||
|
|
||||||
Extensions register the information here that will show
|
|
||||||
up in the /extensions page as a way to indicate that the extension is
|
|
||||||
active.
|
|
||||||
|
|
||||||
url_prefix: unique key for the extension that will appear in the
|
|
||||||
urls generated by the extension.
|
|
||||||
|
|
||||||
extension_data is a dictionary. The expected fields are:
|
|
||||||
'name': short, human readable name of the extension
|
|
||||||
'namespace': xml namespace
|
|
||||||
'alias': identifier for the extension
|
|
||||||
'updated': date the extension was last updated
|
|
||||||
'description': text description of the extension
|
|
||||||
'links': hyperlinks to documents describing the extension
|
|
||||||
|
|
||||||
"""
|
|
||||||
ADMIN_EXTENSIONS[url_prefix] = extension_data
|
|
||||||
|
|
||||||
|
|
||||||
def register_public_extension(url_prefix, extension_data):
|
|
||||||
"""Same as register_admin_extension but for public extensions."""
|
|
||||||
PUBLIC_EXTENSIONS[url_prefix] = extension_data
|
|
@ -1,142 +0,0 @@
|
|||||||
# 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.
|
|
||||||
|
|
||||||
from oslo_log import log as logging
|
|
||||||
|
|
||||||
from pycadf import cadftaxonomy as taxonomy
|
|
||||||
from pycadf import host
|
|
||||||
from pycadf import resource
|
|
||||||
import webob
|
|
||||||
from webob.descriptors import environ_getter
|
|
||||||
|
|
||||||
from keystone.common import authorization
|
|
||||||
from keystone.common import context
|
|
||||||
from keystone.common import utils
|
|
||||||
import keystone.conf
|
|
||||||
from keystone import exception
|
|
||||||
from keystone.i18n import _
|
|
||||||
|
|
||||||
|
|
||||||
# Environment variable used to pass the request context
|
|
||||||
CONTEXT_ENV = 'openstack.context'
|
|
||||||
|
|
||||||
CONF = keystone.conf.CONF
|
|
||||||
LOG = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class Request(webob.Request):
|
|
||||||
|
|
||||||
_context_dict = None
|
|
||||||
|
|
||||||
def _get_context_dict(self):
|
|
||||||
# allow middleware up the stack to provide context, params and headers.
|
|
||||||
context = self.environ.get(CONTEXT_ENV, {})
|
|
||||||
|
|
||||||
# NOTE(jamielennox): The webob package throws UnicodeError when a
|
|
||||||
# param cannot be decoded. If we make webob iterate them now we can
|
|
||||||
# catch this and throw an error early rather than on access.
|
|
||||||
try:
|
|
||||||
self.params.items()
|
|
||||||
except UnicodeDecodeError:
|
|
||||||
msg = _('Query string is not UTF-8 encoded')
|
|
||||||
raise exception.ValidationError(msg)
|
|
||||||
|
|
||||||
context['path'] = self.environ['PATH_INFO']
|
|
||||||
scheme = self.environ.get(CONF.secure_proxy_ssl_header)
|
|
||||||
if scheme:
|
|
||||||
# NOTE(andrey-mp): "wsgi.url_scheme" contains the protocol used
|
|
||||||
# before the proxy removed it ('https' usually). So if
|
|
||||||
# the webob.Request instance is modified in order to use this
|
|
||||||
# scheme instead of the one defined by API, the call to
|
|
||||||
# webob.Request.relative_url() will return a URL with the correct
|
|
||||||
# scheme.
|
|
||||||
self.environ['wsgi.url_scheme'] = scheme
|
|
||||||
context['host_url'] = self.host_url
|
|
||||||
# authentication and authorization attributes are set as environment
|
|
||||||
# values by the container and processed by the pipeline. The complete
|
|
||||||
# set is not yet known.
|
|
||||||
context['environment'] = self.environ
|
|
||||||
|
|
||||||
if self.context:
|
|
||||||
context['is_admin_project'] = self.context.is_admin_project
|
|
||||||
|
|
||||||
context.setdefault('is_admin', False)
|
|
||||||
context['token_id'] = self.auth_token
|
|
||||||
if self.subject_token:
|
|
||||||
context['subject_token_id'] = self.subject_token
|
|
||||||
|
|
||||||
return context
|
|
||||||
|
|
||||||
@property
|
|
||||||
def context_dict(self):
|
|
||||||
if not self._context_dict:
|
|
||||||
self._context_dict = self._get_context_dict()
|
|
||||||
|
|
||||||
return self._context_dict
|
|
||||||
|
|
||||||
@property
|
|
||||||
def auth_context(self):
|
|
||||||
return self.environ.get(authorization.AUTH_CONTEXT_ENV, {})
|
|
||||||
|
|
||||||
def assert_authenticated(self):
|
|
||||||
"""Ensure that the current request has been authenticated."""
|
|
||||||
if not self.context:
|
|
||||||
msg = ('An authenticated call was made and there is '
|
|
||||||
'no request.context. This means the '
|
|
||||||
'auth_context middleware is not in place. You '
|
|
||||||
'must have this middleware in your pipeline '
|
|
||||||
'to perform authenticated calls')
|
|
||||||
tr_msg = _('An authenticated call was made and there is '
|
|
||||||
'no request.context. This means the '
|
|
||||||
'auth_context middleware is not in place. You '
|
|
||||||
'must have this middleware in your pipeline '
|
|
||||||
'to perform authenticated calls')
|
|
||||||
LOG.warning(msg)
|
|
||||||
raise exception.Unauthorized(tr_msg)
|
|
||||||
|
|
||||||
if not self.context.authenticated:
|
|
||||||
# auth_context didn't decode anything we can use
|
|
||||||
raise exception.Unauthorized(
|
|
||||||
_('auth_context did not decode anything useful'))
|
|
||||||
|
|
||||||
@property
|
|
||||||
def audit_initiator(self):
|
|
||||||
"""A pyCADF initiator describing the current authenticated context."""
|
|
||||||
pycadf_host = host.Host(address=self.remote_addr,
|
|
||||||
agent=self.user_agent)
|
|
||||||
initiator = resource.Resource(typeURI=taxonomy.ACCOUNT_USER,
|
|
||||||
host=pycadf_host)
|
|
||||||
|
|
||||||
if self.context.user_id:
|
|
||||||
initiator.id = utils.resource_uuid(self.context.user_id)
|
|
||||||
initiator.user_id = self.context.user_id
|
|
||||||
|
|
||||||
if self.context.project_id:
|
|
||||||
initiator.project_id = self.context.project_id
|
|
||||||
|
|
||||||
if self.context.domain_id:
|
|
||||||
initiator.domain_id = self.context.domain_id
|
|
||||||
|
|
||||||
return initiator
|
|
||||||
|
|
||||||
@property
|
|
||||||
def auth_token(self):
|
|
||||||
return self.headers.get(authorization.AUTH_TOKEN_HEADER, None)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def subject_token(self):
|
|
||||||
return self.headers.get(authorization.SUBJECT_TOKEN_HEADER, None)
|
|
||||||
|
|
||||||
auth_type = environ_getter('AUTH_TYPE', None)
|
|
||||||
remote_domain = environ_getter('REMOTE_DOMAIN', None)
|
|
||||||
context = environ_getter(context.REQUEST_CONTEXT_ENV, None)
|
|
||||||
token_auth = environ_getter('keystone.token_auth', None)
|
|
@ -1,85 +0,0 @@
|
|||||||
# Copyright 2012 OpenStack Foundation
|
|
||||||
#
|
|
||||||
# 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.
|
|
||||||
|
|
||||||
from keystone.common import json_home
|
|
||||||
from keystone.common import wsgi
|
|
||||||
|
|
||||||
|
|
||||||
class Router(wsgi.ComposableRouter):
|
|
||||||
def __init__(self, controller, collection_key, key,
|
|
||||||
resource_descriptions=None,
|
|
||||||
is_entity_implemented=True,
|
|
||||||
method_template=None):
|
|
||||||
self.controller = controller
|
|
||||||
self.key = key
|
|
||||||
self.collection_key = collection_key
|
|
||||||
self._resource_descriptions = resource_descriptions
|
|
||||||
self._is_entity_implemented = is_entity_implemented
|
|
||||||
self.method_template = method_template or '%s'
|
|
||||||
|
|
||||||
def add_routes(self, mapper):
|
|
||||||
collection_path = '/%(collection_key)s' % {
|
|
||||||
'collection_key': self.collection_key}
|
|
||||||
entity_path = '/%(collection_key)s/{%(key)s_id}' % {
|
|
||||||
'collection_key': self.collection_key,
|
|
||||||
'key': self.key}
|
|
||||||
|
|
||||||
mapper.connect(
|
|
||||||
collection_path,
|
|
||||||
controller=self.controller,
|
|
||||||
action=self.method_template % 'create_%s' % self.key,
|
|
||||||
conditions=dict(method=['POST']))
|
|
||||||
mapper.connect(
|
|
||||||
collection_path,
|
|
||||||
controller=self.controller,
|
|
||||||
action=self.method_template % 'list_%s' % self.collection_key,
|
|
||||||
conditions=dict(method=['GET', 'HEAD']))
|
|
||||||
mapper.connect(
|
|
||||||
entity_path,
|
|
||||||
controller=self.controller,
|
|
||||||
action=self.method_template % 'get_%s' % self.key,
|
|
||||||
conditions=dict(method=['GET', 'HEAD']))
|
|
||||||
mapper.connect(
|
|
||||||
entity_path,
|
|
||||||
controller=self.controller,
|
|
||||||
action=self.method_template % 'update_%s' % self.key,
|
|
||||||
conditions=dict(method=['PATCH']))
|
|
||||||
mapper.connect(
|
|
||||||
entity_path,
|
|
||||||
controller=self.controller,
|
|
||||||
action=self.method_template % 'delete_%s' % self.key,
|
|
||||||
conditions=dict(method=['DELETE']))
|
|
||||||
|
|
||||||
# Add the collection resource and entity resource to the resource
|
|
||||||
# descriptions.
|
|
||||||
|
|
||||||
collection_rel = json_home.build_v3_resource_relation(
|
|
||||||
self.collection_key)
|
|
||||||
rel_data = {'href': collection_path, }
|
|
||||||
self._resource_descriptions.append((collection_rel, rel_data))
|
|
||||||
json_home.JsonHomeResources.append_resource(collection_rel, rel_data)
|
|
||||||
|
|
||||||
if self._is_entity_implemented:
|
|
||||||
entity_rel = json_home.build_v3_resource_relation(self.key)
|
|
||||||
id_str = '%s_id' % self.key
|
|
||||||
id_param_rel = json_home.build_v3_parameter_relation(id_str)
|
|
||||||
entity_rel_data = {
|
|
||||||
'href-template': entity_path,
|
|
||||||
'href-vars': {
|
|
||||||
id_str: id_param_rel,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
self._resource_descriptions.append((entity_rel, entity_rel_data))
|
|
||||||
json_home.JsonHomeResources.append_resource(
|
|
||||||
entity_rel, entity_rel_data)
|
|
@ -1,703 +0,0 @@
|
|||||||
# Copyright 2012 OpenStack Foundation
|
|
||||||
# Copyright 2010 United States Government as represented by the
|
|
||||||
# Administrator of the National Aeronautics and Space Administration.
|
|
||||||
# Copyright 2010 OpenStack Foundation
|
|
||||||
# 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.
|
|
||||||
|
|
||||||
"""Utility methods for working with WSGI servers."""
|
|
||||||
|
|
||||||
import functools
|
|
||||||
import itertools
|
|
||||||
import re
|
|
||||||
import wsgiref.util
|
|
||||||
|
|
||||||
import oslo_i18n
|
|
||||||
from oslo_log import log
|
|
||||||
from oslo_serialization import jsonutils
|
|
||||||
from oslo_utils import importutils
|
|
||||||
from oslo_utils import strutils
|
|
||||||
import routes.middleware
|
|
||||||
import six
|
|
||||||
from six.moves import http_client
|
|
||||||
import webob.dec
|
|
||||||
import webob.exc
|
|
||||||
|
|
||||||
from keystone.common import authorization
|
|
||||||
from keystone.common import json_home
|
|
||||||
from keystone.common import request as request_mod
|
|
||||||
from keystone.common import utils
|
|
||||||
import keystone.conf
|
|
||||||
from keystone import exception
|
|
||||||
from keystone.i18n import _
|
|
||||||
|
|
||||||
|
|
||||||
CONF = keystone.conf.CONF
|
|
||||||
LOG = log.getLogger(__name__)
|
|
||||||
|
|
||||||
# Environment variable used to pass the request context
|
|
||||||
CONTEXT_ENV = 'openstack.context'
|
|
||||||
|
|
||||||
# Environment variable used to pass the request params
|
|
||||||
PARAMS_ENV = 'openstack.params'
|
|
||||||
|
|
||||||
JSON_ENCODE_CONTENT_TYPES = set(['application/json',
|
|
||||||
'application/json-home'])
|
|
||||||
|
|
||||||
|
|
||||||
def best_match_language(req):
|
|
||||||
"""Determine the best available locale.
|
|
||||||
|
|
||||||
This returns best available locale based on the Accept-Language HTTP
|
|
||||||
header passed in the request.
|
|
||||||
"""
|
|
||||||
if not req.accept_language:
|
|
||||||
return None
|
|
||||||
return req.accept_language.best_match(
|
|
||||||
oslo_i18n.get_available_languages('keystone'))
|
|
||||||
|
|
||||||
|
|
||||||
class BaseApplication(object):
|
|
||||||
"""Base WSGI application wrapper. Subclasses need to implement __call__."""
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def factory(cls, global_config, **local_config):
|
|
||||||
"""Used for loading in middleware (holdover from paste.deploy)."""
|
|
||||||
return cls(**local_config)
|
|
||||||
|
|
||||||
def __call__(self, environ, start_response):
|
|
||||||
r"""Provide subclasses on how to implement __call__.
|
|
||||||
|
|
||||||
Probably like this:
|
|
||||||
|
|
||||||
@webob.dec.wsgify()
|
|
||||||
def __call__(self, req):
|
|
||||||
# Any of the following objects work as responses:
|
|
||||||
|
|
||||||
# Option 1: simple string
|
|
||||||
res = 'message\n'
|
|
||||||
|
|
||||||
# Option 2: a nicely formatted HTTP exception page
|
|
||||||
res = exc.HTTPForbidden(explanation='Nice try')
|
|
||||||
|
|
||||||
# Option 3: a webob Response object (in case you need to play with
|
|
||||||
# headers, or you want to be treated like an iterable, or or or)
|
|
||||||
res = Response();
|
|
||||||
res.app_iter = open('somefile')
|
|
||||||
|
|
||||||
# Option 4: any wsgi app to be run next
|
|
||||||
res = self.application
|
|
||||||
|
|
||||||
# Option 5: you can get a Response object for a wsgi app, too, to
|
|
||||||
# play with headers etc
|
|
||||||
res = req.get_response(self.application)
|
|
||||||
|
|
||||||
# You can then just return your response...
|
|
||||||
return res
|
|
||||||
# ... or set req.response and return None.
|
|
||||||
req.response = res
|
|
||||||
|
|
||||||
See the end of http://pythonpaste.org/webob/modules/dec.html
|
|
||||||
for more info.
|
|
||||||
|
|
||||||
NOTE: this is now strictly used in conversion from old wsgi
|
|
||||||
implementation to flask. Once the flask implementation is complete,
|
|
||||||
the __call__ will not be needed as the flask app will handle
|
|
||||||
dispatching and __call__.
|
|
||||||
|
|
||||||
"""
|
|
||||||
raise NotImplementedError('You must implement __call__')
|
|
||||||
|
|
||||||
|
|
||||||
class Application(BaseApplication):
|
|
||||||
|
|
||||||
@webob.dec.wsgify(RequestClass=request_mod.Request)
|
|
||||||
def __call__(self, req):
|
|
||||||
arg_dict = req.environ['wsgiorg.routing_args'][1]
|
|
||||||
action = arg_dict.pop('action')
|
|
||||||
del arg_dict['controller']
|
|
||||||
|
|
||||||
params = req.environ.get(PARAMS_ENV, {})
|
|
||||||
params.update(arg_dict)
|
|
||||||
|
|
||||||
# TODO(termie): do some basic normalization on methods
|
|
||||||
method = getattr(self, action)
|
|
||||||
|
|
||||||
# NOTE(morganfainberg): use the request method to normalize the
|
|
||||||
# response code between GET and HEAD requests. The HTTP status should
|
|
||||||
# be the same.
|
|
||||||
LOG.info('%(req_method)s %(uri)s', {
|
|
||||||
'req_method': req.method.upper(),
|
|
||||||
'uri': wsgiref.util.request_uri(req.environ),
|
|
||||||
})
|
|
||||||
|
|
||||||
params = self._normalize_dict(params)
|
|
||||||
|
|
||||||
try:
|
|
||||||
result = method(req, **params)
|
|
||||||
except exception.Unauthorized as e:
|
|
||||||
LOG.warning(
|
|
||||||
"Authorization failed. %(exception)s from "
|
|
||||||
"%(remote_addr)s",
|
|
||||||
{'exception': e, 'remote_addr': req.environ['REMOTE_ADDR']})
|
|
||||||
return render_exception(e,
|
|
||||||
context=req.context_dict,
|
|
||||||
user_locale=best_match_language(req))
|
|
||||||
except exception.Error as e:
|
|
||||||
if isinstance(e, exception.UnexpectedError):
|
|
||||||
LOG.exception(six.text_type(e))
|
|
||||||
else:
|
|
||||||
LOG.warning(six.text_type(e))
|
|
||||||
return render_exception(e,
|
|
||||||
context=req.context_dict,
|
|
||||||
user_locale=best_match_language(req))
|
|
||||||
except TypeError as e:
|
|
||||||
LOG.exception(six.text_type(e))
|
|
||||||
return render_exception(exception.ValidationError(e),
|
|
||||||
context=req.context_dict,
|
|
||||||
user_locale=best_match_language(req))
|
|
||||||
except Exception as e:
|
|
||||||
LOG.exception(six.text_type(e))
|
|
||||||
return render_exception(exception.UnexpectedError(exception=e),
|
|
||||||
context=req.context_dict,
|
|
||||||
user_locale=best_match_language(req))
|
|
||||||
|
|
||||||
if result is None:
|
|
||||||
return render_response(
|
|
||||||
status=(http_client.NO_CONTENT,
|
|
||||||
http_client.responses[http_client.NO_CONTENT]))
|
|
||||||
elif isinstance(result, six.string_types):
|
|
||||||
return result
|
|
||||||
elif isinstance(result, webob.Response):
|
|
||||||
return result
|
|
||||||
elif isinstance(result, webob.exc.WSGIHTTPException):
|
|
||||||
return result
|
|
||||||
|
|
||||||
response_code = self._get_response_code(req)
|
|
||||||
return render_response(body=result,
|
|
||||||
status=response_code,
|
|
||||||
method=req.method)
|
|
||||||
|
|
||||||
def _get_response_code(self, req):
|
|
||||||
req_method = req.environ['REQUEST_METHOD']
|
|
||||||
controller = importutils.import_class('keystone.common.controller')
|
|
||||||
code = None
|
|
||||||
if isinstance(self, controller.V3Controller) and req_method == 'POST':
|
|
||||||
code = (http_client.CREATED,
|
|
||||||
http_client.responses[http_client.CREATED])
|
|
||||||
return code
|
|
||||||
|
|
||||||
def _normalize_arg(self, arg):
|
|
||||||
return arg.replace(':', '_').replace('-', '_')
|
|
||||||
|
|
||||||
def _normalize_dict(self, d):
|
|
||||||
return {self._normalize_arg(k): v for (k, v) in d.items()}
|
|
||||||
|
|
||||||
def assert_admin(self, request):
|
|
||||||
"""Ensure the user is an admin.
|
|
||||||
|
|
||||||
:raises keystone.exception.Unauthorized: if a token could not be
|
|
||||||
found/authorized, a user is invalid, or a tenant is
|
|
||||||
invalid/not scoped.
|
|
||||||
:raises keystone.exception.Forbidden: if the user is not an admin and
|
|
||||||
does not have the admin role
|
|
||||||
|
|
||||||
"""
|
|
||||||
authorization.assert_admin(self, request)
|
|
||||||
|
|
||||||
def _attribute_is_empty(self, ref, attribute):
|
|
||||||
"""Determine if the attribute in ref is empty or None."""
|
|
||||||
return ref.get(attribute) is None or ref.get(attribute) == ''
|
|
||||||
|
|
||||||
def _require_attribute(self, ref, attribute):
|
|
||||||
"""Ensure the reference contains the specified attribute.
|
|
||||||
|
|
||||||
Raise a ValidationError if the given attribute is not present
|
|
||||||
"""
|
|
||||||
if self._attribute_is_empty(ref, attribute):
|
|
||||||
msg = _('%s field is required and cannot be empty') % attribute
|
|
||||||
raise exception.ValidationError(message=msg)
|
|
||||||
|
|
||||||
def _require_attributes(self, ref, attrs):
|
|
||||||
"""Ensure the reference contains the specified attributes.
|
|
||||||
|
|
||||||
Raise a ValidationError if any of the given attributes is not present
|
|
||||||
"""
|
|
||||||
missing_attrs = [attribute for attribute in attrs
|
|
||||||
if self._attribute_is_empty(ref, attribute)]
|
|
||||||
|
|
||||||
if missing_attrs:
|
|
||||||
msg = _('%s field(s) cannot be empty') % ', '.join(missing_attrs)
|
|
||||||
raise exception.ValidationError(message=msg)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def base_url(cls, context, endpoint_type=None):
|
|
||||||
url = CONF['public_endpoint']
|
|
||||||
|
|
||||||
if url:
|
|
||||||
substitutions = dict(
|
|
||||||
itertools.chain(CONF.items(), CONF.eventlet_server.items()))
|
|
||||||
|
|
||||||
url = url % substitutions
|
|
||||||
elif 'environment' in context:
|
|
||||||
url = wsgiref.util.application_uri(context['environment'])
|
|
||||||
# remove version from the URL as it may be part of SCRIPT_NAME but
|
|
||||||
# it should not be part of base URL
|
|
||||||
url = re.sub(r'/v(3|(2\.0))/*$', '', url)
|
|
||||||
|
|
||||||
# now remove the standard port
|
|
||||||
url = utils.remove_standard_port(url)
|
|
||||||
else:
|
|
||||||
# if we don't have enough information to come up with a base URL,
|
|
||||||
# then fall back to localhost. This should never happen in
|
|
||||||
# production environment.
|
|
||||||
url = 'http://localhost:%d' % CONF.eventlet_server.public_port
|
|
||||||
|
|
||||||
return url.rstrip('/')
|
|
||||||
|
|
||||||
|
|
||||||
def middleware_exceptions(method):
|
|
||||||
|
|
||||||
@functools.wraps(method)
|
|
||||||
def _inner(self, request):
|
|
||||||
try:
|
|
||||||
return method(self, request)
|
|
||||||
except exception.Error as e:
|
|
||||||
LOG.warning(six.text_type(e))
|
|
||||||
return render_exception(e, request=request,
|
|
||||||
user_locale=best_match_language(request))
|
|
||||||
except TypeError as e:
|
|
||||||
LOG.exception(six.text_type(e))
|
|
||||||
return render_exception(exception.ValidationError(e),
|
|
||||||
request=request,
|
|
||||||
user_locale=best_match_language(request))
|
|
||||||
except Exception as e:
|
|
||||||
LOG.exception(six.text_type(e))
|
|
||||||
return render_exception(exception.UnexpectedError(exception=e),
|
|
||||||
request=request,
|
|
||||||
user_locale=best_match_language(request))
|
|
||||||
|
|
||||||
return _inner
|
|
||||||
|
|
||||||
|
|
||||||
class Middleware(Application):
|
|
||||||
"""Base WSGI middleware.
|
|
||||||
|
|
||||||
These classes require an application to be
|
|
||||||
initialized that will be called next. By default the middleware will
|
|
||||||
simply call its wrapped app, or you can override __call__ to customize its
|
|
||||||
behavior.
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def factory(cls, global_config):
|
|
||||||
"""Used for paste app factories in paste.deploy config files."""
|
|
||||||
def _factory(app):
|
|
||||||
return cls(app)
|
|
||||||
return _factory
|
|
||||||
|
|
||||||
def __init__(self, application, conf=None):
|
|
||||||
super(Middleware, self).__init__()
|
|
||||||
self.application = application
|
|
||||||
|
|
||||||
def process_request(self, request):
|
|
||||||
"""Called on each request.
|
|
||||||
|
|
||||||
If this returns None, the next application down the stack will be
|
|
||||||
executed. If it returns a response then that response will be returned
|
|
||||||
and execution will stop here.
|
|
||||||
|
|
||||||
"""
|
|
||||||
return None
|
|
||||||
|
|
||||||
def process_response(self, request, response):
|
|
||||||
"""Do whatever you'd like to the response, based on the request."""
|
|
||||||
return response
|
|
||||||
|
|
||||||
@webob.dec.wsgify(RequestClass=request_mod.Request)
|
|
||||||
@middleware_exceptions
|
|
||||||
def __call__(self, request):
|
|
||||||
response = self.process_request(request)
|
|
||||||
if response:
|
|
||||||
return response
|
|
||||||
response = request.get_response(self.application)
|
|
||||||
return self.process_response(request, response)
|
|
||||||
|
|
||||||
|
|
||||||
class Debug(Middleware):
|
|
||||||
"""Helper class for debugging a WSGI application.
|
|
||||||
|
|
||||||
Can be inserted into any WSGI application chain to get information
|
|
||||||
about the request and response.
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
@webob.dec.wsgify(RequestClass=request_mod.Request)
|
|
||||||
def __call__(self, req):
|
|
||||||
if not hasattr(LOG, 'isEnabledFor') or LOG.isEnabledFor(LOG.debug):
|
|
||||||
LOG.debug('%s %s %s', ('*' * 20), 'REQUEST ENVIRON', ('*' * 20))
|
|
||||||
for key, value in req.environ.items():
|
|
||||||
LOG.debug('%s = %s', key,
|
|
||||||
strutils.mask_password(value))
|
|
||||||
LOG.debug('')
|
|
||||||
LOG.debug('%s %s %s', ('*' * 20), 'REQUEST BODY', ('*' * 20))
|
|
||||||
for line in req.body_file:
|
|
||||||
LOG.debug('%s', strutils.mask_password(line))
|
|
||||||
LOG.debug('')
|
|
||||||
|
|
||||||
resp = req.get_response(self.application)
|
|
||||||
if not hasattr(LOG, 'isEnabledFor') or LOG.isEnabledFor(LOG.debug):
|
|
||||||
LOG.debug('%s %s %s', ('*' * 20), 'RESPONSE HEADERS', ('*' * 20))
|
|
||||||
for (key, value) in resp.headers.items():
|
|
||||||
LOG.debug('%s = %s', key, value)
|
|
||||||
LOG.debug('')
|
|
||||||
|
|
||||||
resp.app_iter = self.print_generator(resp.app_iter)
|
|
||||||
|
|
||||||
return resp
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def print_generator(app_iter):
|
|
||||||
"""Iterator that prints the contents of a wrapper string."""
|
|
||||||
LOG.debug('%s %s %s', ('*' * 20), 'RESPONSE BODY', ('*' * 20))
|
|
||||||
for part in app_iter:
|
|
||||||
LOG.debug(part)
|
|
||||||
yield part
|
|
||||||
|
|
||||||
|
|
||||||
class Router(object):
|
|
||||||
"""WSGI middleware that maps incoming requests to WSGI apps."""
|
|
||||||
|
|
||||||
def __init__(self, mapper):
|
|
||||||
"""Create a router for the given routes.Mapper.
|
|
||||||
|
|
||||||
Each route in `mapper` must specify a 'controller', which is a
|
|
||||||
WSGI app to call. You'll probably want to specify an 'action' as
|
|
||||||
well and have your controller be an object that can route
|
|
||||||
the request to the action-specific method.
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
mapper = routes.Mapper()
|
|
||||||
sc = ServerController()
|
|
||||||
|
|
||||||
# Explicit mapping of one route to a controller+action
|
|
||||||
mapper.connect(None, '/svrlist', controller=sc, action='list')
|
|
||||||
|
|
||||||
# Actions are all implicitly defined
|
|
||||||
mapper.resource('server', 'servers', controller=sc)
|
|
||||||
|
|
||||||
# Pointing to an arbitrary WSGI app. You can specify the
|
|
||||||
# {path_info:.*} parameter so the target app can be handed just that
|
|
||||||
# section of the URL.
|
|
||||||
mapper.connect(None, '/v1.0/{path_info:.*}', controller=BlogApp())
|
|
||||||
|
|
||||||
"""
|
|
||||||
self.map = mapper
|
|
||||||
self._router = routes.middleware.RoutesMiddleware(self._dispatch,
|
|
||||||
self.map)
|
|
||||||
|
|
||||||
@webob.dec.wsgify(RequestClass=request_mod.Request)
|
|
||||||
def __call__(self, req):
|
|
||||||
"""Route the incoming request to a controller based on self.map.
|
|
||||||
|
|
||||||
If no match, return a 404.
|
|
||||||
|
|
||||||
"""
|
|
||||||
return self._router
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
@webob.dec.wsgify(RequestClass=request_mod.Request)
|
|
||||||
def _dispatch(req):
|
|
||||||
"""Dispatch the request to the appropriate controller.
|
|
||||||
|
|
||||||
Called by self._router after matching the incoming request to a route
|
|
||||||
and putting the information into req.environ. Either returns 404
|
|
||||||
or the routed WSGI app's response.
|
|
||||||
|
|
||||||
"""
|
|
||||||
match = req.environ['wsgiorg.routing_args'][1]
|
|
||||||
if not match:
|
|
||||||
msg = (_('(%(url)s): The resource could not be found.') %
|
|
||||||
{'url': req.url})
|
|
||||||
return render_exception(exception.NotFound(msg),
|
|
||||||
request=req,
|
|
||||||
user_locale=best_match_language(req))
|
|
||||||
app = match['controller']
|
|
||||||
return app
|
|
||||||
|
|
||||||
|
|
||||||
class ComposingRouter(Router):
|
|
||||||
def __init__(self, mapper=None, routers=None):
|
|
||||||
if mapper is None:
|
|
||||||
mapper = routes.Mapper()
|
|
||||||
if routers is None:
|
|
||||||
routers = []
|
|
||||||
for router in routers:
|
|
||||||
router.add_routes(mapper)
|
|
||||||
super(ComposingRouter, self).__init__(mapper)
|
|
||||||
|
|
||||||
|
|
||||||
class ComposableRouter(Router):
|
|
||||||
"""Router that supports use by ComposingRouter."""
|
|
||||||
|
|
||||||
def __init__(self, mapper=None):
|
|
||||||
if mapper is None:
|
|
||||||
mapper = routes.Mapper()
|
|
||||||
self.add_routes(mapper)
|
|
||||||
super(ComposableRouter, self).__init__(mapper)
|
|
||||||
|
|
||||||
def add_routes(self, mapper):
|
|
||||||
"""Add routes to given mapper."""
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class ExtensionRouter(Router):
|
|
||||||
"""A router that allows extensions to supplement or overwrite routes.
|
|
||||||
|
|
||||||
Expects to be subclassed.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, application, mapper=None):
|
|
||||||
if mapper is None:
|
|
||||||
mapper = routes.Mapper()
|
|
||||||
self.application = application
|
|
||||||
self.add_routes(mapper)
|
|
||||||
mapper.connect('/{path_info:.*}', controller=self.application)
|
|
||||||
super(ExtensionRouter, self).__init__(mapper)
|
|
||||||
|
|
||||||
def add_routes(self, mapper):
|
|
||||||
pass
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def factory(cls, global_config, **local_config):
|
|
||||||
"""Used for loading in middleware (holdover from paste.deploy)."""
|
|
||||||
def _factory(app):
|
|
||||||
conf = global_config.copy()
|
|
||||||
conf.update(local_config)
|
|
||||||
return cls(app, **local_config)
|
|
||||||
return _factory
|
|
||||||
|
|
||||||
|
|
||||||
class RoutersBase(object):
|
|
||||||
"""Base class for Routers."""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self.v3_resources = []
|
|
||||||
|
|
||||||
def append_v3_routers(self, mapper, routers):
|
|
||||||
"""Append v3 routers.
|
|
||||||
|
|
||||||
Subclasses should override this method to map its routes.
|
|
||||||
|
|
||||||
Use self._add_resource() to map routes for a resource.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def _add_resource(self, mapper, controller, path, rel,
|
|
||||||
get_action=None, head_action=None, get_head_action=None,
|
|
||||||
put_action=None, post_action=None, patch_action=None,
|
|
||||||
delete_action=None, get_post_action=None,
|
|
||||||
path_vars=None, status=json_home.Status.STABLE,
|
|
||||||
new_path=None):
|
|
||||||
if get_head_action:
|
|
||||||
getattr(controller, get_head_action) # ensure the attribute exists
|
|
||||||
mapper.connect(path, controller=controller, action=get_head_action,
|
|
||||||
conditions=dict(method=['GET', 'HEAD']))
|
|
||||||
if get_action:
|
|
||||||
getattr(controller, get_action) # ensure the attribute exists
|
|
||||||
mapper.connect(path, controller=controller, action=get_action,
|
|
||||||
conditions=dict(method=['GET']))
|
|
||||||
if head_action:
|
|
||||||
getattr(controller, head_action) # ensure the attribute exists
|
|
||||||
mapper.connect(path, controller=controller, action=head_action,
|
|
||||||
conditions=dict(method=['HEAD']))
|
|
||||||
if put_action:
|
|
||||||
getattr(controller, put_action) # ensure the attribute exists
|
|
||||||
mapper.connect(path, controller=controller, action=put_action,
|
|
||||||
conditions=dict(method=['PUT']))
|
|
||||||
if post_action:
|
|
||||||
getattr(controller, post_action) # ensure the attribute exists
|
|
||||||
mapper.connect(path, controller=controller, action=post_action,
|
|
||||||
conditions=dict(method=['POST']))
|
|
||||||
if patch_action:
|
|
||||||
getattr(controller, patch_action) # ensure the attribute exists
|
|
||||||
mapper.connect(path, controller=controller, action=patch_action,
|
|
||||||
conditions=dict(method=['PATCH']))
|
|
||||||
if delete_action:
|
|
||||||
getattr(controller, delete_action) # ensure the attribute exists
|
|
||||||
mapper.connect(path, controller=controller, action=delete_action,
|
|
||||||
conditions=dict(method=['DELETE']))
|
|
||||||
if get_post_action:
|
|
||||||
getattr(controller, get_post_action) # ensure the attribute exists
|
|
||||||
mapper.connect(path, controller=controller, action=get_post_action,
|
|
||||||
conditions=dict(method=['GET', 'POST']))
|
|
||||||
|
|
||||||
resource_data = dict()
|
|
||||||
|
|
||||||
if path_vars:
|
|
||||||
resource_data['href-template'] = new_path or path
|
|
||||||
resource_data['href-vars'] = path_vars
|
|
||||||
else:
|
|
||||||
resource_data['href'] = new_path or path
|
|
||||||
|
|
||||||
json_home.Status.update_resource_data(resource_data, status)
|
|
||||||
|
|
||||||
self.v3_resources.append((rel, resource_data))
|
|
||||||
json_home.JsonHomeResources.append_resource(rel, resource_data)
|
|
||||||
|
|
||||||
|
|
||||||
class V3ExtensionRouter(ExtensionRouter, RoutersBase):
|
|
||||||
"""Base class for V3 extension router."""
|
|
||||||
|
|
||||||
def __init__(self, application, mapper=None):
|
|
||||||
self.v3_resources = list()
|
|
||||||
super(V3ExtensionRouter, self).__init__(application, mapper)
|
|
||||||
|
|
||||||
def _update_version_response(self, response_data):
|
|
||||||
response_data['resources'].update(self.v3_resources)
|
|
||||||
|
|
||||||
@webob.dec.wsgify(RequestClass=request_mod.Request)
|
|
||||||
def __call__(self, request):
|
|
||||||
if request.path_info != '/':
|
|
||||||
# Not a request for version info so forward to super.
|
|
||||||
return super(V3ExtensionRouter, self).__call__(request)
|
|
||||||
|
|
||||||
response = request.get_response(self.application)
|
|
||||||
|
|
||||||
if response.status_code != http_client.OK:
|
|
||||||
# The request failed, so don't update the response.
|
|
||||||
return response
|
|
||||||
|
|
||||||
if response.headers['Content-Type'] != 'application/json-home':
|
|
||||||
# Not a request for JSON Home document, so don't update the
|
|
||||||
# response.
|
|
||||||
return response
|
|
||||||
|
|
||||||
response_data = jsonutils.loads(response.body)
|
|
||||||
self._update_version_response(response_data)
|
|
||||||
response.body = jsonutils.dump_as_bytes(response_data,
|
|
||||||
cls=utils.SmarterEncoder)
|
|
||||||
return response
|
|
||||||
|
|
||||||
|
|
||||||
def render_response(body=None, status=None, headers=None, method=None):
|
|
||||||
"""Form a WSGI response."""
|
|
||||||
if headers is None:
|
|
||||||
headers = []
|
|
||||||
else:
|
|
||||||
headers = list(headers)
|
|
||||||
headers.append(('Vary', 'X-Auth-Token'))
|
|
||||||
|
|
||||||
if body is None:
|
|
||||||
body = b''
|
|
||||||
status = status or (http_client.NO_CONTENT,
|
|
||||||
http_client.responses[http_client.NO_CONTENT])
|
|
||||||
else:
|
|
||||||
content_types = [v for h, v in headers if h == 'Content-Type']
|
|
||||||
if content_types:
|
|
||||||
content_type = content_types[0]
|
|
||||||
else:
|
|
||||||
content_type = None
|
|
||||||
|
|
||||||
if content_type is None or content_type in JSON_ENCODE_CONTENT_TYPES:
|
|
||||||
body = jsonutils.dump_as_bytes(body, cls=utils.SmarterEncoder)
|
|
||||||
if content_type is None:
|
|
||||||
headers.append(('Content-Type', 'application/json'))
|
|
||||||
status = status or (http_client.OK,
|
|
||||||
http_client.responses[http_client.OK])
|
|
||||||
|
|
||||||
# NOTE(davechen): `mod_wsgi` follows the standards from pep-3333 and
|
|
||||||
# requires the value in response header to be binary type(str) on python2,
|
|
||||||
# unicode based string(str) on python3, or else keystone will not work
|
|
||||||
# under apache with `mod_wsgi`.
|
|
||||||
# keystone needs to check the data type of each header and convert the
|
|
||||||
# type if needed.
|
|
||||||
# see bug:
|
|
||||||
# https://bugs.launchpad.net/keystone/+bug/1528981
|
|
||||||
# see pep-3333:
|
|
||||||
# https://www.python.org/dev/peps/pep-3333/#a-note-on-string-types
|
|
||||||
# see source from mod_wsgi:
|
|
||||||
# https://github.com/GrahamDumpleton/mod_wsgi(methods:
|
|
||||||
# wsgi_convert_headers_to_bytes(...), wsgi_convert_string_to_bytes(...)
|
|
||||||
# and wsgi_validate_header_value(...)).
|
|
||||||
def _convert_to_str(headers):
|
|
||||||
str_headers = []
|
|
||||||
for header in headers:
|
|
||||||
str_header = []
|
|
||||||
for value in header:
|
|
||||||
if not isinstance(value, str):
|
|
||||||
str_header.append(str(value))
|
|
||||||
else:
|
|
||||||
str_header.append(value)
|
|
||||||
# convert the list to the immutable tuple to build the headers.
|
|
||||||
# header's key/value will be guaranteed to be str type.
|
|
||||||
str_headers.append(tuple(str_header))
|
|
||||||
return str_headers
|
|
||||||
|
|
||||||
headers = _convert_to_str(headers)
|
|
||||||
|
|
||||||
resp = webob.Response(body=body,
|
|
||||||
status='%d %s' % status,
|
|
||||||
headerlist=headers,
|
|
||||||
charset='utf-8')
|
|
||||||
|
|
||||||
if method and method.upper() == 'HEAD':
|
|
||||||
# NOTE(morganfainberg): HEAD requests should return the same status
|
|
||||||
# as a GET request and same headers (including content-type and
|
|
||||||
# content-length). The webob.Response object automatically changes
|
|
||||||
# content-length (and other headers) if the body is set to b''. Capture
|
|
||||||
# all headers and reset them on the response object after clearing the
|
|
||||||
# body. The body can only be set to a binary-type (not TextType or
|
|
||||||
# NoneType), so b'' is used here and should be compatible with
|
|
||||||
# both py2x and py3x.
|
|
||||||
stored_headers = resp.headers.copy()
|
|
||||||
resp.body = b''
|
|
||||||
for header, value in stored_headers.items():
|
|
||||||
resp.headers[header] = value
|
|
||||||
|
|
||||||
return resp
|
|
||||||
|
|
||||||
|
|
||||||
def render_exception(error, context=None, request=None, user_locale=None):
|
|
||||||
"""Form a WSGI response based on the current error."""
|
|
||||||
error_message = error.args[0]
|
|
||||||
message = oslo_i18n.translate(error_message, desired_locale=user_locale)
|
|
||||||
if message is error_message:
|
|
||||||
# translate() didn't do anything because it wasn't a Message,
|
|
||||||
# convert to a string.
|
|
||||||
message = six.text_type(message)
|
|
||||||
|
|
||||||
body = {'error': {
|
|
||||||
'code': error.code,
|
|
||||||
'title': error.title,
|
|
||||||
'message': message,
|
|
||||||
}}
|
|
||||||
headers = []
|
|
||||||
if isinstance(error, exception.AuthPluginException):
|
|
||||||
body['error']['identity'] = error.authentication
|
|
||||||
elif isinstance(error, exception.Unauthorized):
|
|
||||||
# NOTE(gyee): we only care about the request environment in the
|
|
||||||
# context. Also, its OK to pass the environment as it is read-only in
|
|
||||||
# Application.base_url()
|
|
||||||
local_context = {}
|
|
||||||
if request:
|
|
||||||
local_context = {'environment': request.environ}
|
|
||||||
elif context and 'environment' in context:
|
|
||||||
local_context = {'environment': context['environment']}
|
|
||||||
url = Application.base_url(local_context)
|
|
||||||
|
|
||||||
headers.append(('WWW-Authenticate', 'Keystone uri="%s"' % url))
|
|
||||||
return render_response(status=(error.code, error.title),
|
|
||||||
body=body,
|
|
||||||
headers=headers)
|
|
@ -10,19 +10,31 @@
|
|||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
|
|
||||||
|
import functools
|
||||||
|
import itertools
|
||||||
|
import re
|
||||||
|
import wsgiref.util
|
||||||
|
|
||||||
from keystonemiddleware import auth_token
|
from keystonemiddleware import auth_token
|
||||||
|
import oslo_i18n
|
||||||
from oslo_log import log
|
from oslo_log import log
|
||||||
|
from oslo_serialization import jsonutils
|
||||||
|
import six
|
||||||
|
from six.moves import http_client
|
||||||
|
import webob.dec
|
||||||
|
import webob.exc
|
||||||
|
|
||||||
from keystone.common import authorization
|
from keystone.common import authorization
|
||||||
from keystone.common import context
|
from keystone.common import context
|
||||||
from keystone.common import provider_api
|
from keystone.common import provider_api
|
||||||
from keystone.common import render_token
|
from keystone.common import render_token
|
||||||
from keystone.common import tokenless_auth
|
from keystone.common import tokenless_auth
|
||||||
from keystone.common import wsgi
|
from keystone.common import utils
|
||||||
import keystone.conf
|
import keystone.conf
|
||||||
from keystone import exception
|
from keystone import exception
|
||||||
from keystone.federation import constants as federation_constants
|
from keystone.federation import constants as federation_constants
|
||||||
from keystone.federation import utils
|
from keystone.federation import utils as federation_utils
|
||||||
from keystone.i18n import _
|
from keystone.i18n import _
|
||||||
from keystone.models import token_model
|
from keystone.models import token_model
|
||||||
|
|
||||||
@ -30,9 +42,193 @@ CONF = keystone.conf.CONF
|
|||||||
LOG = log.getLogger(__name__)
|
LOG = log.getLogger(__name__)
|
||||||
PROVIDERS = provider_api.ProviderAPIs
|
PROVIDERS = provider_api.ProviderAPIs
|
||||||
|
|
||||||
|
# Environment variable used to pass the request context
|
||||||
|
CONTEXT_ENV = 'openstack.context'
|
||||||
|
|
||||||
__all__ = ('AuthContextMiddleware',)
|
__all__ = ('AuthContextMiddleware',)
|
||||||
|
|
||||||
|
|
||||||
|
CONF = keystone.conf.CONF
|
||||||
|
LOG = log.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
JSON_ENCODE_CONTENT_TYPES = set(['application/json',
|
||||||
|
'application/json-home'])
|
||||||
|
|
||||||
|
|
||||||
|
def best_match_language(req):
|
||||||
|
"""Determine the best available locale.
|
||||||
|
|
||||||
|
This returns best available locale based on the Accept-Language HTTP
|
||||||
|
header passed in the request.
|
||||||
|
"""
|
||||||
|
if not req.accept_language:
|
||||||
|
return None
|
||||||
|
return req.accept_language.best_match(
|
||||||
|
oslo_i18n.get_available_languages('keystone'))
|
||||||
|
|
||||||
|
|
||||||
|
def base_url(context):
|
||||||
|
url = CONF['public_endpoint']
|
||||||
|
|
||||||
|
if url:
|
||||||
|
substitutions = dict(
|
||||||
|
itertools.chain(CONF.items(), CONF.eventlet_server.items()))
|
||||||
|
|
||||||
|
url = url % substitutions
|
||||||
|
elif 'environment' in context:
|
||||||
|
url = wsgiref.util.application_uri(context['environment'])
|
||||||
|
# remove version from the URL as it may be part of SCRIPT_NAME but
|
||||||
|
# it should not be part of base URL
|
||||||
|
url = re.sub(r'/v(3|(2\.0))/*$', '', url)
|
||||||
|
|
||||||
|
# now remove the standard port
|
||||||
|
url = utils.remove_standard_port(url)
|
||||||
|
else:
|
||||||
|
# if we don't have enough information to come up with a base URL,
|
||||||
|
# then fall back to localhost. This should never happen in
|
||||||
|
# production environment.
|
||||||
|
url = 'http://localhost:%d' % CONF.eventlet_server.public_port
|
||||||
|
|
||||||
|
return url.rstrip('/')
|
||||||
|
|
||||||
|
|
||||||
|
def middleware_exceptions(method):
|
||||||
|
|
||||||
|
@functools.wraps(method)
|
||||||
|
def _inner(self, request):
|
||||||
|
try:
|
||||||
|
return method(self, request)
|
||||||
|
except exception.Error as e:
|
||||||
|
LOG.warning(six.text_type(e))
|
||||||
|
return render_exception(e, request=request,
|
||||||
|
user_locale=best_match_language(request))
|
||||||
|
except TypeError as e:
|
||||||
|
LOG.exception(six.text_type(e))
|
||||||
|
return render_exception(exception.ValidationError(e),
|
||||||
|
request=request,
|
||||||
|
user_locale=best_match_language(request))
|
||||||
|
except Exception as e:
|
||||||
|
LOG.exception(six.text_type(e))
|
||||||
|
return render_exception(exception.UnexpectedError(exception=e),
|
||||||
|
request=request,
|
||||||
|
user_locale=best_match_language(request))
|
||||||
|
|
||||||
|
return _inner
|
||||||
|
|
||||||
|
|
||||||
|
def render_response(body=None, status=None, headers=None, method=None):
|
||||||
|
"""Form a WSGI response."""
|
||||||
|
if headers is None:
|
||||||
|
headers = []
|
||||||
|
else:
|
||||||
|
headers = list(headers)
|
||||||
|
headers.append(('Vary', 'X-Auth-Token'))
|
||||||
|
|
||||||
|
if body is None:
|
||||||
|
body = b''
|
||||||
|
status = status or (http_client.NO_CONTENT,
|
||||||
|
http_client.responses[http_client.NO_CONTENT])
|
||||||
|
else:
|
||||||
|
content_types = [v for h, v in headers if h == 'Content-Type']
|
||||||
|
if content_types:
|
||||||
|
content_type = content_types[0]
|
||||||
|
else:
|
||||||
|
content_type = None
|
||||||
|
|
||||||
|
if content_type is None or content_type in JSON_ENCODE_CONTENT_TYPES:
|
||||||
|
body = jsonutils.dump_as_bytes(body, cls=utils.SmarterEncoder)
|
||||||
|
if content_type is None:
|
||||||
|
headers.append(('Content-Type', 'application/json'))
|
||||||
|
status = status or (http_client.OK,
|
||||||
|
http_client.responses[http_client.OK])
|
||||||
|
|
||||||
|
# NOTE(davechen): `mod_wsgi` follows the standards from pep-3333 and
|
||||||
|
# requires the value in response header to be binary type(str) on python2,
|
||||||
|
# unicode based string(str) on python3, or else keystone will not work
|
||||||
|
# under apache with `mod_wsgi`.
|
||||||
|
# keystone needs to check the data type of each header and convert the
|
||||||
|
# type if needed.
|
||||||
|
# see bug:
|
||||||
|
# https://bugs.launchpad.net/keystone/+bug/1528981
|
||||||
|
# see pep-3333:
|
||||||
|
# https://www.python.org/dev/peps/pep-3333/#a-note-on-string-types
|
||||||
|
# see source from mod_wsgi:
|
||||||
|
# https://github.com/GrahamDumpleton/mod_wsgi(methods:
|
||||||
|
# wsgi_convert_headers_to_bytes(...), wsgi_convert_string_to_bytes(...)
|
||||||
|
# and wsgi_validate_header_value(...)).
|
||||||
|
def _convert_to_str(headers):
|
||||||
|
str_headers = []
|
||||||
|
for header in headers:
|
||||||
|
str_header = []
|
||||||
|
for value in header:
|
||||||
|
if not isinstance(value, str):
|
||||||
|
str_header.append(str(value))
|
||||||
|
else:
|
||||||
|
str_header.append(value)
|
||||||
|
# convert the list to the immutable tuple to build the headers.
|
||||||
|
# header's key/value will be guaranteed to be str type.
|
||||||
|
str_headers.append(tuple(str_header))
|
||||||
|
return str_headers
|
||||||
|
|
||||||
|
headers = _convert_to_str(headers)
|
||||||
|
|
||||||
|
resp = webob.Response(body=body,
|
||||||
|
status='%d %s' % status,
|
||||||
|
headerlist=headers,
|
||||||
|
charset='utf-8')
|
||||||
|
|
||||||
|
if method and method.upper() == 'HEAD':
|
||||||
|
# NOTE(morganfainberg): HEAD requests should return the same status
|
||||||
|
# as a GET request and same headers (including content-type and
|
||||||
|
# content-length). The webob.Response object automatically changes
|
||||||
|
# content-length (and other headers) if the body is set to b''. Capture
|
||||||
|
# all headers and reset them on the response object after clearing the
|
||||||
|
# body. The body can only be set to a binary-type (not TextType or
|
||||||
|
# NoneType), so b'' is used here and should be compatible with
|
||||||
|
# both py2x and py3x.
|
||||||
|
stored_headers = resp.headers.copy()
|
||||||
|
resp.body = b''
|
||||||
|
for header, value in stored_headers.items():
|
||||||
|
resp.headers[header] = value
|
||||||
|
|
||||||
|
return resp
|
||||||
|
|
||||||
|
|
||||||
|
def render_exception(error, context=None, request=None, user_locale=None):
|
||||||
|
"""Form a WSGI response based on the current error."""
|
||||||
|
error_message = error.args[0]
|
||||||
|
message = oslo_i18n.translate(error_message, desired_locale=user_locale)
|
||||||
|
if message is error_message:
|
||||||
|
# translate() didn't do anything because it wasn't a Message,
|
||||||
|
# convert to a string.
|
||||||
|
message = six.text_type(message)
|
||||||
|
|
||||||
|
body = {'error': {
|
||||||
|
'code': error.code,
|
||||||
|
'title': error.title,
|
||||||
|
'message': message,
|
||||||
|
}}
|
||||||
|
headers = []
|
||||||
|
if isinstance(error, exception.AuthPluginException):
|
||||||
|
body['error']['identity'] = error.authentication
|
||||||
|
elif isinstance(error, exception.Unauthorized):
|
||||||
|
# NOTE(gyee): we only care about the request environment in the
|
||||||
|
# context. Also, its OK to pass the environment as it is read-only in
|
||||||
|
# base_url()
|
||||||
|
local_context = {}
|
||||||
|
if request:
|
||||||
|
local_context = {'environment': request.environ}
|
||||||
|
elif context and 'environment' in context:
|
||||||
|
local_context = {'environment': context['environment']}
|
||||||
|
url = base_url(local_context)
|
||||||
|
|
||||||
|
headers.append(('WWW-Authenticate', 'Keystone uri="%s"' % url))
|
||||||
|
return render_response(status=(error.code, error.title),
|
||||||
|
body=body,
|
||||||
|
headers=headers)
|
||||||
|
|
||||||
|
|
||||||
class AuthContextMiddleware(provider_api.ProviderAPIMixin,
|
class AuthContextMiddleware(provider_api.ProviderAPIMixin,
|
||||||
auth_token.BaseAuthProtocol):
|
auth_token.BaseAuthProtocol):
|
||||||
"""Build the authentication context from the request auth token."""
|
"""Build the authentication context from the request auth token."""
|
||||||
@ -66,7 +262,7 @@ class AuthContextMiddleware(provider_api.ProviderAPIMixin,
|
|||||||
# NOTE(gyee): if it is an ephemeral user, the
|
# NOTE(gyee): if it is an ephemeral user, the
|
||||||
# given X.509 SSL client cert does not need to map to
|
# given X.509 SSL client cert does not need to map to
|
||||||
# an existing user.
|
# an existing user.
|
||||||
if user_ref['type'] == utils.UserType.EPHEMERAL:
|
if user_ref['type'] == federation_utils.UserType.EPHEMERAL:
|
||||||
auth_context = {}
|
auth_context = {}
|
||||||
auth_context['group_ids'] = user_ref['group_ids']
|
auth_context['group_ids'] = user_ref['group_ids']
|
||||||
auth_context[federation_constants.IDENTITY_PROVIDER] = (
|
auth_context[federation_constants.IDENTITY_PROVIDER] = (
|
||||||
@ -130,9 +326,9 @@ class AuthContextMiddleware(provider_api.ProviderAPIMixin,
|
|||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@wsgi.middleware_exceptions
|
@middleware_exceptions
|
||||||
def process_request(self, request):
|
def process_request(self, request):
|
||||||
context_env = request.environ.get(wsgi.CONTEXT_ENV, {})
|
context_env = request.environ.get(CONTEXT_ENV, {})
|
||||||
|
|
||||||
# NOTE(notmorgan): This code is merged over from the admin token
|
# NOTE(notmorgan): This code is merged over from the admin token
|
||||||
# middleware and now emits the security warning when the
|
# middleware and now emits the security warning when the
|
||||||
@ -146,7 +342,7 @@ class AuthContextMiddleware(provider_api.ProviderAPIMixin,
|
|||||||
"not be set. This option is deprecated in favor of using "
|
"not be set. This option is deprecated in favor of using "
|
||||||
"'keystone-manage bootstrap' and will be removed in a "
|
"'keystone-manage bootstrap' and will be removed in a "
|
||||||
"future release.")
|
"future release.")
|
||||||
request.environ[wsgi.CONTEXT_ENV] = context_env
|
request.environ[CONTEXT_ENV] = context_env
|
||||||
|
|
||||||
if not context_env.get('is_admin', False):
|
if not context_env.get('is_admin', False):
|
||||||
resp = super(AuthContextMiddleware, self).process_request(request)
|
resp = super(AuthContextMiddleware, self).process_request(request)
|
||||||
@ -210,7 +406,7 @@ class AuthContextMiddleware(provider_api.ProviderAPIMixin,
|
|||||||
# certificate is effectively disabled if no trusted issuers are
|
# certificate is effectively disabled if no trusted issuers are
|
||||||
# provided.
|
# provided.
|
||||||
|
|
||||||
if request.environ.get(wsgi.CONTEXT_ENV, {}).get('is_admin', False):
|
if request.environ.get(CONTEXT_ENV, {}).get('is_admin', False):
|
||||||
request_context.is_admin = True
|
request_context.is_admin = True
|
||||||
auth_context = {}
|
auth_context = {}
|
||||||
|
|
||||||
|
@ -12,6 +12,9 @@
|
|||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
|
# NOTE(morgan): These test cases are used for AuthContextMiddleware exception
|
||||||
|
# rendering.
|
||||||
|
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
import fixtures
|
import fixtures
|
||||||
@ -20,9 +23,9 @@ from oslo_log import log
|
|||||||
from oslo_serialization import jsonutils
|
from oslo_serialization import jsonutils
|
||||||
import six
|
import six
|
||||||
|
|
||||||
from keystone.common import wsgi
|
|
||||||
import keystone.conf
|
import keystone.conf
|
||||||
from keystone import exception
|
from keystone import exception
|
||||||
|
from keystone.server.flask.request_processing.middleware import auth_context
|
||||||
from keystone.tests import unit
|
from keystone.tests import unit
|
||||||
|
|
||||||
|
|
||||||
@ -31,7 +34,7 @@ CONF = keystone.conf.CONF
|
|||||||
|
|
||||||
class ExceptionTestCase(unit.BaseTestCase):
|
class ExceptionTestCase(unit.BaseTestCase):
|
||||||
def assertValidJsonRendering(self, e):
|
def assertValidJsonRendering(self, e):
|
||||||
resp = wsgi.render_exception(e)
|
resp = auth_context.render_exception(e)
|
||||||
self.assertEqual(e.code, resp.status_int)
|
self.assertEqual(e.code, resp.status_int)
|
||||||
self.assertEqual('%s %s' % (e.code, e.title), resp.status)
|
self.assertEqual('%s %s' % (e.code, e.title), resp.status)
|
||||||
|
|
||||||
@ -74,7 +77,7 @@ class ExceptionTestCase(unit.BaseTestCase):
|
|||||||
|
|
||||||
def test_forbidden_title(self):
|
def test_forbidden_title(self):
|
||||||
e = exception.Forbidden()
|
e = exception.Forbidden()
|
||||||
resp = wsgi.render_exception(e)
|
resp = auth_context.render_exception(e)
|
||||||
j = jsonutils.loads(resp.body)
|
j = jsonutils.loads(resp.body)
|
||||||
self.assertEqual('Forbidden', e.title)
|
self.assertEqual('Forbidden', e.title)
|
||||||
self.assertEqual('Forbidden', j['error'].get('title'))
|
self.assertEqual('Forbidden', j['error'].get('title'))
|
||||||
|
@ -23,7 +23,6 @@ import webtest
|
|||||||
from keystone.common import authorization
|
from keystone.common import authorization
|
||||||
from keystone.common import provider_api
|
from keystone.common import provider_api
|
||||||
from keystone.common import tokenless_auth
|
from keystone.common import tokenless_auth
|
||||||
from keystone.common import wsgi
|
|
||||||
import keystone.conf
|
import keystone.conf
|
||||||
from keystone import exception
|
from keystone import exception
|
||||||
from keystone.federation import constants as federation_constants
|
from keystone.federation import constants as federation_constants
|
||||||
@ -667,7 +666,7 @@ class AuthContextMiddlewareTest(test_backend_sql.SqlTests,
|
|||||||
log_fix = self.useFixture(fixtures.FakeLogger())
|
log_fix = self.useFixture(fixtures.FakeLogger())
|
||||||
headers = {authorization.AUTH_TOKEN_HEADER: 'ADMIN'}
|
headers = {authorization.AUTH_TOKEN_HEADER: 'ADMIN'}
|
||||||
req = self._do_middleware_request(headers=headers)
|
req = self._do_middleware_request(headers=headers)
|
||||||
self.assertTrue(req.environ[wsgi.CONTEXT_ENV]['is_admin'])
|
self.assertTrue(req.environ[auth_context.CONTEXT_ENV]['is_admin'])
|
||||||
self.assertNotIn('Invalid user token', log_fix.output)
|
self.assertNotIn('Invalid user token', log_fix.output)
|
||||||
|
|
||||||
def test_request_non_admin(self):
|
def test_request_non_admin(self):
|
||||||
|
@ -0,0 +1,26 @@
|
|||||||
|
---
|
||||||
|
other:
|
||||||
|
- |
|
||||||
|
Keystone has been fully converted to run under flask. All of the APIs are
|
||||||
|
now natively dispatched under flask.
|
||||||
|
|
||||||
|
Included in this change is a removal of a legacy WSGI environment data
|
||||||
|
holder calld `openstack.params`. The data holder was used exclusively for
|
||||||
|
communicating data down the chain under paste-deploy. The data in
|
||||||
|
`openstack.params` was generally "normalized" in an odd way and
|
||||||
|
unreferenced in the rest of the openstack code-base.
|
||||||
|
|
||||||
|
Some minor changes to the JSON Home document occured to make it consistent
|
||||||
|
with the rest of our convensions (Technically an API contract break) but
|
||||||
|
required for the more strict view the Keystone flask code takes on setting
|
||||||
|
up the values for JSON Home. Notably "application_credentials" now has
|
||||||
|
an appropriate entry for listing and creating new app creds.
|
||||||
|
|
||||||
|
JSON Body and URL Normalizing middleware were move to a flask-native
|
||||||
|
model.
|
||||||
|
|
||||||
|
Any middleware defined in Keystone's tree is no longer loaded via
|
||||||
|
stevedore, and likewise the entry points were removed.
|
||||||
|
|
||||||
|
Original WSGI Framework (custom, home-rolled, based on WEBOB) has been
|
||||||
|
removed from the codebase.
|
@ -8,7 +8,6 @@ Babel!=2.4.0,>=2.3.4 # BSD
|
|||||||
|
|
||||||
pbr!=2.1.0,>=2.0.0 # Apache-2.0
|
pbr!=2.1.0,>=2.0.0 # Apache-2.0
|
||||||
WebOb>=1.7.1 # MIT
|
WebOb>=1.7.1 # MIT
|
||||||
Routes>=2.3.1 # MIT
|
|
||||||
Flask!=0.11,>=1.0.2 # BSD
|
Flask!=0.11,>=1.0.2 # BSD
|
||||||
Flask-RESTful>=0.3.5 # BSD
|
Flask-RESTful>=0.3.5 # BSD
|
||||||
cryptography>=2.1 # BSD/Apache-2.0
|
cryptography>=2.1 # BSD/Apache-2.0
|
||||||
|
Loading…
Reference in New Issue
Block a user