diff --git a/magnum/api/auth.py b/magnum/api/auth.py new file mode 100644 index 0000000000..85ed1b8a37 --- /dev/null +++ b/magnum/api/auth.py @@ -0,0 +1,161 @@ +# -*- encoding: utf-8 -*- +# +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import re + +from keystoneclient.middleware import auth_token +from oslo.config import cfg +from oslo.utils import importutils +from pecan import hooks + +from magnum.common import context +from magnum.openstack.common._i18n import _ +from magnum.openstack.common import log as logging + + +LOG = logging.getLogger(__name__) + +OPT_GROUP_NAME = 'keystone_authtoken' + +AUTH_OPTS = [ + cfg.BoolOpt('enable_authentication', + default=True, + help='This option enables or disables user authentication ' + 'via keystone. Default value is True.'), +] + +CONF = cfg.CONF +CONF.register_opts(AUTH_OPTS) +CONF.register_opts(auth_token.opts, group=OPT_GROUP_NAME) + +PUBLIC_ENDPOINTS = [ + '^/?$', + '^/v[0-9]+/?$', + '^/v[0-9]+/triggers', + '^/camp/platform_endpoints', + '^/camp/camp_v1_1_endpoint' +] + + +def install(app, conf): + if conf.get('enable_authentication'): + return AuthProtocolWrapper(app, conf=dict(conf.get(OPT_GROUP_NAME))) + else: + LOG.warning(_('Keystone authentication is disabled by Magnum ' + 'configuration parameter enable_authentication. ' + 'Magnum will not authenticate incoming request. ' + 'In order to enable authentication set ' + 'enable_authentication option to True.')) + + return app + + +class AuthHelper(object): + """Helper methods for Auth.""" + + def __init__(self): + endpoints_pattern = '|'.join(pe for pe in PUBLIC_ENDPOINTS) + self._public_endpoints_regexp = re.compile(endpoints_pattern) + + def is_endpoint_public(self, path): + return self._public_endpoints_regexp.match(path) + + +class AuthProtocolWrapper(auth_token.AuthProtocol): + """A wrapper on Keystone auth_token AuthProtocol. + + Does not perform verification of authentication tokens for pub routes in + the API. Public routes are those defined by PUBLIC_ENDPOINTS + + """ + + def __call__(self, env, start_response): + path = env.get('PATH_INFO') + if AUTH.is_endpoint_public(path): + return self.app(env, start_response) + return super(AuthProtocolWrapper, self).__call__(env, start_response) + + +class AuthInformationHook(hooks.PecanHook): + + def before(self, state): + if not CONF.get('enable_authentication'): + return + # Skip authentication for public endpoints + if AUTH.is_endpoint_public(state.request.path): + return + + headers = state.request.headers + user_id = headers.get('X-User-Id') + if user_id is None: + LOG.debug("X-User-Id header was not found in the request") + raise Exception('Not authorized') + + roles = self._get_roles(state.request) + + project_id = headers.get('X-Project-Id') + user_name = headers.get('X-User-Name', '') + + domain = headers.get('X-Domain-Name') + project_domain_id = headers.get('X-Project-Domain-Id', '') + user_domain_id = headers.get('X-User-Domain-Id', '') + + # Get the auth token + try: + recv_auth_token = headers.get('X-Auth-Token', + headers.get( + 'X-Storage-Token')) + except ValueError: + LOG.debug("No auth token found in the request.") + raise Exception('Not authorized') + auth_url = headers.get('X-Auth-Url') + if auth_url is None: + importutils.import_module('keystoneclient.middleware.auth_token') + auth_url = cfg.CONF.keystone_authtoken.auth_uri + + auth_token_info = state.request.environ.get('keystone.token_info') + identity_status = headers.get('X-Identity-Status') + if identity_status == 'Confirmed': + ctx = context.RequestContext(auth_token=recv_auth_token, + auth_token_info=auth_token_info, + user=user_id, + tenant=project_id, + domain=domain, + user_domain=user_domain_id, + project_domain=project_domain_id, + user_name=user_name, + roles=roles, + auth_url=auth_url) + state.request.security_context = ctx + else: + LOG.debug("The provided identity is not confirmed.") + raise Exception('Not authorized. Identity not confirmed.') + return + + def _get_roles(self, req): + """Get the list of roles.""" + + if 'X-Roles' in req.headers: + roles = req.headers.get('X-Roles', '') + else: + # Fallback to deprecated role header: + roles = req.headers.get('X-Role', '') + if roles: + LOG.warn(_("X-Roles is missing. Using deprecated X-Role " + "header")) + return [r.strip() for r in roles.split(',')] + + +AUTH = AuthHelper() diff --git a/magnum/api/config.py b/magnum/api/config.py index ba7319fbb3..cb85638604 100644 --- a/magnum/api/config.py +++ b/magnum/api/config.py @@ -14,11 +14,14 @@ # License for the specific language governing permissions and limitations # under the License. +from magnum.api import auth + # Pecan Application Configurations app = { 'root': 'magnum.api.controllers.root.RootController', 'modules': ['magnum.api'], - 'debug': False + 'debug': False, + 'hooks': [auth.AuthInformationHook()] } # Custom Configurations must be in Python dictionary format:: diff --git a/magnum/common/context.py b/magnum/common/context.py new file mode 100644 index 0000000000..53d73eb1f7 --- /dev/null +++ b/magnum/common/context.py @@ -0,0 +1,54 @@ +# Copyright 2014 - Mirantis, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import inspect + +from magnum.openstack.common import context + + +class RequestContext(context.RequestContext): + def __init__(self, auth_token=None, user=None, tenant=None, domain=None, + user_domain=None, project_domain=None, is_admin=False, + read_only=False, request_id=None, user_name=None, roles=None, + auth_url=None, trust_id=None, auth_token_info=None): + super(RequestContext, self).__init__(auth_token=auth_token, + user=user, tenant=tenant, + domain=domain, + user_domain=user_domain, + project_domain=project_domain, + is_admin=is_admin, + read_only=read_only, + show_deleted=False, + request_id=request_id) + self.roles = roles + self.user_name = user_name + self.auth_url = auth_url + self.trust_id = trust_id + self.auth_token_info = auth_token_info + + def to_dict(self): + data = super(RequestContext, self).to_dict() + data.update(roles=self.roles, user_name=self.user_name, + auth_url=self.auth_url, + auth_token_info=self.auth_token_info, + trust_id=self.trust_id) + return data + + @classmethod + def from_dict(cls, values): + allowed = [arg for arg in + inspect.getargspec(RequestContext.__init__).args + if arg != 'self'] + kwargs = dict((k, v) for (k, v) in values.items() if k in allowed) + return cls(**kwargs)