Custom Search

Tuesday, July 8, 2014

OpenStack How to heat-api validates token against keystone

1)
http://fosshelp.blogspot.in/2014/03/how-to-openstack-api-and-wsgi-api.html

2)
vim /etc/heat/api-paste.ini

# heat-api pipeline
[pipeline:heat-api]
pipeline = faultwrap ssl versionnegotiation authurl authtoken context apiv1app

[filter:faultwrap]
paste.filter_factory = heat.common.wsgi:filter_factory
heat.filter_factory = heat.api.openstack:faultwrap_filter

[filter:ssl]
paste.filter_factory = heat.common.wsgi:filter_factory
heat.filter_factory = heat.api.openstack:sslmiddleware_filter

[filter:versionnegotiation]
paste.filter_factory = heat.common.wsgi:filter_factory
heat.filter_factory = heat.api.openstack:version_negotiation_filter

# Middleware to set auth_url header appropriately
[filter:authurl]
paste.filter_factory = heat.common.auth_url:filter_factory


# Auth middleware that validates token against keystone
[filter:authtoken]
paste.filter_factory = keystoneclient.middleware.auth_token:filter_factory <===


[filter:context]
paste.filter_factory = heat.common.context:ContextMiddleware_filter_factory

[app:apiv1app]
paste.app_factory = heat.common.wsgi:app_factory
heat.app_factory = heat.api.openstack.v1:API


3)
authurl filter

/usr/lib/python2.7/dist-packages/heat/common/auth_url.py

from oslo.config import cfg
from webob.exc import HTTPBadRequest
from webob.exc import HTTPUnauthorized

from heat.common import wsgi
from heat.openstack.common import importutils

class AuthUrlFilter(wsgi.Middleware):

    def __init__(self, app, conf):
        super(AuthUrlFilter, self).__init__(app)
        self.conf = conf
        self.auth_url = self._get_auth_url() <===

    def _get_auth_url(self):
        if 'auth_uri' in self.conf:
            return self.conf['auth_uri']
        else:
            # Import auth_token to have keystone_authtoken settings setup.
            auth_token_module = 'keystoneclient.middleware.auth_token' <===
            importutils.import_module(auth_token_module) <===
            return cfg.CONF.keystone_authtoken.auth_uri <===


def filter_factory(global_conf, **local_conf):
    conf = global_conf.copy()
    conf.update(local_conf)

    def auth_url_filter(app):
        return AuthUrlFilter(app, conf)
    return auth_url_filter

4)
authtoken filter

/usr/lib/python2.7/dist-packages/keystoneclient/middleware/auth_token.py

"""
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
* SM:Please read the doc string of above "auth_token.py" file.

Refer to: http://docs.openstack.org/developer/python-keystoneclient/
middlewarearchitecture.html
"""

####SM:Read conf from /etc/heat/heat.conf file
opts = [
    cfg.StrOpt('auth_admin_prefix',
               default='',
               help='Prefix to prepend at the beginning of the path'),
    cfg.StrOpt('auth_host',
               default='127.0.0.1',
               help='Host providing the admin Identity API endpoint'),
    cfg.IntOpt('auth_port',
               default=35357,
               help='Port of the admin Identity API endpoint'),
    cfg.StrOpt('auth_protocol',
               default='https',
               help='Protocol of the admin Identity API endpoint'
               '(http or https)'),
    cfg.StrOpt('auth_uri',
               default=None,
               help='Complete public Identity API endpoint'),
]

####SM:Read
CONF = cfg.CONF
CONF.register_opts(opts, group='keystone_authtoken')


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

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
        auth_host = self._conf_get('auth_host')
        auth_port = int(self._conf_get('auth_port'))
        self.auth_uri = self._conf_get('auth_uri')
        self.admin_token = self._conf_get('admin_token')

    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 __call__(self, env, start_response): <===IMP
        """Handle incoming request.

        Authenticate send downstream on success. Reject request if
        we can't authenticate.

        """
        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) <===

    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

    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 = cms.cms_hash_token(user_token)
        cached = self._cache_get(token_id)
        if cached:
            return cached
        if cms.is_ans1_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

    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')

5)
apiv1app filter


5a)paste.app_factory = heat.common.wsgi:app_factory

/usr/lib/python2.7/dist-packages/heat/common/wsgi.py

class AppFactory(BasePasteFactory):

    """A Generic paste.deploy app factory.

    This requires heat.app_factory to be set to a callable which returns a
    WSGI app when invoked. The format of the name is : e.g.

      [app:apiv1app]
      paste.app_factory = heat.common.wsgi:app_factory
      heat.app_factory = heat.api.cfn.v1:API

    The WSGI app constructor must accept a ConfigOpts object and a local config
    dict as its two arguments.
    """

    KEY = 'heat.app_factory'

    def __call__(self, global_conf, **local_conf):
        """The actual paste.app_factory protocol method."""
        factory = self._import_factory(local_conf)
        return factory(self.conf, **local_conf)

5b)
heat.app_factory = heat.api.openstack.v1:API

/usr/lib/python2.7/dist-packages/heat/api/openstack/v1/__init__.py

class API(wsgi.Router):
    """
    WSGI router for Heat v1 ReST API requests.
    """
    def __init__(self, conf, **local_conf):
        self.conf = conf
        mapper = routes.Mapper()

        # Stacks
        stacks_resource = stacks.create_resource(conf)
        with mapper.submapper(controller=stacks_resource,
                              path_prefix="/{tenant_id}") as stack_mapper:

            # Template handling
            stack_mapper.connect("template_validate",
                                 "/validate",
                                 action="validate_template",
                                 conditions={'method': 'POST'})
            stack_mapper.connect("resource_types",
                                 "/resource_types",
                                 action="list_resource_types",
                                 conditions={'method': 'GET'})
            stack_mapper.connect("resource_schema",
                                 "/resource_types/{type_name}",
                                 action="resource_schema",
                                 conditions={'method': 'GET'})
            stack_mapper.connect("generate_template",
                                 "/resource_types/{type_name}/template",
                                 action="generate_template",
                                 conditions={'method': 'GET'})
            ---- blabla ----                    
        super(API, self).__init__(mapper)


No comments:

Post a Comment