# Copyright 2012 OpenStack Foundation # # 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 abc import base64 from oslo_config import cfg from oslo_log import log as logging from oslo_middleware import base import requests from tacker.api.vnflcm.v1 import router as vnflcm_router from tacker.api.vnfpkgm.v1 import router as vnfpkgm_router import threading import webob.dec import webob.exc from tacker.common import utils from tacker import context LOG = logging.getLogger(__name__) class TackerKeystoneContext(base.ConfigurableMiddleware): """Make a request context from keystone headers.""" @webob.dec.wsgify def __call__(self, req): ctx = context.Context.from_environ(req.environ) if not ctx.user_id: LOG.debug("X_USER_ID is not found in request") return webob.exc.HTTPUnauthorized() # Inject the context... req.environ['tacker.context'] = ctx return self.application class _BearerAuth(requests.auth.AuthBase): """Attaches HTTP Bearer Authentication to the given Request object.""" def __init__(self, token): self.token = token def __call__(self, r): r.headers["Authorization"] = "Bearer " + self.token return r class _OAuth2GrantBase(metaclass=abc.ABCMeta): """Base class that all OAuth2.0 grant type implementations derive from.""" grant_type = None @abc.abstractmethod def get_accsess_token(self): pass class _ClientCredentialsGrant(_OAuth2GrantBase): """OAuth2.0 grant type ClientCredentials implementation.""" grant_type = 'client_credentials' def __init__(self, token_endpoint, client_id, client_password): super(_ClientCredentialsGrant, self).__init__() self.token_endpoint = token_endpoint self.client_id = client_id self.client_password = client_password def get_accsess_token(self): """Get access token. Returns: dict: access token response. """ kwargs = { 'headers': { 'Connection': 'keep-alive', 'Content-Type': 'application/x-www-form-urlencoded'}, 'data': { 'grant_type': self.grant_type}, 'timeout': cfg.CONF.authentication.timeout} basic_auth_request = _BasicAuthSession( self.client_id, self.client_password) LOG.info( "Get Access Token, Connecting to ".format( self.token_endpoint)) LOG.info("Request Headers={}".format(kwargs.get('headers'))) LOG.info("Request Body={}".format(kwargs.get('data'))) response = basic_auth_request.get(self.token_endpoint, **kwargs) response.raise_for_status() response_body = response.json() LOG.info("[RES] Headers={}".format(response.headers)) LOG.info("[RES] Body={}".format(response_body)) return response_body class _OAuth2Session(requests.Session): """Provides OAuth 2.0 authentication.""" def __init__(self, grant): super(_OAuth2Session, self).__init__() self.grant = grant self.__access_token_info = {} self.__lock = threading.RLock() def request(self, method, url, **kwargs): """Override function.""" kwargs['auth'] = _BearerAuth( self.__access_token_info.get('access_token')) response = super().request(method, url, **kwargs) if response.status_code == 401: LOG.error( 'Authentication error {}, details={}'.format( response, response.text)) self.apply_access_token_info() return response def apply_access_token_info(self): """Get access token.""" try: self.__set_access_token_info(self.grant.get_accsess_token()) self.schedule_refrash_token() except requests.exceptions.RequestException as e: if hasattr(e, 'response') and e.response: LOG.error( "Get Access Token, error details={}".format( e.response.json())) LOG.error(e) def __set_access_token_info(self, update_dict): with self.__lock: self.__access_token_info = update_dict def schedule_refrash_token(self): """expires_in Scheduler registration at expiration.""" if not ('expires_in' in self.__access_token_info): LOG.debug("'expires_in' does not exist in the response body.") return try: expires_in = int(self.__access_token_info.get('expires_in')) expires_in_timer = threading.Timer( expires_in, self.apply_access_token_info) expires_in_timer.start() LOG.info( "expires_in=<{}> exist, scheduler regist.".format(expires_in)) except (ValueError, TypeError): pass class _BasicAuthSession(requests.Session): """Provide Basic authentication.""" def __init__(self, user_name, password): super(_BasicAuthSession, self).__init__() self.user_name = user_name self.password = password self.auth = requests.auth.HTTPBasicAuth( self.user_name, self.password) def request(self, method, url, **kwargs): """Override function.""" kwargs['auth'] = self.auth return super().request(method, url, **kwargs) class _AuthManager: OPTS = [ cfg.StrOpt('auth_type', default=None, choices=['BASIC', 'OAUTH2_CLIENT_CREDENTIALS'], help="auth_type used for external connection"), cfg.IntOpt('timeout', default=20, help="timeout used for external connection"), cfg.StrOpt('token_endpoint', default=None, help="token_endpoint used to get the oauth2 token"), cfg.StrOpt('client_id', default=None, help="client_id used to get the oauth2 token"), cfg.StrOpt('client_password', default=None, help="client_password used to get the oauth2 token"), cfg.StrOpt('user_name', default=None, help="user_name used in basic authentication"), cfg.StrOpt('password', default=None, help="password used in basic authentication") ] cfg.CONF.register_opts(OPTS, group='authentication') __DEFAULT_CLIENT = requests.Session() def __init__(self): self.__manages = {} self.__lock = threading.RLock() # local auth setting. self.set_auth_client( auth_type=cfg.CONF.authentication.auth_type, auth_params={'client_id': cfg.CONF.authentication.client_id, 'client_password': cfg.CONF.authentication.client_password, 'token_endpoint': cfg.CONF.authentication.token_endpoint, 'user_name': cfg.CONF.authentication.user_name, 'password': cfg.CONF.authentication.password}) def __empty(self, val): if val is None: return True elif isinstance(val, str): return val.strip() == '' return len(val) == 0 def set_auth_client(self, id='local', auth_type=None, auth_params=None): """Set up an Auth client. Args: id (str, optional): Management ID auth_type (str, optional): Authentication type. auth_params (dict, optional): Authentication information. """ snakecase_auth_params = utils.convert_camelcase_to_snakecase( auth_params) if self.__empty(auth_type) or self.__empty(snakecase_auth_params): return if id in self.__manages: LOG.debug("Use cache, Auth Managed Id=<{}>".format(id)) return client = self.__DEFAULT_CLIENT if auth_type == 'BASIC': client = _BasicAuthSession( user_name=snakecase_auth_params.get('user_name'), password=snakecase_auth_params.get('password')) elif (auth_type == 'OAUTH2_CLIENT_CREDENTIALS' and not self.__empty(snakecase_auth_params.get('token_endpoint'))): grant = _ClientCredentialsGrant( client_id=snakecase_auth_params.get('client_id'), client_password=snakecase_auth_params.get('client_password'), token_endpoint=snakecase_auth_params.get('token_endpoint')) client = _OAuth2Session(grant) client.apply_access_token_info() LOG.info( "Add to Auth management, id=<{}>, type=<{}>, class=<{}>".format( id, auth_type, client.__class__.__name__)) self.__add_manages(id, client) def __add_manages(self, id, client): with self.__lock: self.__manages[id] = client def get_auth_client(self, id="local"): """Get the Auth client. Args: id (str, optional): Management ID Returns: based on class. """ return self.__manages.get(id, self.__DEFAULT_CLIENT) class _AuthBase(metaclass=abc.ABCMeta): def __init__(self, api_name, token_type, token_value): self.api_name = api_name self.token_type = token_type self.token_value = token_value @abc.abstractmethod def do_auth(self): pass def _check_token_type(self): if self.token_type == cfg.CONF.authentication.token_type: return msg = ("Auth Scheme is not expected token_type: expect={%s},\ actual={%s}" % (cfg.CONF.authentication.token_type, self.token_type)) raise webob.exc.HTTPUnauthorized(msg) class _AuthValidateIgnore(_AuthBase): def __init__(self, api_name, token_type, token_value): super().__init__(api_name, token_type, token_value) def do_auth(self): conf_auth_type = cfg.CONF.authentication.token_type if (conf_auth_type is not None and self.token_type is None): msg = ("Request header is None but tacker conf\ has auth information.") raise webob.exc.HTTPUnauthorized(msg) return None class _AuthValidateBearer(_AuthBase): def __init__(self, application_type, api_name, token_type, token_value): super().__init__(api_name, token_type, token_value) self.application_type = application_type self.expires_in = 0 self.res_access_token = None self.__access_token_info = {} def request(self): auth_url = cfg.CONF.authentication.auth_url response = requests.Session().get(auth_url) if response.status_code == 401: return None return response def do_auth(self): self._check_token_type() if self.res_access_token is None: response = self.request() if response is None: msg = "No responce from Authorization Server." raise webob.exc.HTTPUnauthorized(msg) response_body = response.json() if (response_body.get('access_token') is None or response_body.get('token_type') is None): msg = "No access_token or token_type exist." raise webob.exc.HTTPUnauthorized(msg) if self.token_value != response_body.get('access_token'): msg = "access_token is invalid." raise webob.exc.HTTPUnauthorized(msg) self.expires_in = response_body.get('expires_in') self.res_access_token = response_body.get('access_token') self._validate_scope(response_body.get('scope')) self._scheduler_delete_token(response_body) def _scheduler_delete_token(self, response_body): if not (response_body.get('expires_in')): LOG.debug("'expires_in' does not exist in the response body.") return try: expires_in = int(response_body.get('expires_in')) expires_in_timer = threading.Timer( expires_in, self._delete_access_token) expires_in_timer.start() LOG.info( "expires_in=<{}> exist, scheduler regist.".format(expires_in)) except (ValueError, TypeError): pass def _delete_access_token(self): self.access_token = None self.expires_in = 0 def _generate_api_scope_name(self): if self.application_type == vnflcm_router.VnflcmAPIRouter: scope_prefix = 'vnflcm_' return scope_prefix + self.api_name elif self.application_type == vnfpkgm_router.VnfpkgmAPIRouter: scope_prefix = 'vnfpkgm_' return scope_prefix + self.api_name return '' def _validate_scope(self, res_scope): if res_scope is None: return try: api_scope_name = self._generate_api_scope_name() + '_scope' scopes = cfg.CONF.authentication.__getitem__(api_scope_name) except Exception: msg = ("Getting scope error." "api_scope_name={%s}, scopes={%s}", api_scope_name, scopes) raise webob.exc.HTTPUnauthorized(msg) if len(scopes) == 0: return if res_scope in scopes: return raise webob.exc.HTTPForbidden("scope is undefined in tacker.conf") class _AuthValidateBasic(_AuthBase): def __init__(self, api_name, token_type, token_value): super().__init__(api_name, token_type, token_value) def do_auth(self): self._check_token_type() base = cfg.CONF.authentication.user_name +\ cfg.CONF.authentication.password base64_encode = base64.b64encode(base.encode()) if self.token_value != base64_encode: msg = "access_token and encoded base64 are not same." raise webob.exc.HTTPUnauthorized(msg) class _AuthValidateManager: atuh_opts = [ cfg.StrOpt('token_type', default=None, choices=['Bearer', 'Basic'], help="authentication type"), cfg.StrOpt('user_name', default=None, help="user_name used in basic authentication"), cfg.StrOpt('password', default=None, help="password used in basic authentication"), cfg.StrOpt('auth_url', default=None, help="URL of the authorization server") ] cfg.CONF.register_opts(atuh_opts, group='authentication') def __init__(self): self.__manages = {} self.__lock = threading.RLock() def __add_manages(self, token_value, auth_obj): with self.__lock: self.__manages[token_value] = auth_obj def _parse_request_header(self, request): auth_req = request.headers.get('Authorization') if auth_req is None: return (None, None) auth_req_arry = auth_req.split(' ') if len(auth_req_arry) > 3: msg = "Invalid Authorization header." raise webob.exc.HTTPUnauthorized(msg) return (auth_req_arry[0], auth_req_arry[1]) def _get_auth_type(self, request, application): token_type, token_value = self._parse_request_header(request) if token_value in self.__manages: return self.__manages.get(token_value) match = application.map.match(request.path_info) api_name = match[0].get("action") if token_type == 'Bearer': auth_obj = _AuthValidateBearer( type(application), api_name, token_type, token_value) self.__add_manages(token_value, auth_obj) return auth_obj elif token_type == 'Basic': auth_obj = _AuthValidateBasic(api_name, token_type, token_value) self.__add_manages(token_value, auth_obj) return auth_obj return _AuthValidateIgnore(api_name, token_type, token_value) def auth_main(self, request, application): auth_validator = self._get_auth_type(request, application) if auth_validator: auth_validator.do_auth() class AuthValidatorExecution(base.ConfigurableMiddleware): @ webob.dec.wsgify def __call__(self, req): auth_validator_manager.auth_main(req, self.application) return self.application def pipeline_factory(loader, global_conf, **local_conf): """Create a paste pipeline based on the 'auth_strategy' config option.""" pipeline = local_conf[cfg.CONF.auth_strategy] pipeline = pipeline.split() filters = [loader.get_filter(n) for n in pipeline[:-1]] app = loader.get_app(pipeline[-1]) filters.reverse() for f in filters: app = f(app) return app auth_manager = _AuthManager() auth_validator_manager = _AuthValidateManager()