]> review.fuel-infra Code Review - openstack-build/heat-build.git/commitdiff
Added Subclass_keystone_middleware_to_set_headers.patch
authorThomas Goirand <thomas@goirand.fr>
Thu, 23 May 2013 15:17:36 +0000 (23:17 +0800)
committerThomas Goirand <thomas@goirand.fr>
Thu, 23 May 2013 15:17:36 +0000 (23:17 +0800)
Rewritten-From: 96bc6843dd5cf9acc957dbda5997ac354ad826dc

xenial/debian/changelog
xenial/debian/patches/Subclass_keystone_middleware_to_set_headers.patch [new file with mode: 0644]
xenial/debian/patches/series

index 9d8b5e00595bf96263a885c6a2092c63fb021b0b..3eb1c6084344c996e6ff666aea6b83c9690fb799 100644 (file)
@@ -1,3 +1,9 @@
+heat (2013.1-5) unstable; urgency=low
+
+  * Added Subclass_keystone_middleware_to_set_headers.patch
+
+ -- Thomas Goirand <zigo@debian.org>  Thu, 23 May 2013 23:16:57 +0800
+
 heat (2013.1-4) unstable; urgency=low
 
   * Fixed the dbsync to the new thing upstream is using (it was crashing the
diff --git a/xenial/debian/patches/Subclass_keystone_middleware_to_set_headers.patch b/xenial/debian/patches/Subclass_keystone_middleware_to_set_headers.patch
new file mode 100644 (file)
index 0000000..23ae70c
--- /dev/null
@@ -0,0 +1,582 @@
+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.
index 4aa2d4aa7dbdf76c774d4af00c80c7857f2e539f..670770884be6fda6a84d605db59fb6b6189b6061 100644 (file)
@@ -1,2 +1,3 @@
 removes-lxml-version-limitation-from-pip-requires.patch
 fix-package-version-detection-when-building-doc.patch
+Subclass_keystone_middleware_to_set_headers.patch