Remove pre-flask legacy code

This removes common.controller, common.extension, common.router, and
common.wsgi. Relevant code from common.wsgi (used by AuthContext) was
moved into keystone.server.flask.request_processing.middleware.auth_context.

keystone.api.discovery now uses keystone.flask.base_url

test_middleware and test_exception were modified to reflect the changes
to the remaining code from keystone.common.wsgi

keystone.common.authorization only holds a couple constants for auth
work now.

Routes is removed from requirements.txt

Release-Note for migration to flask added.

Change-Id: I81563b6a49c8f12ecade058a9483f3b6f070dc72
Closes-Bug: #1776504
This commit is contained in:
Morgan Fainberg 2018-10-11 15:16:02 -07:00
parent 5c70aef2da
commit 184c84ae76
13 changed files with 240 additions and 1829 deletions

View File

@ -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']}),

View File

@ -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()

View File

@ -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

View File

@ -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."""

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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 = {}

View File

@ -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'))

View File

@ -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):

View File

@ -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.

View File

@ -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