From 55d55bcdf1240598dd570917531f5100cedfaa73 Mon Sep 17 00:00:00 2001 From: Winson Chan Date: Fri, 16 Sep 2016 01:28:10 +0000 Subject: [PATCH] Abstract authentication function Abstract authentication function so plugins for other authentication backends can be implemented in cases where keystone is not used. Currently, mistral is hard coded to support keystone and keycloak. Change-Id: If6ff35e91c3d35c2741332c7e739bb92b1234c54 Implements: blueprint mistral-abstract-auth --- mistralclient/api/client.py | 49 +---- mistralclient/api/httpclient.py | 52 ++--- mistralclient/api/v2/client.py | 101 ++-------- mistralclient/auth/__init__.py | 37 ++++ mistralclient/auth/auth_types.py | 17 +- mistralclient/auth/keycloak.py | 210 +++++++++++--------- mistralclient/auth/keystone.py | 153 +++++++++----- mistralclient/shell.py | 2 +- mistralclient/tests/unit/test_client.py | 67 ++++--- mistralclient/tests/unit/test_httpclient.py | 24 +-- requirements.txt | 1 + setup.cfg | 9 + 12 files changed, 374 insertions(+), 348 deletions(-) diff --git a/mistralclient/api/client.py b/mistralclient/api/client.py index b65ed93d..378444ca 100644 --- a/mistralclient/api/client.py +++ b/mistralclient/api/client.py @@ -12,58 +12,15 @@ # See the License for the specific language governing permissions and # limitations under the License. -import six - from mistralclient.api.v2 import client as client_v2 -from mistralclient.auth import auth_types -def client(mistral_url=None, username=None, api_key=None, - project_name=None, auth_url=None, project_id=None, - endpoint_type='publicURL', service_type='workflow', - auth_token=None, user_id=None, cacert=None, insecure=False, - profile=None, auth_type=auth_types.KEYSTONE, client_id=None, - client_secret=None, target_username=None, target_api_key=None, - target_project_name=None, target_auth_url=None, - target_project_id=None, target_auth_token=None, - target_user_id=None, target_cacert=None, target_insecure=False, - **kwargs): - - if mistral_url and not isinstance(mistral_url, six.string_types): - raise RuntimeError('Mistral url should be a string.') - - return client_v2.Client( - mistral_url=mistral_url, - username=username, - api_key=api_key, - project_name=project_name, - auth_url=auth_url, - project_id=project_id, - endpoint_type=endpoint_type, - service_type=service_type, - auth_token=auth_token, - user_id=user_id, - cacert=cacert, - insecure=insecure, - profile=profile, - auth_type=auth_type, - client_id=client_id, - client_secret=client_secret, - target_username=target_username, - target_api_key=target_api_key, - target_project_name=target_project_name, - target_auth_url=target_auth_url, - target_project_id=target_project_id, - target_auth_token=target_auth_token, - target_user_id=target_user_id, - target_cacert=target_cacert, - target_insecure=target_insecure, - **kwargs - ) +def client(auth_type='keystone', **kwargs): + return client_v2.Client(auth_type=auth_type, **kwargs) def determine_client_version(mistral_version): if mistral_version.find("v2") != -1: return 2 - raise RuntimeError("Can not determine mistral API version") + raise RuntimeError("Cannot determine mistral API version") diff --git a/mistralclient/api/httpclient.py b/mistralclient/api/httpclient.py index f04598f1..9492c98a 100644 --- a/mistralclient/api/httpclient.py +++ b/mistralclient/api/httpclient.py @@ -37,31 +37,31 @@ def log_request(func): class HTTPClient(object): - def __init__(self, base_url, token=None, project_id=None, user_id=None, - cacert=None, insecure=False, target_token=None, - target_auth_uri=None, **kwargs): + def __init__(self, base_url, **kwargs): self.base_url = base_url - self.token = token - self.project_id = project_id - self.user_id = user_id - self.target_token = target_token - self.target_auth_uri = target_auth_uri + self.auth_token = kwargs.get('auth_token', None) + self.project_id = kwargs.get('project_id', None) + self.user_id = kwargs.get('user_id', None) + self.target_auth_token = kwargs.get('target_auth_token', None) + self.target_auth_url = kwargs.get('target_auth_url', None) + self.cacert = kwargs.get('cacert', None) + self.insecure = kwargs.get('insecure', False) self.ssl_options = {} if self.base_url.startswith('https'): - if cacert and not os.path.exists(cacert): + if self.cacert and not os.path.exists(self.cacert): raise ValueError('Unable to locate cacert file ' - 'at %s.' % cacert) + 'at %s.' % self.cacert) - if cacert and insecure: + if self.cacert and self.insecure: LOG.warning('Client is set to not verify even though ' 'cacert is provided.') - if insecure: + if self.insecure: self.ssl_options['verify'] = False else: - if cacert: - self.ssl_options['verify'] = cacert + if self.cacert: + self.ssl_options['verify'] = self.cacert else: self.ssl_options['verify'] = True @@ -107,9 +107,9 @@ class HTTPClient(object): if not headers: headers = {} - token = headers.get('x-auth-token', self.token) - if token: - headers['x-auth-token'] = token + auth_token = headers.get('x-auth-token', self.auth_token) + if auth_token: + headers['x-auth-token'] = auth_token project_id = headers.get('X-Project-Id', self.project_id) if project_id: @@ -119,14 +119,18 @@ class HTTPClient(object): if user_id: headers['X-User-Id'] = user_id - target_token = headers.get('X-Target-Auth-Token', self.target_token) - if target_token: - headers['X-Target-Auth-Token'] = target_token + target_auth_token = headers.get( + 'X-Target-Auth-Token', + self.target_auth_token + ) - target_auth_uri = headers.get('X-Target-Auth-Uri', - self.target_auth_uri) - if target_auth_uri: - headers['X-Target-Auth-Uri'] = target_auth_uri + if target_auth_token: + headers['X-Target-Auth-Token'] = target_auth_token + + target_auth_url = headers.get('X-Target-Auth-Uri', + self.target_auth_url) + if target_auth_url: + headers['X-Target-Auth-Uri'] = target_auth_url if osprofiler_web: # Add headers for osprofiler. diff --git a/mistralclient/api/v2/client.py b/mistralclient/api/v2/client.py index 1fd64f00..062bf878 100644 --- a/mistralclient/api/v2/client.py +++ b/mistralclient/api/v2/client.py @@ -13,6 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +import copy import six from oslo_utils import importutils @@ -28,9 +29,8 @@ from mistralclient.api.v2 import services from mistralclient.api.v2 import tasks from mistralclient.api.v2 import workbooks from mistralclient.api.v2 import workflows -from mistralclient.auth import auth_types -from mistralclient.auth import keycloak -from mistralclient.auth import keystone +from mistralclient import auth + osprofiler_profiler = importutils.try_import("osprofiler.profiler") @@ -38,62 +38,25 @@ _DEFAULT_MISTRAL_URL = "http://localhost:8989/v2" class Client(object): - def __init__(self, mistral_url=None, username=None, api_key=None, - project_name=None, auth_url=None, project_id=None, - endpoint_type='publicURL', service_type='workflowv2', - auth_token=None, user_id=None, cacert=None, insecure=False, - profile=None, auth_type=auth_types.KEYSTONE, client_id=None, - client_secret=None, target_username=None, target_api_key=None, - target_project_name=None, target_auth_url=None, - target_project_id=None, target_auth_token=None, - target_user_id=None, target_cacert=None, - target_insecure=False, **kwargs): + def __init__(self, auth_type='keystone', **kwargs): + req = copy.deepcopy(kwargs) + mistral_url = req.get('mistral_url') + auth_url = req.get('auth_url') + auth_token = req.get('auth_token') + project_id = req.get('project_id') + user_id = req.get('user_id') + profile = req.get('profile') if mistral_url and not isinstance(mistral_url, six.string_types): raise RuntimeError('Mistral url should be a string.') - if auth_url: - if auth_type == auth_types.KEYSTONE: - (mistral_url, auth_token, project_id, user_id) = ( - keystone.authenticate( - mistral_url, - username, - api_key, - project_name, - auth_url, - project_id, - endpoint_type, - service_type, - auth_token, - user_id, - cacert, - insecure - ) - ) - elif auth_type == auth_types.KEYCLOAK_OIDC: - auth_token = keycloak.authenticate( - auth_url, - client_id, - client_secret, - project_name, - username, - api_key, - auth_token, - cacert, - insecure - ) - - # In case of KeyCloak OpenID Connect we can treat project - # name and id in the same way because KeyCloak realm is - # essentially a different OpenID Connect Issuer which in - # KeyCloak is represented just as a URL path component - # (see http://openid.net/specs/openid-connect-core-1_0.html). - project_id = project_name - else: - raise RuntimeError( - 'Invalid authentication type [value=%s, valid_values=%s]' - % (auth_type, auth_types.ALL) - ) + if auth_url and not auth_token: + auth_handler = auth.get_auth_handler(auth_type) + auth_response = auth_handler.authenticate(req) or {} + mistral_url = auth_response.get('mistral_url') or mistral_url + req['auth_token'] = auth_response.get('token') + req['project_id'] = auth_response.get('project_id') or project_id + req['user_id'] = auth_response.get('user_id') or user_id if not mistral_url: mistral_url = _DEFAULT_MISTRAL_URL @@ -101,33 +64,7 @@ class Client(object): if profile: osprofiler_profiler.init(profile) - if target_auth_url: - keystone.authenticate( - mistral_url, - target_username, - target_api_key, - target_project_name, - target_auth_url, - target_project_id, - endpoint_type, - service_type, - target_auth_token, - target_user_id, - target_cacert, - target_insecure - ) - - http_client = httpclient.HTTPClient( - mistral_url, - auth_token, - project_id, - user_id, - cacert=cacert, - insecure=insecure, - target_token=target_auth_token, - target_auth_uri=target_auth_url, - **kwargs - ) + http_client = httpclient.HTTPClient(mistral_url, **req) # Create all resource managers. self.workbooks = workbooks.WorkbookManager(http_client) diff --git a/mistralclient/auth/__init__.py b/mistralclient/auth/__init__.py index e69de29b..85cecd41 100644 --- a/mistralclient/auth/__init__.py +++ b/mistralclient/auth/__init__.py @@ -0,0 +1,37 @@ +# Copyright 2016 - Brocade Communications Systems, 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 abc + +import six +from stevedore import driver + + +def get_auth_handler(auth_type): + mgr = driver.DriverManager( + 'mistralclient.auth', + auth_type, + invoke_on_load=True + ) + + return mgr.driver + + +@six.add_metaclass(abc.ABCMeta) +class AuthHandler(object): + """Abstract base class for an authentication plugin.""" + + @abc.abstractmethod + def authenticate(self, req): + raise NotImplementedError() diff --git a/mistralclient/auth/auth_types.py b/mistralclient/auth/auth_types.py index 71762601..76f5a2e5 100644 --- a/mistralclient/auth/auth_types.py +++ b/mistralclient/auth/auth_types.py @@ -12,15 +12,10 @@ # See the License for the specific language governing permissions and # limitations under the License. +from stevedore import extension + # Valid authentication types. - -# Standard Keystone authentication. -KEYSTONE = 'keystone' - -# Authentication using OpenID Connect protocol but specific to KeyCloak -# server regarding multi-tenancy support. KeyCloak has a notion of realm -# used as an analog of Keystone project/tenant. -KEYCLOAK_OIDC = 'keycloak-oidc' - - -ALL = [KEYSTONE, KEYCLOAK_OIDC] +ALL = extension.ExtensionManager( + namespace='mistralclient.auth', + invoke_on_load=False +).names() diff --git a/mistralclient/auth/keycloak.py b/mistralclient/auth/keycloak.py index 9c3f849d..a3d6a432 100644 --- a/mistralclient/auth/keycloak.py +++ b/mistralclient/auth/keycloak.py @@ -16,119 +16,147 @@ import logging import pprint import requests +from mistralclient import auth + LOG = logging.getLogger(__name__) -def authenticate(auth_url, client_id, client_secret, realm_name, - username=None, password=None, access_token=None, - cacert=None, insecure=False): - """Performs authentication using Keycloak OpenID Protocol. +class KeycloakAuthHandler(auth.AuthHandler): - :param auth_url: Base authentication url of KeyCloak server (e.g. - "https://my.keycloak:8443/auth" - :param client_id: Client ID (according to OpenID Connect protocol). - :param client_secret: Client secret (according to OpenID Connect protocol). - :param realm_name: KeyCloak realm name. - :param username: User name (Optional, if None then access_token must be - provided). - :param password: Password (Optional). - :param access_token: Access token. If passed, username and password are - not used and this method just validates the token and refreshes it, - if needed. (Optional, if None then username must be provided) - :param cacert: SSL certificate file (Optional). - :param insecure: If True, SSL certificate is not verified (Optional). + def authenticate(self, req): + """Performs authentication using Keycloak OpenID Protocol. - """ - if not auth_url: - raise ValueError('Base authentication url is not provided.') + :param req: Request dict containing list of parameters required + for Keycloak authentication. - if not client_id: - raise ValueError('Client ID is not provided.') + auth_url: Base authentication url of KeyCloak server (e.g. + "https://my.keycloak:8443/auth" + client_id: Client ID (according to OpenID Connect protocol). + client_secret: Client secret (according to OpenID Connect + protocol). + realm_name: KeyCloak realm name. + username: User name (Optional, if None then access_token must be + provided). + password: Password (Optional). + access_token: Access token. If passed, username and password are + not used and this method just validates the token and refreshes + it if needed (Optional, if None then username must be + provided). + cacert: SSL certificate file (Optional). + insecure: If True, SSL certificate is not verified (Optional). - if not client_secret: - raise ValueError('Client secret is not provided.') + """ + if not isinstance(req, dict): + raise TypeError('The input "req" is not typeof dict.') - if not realm_name: - raise ValueError('Project(realm) name is not provided.') + auth_url = req.get('auth_url') + client_id = req.get('client_id') + client_secret = req.get('client_secret') + realm_name = req.get('realm_name') + username = req.get('username') + password = req.get('password') + access_token = req.get('access_token') + cacert = req.get('cacert') + insecure = req.get('insecure', False) - if username and access_token: - raise ValueError( - "User name and access token can't be provided at the same time." + if not auth_url: + raise ValueError('Base authentication url is not provided.') + + if not client_id: + raise ValueError('Client ID is not provided.') + + if not client_secret: + raise ValueError('Client secret is not provided.') + + if not realm_name: + raise ValueError('Project(realm) name is not provided.') + + if username and access_token: + raise ValueError( + "User name and access token can't be " + "provided at the same time." + ) + + if not username and not access_token: + raise ValueError( + 'Either user name or access token must be provided.' + ) + + if access_token: + response = self._authenticate_with_token( + auth_url, + client_id, + client_secret, + access_token, + cacert, + insecure + ) + else: + response = self._authenticate_with_password( + auth_url, + client_id, + client_secret, + realm_name, + username, + password, + cacert, + insecure + ) + + response['project_id'] = realm_name + + return response + + def _authenticate_with_token(auth_url, client_id, client_secret, + auth_token, cacert=None, insecure=None): + # TODO(rakhmerov): Implement. + raise NotImplementedError + + def _authenticate_with_password(auth_url, client_id, client_secret, + realm_name, username, password, + cacert=None, insecure=None): + access_token_endpoint = ( + "%s/realms/%s/protocol/openid-connect/token" % + (auth_url, realm_name) ) - if access_token: - return _authenticate_with_token( - auth_url, - client_id, - client_secret, - access_token, - cacert, - insecure + client_auth = (client_id, client_secret) + + body = { + 'grant_type': 'password', + 'username': username, + 'password': password, + 'scope': 'profile' + } + + resp = requests.post( + access_token_endpoint, + auth=client_auth, + data=body, + verify=not insecure ) - if not username: - raise ValueError('Either user name or access token must be provided.') + try: + resp.raise_for_status() + except Exception as e: + raise Exception("Failed to get access token:\n %s" % str(e)) - return _authenticate_with_password( - auth_url, - client_id, - client_secret, - realm_name, - username, - password, - cacert, - insecure - ) + LOG.debug( + "HTTP response from OIDC provider: %s" % + pprint.pformat(resp.json()) + ) - -def _authenticate_with_token(auth_url, client_id, client_secret, auth_token, - cacert=None, insecure=None): - # TODO(rakhmerov): Implement. - raise NotImplementedError - - -def _authenticate_with_password(auth_url, client_id, client_secret, - realm_name, username, password, - cacert=None, insecure=None): - access_token_endpoint = ( - "%s/realms/%s/protocol/openid-connect/token" % (auth_url, realm_name) - ) - - client_auth = (client_id, client_secret) - - body = { - 'grant_type': 'password', - 'username': username, - 'password': password, - 'scope': 'profile' - } - - resp = requests.post( - access_token_endpoint, - auth=client_auth, - data=body, - verify=not insecure - ) - - try: - resp.raise_for_status() - except Exception as e: - raise Exception("Failed to get access token:\n %s" % str(e)) - - LOG.debug( - "HTTP response from OIDC provider: %s" % pprint.pformat(resp.json()) - ) - - return resp.json()['access_token'] + return resp.json()['access_token'] # An example of using KeyCloak OpenID authentication. - if __name__ == '__main__': print("Using username/password to get access token from KeyCloak...") - a_token = authenticate( + auth_handler = KeycloakAuthHandler() + + a_token = auth_handler.authenticate( "https://my.keycloak:8443/auth", client_id="mistral_client", client_secret="4a080907-921b-409a-b793-c431609c3a47", diff --git a/mistralclient/auth/keystone.py b/mistralclient/auth/keystone.py index 49a7fc09..6c8bb968 100644 --- a/mistralclient/auth/keystone.py +++ b/mistralclient/auth/keystone.py @@ -12,60 +12,115 @@ # See the License for the specific language governing permissions and # limitations under the License. - -def authenticate(mistral_url=None, username=None, - api_key=None, project_name=None, auth_url=None, - project_id=None, endpoint_type='publicURL', - service_type='workflowv2', auth_token=None, user_id=None, - cacert=None, insecure=False): - - if project_name and project_id: - raise RuntimeError( - 'Only project name or project id should be set' - ) - - if username and user_id: - raise RuntimeError( - 'Only user name or user id should be set' - ) - - keystone_client = _get_keystone_client(auth_url) - - keystone = keystone_client.Client( - username=username, - user_id=user_id, - password=api_key, - token=auth_token, - tenant_id=project_id, - tenant_name=project_name, - auth_url=auth_url, - endpoint=auth_url, - cacert=cacert, - insecure=insecure - ) - - keystone.authenticate() - - token = keystone.auth_token - user_id = keystone.user_id - project_id = keystone.project_id - - if not mistral_url: - try: - mistral_url = keystone.service_catalog.url_for( - service_type=service_type, - endpoint_type=endpoint_type - ) - except Exception: - mistral_url = None - - return mistral_url, token, project_id, user_id +from mistralclient import auth def _get_keystone_client(auth_url): - if "v2.0" in auth_url: + if 'v2.0' in auth_url: from keystoneclient.v2_0 import client else: from keystoneclient.v3 import client return client + + +class KeystoneAuthHandler(auth.AuthHandler): + + def authenticate(self, req): + """Performs authentication via Keystone. + + :param req: Request dict containing list of parameters required + for Keystone authentication. + + """ + if not isinstance(req, dict): + raise TypeError('The input "req" is not typeof dict.') + + auth_url = req.get('auth_url') + mistral_url = req.get('mistral_url') + endpoint_type = req.get('endpoint_type', 'publicURL') + service_type = req.get('service_type', 'workflow2') + username = req.get('username') + user_id = req.get('user_id') + api_key = req.get('api_key') + auth_token = req.get('auth_token') + project_name = req.get('project_name') + project_id = req.get('project_id') + cacert = req.get('cacert') + insecure = req.get('insecure', False) + target_username = req.get('target_username') + target_api_key = req.get('target_api_key') + target_project_name = req.get('target_project_name') + target_auth_url = req.get('target_auth_url') + target_project_id = req.get('target_project_id') + target_auth_token = req.get('target_auth_token') + target_user_id = req.get('target_user_id') + target_cacert = req.get('target_cacert') + target_insecure = req.get('target_insecure') + + if project_name and project_id: + raise RuntimeError( + 'Only project name or project id should be set' + ) + + if username and user_id: + raise RuntimeError( + 'Only user name or user id should be set' + ) + + if auth_url: + keystone_client = _get_keystone_client(auth_url) + + keystone = keystone_client.Client( + username=username, + user_id=user_id, + password=api_key, + token=auth_token, + tenant_id=project_id, + tenant_name=project_name, + auth_url=auth_url, + endpoint=auth_url, + cacert=cacert, + insecure=insecure + ) + + keystone.authenticate() + auth_token = keystone.auth_token + user_id = keystone.user_id + project_id = keystone.project_id + + if target_auth_url: + target_keystone_client = _get_keystone_client(target_auth_url) + + target_keystone = target_keystone_client.Client( + username=target_username, + user_id=target_user_id, + password=target_api_key, + token=target_auth_token, + tenant_id=target_project_id, + tenant_name=target_project_name, + auth_url=target_auth_url, + endpoint=target_auth_url, + cacert=target_cacert, + insecure=target_insecure + ) + + target_keystone.authenticate() + + if not mistral_url: + try: + mistral_url = keystone.service_catalog.url_for( + service_type=service_type, + endpoint_type=endpoint_type + ) + except Exception: + mistral_url = None + + return { + 'mistral_url': mistral_url, + 'token': auth_token, + 'project_id': target_project_id if target_auth_url else project_id, + 'user_id': target_user_id if target_auth_url else user_id, + 'target_auth_token': target_auth_token, + 'target_auth_url': target_auth_url + } diff --git a/mistralclient/shell.py b/mistralclient/shell.py index 6b83d91b..7543c1d4 100644 --- a/mistralclient/shell.py +++ b/mistralclient/shell.py @@ -317,7 +317,7 @@ class MistralShell(app.App): '--auth-type', action='store', dest='auth_type', - default=c.env('MISTRAL_AUTH_TYPE', default=auth_types.KEYSTONE), + default=c.env('MISTRAL_AUTH_TYPE', default='keystone'), help='Authentication type. Valid options are: %s.' ' (Env: MISTRAL_AUTH_TYPE)' % auth_types.ALL ) diff --git a/mistralclient/tests/unit/test_client.py b/mistralclient/tests/unit/test_client.py index ba4b7d79..d0d4fef9 100644 --- a/mistralclient/tests/unit/test_client.py +++ b/mistralclient/tests/unit/test_client.py @@ -92,16 +92,15 @@ class BaseClientTests(base.BaseTestCase): expected_args = ( MISTRAL_HTTP_URL, - keystone_client_instance.auth_token, - keystone_client_instance.project_id, - keystone_client_instance.user_id ) expected_kwargs = { - 'cacert': None, - 'insecure': False, - 'target_auth_uri': None, - 'target_token': None + 'username': 'mistral', + 'project_name': 'mistral', + 'auth_url': AUTH_HTTP_URL_v3, + 'auth_token': keystone_client_instance.auth_token, + 'project_id': keystone_client_instance.project_id, + 'user_id': keystone_client_instance.user_id } client.client( @@ -111,8 +110,8 @@ class BaseClientTests(base.BaseTestCase): ) self.assertTrue(mocked.called) - self.assertEqual(mocked.call_args[0], expected_args) - self.assertDictEqual(mocked.call_args[1], expected_kwargs) + self.assertEqual(expected_args, mocked.call_args[0]) + self.assertDictEqual(expected_kwargs, mocked.call_args[1]) @mock.patch('keystoneclient.v3.client.Client') @mock.patch('mistralclient.api.httpclient.HTTPClient') @@ -126,16 +125,18 @@ class BaseClientTests(base.BaseTestCase): expected_args = ( MISTRAL_HTTPS_URL, - keystone_client_instance.auth_token, - keystone_client_instance.project_id, - keystone_client_instance.user_id ) expected_kwargs = { + 'mistral_url': MISTRAL_HTTPS_URL, + 'username': 'mistral', + 'project_name': 'mistral', + 'auth_url': AUTH_HTTP_URL_v3, 'cacert': None, 'insecure': True, - 'target_auth_uri': None, - 'target_token': None + 'auth_token': keystone_client_instance.auth_token, + 'project_id': keystone_client_instance.project_id, + 'user_id': keystone_client_instance.user_id } client.client( @@ -148,8 +149,8 @@ class BaseClientTests(base.BaseTestCase): ) self.assertTrue(mocked.called) - self.assertEqual(mocked.call_args[0], expected_args) - self.assertDictEqual(mocked.call_args[1], expected_kwargs) + self.assertEqual(expected_args, mocked.call_args[0]) + self.assertDictEqual(expected_kwargs, mocked.call_args[1]) @mock.patch('keystoneclient.v3.client.Client') @mock.patch('mistralclient.api.httpclient.HTTPClient') @@ -163,16 +164,18 @@ class BaseClientTests(base.BaseTestCase): expected_args = ( MISTRAL_HTTPS_URL, - keystone_client_instance.auth_token, - keystone_client_instance.project_id, - keystone_client_instance.user_id ) expected_kwargs = { + 'mistral_url': MISTRAL_HTTPS_URL, + 'username': 'mistral', + 'project_name': 'mistral', + 'auth_url': AUTH_HTTP_URL_v3, 'cacert': path, 'insecure': False, - 'target_auth_uri': None, - 'target_token': None + 'auth_token': keystone_client_instance.auth_token, + 'project_id': keystone_client_instance.project_id, + 'user_id': keystone_client_instance.user_id } try: @@ -189,8 +192,8 @@ class BaseClientTests(base.BaseTestCase): os.unlink(path) self.assertTrue(mock.called) - self.assertEqual(mock.call_args[0], expected_args) - self.assertDictEqual(mock.call_args[1], expected_kwargs) + self.assertEqual(expected_args, mock.call_args[0]) + self.assertDictEqual(expected_kwargs, mock.call_args[1]) @mock.patch('keystoneclient.v3.client.Client') def test_mistral_url_https_bad_cacert(self, keystone_client_mock): @@ -248,16 +251,16 @@ class BaseClientTests(base.BaseTestCase): expected_args = ( MISTRAL_HTTP_URL, - keystone_client_instance.auth_token, - keystone_client_instance.project_id, - keystone_client_instance.user_id ) expected_kwargs = { - 'cacert': None, - 'insecure': False, - 'target_auth_uri': None, - 'target_token': None + 'username': 'mistral', + 'project_name': 'mistral', + 'auth_url': AUTH_HTTP_URL_v3, + 'profile': PROFILER_HMAC_KEY, + 'auth_token': keystone_client_instance.auth_token, + 'project_id': keystone_client_instance.project_id, + 'user_id': keystone_client_instance.user_id } client.client( @@ -268,8 +271,8 @@ class BaseClientTests(base.BaseTestCase): ) self.assertTrue(mocked.called) - self.assertEqual(mocked.call_args[0], expected_args) - self.assertDictEqual(mocked.call_args[1], expected_kwargs) + self.assertEqual(expected_args, mocked.call_args[0]) + self.assertDictEqual(expected_kwargs, mocked.call_args[1]) profiler = osprofiler.profiler.get() diff --git a/mistralclient/tests/unit/test_httpclient.py b/mistralclient/tests/unit/test_httpclient.py index f553198f..bbd94cc5 100644 --- a/mistralclient/tests/unit/test_httpclient.py +++ b/mistralclient/tests/unit/test_httpclient.py @@ -73,9 +73,9 @@ class HTTPClientTest(base.BaseTestCase): osprofiler.profiler.init(None) self.client = httpclient.HTTPClient( API_BASE_URL, - AUTH_TOKEN, - PROJECT_ID, - USER_ID + auth_token=AUTH_TOKEN, + project_id=PROJECT_ID, + user_id=USER_ID ) @mock.patch.object( @@ -133,23 +133,23 @@ class HTTPClientTest(base.BaseTestCase): mock.MagicMock(return_value=FakeResponse('get', EXPECTED_URL, 200)) ) def test_get_request_options_with_headers_for_get(self): - target_auth_uri = str(uuid.uuid4()) - target_token = str(uuid.uuid4()) + target_auth_url = str(uuid.uuid4()) + target_auth_token = str(uuid.uuid4()) target_client = httpclient.HTTPClient( API_BASE_URL, - AUTH_TOKEN, - PROJECT_ID, - USER_ID, - target_auth_uri=target_auth_uri, - target_token=target_token + auth_token=AUTH_TOKEN, + project_id=PROJECT_ID, + user_id=USER_ID, + target_auth_url=target_auth_url, + target_auth_token=target_auth_token ) target_client.get(API_URL) expected_options = copy.deepcopy(EXPECTED_REQ_OPTIONS) - expected_options["headers"]["X-Target-Auth-Uri"] = target_auth_uri - expected_options["headers"]["X-Target-Auth-Token"] = target_token + expected_options["headers"]["X-Target-Auth-Uri"] = target_auth_url + expected_options["headers"]["X-Target-Auth-Token"] = target_auth_token requests.get.assert_called_with( EXPECTED_URL, diff --git a/requirements.txt b/requirements.txt index 93f3901f..8f1f5837 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,3 +9,4 @@ python-keystoneclient!=2.1.0,>=2.0.0 # Apache-2.0 PyYAML>=3.1.0 # MIT requests>=2.10.0 # Apache-2.0 six>=1.9.0 # MIT +stevedore>=1.16.0 # Apache-2.0 diff --git a/setup.cfg b/setup.cfg index f1940452..a5c056a3 100644 --- a/setup.cfg +++ b/setup.cfg @@ -102,6 +102,15 @@ openstack.workflow_engine.v2 = resource_member_delete = mistralclient.commands.v2.members:Delete resource_member_update = mistralclient.commands.v2.members:Update +mistralclient.auth = + # Standard Keystone authentication. + keystone = mistralclient.auth.keystone:KeystoneAuthHandler + + # Authentication using OpenID Connect protocol but specific to KeyCloak + # server regarding multi-tenancy support. KeyCloak has a notion of realm + # used as an analog of Keystone project/tenant. + keycloak-oidc = mistralclient.auth.keycloak:KeycloakAuthHandler + [nosetests] cover-package = mistralclient