]> review.fuel-infra Code Review - openstack-build/heat-build.git/commitdiff
Subclass keystone middleware to set headers
authorSteve Baker <sbaker@redhat.com>
Mon, 8 Apr 2013 03:35:55 +0000 (15:35 +1200)
committerSteve Baker <sbaker@redhat.com>
Mon, 22 Apr 2013 22:28:58 +0000 (10:28 +1200)
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

The need to override _build_user_headers should be a short-term thing,
X-Admin-* isn't actually used currently, and there are a few options
that need to be discussed for getting X-Auth-Url to the engine.

Change-Id: Iacc5046fbf559724a4ae0bd6091d662e23d65544
Blueprint: keystone-middleware

etc/heat/heat-api-cfn-paste.ini
etc/heat/heat-api-cloudwatch-paste.ini
etc/heat/heat-api-paste.ini
heat/common/auth_token.py

index 18625852dc0192f04abd0ec13a796b6a07e6d186..83adcd67b46d34fbf3912640f17ca7fa133138b7 100644 (file)
@@ -29,9 +29,6 @@ keystone_ec2_uri = http://localhost:5000/v2.0/ec2tokens
 
 [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
index af144be51d0ec3975ad9b1932ca1c6ffba82dc2a..3396e5bdc6f7e178771f97f42b4d00de0199ea73 100644 (file)
@@ -29,9 +29,6 @@ keystone_ec2_uri = http://localhost:5000/v2.0/ec2tokens
 
 [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
index 18bad73dc9356f139d9864d67f6c9fc0f87c3d53..68bd6a9d8139d02b642de78af6d04af75e4756c1 100644 (file)
@@ -32,13 +32,10 @@ paste.filter_factory = heat.common.context:ContextMiddleware_filter_factory
 
 [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.
index 10c48bc208b25b8524968020f126730bb4afd0f6..3fa3845821f1bbd2fe8cde73cf14fc234cdcdb3c 100644 (file)
 # 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 as 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 as 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):