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
This commit is contained in:
Winson Chan 2016-09-16 01:28:10 +00:00
parent 5438b20c46
commit 55d55bcdf1
12 changed files with 374 additions and 348 deletions

View File

@ -12,54 +12,11 @@
# 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):

View File

@ -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.

View File

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

View File

@ -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()

View File

@ -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()

View File

@ -16,30 +16,50 @@ 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):
class KeycloakAuthHandler(auth.AuthHandler):
def authenticate(self, req):
"""Performs authentication using Keycloak OpenID Protocol.
:param auth_url: Base authentication url of KeyCloak server (e.g.
:param req: Request dict containing list of parameters required
for Keycloak authentication.
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
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).
: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).
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 isinstance(req, dict):
raise TypeError('The input "req" is not typeof dict.')
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 not auth_url:
raise ValueError('Base authentication url is not provided.')
@ -54,11 +74,17 @@ def authenticate(auth_url, client_id, client_secret, realm_name,
if username and access_token:
raise ValueError(
"User name and access token can't be provided at the same time."
"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:
return _authenticate_with_token(
response = self._authenticate_with_token(
auth_url,
client_id,
client_secret,
@ -66,11 +92,8 @@ def authenticate(auth_url, client_id, client_secret, realm_name,
cacert,
insecure
)
if not username:
raise ValueError('Either user name or access token must be provided.')
return _authenticate_with_password(
else:
response = self._authenticate_with_password(
auth_url,
client_id,
client_secret,
@ -81,18 +104,21 @@ def authenticate(auth_url, client_id, client_secret, realm_name,
insecure
)
response['project_id'] = realm_name
def _authenticate_with_token(auth_url, client_id, client_secret, auth_token,
cacert=None, insecure=None):
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)
"%s/realms/%s/protocol/openid-connect/token" %
(auth_url, realm_name)
)
client_auth = (client_id, client_secret)
@ -117,18 +143,20 @@ def _authenticate_with_password(auth_url, client_id, client_secret,
raise Exception("Failed to get access token:\n %s" % str(e))
LOG.debug(
"HTTP response from OIDC provider: %s" % pprint.pformat(resp.json())
"HTTP response from OIDC provider: %s" %
pprint.pformat(resp.json())
)
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",

View File

@ -12,12 +12,51 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from mistralclient import auth
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):
def _get_keystone_client(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(
@ -29,6 +68,7 @@ def authenticate(mistral_url=None, username=None,
'Only user name or user id should be set'
)
if auth_url:
keystone_client = _get_keystone_client(auth_url)
keystone = keystone_client.Client(
@ -45,11 +85,28 @@ def authenticate(mistral_url=None, username=None,
)
keystone.authenticate()
token = keystone.auth_token
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(
@ -59,13 +116,11 @@ def authenticate(mistral_url=None, username=None,
except Exception:
mistral_url = None
return mistral_url, token, project_id, user_id
def _get_keystone_client(auth_url):
if "v2.0" in auth_url:
from keystoneclient.v2_0 import client
else:
from keystoneclient.v3 import client
return client
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
}

View File

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

View File

@ -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()

View File

@ -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,

View File

@ -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

View File

@ -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