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 keystone.common import json_home
|
||||
from keystone.common import wsgi
|
||||
import keystone.conf
|
||||
from keystone import exception
|
||||
from keystone.server import flask as ks_flask
|
||||
|
||||
|
||||
CONF = keystone.conf.CONF
|
||||
@ -87,11 +87,7 @@ def get_versions():
|
||||
return flask.Response(response=jsonutils.dumps(v3_json_home),
|
||||
mimetype=MimeTypes.JSON_HOME)
|
||||
else:
|
||||
# NOTE(morgan): wsgi.Application.base_url will eventually need to
|
||||
# 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})
|
||||
identity_url = '%s/' % ks_flask.base_url()
|
||||
versions = _get_versions_list(identity_url)
|
||||
return flask.Response(
|
||||
response=jsonutils.dumps(
|
||||
@ -113,11 +109,7 @@ def get_version_v3():
|
||||
return flask.Response(response=jsonutils.dumps(content),
|
||||
mimetype=MimeTypes.JSON_HOME)
|
||||
else:
|
||||
# NOTE(morgan): wsgi.Application.base_url will eventually need to
|
||||
# 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})
|
||||
identity_url = '%s/' % ks_flask.base_url()
|
||||
versions = _get_versions_list(identity_url)
|
||||
return flask.Response(
|
||||
response=jsonutils.dumps({'version': versions['v3']}),
|
||||
|
@ -15,163 +15,15 @@
|
||||
# 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
|
||||
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
|
||||
AUTH_TOKEN_HEADER = 'X-Auth-Token'
|
||||
|
||||
|
||||
# Header used to transmit the subject token
|
||||
SUBJECT_TOKEN_HEADER = 'X-Subject-Token'
|
||||
|
||||
|
||||
CONF = conf.CONF
|
||||
|
||||
# Environment variable used to convey the Keystone auth context,
|
||||
# the user credential used for policy enforcement.
|
||||
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
|
||||
# under the License.
|
||||
|
||||
|
||||
import functools
|
||||
import itertools
|
||||
import re
|
||||
import wsgiref.util
|
||||
|
||||
from keystonemiddleware import auth_token
|
||||
import oslo_i18n
|
||||
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 context
|
||||
from keystone.common import provider_api
|
||||
from keystone.common import render_token
|
||||
from keystone.common import tokenless_auth
|
||||
from keystone.common import wsgi
|
||||
from keystone.common import utils
|
||||
import keystone.conf
|
||||
from keystone import exception
|
||||
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.models import token_model
|
||||
|
||||
@ -30,9 +42,193 @@ CONF = keystone.conf.CONF
|
||||
LOG = log.getLogger(__name__)
|
||||
PROVIDERS = provider_api.ProviderAPIs
|
||||
|
||||
# Environment variable used to pass the request context
|
||||
CONTEXT_ENV = 'openstack.context'
|
||||
|
||||
__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,
|
||||
auth_token.BaseAuthProtocol):
|
||||
"""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
|
||||
# given X.509 SSL client cert does not need to map to
|
||||
# an existing user.
|
||||
if user_ref['type'] == utils.UserType.EPHEMERAL:
|
||||
if user_ref['type'] == federation_utils.UserType.EPHEMERAL:
|
||||
auth_context = {}
|
||||
auth_context['group_ids'] = user_ref['group_ids']
|
||||
auth_context[federation_constants.IDENTITY_PROVIDER] = (
|
||||
@ -130,9 +326,9 @@ class AuthContextMiddleware(provider_api.ProviderAPIMixin,
|
||||
|
||||
return False
|
||||
|
||||
@wsgi.middleware_exceptions
|
||||
@middleware_exceptions
|
||||
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
|
||||
# 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 "
|
||||
"'keystone-manage bootstrap' and will be removed in a "
|
||||
"future release.")
|
||||
request.environ[wsgi.CONTEXT_ENV] = context_env
|
||||
request.environ[CONTEXT_ENV] = context_env
|
||||
|
||||
if not context_env.get('is_admin', False):
|
||||
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
|
||||
# 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
|
||||
auth_context = {}
|
||||
|
||||
|
@ -12,6 +12,9 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
# NOTE(morgan): These test cases are used for AuthContextMiddleware exception
|
||||
# rendering.
|
||||
|
||||
import uuid
|
||||
|
||||
import fixtures
|
||||
@ -20,9 +23,9 @@ from oslo_log import log
|
||||
from oslo_serialization import jsonutils
|
||||
import six
|
||||
|
||||
from keystone.common import wsgi
|
||||
import keystone.conf
|
||||
from keystone import exception
|
||||
from keystone.server.flask.request_processing.middleware import auth_context
|
||||
from keystone.tests import unit
|
||||
|
||||
|
||||
@ -31,7 +34,7 @@ CONF = keystone.conf.CONF
|
||||
|
||||
class ExceptionTestCase(unit.BaseTestCase):
|
||||
def assertValidJsonRendering(self, e):
|
||||
resp = wsgi.render_exception(e)
|
||||
resp = auth_context.render_exception(e)
|
||||
self.assertEqual(e.code, resp.status_int)
|
||||
self.assertEqual('%s %s' % (e.code, e.title), resp.status)
|
||||
|
||||
@ -74,7 +77,7 @@ class ExceptionTestCase(unit.BaseTestCase):
|
||||
|
||||
def test_forbidden_title(self):
|
||||
e = exception.Forbidden()
|
||||
resp = wsgi.render_exception(e)
|
||||
resp = auth_context.render_exception(e)
|
||||
j = jsonutils.loads(resp.body)
|
||||
self.assertEqual('Forbidden', e.title)
|
||||
self.assertEqual('Forbidden', j['error'].get('title'))
|
||||
|
@ -23,7 +23,6 @@ import webtest
|
||||
from keystone.common import authorization
|
||||
from keystone.common import provider_api
|
||||
from keystone.common import tokenless_auth
|
||||
from keystone.common import wsgi
|
||||
import keystone.conf
|
||||
from keystone import exception
|
||||
from keystone.federation import constants as federation_constants
|
||||
@ -667,7 +666,7 @@ class AuthContextMiddlewareTest(test_backend_sql.SqlTests,
|
||||
log_fix = self.useFixture(fixtures.FakeLogger())
|
||||
headers = {authorization.AUTH_TOKEN_HEADER: 'ADMIN'}
|
||||
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)
|
||||
|
||||
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
|
||||
WebOb>=1.7.1 # MIT
|
||||
Routes>=2.3.1 # MIT
|
||||
Flask!=0.11,>=1.0.2 # BSD
|
||||
Flask-RESTful>=0.3.5 # BSD
|
||||
cryptography>=2.1 # BSD/Apache-2.0
|
||||
|
Loading…
Reference in New Issue
Block a user