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