+++ /dev/null
-Description: Subclass keystone middleware to set headers
- Replace old forked auth_token with a subclass of keystoneclient middleware.
- The advantages of moving to keystoneclient middleware:
- - can use v3 (or v2) keystone api
- - PKI tokens
- - token revocation
- .
- The subclass sets the following headers to be consumed by our request context
- filter:
- - X-Admin-User
- - X-Admin-Pass
- - X-Admin-Tenant-Name
- - X-Auth-Url
-Author: Steve Baker <steve@stevebaker.org>
-Origin: upstream, https://github.com/openstack/heat/commit/cfda18b43e2e5782e5595e54f4a48f30b860e5dd
-Last-Update: 2013-05-23
-
---- heat-2013.1.orig/heat/common/auth_token.py
-+++ heat-2013.1/heat/common/auth_token.py
-@@ -15,509 +15,24 @@
- # 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://keystone.openstack.org/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_TENANT_ID
-- Identity service managed unique identifier, string
--
--HTTP_X_TENANT_NAME
-- Unique tenant identifier, string
--
--HTTP_X_USER_ID
-- Identity-service managed unique identifier, string
--
--HTTP_X_USER_NAME
-- Unique user identifier, string
--
--HTTP_X_ROLES
-- Comma delimited list of case-sensitive Roles
--
--HTTP_X_TENANT
-- *Deprecated* in favor of HTTP_X_TENANT_ID and HTTP_X_TENANT_NAME
-- Keystone-assigned unique identifier, deprecated
--
--HTTP_X_USER
-- *Deprecated* in favor of HTTP_X_USER_ID and HTTP_X_USER_NAME
-- Unique user name, string
--
--HTTP_X_ROLE
-- *Deprecated* in favor of HTTP_X_ROLES
-- This is being renamed, and the new header contains the same data.
--
--"""
--
--import httplib
--import json
- import logging
--import time
--
--import webob
--import webob.exc
--
-+from keystoneclient.middleware import auth_token
-
- LOG = logging.getLogger(__name__)
-
-
--class InvalidUserToken(Exception):
-- pass
--
--
--class ServiceError(Exception):
-- pass
--
--
--class AuthProtocol(object):
-- """Auth Middleware that handles authenticating client calls."""
--
-- def __init__(self, app, conf):
-- 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 = int(conf.get('delay_auth_decision', 0))
--
-- # where to find the auth service (we use this to validate tokens)
-- self.auth_host = conf.get('auth_host')
-- self.auth_port = int(conf.get('auth_port', 35357))
-- auth_protocol = conf.get('auth_protocol', 'https')
-- if auth_protocol == 'http':
-- self.http_client_class = httplib.HTTPConnection
-- else:
-- self.http_client_class = httplib.HTTPSConnection
--
-- default_auth_uri = '%s://%s:%s' % (auth_protocol,
-- self.auth_host,
-- self.auth_port)
-- self.auth_uri = conf.get('auth_uri', default_auth_uri)
--
-- # Credentials used to verify this component with the Auth service since
-- # validating tokens is a privileged call
-- self.admin_token = conf.get('admin_token')
-- self.admin_user = conf.get('admin_user')
-- self.admin_password = conf.get('admin_password')
-- self.admin_tenant_name = conf.get('admin_tenant_name', 'admin')
--
-- # Token caching via memcache
-- self._cache = None
-- self._iso8601 = None
-- memcache_servers = conf.get('memcache_servers')
-- # By default the token will be cached for 5 minutes
-- self.token_cache_time = conf.get('token_cache_time', 300)
-- if memcache_servers:
-- try:
-- import memcache
-- import iso8601
-- LOG.info('Using memcache for caching token')
-- self._cache = memcache.Client(memcache_servers.split(','))
-- self._iso8601 = iso8601
-- except NameError as e:
-- LOG.warn('disabled caching due to missing libraries %s', e)
--
-- def __call__(self, env, start_response):
-- """Handle incoming request.
--
-- Authenticate send downstream on success. Reject request if
-- we can't authenticate.
--
-- """
-- LOG.debug('Authenticating user token')
-- try:
-- self._remove_auth_headers(env)
-- user_token = self._get_user_token_from_header(env)
-- token_info = self._validate_user_token(user_token)
-- 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:
-- LOG.info('Invalid user token - deferring reject downstream')
-- self._add_headers(env, {'X-Identity-Status': 'Invalid'})
-- return self.app(env, start_response)
-- else:
-- LOG.info('Invalid user token - rejecting request')
-- return self._reject_request(env, start_response)
--
-- except ServiceError, e:
-- LOG.critical('Unable to obtain admin token: %s' % e)
-- resp = webob.exc.HTTPServiceUnavailable()
-- return resp(env, start_response)
--
-- 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-Tenant-Id',
-- 'X-Tenant-Name',
-- 'X-User-Id',
-- 'X-User-Name',
-- 'X-Roles',
-- # Deprecated
-- 'X-User',
-- 'X-Tenant',
-- 'X-Role',
-- )
-- 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:
-- LOG.warn("Unable to find authentication token in 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 = webob.exc.HTTPUnauthorized('Authentication required', headers)
-- return resp(env, start_response)
--
-- def get_admin_token(self):
-- """Return admin token, possibly fetching a new one.
--
-- :return admin token id
-- :raise ServiceError when unable to retrieve token from keystone
--
-- """
-- if not self.admin_token:
-- self.admin_token = self._request_admin_token()
--
-- return self.admin_token
--
-- def _get_http_connection(self):
-- return self.http_client_class(self.auth_host, self.auth_port)
--
-- 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
--
-- """
-- conn = self._get_http_connection()
--
-- kwargs = {
-- 'headers': {
-- 'Content-type': 'application/json',
-- 'Accept': 'application/json',
-- },
-- }
--
-- if additional_headers:
-- kwargs['headers'].update(additional_headers)
--
-- if body:
-- kwargs['body'] = json.dumps(body)
--
-- try:
-- conn.request(method, path, **kwargs)
-- response = conn.getresponse()
-- body = response.read()
-- except Exception, e:
-- LOG.error('HTTP connection exception: %s' % e)
-- raise ServiceError('Unable to communicate with keystone')
-- finally:
-- conn.close()
--
-- try:
-- data = json.loads(body)
-- except ValueError:
-- 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
--
-- """
-- 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']
-- assert token
-- return token
-- except (AssertionError, KeyError):
-- LOG.warn("Unexpected response from keystone service: %s", data)
-- raise ServiceError('invalid json response')
--
-- def _validate_user_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
--
-- """
-- cached = self._cache_get(user_token)
-- if cached:
-- return cached
--
-- headers = {'X-Auth-Token': self.get_admin_token()}
-- response, data = self._json_request('GET',
-- '/v2.0/tokens/%s' % user_token,
-- additional_headers=headers)
--
-- if response.status == 200:
-- self._cache_put(user_token, data)
-- return data
-- if response.status == 404:
-- # FIXME(ja): I'm assuming the 404 status means that user_token is
-- # invalid - not that the admin_token is invalid
-- self._cache_store_invalid(user_token)
-- LOG.warn("Authorization failed for token %s", user_token)
-- raise InvalidUserToken('Token authorization failed')
-- if response.status == 401:
-- LOG.info('Keystone rejected admin token %s, resetting', headers)
-- self.admin_token = None
-- else:
-- LOG.error('Bad response code while validating token: %s' %
-- response.status)
-- if retry:
-- LOG.info('Retrying validation')
-- return self._validate_user_token(user_token, False)
-- else:
-- LOG.warn("Invalid user token: %s. Keystone response: %s.",
-- user_token, data)
--
-- raise InvalidUserToken()
--
-+class AuthProtocol(auth_token.AuthProtocol):
-+ """
-+ Subclass of keystoneclient auth_token middleware which also
-+ sets the 'X-Auth-Url' header to the value specified in the config.
-+ """
- def _build_user_headers(self, token_info):
-- """Convert token object into headers.
--
-- Build headers that represent authenticated user:
-- * X_IDENTITY_STATUS: Confirmed or Invalid
-- * X_TENANT_ID: id of tenant if tenant is present
-- * X_TENANT_NAME: name of tenant if tenant is present
-- * X_USER_ID: id of user
-- * X_USER_NAME: name of user
-- * X_ROLES: list of roles
--
-- Additional (deprecated) headers include:
-- * X_USER: name of user
-- * X_TENANT: For legacy compatibility before we had ID and Name
-- * X_ROLE: list of roles
--
-- :param token_info: token object returned by keystone on authentication
-- :raise InvalidUserToken when unable to parse token object
--
-- """
-- user = token_info['access']['user']
-- token = token_info['access']['token']
-- roles = ','.join([role['name'] for role in user.get('roles', [])])
--
-- def get_tenant_info():
-- """Returns a (tenant_id, tenant_name) tuple from context."""
-- def essex():
-- """Essex puts the tenant ID and name on the token."""
-- return (token['tenant']['id'], token['tenant']['name'])
--
-- def pre_diablo():
-- """Pre-diablo, Keystone only provided tenantId."""
-- return (token['tenantId'], token['tenantId'])
--
-- def default_tenant():
-- """Assume the user's default tenant."""
-- return (user['tenantId'], user['tenantName'])
--
-- for method in [essex, pre_diablo, default_tenant]:
-- try:
-- return method()
-- except KeyError:
-- pass
--
-- raise InvalidUserToken('Unable to determine tenancy.')
--
-- tenant_id, tenant_name = get_tenant_info()
--
-- user_id = user['id']
-- user_name = user['name']
--
-- return {
-- 'X-Identity-Status': 'Confirmed',
-- 'X-Tenant-Id': tenant_id,
-- 'X-Tenant-Name': tenant_name,
-- 'X-User-Id': user_id,
-- 'X-User-Name': user_name,
-- 'X-Roles': roles,
-- # Deprecated
-- 'X-User': user_name,
-- 'X-Tenant': tenant_name,
-- 'X-Role': roles,
-- 'X-Admin-User': self.admin_user,
-- 'X-Admin-Pass': self.admin_password,
-- 'X-Admin-Tenant-Name': self.admin_tenant_name,
-- 'X-Auth-Url': self.conf['auth_uri'],
-- }
--
-- 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 headers.iteritems():
-- 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):
-- """Return token information from cache.
--
-- If token is invalid raise InvalidUserToken
-- return token only if fresh (not expired).
-- """
-- if self._cache and token:
-- key = 'tokens/%s' % token
-- cached = self._cache.get(key)
-- if cached == 'invalid':
-- LOG.debug('Cached Token %s is marked unauthorized', token)
-- raise InvalidUserToken('Token authorization failed')
-- if cached:
-- data, expires = cached
-- if time.time() < float(expires):
-- LOG.debug('Returning cached token %s', token)
-- return data
-- else:
-- LOG.debug('Cached Token %s seems expired', token)
--
-- def _cache_put(self, token, data):
-- """Put token data into the cache.
--
-- Stores the parsed expire date in cache allowing
-- quick check of token freshness on retrieval.
-- """
-- if self._cache and data:
-- key = 'tokens/%s' % token
-- if 'token' in data.get('access', {}):
-- timestamp = data['access']['token']['expires']
-- expires = self._iso8601.parse_date(timestamp).strftime('%s')
-- else:
-- LOG.error('invalid token format')
-- return
-- LOG.debug('Storing %s token in memcache', token)
-- self._cache.set(key,
-- (data, expires),
-- time=self.token_cache_time)
--
-- def _cache_store_invalid(self, token):
-- """Store invalid token in cache."""
-- if self._cache:
-- key = 'tokens/%s' % token
-- LOG.debug('Marking token %s as unauthorized in memcache', token)
-- self._cache.set(key,
-- 'invalid',
-- time=self.token_cache_time)
--
-+ rval = super(AuthProtocol, self)._build_user_headers(token_info)
-+ rval['X-Auth-Url'] = self.auth_uri
-+ rval['X-Admin-User'] = self.admin_user
-+ rval['X-Admin-Pass'] = self.admin_password
-+ rval['X-Admin-Tenant-Name'] = self.admin_tenant_name
-+ return rval
-
- def filter_factory(global_conf, **local_conf):
- """Returns a WSGI filter app for use with paste.deploy."""
---- heat-2013.1.orig/etc/heat/heat-api-cfn-paste.ini
-+++ heat-2013.1/etc/heat/heat-api-cfn-paste.ini
-@@ -69,9 +69,6 @@ keystone_ec2_uri = http://localhost:5000
-
- [filter:authtoken]
- paste.filter_factory = heat.common.auth_token:filter_factory
--service_protocol = http
--service_host = 127.0.0.1
--service_port = 5000
- auth_host = 127.0.0.1
- auth_port = 35357
- auth_protocol = http
---- heat-2013.1.orig/etc/heat/heat-api-cloudwatch-paste.ini
-+++ heat-2013.1/etc/heat/heat-api-cloudwatch-paste.ini
-@@ -69,9 +69,6 @@ keystone_ec2_uri = http://localhost:5000
-
- [filter:authtoken]
- paste.filter_factory = heat.common.auth_token:filter_factory
--service_protocol = http
--service_host = 127.0.0.1
--service_port = 5000
- auth_host = 127.0.0.1
- auth_port = 35357
- auth_protocol = http
---- heat-2013.1.orig/etc/heat/heat-api-paste.ini
-+++ heat-2013.1/etc/heat/heat-api-paste.ini
-@@ -72,13 +72,10 @@ paste.filter_factory = heat.common.conte
-
- [filter:authtoken]
- paste.filter_factory = heat.common.auth_token:filter_factory
--service_protocol = http
--service_host = 127.0.0.1
--service_port = 5000
- auth_host = 127.0.0.1
- auth_port = 35357
- auth_protocol = http
--auth_uri = http://127.0.0.1:35357/v2.0
-+auth_uri = http://127.0.0.1:5000/v2.0
-
- # These must be set to your local values in order for the token
- # authentication to work.