1423 lines
		
	
	
		
			55 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			1423 lines
		
	
	
		
			55 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
# Copyright 2010-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.
 | 
						|
 | 
						|
"""
 | 
						|
TOKEN-BASED AUTH MIDDLEWARE
 | 
						|
 | 
						|
This WSGI component:
 | 
						|
 | 
						|
* Verifies that incoming client requests have valid tokens by validating
 | 
						|
  tokens with the auth service.
 | 
						|
* Rejects unauthenticated requests UNLESS it is in 'delay_auth_decision'
 | 
						|
  mode, which means the final decision is delegated to the downstream WSGI
 | 
						|
  component (usually the OpenStack service)
 | 
						|
* Collects and forwards identity information based on a valid token
 | 
						|
  such as user name, tenant, etc
 | 
						|
 | 
						|
Refer to: http://docs.openstack.org/developer/python-keystoneclient/
 | 
						|
middlewarearchitecture.html
 | 
						|
 | 
						|
HEADERS
 | 
						|
-------
 | 
						|
 | 
						|
* Headers starting with HTTP\_ is a standard http header
 | 
						|
* Headers starting with HTTP_X is an extended http header
 | 
						|
 | 
						|
Coming in from initial call from client or customer
 | 
						|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 | 
						|
 | 
						|
HTTP_X_AUTH_TOKEN
 | 
						|
    The client token being passed in.
 | 
						|
 | 
						|
HTTP_X_STORAGE_TOKEN
 | 
						|
    The client token being passed in (legacy Rackspace use) to support
 | 
						|
    swift/cloud files
 | 
						|
 | 
						|
Used for communication between components
 | 
						|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 | 
						|
 | 
						|
WWW-Authenticate
 | 
						|
    HTTP header returned to a user indicating which endpoint to use
 | 
						|
    to retrieve a new token
 | 
						|
 | 
						|
What we add to the request for use by the OpenStack service
 | 
						|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 | 
						|
 | 
						|
HTTP_X_IDENTITY_STATUS
 | 
						|
    'Confirmed' or 'Invalid'
 | 
						|
    The underlying service will only see a value of 'Invalid' if the Middleware
 | 
						|
    is configured to run in 'delay_auth_decision' mode
 | 
						|
 | 
						|
HTTP_X_DOMAIN_ID
 | 
						|
    Identity service managed unique identifier, string. Only present if
 | 
						|
    this is a domain-scoped v3 token.
 | 
						|
 | 
						|
HTTP_X_DOMAIN_NAME
 | 
						|
    Unique domain name, string. Only present if this is a domain-scoped
 | 
						|
    v3 token.
 | 
						|
 | 
						|
HTTP_X_PROJECT_ID
 | 
						|
    Identity service managed unique identifier, string. Only present if
 | 
						|
    this is a project-scoped v3 token, or a tenant-scoped v2 token.
 | 
						|
 | 
						|
HTTP_X_PROJECT_NAME
 | 
						|
    Project name, unique within owning domain, string. Only present if
 | 
						|
    this is a project-scoped v3 token, or a tenant-scoped v2 token.
 | 
						|
 | 
						|
HTTP_X_PROJECT_DOMAIN_ID
 | 
						|
    Identity service managed unique identifier of owning domain of
 | 
						|
    project, string.  Only present if this is a project-scoped v3 token. If
 | 
						|
    this variable is set, this indicates that the PROJECT_NAME can only
 | 
						|
    be assumed to be unique within this domain.
 | 
						|
 | 
						|
HTTP_X_PROJECT_DOMAIN_NAME
 | 
						|
    Name of owning domain of project, string. Only present if this is a
 | 
						|
    project-scoped v3 token. If this variable is set, this indicates that
 | 
						|
    the PROJECT_NAME can only be assumed to be unique within this domain.
 | 
						|
 | 
						|
HTTP_X_USER_ID
 | 
						|
    Identity-service managed unique identifier, string
 | 
						|
 | 
						|
HTTP_X_USER_NAME
 | 
						|
    User identifier, unique within owning domain, string
 | 
						|
 | 
						|
HTTP_X_USER_DOMAIN_ID
 | 
						|
    Identity service managed unique identifier of owning domain of
 | 
						|
    user, string. If this variable is set, this indicates that the USER_NAME
 | 
						|
    can only be assumed to be unique within this domain.
 | 
						|
 | 
						|
HTTP_X_USER_DOMAIN_NAME
 | 
						|
    Name of owning domain of user, string. If this variable is set, this
 | 
						|
    indicates that the USER_NAME can only be assumed to be unique within
 | 
						|
    this domain.
 | 
						|
 | 
						|
HTTP_X_ROLES
 | 
						|
    Comma delimited list of case-sensitive role names
 | 
						|
 | 
						|
HTTP_X_SERVICE_CATALOG
 | 
						|
    json encoded keystone service catalog (optional).
 | 
						|
 | 
						|
HTTP_X_TENANT_ID
 | 
						|
    *Deprecated* in favor of HTTP_X_PROJECT_ID
 | 
						|
    Identity service managed unique identifier, string. For v3 tokens, this
 | 
						|
    will be set to the same value as HTTP_X_PROJECT_ID
 | 
						|
 | 
						|
HTTP_X_TENANT_NAME
 | 
						|
    *Deprecated* in favor of HTTP_X_PROJECT_NAME
 | 
						|
    Project identifier, unique within owning domain, string. For v3 tokens,
 | 
						|
    this will be set to the same value as HTTP_X_PROJECT_NAME
 | 
						|
 | 
						|
HTTP_X_TENANT
 | 
						|
    *Deprecated* in favor of HTTP_X_TENANT_ID and HTTP_X_TENANT_NAME
 | 
						|
    Keystone-assigned unique identifier, string. For v3 tokens, this
 | 
						|
    will be set to the same value as HTTP_X_PROJECT_ID
 | 
						|
 | 
						|
HTTP_X_USER
 | 
						|
    *Deprecated* in favor of HTTP_X_USER_ID and HTTP_X_USER_NAME
 | 
						|
    User name, unique within owning domain, string
 | 
						|
 | 
						|
HTTP_X_ROLE
 | 
						|
    *Deprecated* in favor of HTTP_X_ROLES
 | 
						|
    Will contain the same values as HTTP_X_ROLES.
 | 
						|
 | 
						|
OTHER ENVIRONMENT VARIABLES
 | 
						|
---------------------------
 | 
						|
 | 
						|
keystone.token_info
 | 
						|
    Information about the token discovered in the process of
 | 
						|
    validation.  This may include extended information returned by the
 | 
						|
    Keystone token validation call, as well as basic information about
 | 
						|
    the tenant and user.
 | 
						|
 | 
						|
"""
 | 
						|
 | 
						|
import contextlib
 | 
						|
import datetime
 | 
						|
import logging
 | 
						|
import os
 | 
						|
import requests
 | 
						|
import stat
 | 
						|
import tempfile
 | 
						|
import time
 | 
						|
 | 
						|
import netaddr
 | 
						|
from oslo.config import cfg
 | 
						|
import six
 | 
						|
from six.moves import urllib
 | 
						|
 | 
						|
from keystoneclient import access
 | 
						|
from keystoneclient.common import cms
 | 
						|
from keystoneclient import exceptions
 | 
						|
from keystoneclient.middleware import memcache_crypt
 | 
						|
from keystoneclient.openstack.common import jsonutils
 | 
						|
from keystoneclient.openstack.common import memorycache
 | 
						|
from keystoneclient.openstack.common import timeutils
 | 
						|
from keystoneclient import utils
 | 
						|
 | 
						|
 | 
						|
# alternative middleware configuration in the main application's
 | 
						|
# configuration file e.g. in nova.conf
 | 
						|
# [keystone_authtoken]
 | 
						|
# auth_host = 127.0.0.1
 | 
						|
# auth_port = 35357
 | 
						|
# auth_protocol = http
 | 
						|
# admin_tenant_name = admin
 | 
						|
# admin_user = admin
 | 
						|
# admin_password = badpassword
 | 
						|
 | 
						|
# when deploy Keystone auth_token middleware with Swift, user may elect
 | 
						|
# to use Swift memcache instead of the local Keystone memcache. Swift memcache
 | 
						|
# is passed in from the request environment and its identified by the
 | 
						|
# 'swift.cache' key. However it could be different, depending on deployment.
 | 
						|
# To use Swift memcache, you must set the 'cache' option to the environment
 | 
						|
# key where the Swift cache object is stored.
 | 
						|
 | 
						|
 | 
						|
# NOTE(jamielennox): A number of options below are deprecated however are left
 | 
						|
# in the list and only mentioned as deprecated in the help string. This is
 | 
						|
# because we have to provide the same deprecation functionality for arguments
 | 
						|
# passed in via the conf in __init__ (from paste) and there is no way to test
 | 
						|
# that the default value was set or not in CONF.
 | 
						|
# Also if we were to remove the options from the CONF list (as typical CONF
 | 
						|
# deprecation works) then other projects will not be able to override the
 | 
						|
# options via CONF.
 | 
						|
 | 
						|
opts = [
 | 
						|
    cfg.StrOpt('auth_admin_prefix',
 | 
						|
               default='',
 | 
						|
               help='Prefix to prepend at the beginning of the path. '
 | 
						|
                    'Deprecated, use identity_uri.'),
 | 
						|
    cfg.StrOpt('auth_host',
 | 
						|
               default='127.0.0.1',
 | 
						|
               help='Host providing the admin Identity API endpoint. '
 | 
						|
                    'Deprecated, use identity_uri.'),
 | 
						|
    cfg.IntOpt('auth_port',
 | 
						|
               default=35357,
 | 
						|
               help='Port of the admin Identity API endpoint. '
 | 
						|
                    'Deprecated, use identity_uri.'),
 | 
						|
    cfg.StrOpt('auth_protocol',
 | 
						|
               default='https',
 | 
						|
               help='Protocol of the admin Identity API endpoint '
 | 
						|
                    '(http or https). Deprecated, use identity_uri.'),
 | 
						|
    cfg.StrOpt('auth_uri',
 | 
						|
               default=None,
 | 
						|
               # FIXME(dolph): should be default='http://127.0.0.1:5000/v2.0/',
 | 
						|
               # or (depending on client support) an unversioned, publicly
 | 
						|
               # accessible identity endpoint (see bug 1207517)
 | 
						|
               help='Complete public Identity API endpoint'),
 | 
						|
    cfg.StrOpt('identity_uri',
 | 
						|
               default=None,
 | 
						|
               help='Complete admin Identity API endpoint. This should '
 | 
						|
                    'specify the unversioned root endpoint '
 | 
						|
                    'eg. https://localhost:35357/'),
 | 
						|
    cfg.StrOpt('auth_version',
 | 
						|
               default=None,
 | 
						|
               help='API version of the admin Identity API endpoint'),
 | 
						|
    cfg.BoolOpt('delay_auth_decision',
 | 
						|
                default=False,
 | 
						|
                help='Do not handle authorization requests within the'
 | 
						|
                ' middleware, but delegate the authorization decision to'
 | 
						|
                ' downstream WSGI components'),
 | 
						|
    cfg.BoolOpt('http_connect_timeout',
 | 
						|
                default=None,
 | 
						|
                help='Request timeout value for communicating with Identity'
 | 
						|
                ' API server.'),
 | 
						|
    cfg.IntOpt('http_request_max_retries',
 | 
						|
               default=3,
 | 
						|
               help='How many times are we trying to reconnect when'
 | 
						|
               ' communicating with Identity API Server.'),
 | 
						|
    cfg.StrOpt('admin_token',
 | 
						|
               secret=True,
 | 
						|
               help='Single shared secret with the Keystone configuration'
 | 
						|
               ' used for bootstrapping a Keystone installation, or otherwise'
 | 
						|
               ' bypassing the normal authentication process.'),
 | 
						|
    cfg.StrOpt('admin_user',
 | 
						|
               help='Keystone account username'),
 | 
						|
    cfg.StrOpt('admin_password',
 | 
						|
               secret=True,
 | 
						|
               help='Keystone account password'),
 | 
						|
    cfg.StrOpt('admin_tenant_name',
 | 
						|
               default='admin',
 | 
						|
               help='Keystone service account tenant name to validate'
 | 
						|
               ' user tokens'),
 | 
						|
    cfg.StrOpt('cache',
 | 
						|
               default=None,
 | 
						|
               help='Env key for the swift cache'),
 | 
						|
    cfg.StrOpt('certfile',
 | 
						|
               help='Required if Keystone server requires client certificate'),
 | 
						|
    cfg.StrOpt('keyfile',
 | 
						|
               help='Required if Keystone server requires client certificate'),
 | 
						|
    cfg.StrOpt('cafile', default=None,
 | 
						|
               help='A PEM encoded Certificate Authority to use when '
 | 
						|
                    'verifying HTTPs connections. Defaults to system CAs.'),
 | 
						|
    cfg.BoolOpt('insecure', default=False, help='Verify HTTPS connections.'),
 | 
						|
    cfg.StrOpt('signing_dir',
 | 
						|
               help='Directory used to cache files related to PKI tokens'),
 | 
						|
    cfg.ListOpt('memcached_servers',
 | 
						|
                deprecated_name='memcache_servers',
 | 
						|
                help='Optionally specify a list of memcached server(s) to'
 | 
						|
                ' use for caching. If left undefined, tokens will instead be'
 | 
						|
                ' cached in-process.'),
 | 
						|
    cfg.IntOpt('token_cache_time',
 | 
						|
               default=300,
 | 
						|
               help='In order to prevent excessive effort spent validating'
 | 
						|
               ' tokens, the middleware caches previously-seen tokens for a'
 | 
						|
               ' configurable duration (in seconds). Set to -1 to disable'
 | 
						|
               ' caching completely.'),
 | 
						|
    cfg.IntOpt('revocation_cache_time',
 | 
						|
               default=300,
 | 
						|
               help='Determines the frequency at which the list of revoked'
 | 
						|
               ' tokens is retrieved from the Identity service (in seconds). A'
 | 
						|
               ' high number of revocation events combined with a low cache'
 | 
						|
               ' duration may significantly reduce performance.'),
 | 
						|
    cfg.StrOpt('memcache_security_strategy',
 | 
						|
               default=None,
 | 
						|
               help='(optional) if defined, indicate whether token data'
 | 
						|
               ' should be authenticated or authenticated and encrypted.'
 | 
						|
               ' Acceptable values are MAC or ENCRYPT.  If MAC, token data is'
 | 
						|
               ' authenticated (with HMAC) in the cache. If ENCRYPT, token'
 | 
						|
               ' data is encrypted and authenticated in the cache. If the'
 | 
						|
               ' value is not one of these options or empty, auth_token will'
 | 
						|
               ' raise an exception on initialization.'),
 | 
						|
    cfg.StrOpt('memcache_secret_key',
 | 
						|
               default=None,
 | 
						|
               secret=True,
 | 
						|
               help='(optional, mandatory if memcache_security_strategy is'
 | 
						|
               ' defined) this string is used for key derivation.'),
 | 
						|
    cfg.BoolOpt('include_service_catalog',
 | 
						|
                default=True,
 | 
						|
                help='(optional) indicate whether to set the X-Service-Catalog'
 | 
						|
                ' header. If False, middleware will not ask for service'
 | 
						|
                ' catalog on token validation and will not set the'
 | 
						|
                ' X-Service-Catalog header.'),
 | 
						|
    cfg.StrOpt('enforce_token_bind',
 | 
						|
               default='permissive',
 | 
						|
               help='Used to control the use and type of token binding. Can'
 | 
						|
               ' be set to: "disabled" to not check token binding.'
 | 
						|
               ' "permissive" (default) to validate binding information if the'
 | 
						|
               ' bind type is of a form known to the server and ignore it if'
 | 
						|
               ' not. "strict" like "permissive" but if the bind type is'
 | 
						|
               ' unknown the token will be rejected. "required" any form of'
 | 
						|
               ' token binding is needed to be allowed. Finally the name of a'
 | 
						|
               ' binding method that must be present in tokens.'),
 | 
						|
]
 | 
						|
 | 
						|
CONF = cfg.CONF
 | 
						|
CONF.register_opts(opts, group='keystone_authtoken')
 | 
						|
 | 
						|
LIST_OF_VERSIONS_TO_ATTEMPT = ['v2.0', 'v3.0']
 | 
						|
CACHE_KEY_TEMPLATE = 'tokens/%s'
 | 
						|
 | 
						|
 | 
						|
class BIND_MODE:
 | 
						|
    DISABLED = 'disabled'
 | 
						|
    PERMISSIVE = 'permissive'
 | 
						|
    STRICT = 'strict'
 | 
						|
    REQUIRED = 'required'
 | 
						|
    KERBEROS = 'kerberos'
 | 
						|
 | 
						|
 | 
						|
def will_expire_soon(expiry):
 | 
						|
    """Determines if expiration is about to occur.
 | 
						|
 | 
						|
    :param expiry: a datetime of the expected expiration
 | 
						|
    :returns: boolean : true if expiration is within 30 seconds
 | 
						|
    """
 | 
						|
    soon = (timeutils.utcnow() + datetime.timedelta(seconds=30))
 | 
						|
    return expiry < soon
 | 
						|
 | 
						|
 | 
						|
def _token_is_v2(token_info):
 | 
						|
    return ('access' in token_info)
 | 
						|
 | 
						|
 | 
						|
def _token_is_v3(token_info):
 | 
						|
    return ('token' in token_info)
 | 
						|
 | 
						|
 | 
						|
def confirm_token_not_expired(data):
 | 
						|
    if not data:
 | 
						|
        raise InvalidUserToken('Token authorization failed')
 | 
						|
    if _token_is_v2(data):
 | 
						|
        timestamp = data['access']['token']['expires']
 | 
						|
    elif _token_is_v3(data):
 | 
						|
        timestamp = data['token']['expires_at']
 | 
						|
    else:
 | 
						|
        raise InvalidUserToken('Token authorization failed')
 | 
						|
    expires = timeutils.parse_isotime(timestamp)
 | 
						|
    expires = timeutils.normalize_time(expires)
 | 
						|
    utcnow = timeutils.utcnow()
 | 
						|
    if utcnow >= expires:
 | 
						|
        raise InvalidUserToken('Token authorization failed')
 | 
						|
    return timeutils.isotime(at=expires, subsecond=True)
 | 
						|
 | 
						|
 | 
						|
def safe_quote(s):
 | 
						|
    """URL-encode strings that are not already URL-encoded."""
 | 
						|
    return urllib.parse.quote(s) if s == urllib.parse.unquote(s) else s
 | 
						|
 | 
						|
 | 
						|
class InvalidUserToken(Exception):
 | 
						|
    pass
 | 
						|
 | 
						|
 | 
						|
class ServiceError(Exception):
 | 
						|
    pass
 | 
						|
 | 
						|
 | 
						|
class ConfigurationError(Exception):
 | 
						|
    pass
 | 
						|
 | 
						|
 | 
						|
class NetworkError(Exception):
 | 
						|
    pass
 | 
						|
 | 
						|
 | 
						|
class MiniResp(object):
 | 
						|
    def __init__(self, error_message, env, headers=[]):
 | 
						|
        # The HEAD method is unique: it must never return a body, even if
 | 
						|
        # it reports an error (RFC-2616 clause 9.4). We relieve callers
 | 
						|
        # from varying the error responses depending on the method.
 | 
						|
        if env['REQUEST_METHOD'] == 'HEAD':
 | 
						|
            self.body = ['']
 | 
						|
        else:
 | 
						|
            self.body = [error_message]
 | 
						|
        self.headers = list(headers)
 | 
						|
        self.headers.append(('Content-type', 'text/plain'))
 | 
						|
 | 
						|
 | 
						|
class AuthProtocol(object):
 | 
						|
    """Auth Middleware that handles authenticating client calls."""
 | 
						|
 | 
						|
    def __init__(self, app, conf):
 | 
						|
        self.LOG = logging.getLogger(conf.get('log_name', __name__))
 | 
						|
        self.LOG.info('Starting keystone auth_token middleware')
 | 
						|
        self.conf = conf
 | 
						|
        self.app = app
 | 
						|
 | 
						|
        # delay_auth_decision means we still allow unauthenticated requests
 | 
						|
        # through and we let the downstream service make the final decision
 | 
						|
        self.delay_auth_decision = (self._conf_get('delay_auth_decision') in
 | 
						|
                                    (True, 'true', 't', '1', 'on', 'yes', 'y'))
 | 
						|
 | 
						|
        # where to find the auth service (we use this to validate tokens)
 | 
						|
        self.identity_uri = self._conf_get('identity_uri')
 | 
						|
        self.auth_uri = self._conf_get('auth_uri')
 | 
						|
 | 
						|
        # NOTE(jamielennox): it does appear here that our defaults arguments
 | 
						|
        # are backwards. We need to do it this way so that we can handle the
 | 
						|
        # same deprecation strategy for CONF and the conf variable.
 | 
						|
        if not self.identity_uri:
 | 
						|
            self.LOG.warning('Configuring admin URI using auth fragments. '
 | 
						|
                             'This is deprecated, use \'identity_uri\''
 | 
						|
                             ' instead.')
 | 
						|
 | 
						|
            auth_host = self._conf_get('auth_host')
 | 
						|
            auth_port = int(self._conf_get('auth_port'))
 | 
						|
            auth_protocol = self._conf_get('auth_protocol')
 | 
						|
            auth_admin_prefix = self._conf_get('auth_admin_prefix')
 | 
						|
 | 
						|
            if netaddr.valid_ipv6(auth_host):
 | 
						|
                # Note(dzyu) it is an IPv6 address, so it needs to be wrapped
 | 
						|
                # with '[]' to generate a valid IPv6 URL, based on
 | 
						|
                # http://www.ietf.org/rfc/rfc2732.txt
 | 
						|
                auth_host = '[%s]' % auth_host
 | 
						|
 | 
						|
            self.identity_uri = '%s://%s:%s' % (auth_protocol, auth_host,
 | 
						|
                                                auth_port)
 | 
						|
            if auth_admin_prefix:
 | 
						|
                self.identity_uri = '%s/%s' % (self.identity_uri,
 | 
						|
                                               auth_admin_prefix.strip('/'))
 | 
						|
        else:
 | 
						|
            self.identity_uri = self.identity_uri.rstrip('/')
 | 
						|
 | 
						|
        if self.auth_uri is None:
 | 
						|
            self.LOG.warning(
 | 
						|
                'Configuring auth_uri to point to the public identity '
 | 
						|
                'endpoint is required; clients may not be able to '
 | 
						|
                'authenticate against an admin endpoint')
 | 
						|
 | 
						|
            # FIXME(dolph): drop support for this fallback behavior as
 | 
						|
            # documented in bug 1207517.
 | 
						|
            # NOTE(jamielennox): we urljoin '/' to get just the base URI as
 | 
						|
            # this is the original behaviour.
 | 
						|
            self.auth_uri = urllib.parse.urljoin(self.identity_uri, '/')
 | 
						|
            self.auth_uri = self.auth_uri.rstrip('/')
 | 
						|
 | 
						|
        # SSL
 | 
						|
        self.cert_file = self._conf_get('certfile')
 | 
						|
        self.key_file = self._conf_get('keyfile')
 | 
						|
        self.ssl_ca_file = self._conf_get('cafile')
 | 
						|
        self.ssl_insecure = self._conf_get('insecure')
 | 
						|
 | 
						|
        # signing
 | 
						|
        self.signing_dirname = self._conf_get('signing_dir')
 | 
						|
        if self.signing_dirname is None:
 | 
						|
            self.signing_dirname = tempfile.mkdtemp(prefix='keystone-signing-')
 | 
						|
        self.LOG.info('Using %s as cache directory for signing certificate',
 | 
						|
                      self.signing_dirname)
 | 
						|
        self.verify_signing_dir()
 | 
						|
 | 
						|
        val = '%s/signing_cert.pem' % self.signing_dirname
 | 
						|
        self.signing_cert_file_name = val
 | 
						|
        val = '%s/cacert.pem' % self.signing_dirname
 | 
						|
        self.signing_ca_file_name = val
 | 
						|
        val = '%s/revoked.pem' % self.signing_dirname
 | 
						|
        self.revoked_file_name = val
 | 
						|
 | 
						|
        # Credentials used to verify this component with the Auth service since
 | 
						|
        # validating tokens is a privileged call
 | 
						|
        self.admin_token = self._conf_get('admin_token')
 | 
						|
        self.admin_token_expiry = None
 | 
						|
        self.admin_user = self._conf_get('admin_user')
 | 
						|
        self.admin_password = self._conf_get('admin_password')
 | 
						|
        self.admin_tenant_name = self._conf_get('admin_tenant_name')
 | 
						|
 | 
						|
        # Token caching
 | 
						|
        self._cache_pool = None
 | 
						|
        self._cache_initialized = False
 | 
						|
        # memcache value treatment, ENCRYPT or MAC
 | 
						|
        self._memcache_security_strategy = (
 | 
						|
            self._conf_get('memcache_security_strategy'))
 | 
						|
        if self._memcache_security_strategy is not None:
 | 
						|
            self._memcache_security_strategy = (
 | 
						|
                self._memcache_security_strategy.upper())
 | 
						|
        self._memcache_secret_key = (
 | 
						|
            self._conf_get('memcache_secret_key'))
 | 
						|
        self._assert_valid_memcache_protection_config()
 | 
						|
        # By default the token will be cached for 5 minutes
 | 
						|
        self.token_cache_time = int(self._conf_get('token_cache_time'))
 | 
						|
        self._token_revocation_list = None
 | 
						|
        self._token_revocation_list_fetched_time = None
 | 
						|
        self.token_revocation_list_cache_timeout = datetime.timedelta(
 | 
						|
            seconds=self._conf_get('revocation_cache_time'))
 | 
						|
        http_connect_timeout_cfg = self._conf_get('http_connect_timeout')
 | 
						|
        self.http_connect_timeout = (http_connect_timeout_cfg and
 | 
						|
                                     int(http_connect_timeout_cfg))
 | 
						|
        self.auth_version = None
 | 
						|
        self.http_request_max_retries = (
 | 
						|
            self._conf_get('http_request_max_retries'))
 | 
						|
 | 
						|
        self.include_service_catalog = self._conf_get(
 | 
						|
            'include_service_catalog')
 | 
						|
 | 
						|
    def _assert_valid_memcache_protection_config(self):
 | 
						|
        if self._memcache_security_strategy:
 | 
						|
            if self._memcache_security_strategy not in ('MAC', 'ENCRYPT'):
 | 
						|
                raise ConfigurationError('memcache_security_strategy must be '
 | 
						|
                                         'ENCRYPT or MAC')
 | 
						|
            if not self._memcache_secret_key:
 | 
						|
                raise ConfigurationError('memcache_secret_key must be defined '
 | 
						|
                                         'when a memcache_security_strategy '
 | 
						|
                                         'is defined')
 | 
						|
 | 
						|
    def _init_cache(self, env):
 | 
						|
        self._cache_pool = CachePool(
 | 
						|
            env.get(self._conf_get('cache')),
 | 
						|
            self._conf_get('memcached_servers'))
 | 
						|
        self._cache_initialized = True
 | 
						|
 | 
						|
    def _conf_get(self, name):
 | 
						|
        # try config from paste-deploy first
 | 
						|
        if name in self.conf:
 | 
						|
            return self.conf[name]
 | 
						|
        else:
 | 
						|
            return CONF.keystone_authtoken[name]
 | 
						|
 | 
						|
    def _choose_api_version(self):
 | 
						|
        """Determine the api version that we should use."""
 | 
						|
 | 
						|
        # If the configuration specifies an auth_version we will just
 | 
						|
        # assume that is correct and use it.  We could, of course, check
 | 
						|
        # that this version is supported by the server, but in case
 | 
						|
        # there are some problems in the field, we want as little code
 | 
						|
        # as possible in the way of letting auth_token talk to the
 | 
						|
        # server.
 | 
						|
        if self._conf_get('auth_version'):
 | 
						|
            version_to_use = self._conf_get('auth_version')
 | 
						|
            self.LOG.info('Auth Token proceeding with requested %s apis',
 | 
						|
                          version_to_use)
 | 
						|
        else:
 | 
						|
            version_to_use = None
 | 
						|
            versions_supported_by_server = self._get_supported_versions()
 | 
						|
            if versions_supported_by_server:
 | 
						|
                for version in LIST_OF_VERSIONS_TO_ATTEMPT:
 | 
						|
                    if version in versions_supported_by_server:
 | 
						|
                        version_to_use = version
 | 
						|
                        break
 | 
						|
            if version_to_use:
 | 
						|
                self.LOG.info('Auth Token confirmed use of %s apis',
 | 
						|
                              version_to_use)
 | 
						|
            else:
 | 
						|
                self.LOG.error(
 | 
						|
                    'Attempted versions [%s] not in list supported by '
 | 
						|
                    'server [%s]',
 | 
						|
                    ', '.join(LIST_OF_VERSIONS_TO_ATTEMPT),
 | 
						|
                    ', '.join(versions_supported_by_server))
 | 
						|
                raise ServiceError('No compatible apis supported by server')
 | 
						|
        return version_to_use
 | 
						|
 | 
						|
    def _get_supported_versions(self):
 | 
						|
        versions = []
 | 
						|
        response, data = self._json_request('GET', '/')
 | 
						|
        if response.status_code == 501:
 | 
						|
            self.LOG.warning('Old keystone installation found...assuming v2.0')
 | 
						|
            versions.append('v2.0')
 | 
						|
        elif response.status_code != 300:
 | 
						|
            self.LOG.error('Unable to get version info from keystone: %s',
 | 
						|
                           response.status_code)
 | 
						|
            raise ServiceError('Unable to get version info from keystone')
 | 
						|
        else:
 | 
						|
            try:
 | 
						|
                for version in data['versions']['values']:
 | 
						|
                    versions.append(version['id'])
 | 
						|
            except KeyError:
 | 
						|
                self.LOG.error(
 | 
						|
                    'Invalid version response format from server')
 | 
						|
                raise ServiceError('Unable to parse version response '
 | 
						|
                                   'from keystone')
 | 
						|
 | 
						|
        self.LOG.debug('Server reports support for api versions: %s',
 | 
						|
                       ', '.join(versions))
 | 
						|
        return versions
 | 
						|
 | 
						|
    def __call__(self, env, start_response):
 | 
						|
        """Handle incoming request.
 | 
						|
 | 
						|
        Authenticate send downstream on success. Reject request if
 | 
						|
        we can't authenticate.
 | 
						|
 | 
						|
        """
 | 
						|
        self.LOG.debug('Authenticating user token')
 | 
						|
 | 
						|
        # initialize memcache if we haven't done so
 | 
						|
        if not self._cache_initialized:
 | 
						|
            self._init_cache(env)
 | 
						|
 | 
						|
        try:
 | 
						|
            self._remove_auth_headers(env)
 | 
						|
            user_token = self._get_user_token_from_header(env)
 | 
						|
            token_info = self._validate_user_token(user_token, env)
 | 
						|
            env['keystone.token_info'] = token_info
 | 
						|
            user_headers = self._build_user_headers(token_info)
 | 
						|
            self._add_headers(env, user_headers)
 | 
						|
            return self.app(env, start_response)
 | 
						|
 | 
						|
        except InvalidUserToken:
 | 
						|
            if self.delay_auth_decision:
 | 
						|
                self.LOG.info(
 | 
						|
                    'Invalid user token - deferring reject downstream')
 | 
						|
                self._add_headers(env, {'X-Identity-Status': 'Invalid'})
 | 
						|
                return self.app(env, start_response)
 | 
						|
            else:
 | 
						|
                self.LOG.info('Invalid user token - rejecting request')
 | 
						|
                return self._reject_request(env, start_response)
 | 
						|
 | 
						|
        except ServiceError as e:
 | 
						|
            self.LOG.critical('Unable to obtain admin token: %s', e)
 | 
						|
            resp = MiniResp('Service unavailable', env)
 | 
						|
            start_response('503 Service Unavailable', resp.headers)
 | 
						|
            return resp.body
 | 
						|
 | 
						|
    def _remove_auth_headers(self, env):
 | 
						|
        """Remove headers so a user can't fake authentication.
 | 
						|
 | 
						|
        :param env: wsgi request environment
 | 
						|
 | 
						|
        """
 | 
						|
        auth_headers = (
 | 
						|
            'X-Identity-Status',
 | 
						|
            'X-Domain-Id',
 | 
						|
            'X-Domain-Name',
 | 
						|
            'X-Project-Id',
 | 
						|
            'X-Project-Name',
 | 
						|
            'X-Project-Domain-Id',
 | 
						|
            'X-Project-Domain-Name',
 | 
						|
            'X-User-Id',
 | 
						|
            'X-User-Name',
 | 
						|
            'X-User-Domain-Id',
 | 
						|
            'X-User-Domain-Name',
 | 
						|
            'X-Roles',
 | 
						|
            'X-Service-Catalog',
 | 
						|
            # Deprecated
 | 
						|
            'X-User',
 | 
						|
            'X-Tenant-Id',
 | 
						|
            'X-Tenant-Name',
 | 
						|
            'X-Tenant',
 | 
						|
            'X-Role',
 | 
						|
        )
 | 
						|
        self.LOG.debug('Removing headers from request environment: %s',
 | 
						|
                       ','.join(auth_headers))
 | 
						|
        self._remove_headers(env, auth_headers)
 | 
						|
 | 
						|
    def _get_user_token_from_header(self, env):
 | 
						|
        """Get token id from request.
 | 
						|
 | 
						|
        :param env: wsgi request environment
 | 
						|
        :return token id
 | 
						|
        :raises InvalidUserToken if no token is provided in request
 | 
						|
 | 
						|
        """
 | 
						|
        token = self._get_header(env, 'X-Auth-Token',
 | 
						|
                                 self._get_header(env, 'X-Storage-Token'))
 | 
						|
        if token:
 | 
						|
            return token
 | 
						|
        else:
 | 
						|
            if not self.delay_auth_decision:
 | 
						|
                self.LOG.warn('Unable to find authentication token'
 | 
						|
                              ' in headers')
 | 
						|
                self.LOG.debug('Headers: %s', env)
 | 
						|
            raise InvalidUserToken('Unable to find token in headers')
 | 
						|
 | 
						|
    def _reject_request(self, env, start_response):
 | 
						|
        """Redirect client to auth server.
 | 
						|
 | 
						|
        :param env: wsgi request environment
 | 
						|
        :param start_response: wsgi response callback
 | 
						|
        :returns HTTPUnauthorized http response
 | 
						|
 | 
						|
        """
 | 
						|
        headers = [('WWW-Authenticate', 'Keystone uri=\'%s\'' % self.auth_uri)]
 | 
						|
        resp = MiniResp('Authentication required', env, headers)
 | 
						|
        start_response('401 Unauthorized', resp.headers)
 | 
						|
        return resp.body
 | 
						|
 | 
						|
    def get_admin_token(self):
 | 
						|
        """Return admin token, possibly fetching a new one.
 | 
						|
 | 
						|
        if self.admin_token_expiry is set from fetching an admin token, check
 | 
						|
        it for expiration, and request a new token is the existing token
 | 
						|
        is about to expire.
 | 
						|
 | 
						|
        :return admin token id
 | 
						|
        :raise ServiceError when unable to retrieve token from keystone
 | 
						|
 | 
						|
        """
 | 
						|
        if self.admin_token_expiry:
 | 
						|
            if will_expire_soon(self.admin_token_expiry):
 | 
						|
                self.admin_token = None
 | 
						|
 | 
						|
        if not self.admin_token:
 | 
						|
            (self.admin_token,
 | 
						|
             self.admin_token_expiry) = self._request_admin_token()
 | 
						|
 | 
						|
        return self.admin_token
 | 
						|
 | 
						|
    def _http_request(self, method, path, **kwargs):
 | 
						|
        """HTTP request helper used to make unspecified content type requests.
 | 
						|
 | 
						|
        :param method: http method
 | 
						|
        :param path: relative request url
 | 
						|
        :return (http response object, response body)
 | 
						|
        :raise ServerError when unable to communicate with keystone
 | 
						|
 | 
						|
        """
 | 
						|
        url = '%s/%s' % (self.identity_uri, path.lstrip('/'))
 | 
						|
 | 
						|
        kwargs.setdefault('timeout', self.http_connect_timeout)
 | 
						|
        if self.cert_file and self.key_file:
 | 
						|
            kwargs['cert'] = (self.cert_file, self.key_file)
 | 
						|
        elif self.cert_file or self.key_file:
 | 
						|
            self.LOG.warn('Cannot use only a cert or key file. '
 | 
						|
                          'Please provide both. Ignoring.')
 | 
						|
 | 
						|
        kwargs['verify'] = self.ssl_ca_file or True
 | 
						|
        if self.ssl_insecure:
 | 
						|
            kwargs['verify'] = False
 | 
						|
 | 
						|
        RETRIES = self.http_request_max_retries
 | 
						|
        retry = 0
 | 
						|
        while True:
 | 
						|
            try:
 | 
						|
                response = requests.request(method, url, **kwargs)
 | 
						|
                break
 | 
						|
            except Exception as e:
 | 
						|
                if retry >= RETRIES:
 | 
						|
                    self.LOG.error('HTTP connection exception: %s', e)
 | 
						|
                    raise NetworkError('Unable to communicate with keystone')
 | 
						|
                # NOTE(vish): sleep 0.5, 1, 2
 | 
						|
                self.LOG.warn('Retrying on HTTP connection exception: %s', e)
 | 
						|
                time.sleep(2.0 ** retry / 2)
 | 
						|
                retry += 1
 | 
						|
 | 
						|
        return response
 | 
						|
 | 
						|
    def _json_request(self, method, path, body=None, additional_headers=None):
 | 
						|
        """HTTP request helper used to make json requests.
 | 
						|
 | 
						|
        :param method: http method
 | 
						|
        :param path: relative request url
 | 
						|
        :param body: dict to encode to json as request body. Optional.
 | 
						|
        :param additional_headers: dict of additional headers to send with
 | 
						|
                                   http request. Optional.
 | 
						|
        :return (http response object, response body parsed as json)
 | 
						|
        :raise ServerError when unable to communicate with keystone
 | 
						|
 | 
						|
        """
 | 
						|
        kwargs = {
 | 
						|
            'headers': {
 | 
						|
                'Content-type': 'application/json',
 | 
						|
                'Accept': 'application/json',
 | 
						|
            },
 | 
						|
        }
 | 
						|
 | 
						|
        if additional_headers:
 | 
						|
            kwargs['headers'].update(additional_headers)
 | 
						|
 | 
						|
        if body:
 | 
						|
            kwargs['data'] = jsonutils.dumps(body)
 | 
						|
 | 
						|
        response = self._http_request(method, path, **kwargs)
 | 
						|
 | 
						|
        try:
 | 
						|
            data = jsonutils.loads(response.text)
 | 
						|
        except ValueError:
 | 
						|
            self.LOG.debug('Keystone did not return json-encoded body')
 | 
						|
            data = {}
 | 
						|
 | 
						|
        return response, data
 | 
						|
 | 
						|
    def _request_admin_token(self):
 | 
						|
        """Retrieve new token as admin user from keystone.
 | 
						|
 | 
						|
        :return token id upon success
 | 
						|
        :raises ServerError when unable to communicate with keystone
 | 
						|
 | 
						|
        Irrespective of the auth version we are going to use for the
 | 
						|
        user token, for simplicity we always use a v2 admin token to
 | 
						|
        validate the user token.
 | 
						|
 | 
						|
        """
 | 
						|
        params = {
 | 
						|
            'auth': {
 | 
						|
                'passwordCredentials': {
 | 
						|
                    'username': self.admin_user,
 | 
						|
                    'password': self.admin_password,
 | 
						|
                },
 | 
						|
                'tenantName': self.admin_tenant_name,
 | 
						|
            }
 | 
						|
        }
 | 
						|
 | 
						|
        response, data = self._json_request('POST',
 | 
						|
                                            '/v2.0/tokens',
 | 
						|
                                            body=params)
 | 
						|
 | 
						|
        try:
 | 
						|
            token = data['access']['token']['id']
 | 
						|
            expiry = data['access']['token']['expires']
 | 
						|
            if not (token and expiry):
 | 
						|
                raise AssertionError('invalid token or expire')
 | 
						|
            datetime_expiry = timeutils.parse_isotime(expiry)
 | 
						|
            return (token, timeutils.normalize_time(datetime_expiry))
 | 
						|
        except (AssertionError, KeyError):
 | 
						|
            self.LOG.warn(
 | 
						|
                'Unexpected response from keystone service: %s', data)
 | 
						|
            raise ServiceError('invalid json response')
 | 
						|
        except (ValueError):
 | 
						|
            data['access']['token']['id'] = '<SANITIZED>'
 | 
						|
            self.LOG.warn(
 | 
						|
                'Unable to parse expiration time from token: %s', data)
 | 
						|
            raise ServiceError('invalid json response')
 | 
						|
 | 
						|
    def _validate_user_token(self, user_token, env, retry=True):
 | 
						|
        """Authenticate user using PKI
 | 
						|
 | 
						|
        :param user_token: user's token id
 | 
						|
        :param retry: Ignored, as it is not longer relevant
 | 
						|
        :return uncrypted body of the token if the token is valid
 | 
						|
        :raise InvalidUserToken if token is rejected
 | 
						|
        :no longer raises ServiceError since it no longer makes RPC
 | 
						|
 | 
						|
        """
 | 
						|
        token_id = None
 | 
						|
 | 
						|
        try:
 | 
						|
            token_id = cms.cms_hash_token(user_token)
 | 
						|
            cached = self._cache_get(token_id)
 | 
						|
            if cached:
 | 
						|
                return cached
 | 
						|
            if cms.is_asn1_token(user_token):
 | 
						|
                verified = self.verify_signed_token(user_token)
 | 
						|
                data = jsonutils.loads(verified)
 | 
						|
            else:
 | 
						|
                data = self.verify_uuid_token(user_token, retry)
 | 
						|
            expires = confirm_token_not_expired(data)
 | 
						|
            self._confirm_token_bind(data, env)
 | 
						|
            self._cache_put(token_id, data, expires)
 | 
						|
            return data
 | 
						|
        except NetworkError:
 | 
						|
            self.LOG.debug('Token validation failure.', exc_info=True)
 | 
						|
            self.LOG.warn('Authorization failed for token')
 | 
						|
            raise InvalidUserToken('Token authorization failed')
 | 
						|
        except Exception:
 | 
						|
            self.LOG.debug('Token validation failure.', exc_info=True)
 | 
						|
            if token_id:
 | 
						|
                self._cache_store_invalid(token_id)
 | 
						|
            self.LOG.warn('Authorization failed for token')
 | 
						|
            raise InvalidUserToken('Token authorization failed')
 | 
						|
 | 
						|
    def _build_user_headers(self, token_info):
 | 
						|
        """Convert token object into headers.
 | 
						|
 | 
						|
        Build headers that represent authenticated user - see main
 | 
						|
        doc info at start of file for details of headers to be defined.
 | 
						|
 | 
						|
        :param token_info: token object returned by keystone on authentication
 | 
						|
        :raise InvalidUserToken when unable to parse token object
 | 
						|
 | 
						|
        """
 | 
						|
        auth_ref = access.AccessInfo.factory(body=token_info)
 | 
						|
        roles = ','.join(auth_ref.role_names)
 | 
						|
 | 
						|
        if _token_is_v2(token_info) and not auth_ref.project_id:
 | 
						|
            raise InvalidUserToken('Unable to determine tenancy.')
 | 
						|
 | 
						|
        rval = {
 | 
						|
            'X-Identity-Status': 'Confirmed',
 | 
						|
            'X-Domain-Id': auth_ref.domain_id,
 | 
						|
            'X-Domain-Name': auth_ref.domain_name,
 | 
						|
            'X-Project-Id': auth_ref.project_id,
 | 
						|
            'X-Project-Name': auth_ref.project_name,
 | 
						|
            'X-Project-Domain-Id': auth_ref.project_domain_id,
 | 
						|
            'X-Project-Domain-Name': auth_ref.project_domain_name,
 | 
						|
            'X-User-Id': auth_ref.user_id,
 | 
						|
            'X-User-Name': auth_ref.username,
 | 
						|
            'X-User-Domain-Id': auth_ref.user_domain_id,
 | 
						|
            'X-User-Domain-Name': auth_ref.user_domain_name,
 | 
						|
            'X-Roles': roles,
 | 
						|
            # Deprecated
 | 
						|
            'X-User': auth_ref.username,
 | 
						|
            'X-Tenant-Id': auth_ref.project_id,
 | 
						|
            'X-Tenant-Name': auth_ref.project_name,
 | 
						|
            'X-Tenant': auth_ref.project_name,
 | 
						|
            'X-Role': roles,
 | 
						|
        }
 | 
						|
 | 
						|
        self.LOG.debug('Received request from user: %s with project_id : %s'
 | 
						|
                       ' and roles: %s ',
 | 
						|
                       auth_ref.user_id, auth_ref.project_id, roles)
 | 
						|
 | 
						|
        if self.include_service_catalog and auth_ref.has_service_catalog():
 | 
						|
            catalog = auth_ref.service_catalog.get_data()
 | 
						|
            rval['X-Service-Catalog'] = jsonutils.dumps(catalog)
 | 
						|
 | 
						|
        return rval
 | 
						|
 | 
						|
    def _header_to_env_var(self, key):
 | 
						|
        """Convert header to wsgi env variable.
 | 
						|
 | 
						|
        :param key: http header name (ex. 'X-Auth-Token')
 | 
						|
        :return wsgi env variable name (ex. 'HTTP_X_AUTH_TOKEN')
 | 
						|
 | 
						|
        """
 | 
						|
        return 'HTTP_%s' % key.replace('-', '_').upper()
 | 
						|
 | 
						|
    def _add_headers(self, env, headers):
 | 
						|
        """Add http headers to environment."""
 | 
						|
        for (k, v) in six.iteritems(headers):
 | 
						|
            env_key = self._header_to_env_var(k)
 | 
						|
            env[env_key] = v
 | 
						|
 | 
						|
    def _remove_headers(self, env, keys):
 | 
						|
        """Remove http headers from environment."""
 | 
						|
        for k in keys:
 | 
						|
            env_key = self._header_to_env_var(k)
 | 
						|
            try:
 | 
						|
                del env[env_key]
 | 
						|
            except KeyError:
 | 
						|
                pass
 | 
						|
 | 
						|
    def _get_header(self, env, key, default=None):
 | 
						|
        """Get http header from environment."""
 | 
						|
        env_key = self._header_to_env_var(key)
 | 
						|
        return env.get(env_key, default)
 | 
						|
 | 
						|
    def _cache_get(self, token_id, ignore_expires=False):
 | 
						|
        """Return token information from cache.
 | 
						|
 | 
						|
        If token is invalid raise InvalidUserToken
 | 
						|
        return token only if fresh (not expired).
 | 
						|
        """
 | 
						|
 | 
						|
        if token_id:
 | 
						|
            if self._memcache_security_strategy is None:
 | 
						|
                key = CACHE_KEY_TEMPLATE % token_id
 | 
						|
                with self._cache_pool.reserve() as cache:
 | 
						|
                    serialized = cache.get(key)
 | 
						|
            else:
 | 
						|
                secret_key = self._memcache_secret_key
 | 
						|
                if isinstance(secret_key, six.string_types):
 | 
						|
                    secret_key = secret_key.encode('utf-8')
 | 
						|
                security_strategy = self._memcache_security_strategy
 | 
						|
                if isinstance(security_strategy, six.string_types):
 | 
						|
                    security_strategy = security_strategy.encode('utf-8')
 | 
						|
                keys = memcache_crypt.derive_keys(
 | 
						|
                    token_id,
 | 
						|
                    secret_key,
 | 
						|
                    security_strategy)
 | 
						|
                cache_key = CACHE_KEY_TEMPLATE % (
 | 
						|
                    memcache_crypt.get_cache_key(keys))
 | 
						|
                with self._cache_pool.reserve() as cache:
 | 
						|
                    raw_cached = cache.get(cache_key)
 | 
						|
                try:
 | 
						|
                    # unprotect_data will return None if raw_cached is None
 | 
						|
                    serialized = memcache_crypt.unprotect_data(keys,
 | 
						|
                                                               raw_cached)
 | 
						|
                except Exception:
 | 
						|
                    msg = 'Failed to decrypt/verify cache data'
 | 
						|
                    self.LOG.exception(msg)
 | 
						|
                    # this should have the same effect as data not
 | 
						|
                    # found in cache
 | 
						|
                    serialized = None
 | 
						|
 | 
						|
            if serialized is None:
 | 
						|
                return None
 | 
						|
 | 
						|
            # Note that 'invalid' and (data, expires) are the only
 | 
						|
            # valid types of serialized cache entries, so there is not
 | 
						|
            # a collision with jsonutils.loads(serialized) == None.
 | 
						|
            if not isinstance(serialized, six.string_types):
 | 
						|
                serialized = serialized.decode('utf-8')
 | 
						|
            cached = jsonutils.loads(serialized)
 | 
						|
            if cached == 'invalid':
 | 
						|
                self.LOG.debug('Cached Token is marked unauthorized')
 | 
						|
                raise InvalidUserToken('Token authorization failed')
 | 
						|
 | 
						|
            data, expires = cached
 | 
						|
 | 
						|
            try:
 | 
						|
                expires = timeutils.parse_isotime(expires)
 | 
						|
            except ValueError:
 | 
						|
                # Gracefully handle upgrade of expiration times from *nix
 | 
						|
                # timestamps to ISO 8601 formatted dates by ignoring old cached
 | 
						|
                # values.
 | 
						|
                return
 | 
						|
 | 
						|
            expires = timeutils.normalize_time(expires)
 | 
						|
            utcnow = timeutils.utcnow()
 | 
						|
            if ignore_expires or utcnow < expires:
 | 
						|
                self.LOG.debug('Returning cached token')
 | 
						|
                return data
 | 
						|
            else:
 | 
						|
                self.LOG.debug('Cached Token seems expired')
 | 
						|
 | 
						|
    def _cache_store(self, token_id, data):
 | 
						|
        """Store value into memcache.
 | 
						|
 | 
						|
        data may be the string 'invalid' or a tuple like (data, expires)
 | 
						|
 | 
						|
        """
 | 
						|
        serialized_data = jsonutils.dumps(data)
 | 
						|
        if isinstance(serialized_data, six.text_type):
 | 
						|
            serialized_data = serialized_data.encode('utf-8')
 | 
						|
        if self._memcache_security_strategy is None:
 | 
						|
            cache_key = CACHE_KEY_TEMPLATE % token_id
 | 
						|
            data_to_store = serialized_data
 | 
						|
        else:
 | 
						|
            secret_key = self._memcache_secret_key
 | 
						|
            if isinstance(secret_key, six.string_types):
 | 
						|
                secret_key = secret_key.encode('utf-8')
 | 
						|
            security_strategy = self._memcache_security_strategy
 | 
						|
            if isinstance(security_strategy, six.string_types):
 | 
						|
                security_strategy = security_strategy.encode('utf-8')
 | 
						|
            keys = memcache_crypt.derive_keys(
 | 
						|
                token_id, secret_key, security_strategy)
 | 
						|
            cache_key = CACHE_KEY_TEMPLATE % memcache_crypt.get_cache_key(keys)
 | 
						|
            data_to_store = memcache_crypt.protect_data(keys, serialized_data)
 | 
						|
 | 
						|
        with self._cache_pool.reserve() as cache:
 | 
						|
            cache.set(cache_key, data_to_store, time=self.token_cache_time)
 | 
						|
 | 
						|
    def _invalid_user_token(self, msg=False):
 | 
						|
        # NOTE(jamielennox): use False as the default so that None is valid
 | 
						|
        if msg is False:
 | 
						|
            msg = 'Token authorization failed'
 | 
						|
 | 
						|
        raise InvalidUserToken(msg)
 | 
						|
 | 
						|
    def _confirm_token_bind(self, data, env):
 | 
						|
        bind_mode = self._conf_get('enforce_token_bind')
 | 
						|
 | 
						|
        if bind_mode == BIND_MODE.DISABLED:
 | 
						|
            return
 | 
						|
 | 
						|
        try:
 | 
						|
            if _token_is_v2(data):
 | 
						|
                bind = data['access']['token']['bind']
 | 
						|
            elif _token_is_v3(data):
 | 
						|
                bind = data['token']['bind']
 | 
						|
            else:
 | 
						|
                self._invalid_user_token()
 | 
						|
        except KeyError:
 | 
						|
            bind = {}
 | 
						|
 | 
						|
        # permissive and strict modes don't require there to be a bind
 | 
						|
        permissive = bind_mode in (BIND_MODE.PERMISSIVE, BIND_MODE.STRICT)
 | 
						|
 | 
						|
        if not bind:
 | 
						|
            if permissive:
 | 
						|
                # no bind provided and none required
 | 
						|
                return
 | 
						|
            else:
 | 
						|
                self.LOG.info('No bind information present in token.')
 | 
						|
                self._invalid_user_token()
 | 
						|
 | 
						|
        # get the named mode if bind_mode is not one of the predefined
 | 
						|
        if permissive or bind_mode == BIND_MODE.REQUIRED:
 | 
						|
            name = None
 | 
						|
        else:
 | 
						|
            name = bind_mode
 | 
						|
 | 
						|
        if name and name not in bind:
 | 
						|
            self.LOG.info('Named bind mode %s not in bind information', name)
 | 
						|
            self._invalid_user_token()
 | 
						|
 | 
						|
        for bind_type, identifier in six.iteritems(bind):
 | 
						|
            if bind_type == BIND_MODE.KERBEROS:
 | 
						|
                if not env.get('AUTH_TYPE', '').lower() == 'negotiate':
 | 
						|
                    self.LOG.info('Kerberos credentials required and '
 | 
						|
                                  'not present.')
 | 
						|
                    self._invalid_user_token()
 | 
						|
 | 
						|
                if not env.get('REMOTE_USER') == identifier:
 | 
						|
                    self.LOG.info('Kerberos credentials do not match '
 | 
						|
                                  'those in bind.')
 | 
						|
                    self._invalid_user_token()
 | 
						|
 | 
						|
                self.LOG.debug('Kerberos bind authentication successful.')
 | 
						|
 | 
						|
            elif bind_mode == BIND_MODE.PERMISSIVE:
 | 
						|
                self.LOG.debug('Ignoring Unknown bind for permissive mode: '
 | 
						|
                               '%(bind_type)s: %(identifier)s.',
 | 
						|
                               {'bind_type': bind_type,
 | 
						|
                                'identifier': identifier})
 | 
						|
 | 
						|
            else:
 | 
						|
                self.LOG.info('Couldn`t verify unknown bind: %(bind_type)s: '
 | 
						|
                              '%(identifier)s.',
 | 
						|
                              {'bind_type': bind_type,
 | 
						|
                               'identifier': identifier})
 | 
						|
                self._invalid_user_token()
 | 
						|
 | 
						|
    def _cache_put(self, token_id, data, expires):
 | 
						|
        """Put token data into the cache.
 | 
						|
 | 
						|
        Stores the parsed expire date in cache allowing
 | 
						|
        quick check of token freshness on retrieval.
 | 
						|
 | 
						|
        """
 | 
						|
        self.LOG.debug('Storing token in cache')
 | 
						|
        self._cache_store(token_id, (data, expires))
 | 
						|
 | 
						|
    def _cache_store_invalid(self, token_id):
 | 
						|
        """Store invalid token in cache."""
 | 
						|
        self.LOG.debug('Marking token as unauthorized in cache')
 | 
						|
        self._cache_store(token_id, 'invalid')
 | 
						|
 | 
						|
    def verify_uuid_token(self, user_token, retry=True):
 | 
						|
        """Authenticate user token with keystone.
 | 
						|
 | 
						|
        :param user_token: user's token id
 | 
						|
        :param retry: flag that forces the middleware to retry
 | 
						|
                      user authentication when an indeterminate
 | 
						|
                      response is received. Optional.
 | 
						|
        :return: token object received from keystone on success
 | 
						|
        :raise InvalidUserToken: if token is rejected
 | 
						|
        :raise ServiceError: if unable to authenticate token
 | 
						|
 | 
						|
        """
 | 
						|
        # Determine the highest api version we can use.
 | 
						|
        if not self.auth_version:
 | 
						|
            self.auth_version = self._choose_api_version()
 | 
						|
 | 
						|
        if self.auth_version == 'v3.0':
 | 
						|
            headers = {'X-Auth-Token': self.get_admin_token(),
 | 
						|
                       'X-Subject-Token': safe_quote(user_token)}
 | 
						|
            path = '/v3/auth/tokens'
 | 
						|
            if not self.include_service_catalog:
 | 
						|
                # NOTE(gyee): only v3 API support this option
 | 
						|
                path = path + '?nocatalog'
 | 
						|
            response, data = self._json_request(
 | 
						|
                'GET',
 | 
						|
                path,
 | 
						|
                additional_headers=headers)
 | 
						|
        else:
 | 
						|
            headers = {'X-Auth-Token': self.get_admin_token()}
 | 
						|
            response, data = self._json_request(
 | 
						|
                'GET',
 | 
						|
                '/v2.0/tokens/%s' % safe_quote(user_token),
 | 
						|
                additional_headers=headers)
 | 
						|
 | 
						|
        if response.status_code == 200:
 | 
						|
            return data
 | 
						|
        if response.status_code == 404:
 | 
						|
            self.LOG.warn('Authorization failed for token')
 | 
						|
            raise InvalidUserToken('Token authorization failed')
 | 
						|
        if response.status_code == 401:
 | 
						|
            self.LOG.info(
 | 
						|
                'Keystone rejected admin token, resetting')
 | 
						|
            self.admin_token = None
 | 
						|
        else:
 | 
						|
            self.LOG.error('Bad response code while validating token: %s',
 | 
						|
                           response.status_code)
 | 
						|
        if retry:
 | 
						|
            self.LOG.info('Retrying validation')
 | 
						|
            return self.verify_uuid_token(user_token, False)
 | 
						|
        else:
 | 
						|
            self.LOG.warn('Invalid user token. Keystone response: %s', data)
 | 
						|
 | 
						|
            raise InvalidUserToken()
 | 
						|
 | 
						|
    def is_signed_token_revoked(self, signed_text):
 | 
						|
        """Indicate whether the token appears in the revocation list."""
 | 
						|
        revocation_list = self.token_revocation_list
 | 
						|
        revoked_tokens = revocation_list.get('revoked', [])
 | 
						|
        if not revoked_tokens:
 | 
						|
            return
 | 
						|
        revoked_ids = (x['id'] for x in revoked_tokens)
 | 
						|
        if isinstance(signed_text, six.text_type):
 | 
						|
            signed_text = signed_text.encode('utf-8')
 | 
						|
        token_id = utils.hash_signed_token(signed_text)
 | 
						|
        for revoked_id in revoked_ids:
 | 
						|
            if token_id == revoked_id:
 | 
						|
                self.LOG.debug('Token is marked as having been revoked')
 | 
						|
                return True
 | 
						|
        return False
 | 
						|
 | 
						|
    def cms_verify(self, data):
 | 
						|
        """Verifies the signature of the provided data's IAW CMS syntax.
 | 
						|
 | 
						|
        If either of the certificate files might be missing, fetch them and
 | 
						|
        retry.
 | 
						|
        """
 | 
						|
        def verify():
 | 
						|
            try:
 | 
						|
                return cms.cms_verify(data, self.signing_cert_file_name,
 | 
						|
                                      self.signing_ca_file_name)
 | 
						|
            except cms.subprocess.CalledProcessError as err:
 | 
						|
                self.LOG.warning('Verify error: %s', err)
 | 
						|
                raise
 | 
						|
 | 
						|
        try:
 | 
						|
            return verify()
 | 
						|
        except exceptions.CertificateConfigError:
 | 
						|
            # the certs might be missing; unconditionally fetch to avoid racing
 | 
						|
            self.fetch_signing_cert()
 | 
						|
            self.fetch_ca_cert()
 | 
						|
 | 
						|
            try:
 | 
						|
                # retry with certs in place
 | 
						|
                return verify()
 | 
						|
            except exceptions.CertificateConfigError as err:
 | 
						|
                # if this is still occurring, something else is wrong and we
 | 
						|
                # need err.output to identify the problem
 | 
						|
                self.LOG.error('CMS Verify output: %s', err.output)
 | 
						|
                raise
 | 
						|
 | 
						|
    def verify_signed_token(self, signed_text):
 | 
						|
        """Check that the token is unrevoked and has a valid signature."""
 | 
						|
        if self.is_signed_token_revoked(signed_text):
 | 
						|
            raise InvalidUserToken('Token has been revoked')
 | 
						|
 | 
						|
        formatted = cms.token_to_cms(signed_text)
 | 
						|
        return self.cms_verify(formatted)
 | 
						|
 | 
						|
    def verify_signing_dir(self):
 | 
						|
        if os.path.exists(self.signing_dirname):
 | 
						|
            if not os.access(self.signing_dirname, os.W_OK):
 | 
						|
                raise ConfigurationError(
 | 
						|
                    'unable to access signing_dir %s' % self.signing_dirname)
 | 
						|
            uid = os.getuid()
 | 
						|
            if os.stat(self.signing_dirname).st_uid != uid:
 | 
						|
                self.LOG.warning(
 | 
						|
                    'signing_dir is not owned by %s', uid)
 | 
						|
            current_mode = stat.S_IMODE(os.stat(self.signing_dirname).st_mode)
 | 
						|
            if current_mode != stat.S_IRWXU:
 | 
						|
                self.LOG.warning(
 | 
						|
                    'signing_dir mode is %s instead of %s',
 | 
						|
                    oct(current_mode), oct(stat.S_IRWXU))
 | 
						|
        else:
 | 
						|
            os.makedirs(self.signing_dirname, stat.S_IRWXU)
 | 
						|
 | 
						|
    @property
 | 
						|
    def token_revocation_list_fetched_time(self):
 | 
						|
        if not self._token_revocation_list_fetched_time:
 | 
						|
            # If the fetched list has been written to disk, use its
 | 
						|
            # modification time.
 | 
						|
            if os.path.exists(self.revoked_file_name):
 | 
						|
                mtime = os.path.getmtime(self.revoked_file_name)
 | 
						|
                fetched_time = datetime.datetime.utcfromtimestamp(mtime)
 | 
						|
            # Otherwise the list will need to be fetched.
 | 
						|
            else:
 | 
						|
                fetched_time = datetime.datetime.min
 | 
						|
            self._token_revocation_list_fetched_time = fetched_time
 | 
						|
        return self._token_revocation_list_fetched_time
 | 
						|
 | 
						|
    @token_revocation_list_fetched_time.setter
 | 
						|
    def token_revocation_list_fetched_time(self, value):
 | 
						|
        self._token_revocation_list_fetched_time = value
 | 
						|
 | 
						|
    @property
 | 
						|
    def token_revocation_list(self):
 | 
						|
        timeout = (self.token_revocation_list_fetched_time +
 | 
						|
                   self.token_revocation_list_cache_timeout)
 | 
						|
        list_is_current = timeutils.utcnow() < timeout
 | 
						|
 | 
						|
        if list_is_current:
 | 
						|
            # Load the list from disk if required
 | 
						|
            if not self._token_revocation_list:
 | 
						|
                open_kwargs = {'encoding': 'utf-8'} if six.PY3 else {}
 | 
						|
                with open(self.revoked_file_name, 'r', **open_kwargs) as f:
 | 
						|
                    self._token_revocation_list = jsonutils.loads(f.read())
 | 
						|
        else:
 | 
						|
            self.token_revocation_list = self.fetch_revocation_list()
 | 
						|
        return self._token_revocation_list
 | 
						|
 | 
						|
    def _atomic_write_to_signing_dir(self, file_name, value):
 | 
						|
        # In Python2, encoding is slow so the following check avoids it if it
 | 
						|
        # is not absolutely necessary.
 | 
						|
        if isinstance(value, six.text_type):
 | 
						|
            value = value.encode('utf-8')
 | 
						|
 | 
						|
        def _atomic_write(destination, data):
 | 
						|
            with tempfile.NamedTemporaryFile(dir=self.signing_dirname,
 | 
						|
                                             delete=False) as f:
 | 
						|
                f.write(data)
 | 
						|
            os.rename(f.name, destination)
 | 
						|
 | 
						|
        try:
 | 
						|
            _atomic_write(file_name, value)
 | 
						|
        except (OSError, IOError):
 | 
						|
            self.verify_signing_dir()
 | 
						|
            _atomic_write(file_name, value)
 | 
						|
 | 
						|
    @token_revocation_list.setter
 | 
						|
    def token_revocation_list(self, value):
 | 
						|
        """Save a revocation list to memory and to disk.
 | 
						|
 | 
						|
        :param value: A json-encoded revocation list
 | 
						|
 | 
						|
        """
 | 
						|
        self._token_revocation_list = jsonutils.loads(value)
 | 
						|
        self.token_revocation_list_fetched_time = timeutils.utcnow()
 | 
						|
        self._atomic_write_to_signing_dir(self.revoked_file_name, value)
 | 
						|
 | 
						|
    def fetch_revocation_list(self, retry=True):
 | 
						|
        headers = {'X-Auth-Token': self.get_admin_token()}
 | 
						|
        response, data = self._json_request('GET', '/v2.0/tokens/revoked',
 | 
						|
                                            additional_headers=headers)
 | 
						|
        if response.status_code == 401:
 | 
						|
            if retry:
 | 
						|
                self.LOG.info(
 | 
						|
                    'Keystone rejected admin token, resetting admin token')
 | 
						|
                self.admin_token = None
 | 
						|
                return self.fetch_revocation_list(retry=False)
 | 
						|
        if response.status_code != 200:
 | 
						|
            raise ServiceError('Unable to fetch token revocation list.')
 | 
						|
        if 'signed' not in data:
 | 
						|
            raise ServiceError('Revocation list improperly formatted.')
 | 
						|
        return self.cms_verify(data['signed'])
 | 
						|
 | 
						|
    def _fetch_cert_file(self, cert_file_name, cert_type):
 | 
						|
        if not self.auth_version:
 | 
						|
            self.auth_version = self._choose_api_version()
 | 
						|
 | 
						|
        if self.auth_version == 'v3.0':
 | 
						|
            if cert_type == 'signing':
 | 
						|
                cert_type = 'certificates'
 | 
						|
            path = '/v3/OS-SIMPLE-CERT/' + cert_type
 | 
						|
        else:
 | 
						|
            path = '/v2.0/certificates/' + cert_type
 | 
						|
        response = self._http_request('GET', path)
 | 
						|
        if response.status_code != 200:
 | 
						|
            raise exceptions.CertificateConfigError(response.text)
 | 
						|
        self._atomic_write_to_signing_dir(cert_file_name, response.text)
 | 
						|
 | 
						|
    def fetch_signing_cert(self):
 | 
						|
        self._fetch_cert_file(self.signing_cert_file_name, 'signing')
 | 
						|
 | 
						|
    def fetch_ca_cert(self):
 | 
						|
        self._fetch_cert_file(self.signing_ca_file_name, 'ca')
 | 
						|
 | 
						|
 | 
						|
class CachePool(list):
 | 
						|
    """A lazy pool of cache references."""
 | 
						|
 | 
						|
    def __init__(self, cache, memcached_servers):
 | 
						|
        self._environment_cache = cache
 | 
						|
        self._memcached_servers = memcached_servers
 | 
						|
 | 
						|
    @contextlib.contextmanager
 | 
						|
    def reserve(self):
 | 
						|
        """Context manager to manage a pooled cache reference."""
 | 
						|
        if self._environment_cache is not None:
 | 
						|
            # skip pooling and just use the cache from the upstream filter
 | 
						|
            yield self._environment_cache
 | 
						|
            return  # otherwise the context manager will continue!
 | 
						|
 | 
						|
        try:
 | 
						|
            c = self.pop()
 | 
						|
        except IndexError:
 | 
						|
            # the pool is empty, so we need to create a new client
 | 
						|
            c = memorycache.get_client(self._memcached_servers)
 | 
						|
 | 
						|
        try:
 | 
						|
            yield c
 | 
						|
        finally:
 | 
						|
            self.append(c)
 | 
						|
 | 
						|
 | 
						|
def filter_factory(global_conf, **local_conf):
 | 
						|
    """Returns a WSGI filter app for use with paste.deploy."""
 | 
						|
    conf = global_conf.copy()
 | 
						|
    conf.update(local_conf)
 | 
						|
 | 
						|
    def auth_filter(app):
 | 
						|
        return AuthProtocol(app, conf)
 | 
						|
    return auth_filter
 | 
						|
 | 
						|
 | 
						|
def app_factory(global_conf, **local_conf):
 | 
						|
    conf = global_conf.copy()
 | 
						|
    conf.update(local_conf)
 | 
						|
    return AuthProtocol(None, conf)
 | 
						|
 | 
						|
 | 
						|
if __name__ == '__main__':
 | 
						|
    """Run this module directly to start a protected echo service::
 | 
						|
 | 
						|
        $ python -m keystoneclient.middleware.auth_token
 | 
						|
 | 
						|
    When the ``auth_token`` module authenticates a request, the echo service
 | 
						|
    will respond with all the environment variables presented to it by this
 | 
						|
    module.
 | 
						|
 | 
						|
    """
 | 
						|
    def echo_app(environ, start_response):
 | 
						|
        """A WSGI application that echoes the CGI environment to the user."""
 | 
						|
        start_response('200 OK', [('Content-Type', 'application/json')])
 | 
						|
        environment = dict((k, v) for k, v in six.iteritems(environ)
 | 
						|
                           if k.startswith('HTTP_X_'))
 | 
						|
        yield jsonutils.dumps(environment)
 | 
						|
 | 
						|
    from wsgiref import simple_server
 | 
						|
 | 
						|
    # hardcode any non-default configuration here
 | 
						|
    conf = {'auth_protocol': 'http', 'admin_token': 'ADMIN'}
 | 
						|
    app = AuthProtocol(echo_app, conf)
 | 
						|
    server = simple_server.make_server('', 8000, app)
 | 
						|
    print('Serving on port 8000 (Ctrl+C to end)...')
 | 
						|
    server.serve_forever()
 |