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