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:
parent
5438b20c46
commit
55d55bcdf1
@ -12,54 +12,11 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
import six
|
|
||||||
|
|
||||||
from mistralclient.api.v2 import client as client_v2
|
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,
|
def client(auth_type='keystone', **kwargs):
|
||||||
project_name=None, auth_url=None, project_id=None,
|
return client_v2.Client(auth_type=auth_type, **kwargs)
|
||||||
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 determine_client_version(mistral_version):
|
def determine_client_version(mistral_version):
|
||||||
|
@ -37,31 +37,31 @@ def log_request(func):
|
|||||||
|
|
||||||
|
|
||||||
class HTTPClient(object):
|
class HTTPClient(object):
|
||||||
def __init__(self, base_url, token=None, project_id=None, user_id=None,
|
def __init__(self, base_url, **kwargs):
|
||||||
cacert=None, insecure=False, target_token=None,
|
|
||||||
target_auth_uri=None, **kwargs):
|
|
||||||
self.base_url = base_url
|
self.base_url = base_url
|
||||||
self.token = token
|
self.auth_token = kwargs.get('auth_token', None)
|
||||||
self.project_id = project_id
|
self.project_id = kwargs.get('project_id', None)
|
||||||
self.user_id = user_id
|
self.user_id = kwargs.get('user_id', None)
|
||||||
self.target_token = target_token
|
self.target_auth_token = kwargs.get('target_auth_token', None)
|
||||||
self.target_auth_uri = target_auth_uri
|
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 = {}
|
self.ssl_options = {}
|
||||||
|
|
||||||
if self.base_url.startswith('https'):
|
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 '
|
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 '
|
LOG.warning('Client is set to not verify even though '
|
||||||
'cacert is provided.')
|
'cacert is provided.')
|
||||||
|
|
||||||
if insecure:
|
if self.insecure:
|
||||||
self.ssl_options['verify'] = False
|
self.ssl_options['verify'] = False
|
||||||
else:
|
else:
|
||||||
if cacert:
|
if self.cacert:
|
||||||
self.ssl_options['verify'] = cacert
|
self.ssl_options['verify'] = self.cacert
|
||||||
else:
|
else:
|
||||||
self.ssl_options['verify'] = True
|
self.ssl_options['verify'] = True
|
||||||
|
|
||||||
@ -107,9 +107,9 @@ class HTTPClient(object):
|
|||||||
if not headers:
|
if not headers:
|
||||||
headers = {}
|
headers = {}
|
||||||
|
|
||||||
token = headers.get('x-auth-token', self.token)
|
auth_token = headers.get('x-auth-token', self.auth_token)
|
||||||
if token:
|
if auth_token:
|
||||||
headers['x-auth-token'] = token
|
headers['x-auth-token'] = auth_token
|
||||||
|
|
||||||
project_id = headers.get('X-Project-Id', self.project_id)
|
project_id = headers.get('X-Project-Id', self.project_id)
|
||||||
if project_id:
|
if project_id:
|
||||||
@ -119,14 +119,18 @@ class HTTPClient(object):
|
|||||||
if user_id:
|
if user_id:
|
||||||
headers['X-User-Id'] = user_id
|
headers['X-User-Id'] = user_id
|
||||||
|
|
||||||
target_token = headers.get('X-Target-Auth-Token', self.target_token)
|
target_auth_token = headers.get(
|
||||||
if target_token:
|
'X-Target-Auth-Token',
|
||||||
headers['X-Target-Auth-Token'] = target_token
|
self.target_auth_token
|
||||||
|
)
|
||||||
|
|
||||||
target_auth_uri = headers.get('X-Target-Auth-Uri',
|
if target_auth_token:
|
||||||
self.target_auth_uri)
|
headers['X-Target-Auth-Token'] = target_auth_token
|
||||||
if target_auth_uri:
|
|
||||||
headers['X-Target-Auth-Uri'] = target_auth_uri
|
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:
|
if osprofiler_web:
|
||||||
# Add headers for osprofiler.
|
# Add headers for osprofiler.
|
||||||
|
@ -13,6 +13,7 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
|
import copy
|
||||||
import six
|
import six
|
||||||
|
|
||||||
from oslo_utils import importutils
|
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 tasks
|
||||||
from mistralclient.api.v2 import workbooks
|
from mistralclient.api.v2 import workbooks
|
||||||
from mistralclient.api.v2 import workflows
|
from mistralclient.api.v2 import workflows
|
||||||
from mistralclient.auth import auth_types
|
from mistralclient import auth
|
||||||
from mistralclient.auth import keycloak
|
|
||||||
from mistralclient.auth import keystone
|
|
||||||
|
|
||||||
osprofiler_profiler = importutils.try_import("osprofiler.profiler")
|
osprofiler_profiler = importutils.try_import("osprofiler.profiler")
|
||||||
|
|
||||||
@ -38,62 +38,25 @@ _DEFAULT_MISTRAL_URL = "http://localhost:8989/v2"
|
|||||||
|
|
||||||
|
|
||||||
class Client(object):
|
class Client(object):
|
||||||
def __init__(self, mistral_url=None, username=None, api_key=None,
|
def __init__(self, auth_type='keystone', **kwargs):
|
||||||
project_name=None, auth_url=None, project_id=None,
|
req = copy.deepcopy(kwargs)
|
||||||
endpoint_type='publicURL', service_type='workflowv2',
|
mistral_url = req.get('mistral_url')
|
||||||
auth_token=None, user_id=None, cacert=None, insecure=False,
|
auth_url = req.get('auth_url')
|
||||||
profile=None, auth_type=auth_types.KEYSTONE, client_id=None,
|
auth_token = req.get('auth_token')
|
||||||
client_secret=None, target_username=None, target_api_key=None,
|
project_id = req.get('project_id')
|
||||||
target_project_name=None, target_auth_url=None,
|
user_id = req.get('user_id')
|
||||||
target_project_id=None, target_auth_token=None,
|
profile = req.get('profile')
|
||||||
target_user_id=None, target_cacert=None,
|
|
||||||
target_insecure=False, **kwargs):
|
|
||||||
|
|
||||||
if mistral_url and not isinstance(mistral_url, six.string_types):
|
if mistral_url and not isinstance(mistral_url, six.string_types):
|
||||||
raise RuntimeError('Mistral url should be a string.')
|
raise RuntimeError('Mistral url should be a string.')
|
||||||
|
|
||||||
if auth_url:
|
if auth_url and not auth_token:
|
||||||
if auth_type == auth_types.KEYSTONE:
|
auth_handler = auth.get_auth_handler(auth_type)
|
||||||
(mistral_url, auth_token, project_id, user_id) = (
|
auth_response = auth_handler.authenticate(req) or {}
|
||||||
keystone.authenticate(
|
mistral_url = auth_response.get('mistral_url') or mistral_url
|
||||||
mistral_url,
|
req['auth_token'] = auth_response.get('token')
|
||||||
username,
|
req['project_id'] = auth_response.get('project_id') or project_id
|
||||||
api_key,
|
req['user_id'] = auth_response.get('user_id') or user_id
|
||||||
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 not mistral_url:
|
if not mistral_url:
|
||||||
mistral_url = _DEFAULT_MISTRAL_URL
|
mistral_url = _DEFAULT_MISTRAL_URL
|
||||||
@ -101,33 +64,7 @@ class Client(object):
|
|||||||
if profile:
|
if profile:
|
||||||
osprofiler_profiler.init(profile)
|
osprofiler_profiler.init(profile)
|
||||||
|
|
||||||
if target_auth_url:
|
http_client = httpclient.HTTPClient(mistral_url, **req)
|
||||||
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
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create all resource managers.
|
# Create all resource managers.
|
||||||
self.workbooks = workbooks.WorkbookManager(http_client)
|
self.workbooks = workbooks.WorkbookManager(http_client)
|
||||||
|
@ -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()
|
@ -12,15 +12,10 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
|
from stevedore import extension
|
||||||
|
|
||||||
# Valid authentication types.
|
# Valid authentication types.
|
||||||
|
ALL = extension.ExtensionManager(
|
||||||
# Standard Keystone authentication.
|
namespace='mistralclient.auth',
|
||||||
KEYSTONE = 'keystone'
|
invoke_on_load=False
|
||||||
|
).names()
|
||||||
# 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]
|
|
||||||
|
@ -16,30 +16,50 @@ import logging
|
|||||||
import pprint
|
import pprint
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
|
from mistralclient import auth
|
||||||
|
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def authenticate(auth_url, client_id, client_secret, realm_name,
|
class KeycloakAuthHandler(auth.AuthHandler):
|
||||||
username=None, password=None, access_token=None,
|
|
||||||
cacert=None, insecure=False):
|
def authenticate(self, req):
|
||||||
"""Performs authentication using Keycloak OpenID Protocol.
|
"""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"
|
"https://my.keycloak:8443/auth"
|
||||||
:param client_id: Client ID (according to OpenID Connect protocol).
|
client_id: Client ID (according to OpenID Connect protocol).
|
||||||
:param client_secret: Client secret (according to OpenID Connect protocol).
|
client_secret: Client secret (according to OpenID Connect
|
||||||
:param realm_name: KeyCloak realm name.
|
protocol).
|
||||||
:param username: User name (Optional, if None then access_token must be
|
realm_name: KeyCloak realm name.
|
||||||
|
username: User name (Optional, if None then access_token must be
|
||||||
provided).
|
provided).
|
||||||
:param password: Password (Optional).
|
password: Password (Optional).
|
||||||
:param access_token: Access token. If passed, username and password are
|
access_token: Access token. If passed, username and password are
|
||||||
not used and this method just validates the token and refreshes it,
|
not used and this method just validates the token and refreshes
|
||||||
if needed. (Optional, if None then username must be provided)
|
it if needed (Optional, if None then username must be
|
||||||
:param cacert: SSL certificate file (Optional).
|
provided).
|
||||||
:param insecure: If True, SSL certificate is not verified (Optional).
|
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:
|
if not auth_url:
|
||||||
raise ValueError('Base authentication url is not provided.')
|
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:
|
if username and access_token:
|
||||||
raise ValueError(
|
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:
|
if access_token:
|
||||||
return _authenticate_with_token(
|
response = self._authenticate_with_token(
|
||||||
auth_url,
|
auth_url,
|
||||||
client_id,
|
client_id,
|
||||||
client_secret,
|
client_secret,
|
||||||
@ -66,11 +92,8 @@ def authenticate(auth_url, client_id, client_secret, realm_name,
|
|||||||
cacert,
|
cacert,
|
||||||
insecure
|
insecure
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
if not username:
|
response = self._authenticate_with_password(
|
||||||
raise ValueError('Either user name or access token must be provided.')
|
|
||||||
|
|
||||||
return _authenticate_with_password(
|
|
||||||
auth_url,
|
auth_url,
|
||||||
client_id,
|
client_id,
|
||||||
client_secret,
|
client_secret,
|
||||||
@ -81,18 +104,21 @@ def authenticate(auth_url, client_id, client_secret, realm_name,
|
|||||||
insecure
|
insecure
|
||||||
)
|
)
|
||||||
|
|
||||||
|
response['project_id'] = realm_name
|
||||||
|
|
||||||
def _authenticate_with_token(auth_url, client_id, client_secret, auth_token,
|
return response
|
||||||
cacert=None, insecure=None):
|
|
||||||
|
def _authenticate_with_token(auth_url, client_id, client_secret,
|
||||||
|
auth_token, cacert=None, insecure=None):
|
||||||
# TODO(rakhmerov): Implement.
|
# TODO(rakhmerov): Implement.
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
def _authenticate_with_password(auth_url, client_id, client_secret,
|
def _authenticate_with_password(auth_url, client_id, client_secret,
|
||||||
realm_name, username, password,
|
realm_name, username, password,
|
||||||
cacert=None, insecure=None):
|
cacert=None, insecure=None):
|
||||||
access_token_endpoint = (
|
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)
|
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))
|
raise Exception("Failed to get access token:\n %s" % str(e))
|
||||||
|
|
||||||
LOG.debug(
|
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']
|
return resp.json()['access_token']
|
||||||
|
|
||||||
|
|
||||||
# An example of using KeyCloak OpenID authentication.
|
# An example of using KeyCloak OpenID authentication.
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
print("Using username/password to get access token from KeyCloak...")
|
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",
|
"https://my.keycloak:8443/auth",
|
||||||
client_id="mistral_client",
|
client_id="mistral_client",
|
||||||
client_secret="4a080907-921b-409a-b793-c431609c3a47",
|
client_secret="4a080907-921b-409a-b793-c431609c3a47",
|
||||||
|
@ -12,12 +12,51 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
|
from mistralclient import auth
|
||||||
|
|
||||||
def authenticate(mistral_url=None, username=None,
|
|
||||||
api_key=None, project_name=None, auth_url=None,
|
def _get_keystone_client(auth_url):
|
||||||
project_id=None, endpoint_type='publicURL',
|
if 'v2.0' in auth_url:
|
||||||
service_type='workflowv2', auth_token=None, user_id=None,
|
from keystoneclient.v2_0 import client
|
||||||
cacert=None, insecure=False):
|
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:
|
if project_name and project_id:
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
@ -29,6 +68,7 @@ def authenticate(mistral_url=None, username=None,
|
|||||||
'Only user name or user id should be set'
|
'Only user name or user id should be set'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if auth_url:
|
||||||
keystone_client = _get_keystone_client(auth_url)
|
keystone_client = _get_keystone_client(auth_url)
|
||||||
|
|
||||||
keystone = keystone_client.Client(
|
keystone = keystone_client.Client(
|
||||||
@ -45,11 +85,28 @@ def authenticate(mistral_url=None, username=None,
|
|||||||
)
|
)
|
||||||
|
|
||||||
keystone.authenticate()
|
keystone.authenticate()
|
||||||
|
auth_token = keystone.auth_token
|
||||||
token = keystone.auth_token
|
|
||||||
user_id = keystone.user_id
|
user_id = keystone.user_id
|
||||||
project_id = keystone.project_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:
|
if not mistral_url:
|
||||||
try:
|
try:
|
||||||
mistral_url = keystone.service_catalog.url_for(
|
mistral_url = keystone.service_catalog.url_for(
|
||||||
@ -59,13 +116,11 @@ def authenticate(mistral_url=None, username=None,
|
|||||||
except Exception:
|
except Exception:
|
||||||
mistral_url = None
|
mistral_url = None
|
||||||
|
|
||||||
return mistral_url, token, project_id, user_id
|
return {
|
||||||
|
'mistral_url': mistral_url,
|
||||||
|
'token': auth_token,
|
||||||
def _get_keystone_client(auth_url):
|
'project_id': target_project_id if target_auth_url else project_id,
|
||||||
if "v2.0" in auth_url:
|
'user_id': target_user_id if target_auth_url else user_id,
|
||||||
from keystoneclient.v2_0 import client
|
'target_auth_token': target_auth_token,
|
||||||
else:
|
'target_auth_url': target_auth_url
|
||||||
from keystoneclient.v3 import client
|
}
|
||||||
|
|
||||||
return client
|
|
||||||
|
@ -317,7 +317,7 @@ class MistralShell(app.App):
|
|||||||
'--auth-type',
|
'--auth-type',
|
||||||
action='store',
|
action='store',
|
||||||
dest='auth_type',
|
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.'
|
help='Authentication type. Valid options are: %s.'
|
||||||
' (Env: MISTRAL_AUTH_TYPE)' % auth_types.ALL
|
' (Env: MISTRAL_AUTH_TYPE)' % auth_types.ALL
|
||||||
)
|
)
|
||||||
|
@ -92,16 +92,15 @@ class BaseClientTests(base.BaseTestCase):
|
|||||||
|
|
||||||
expected_args = (
|
expected_args = (
|
||||||
MISTRAL_HTTP_URL,
|
MISTRAL_HTTP_URL,
|
||||||
keystone_client_instance.auth_token,
|
|
||||||
keystone_client_instance.project_id,
|
|
||||||
keystone_client_instance.user_id
|
|
||||||
)
|
)
|
||||||
|
|
||||||
expected_kwargs = {
|
expected_kwargs = {
|
||||||
'cacert': None,
|
'username': 'mistral',
|
||||||
'insecure': False,
|
'project_name': 'mistral',
|
||||||
'target_auth_uri': None,
|
'auth_url': AUTH_HTTP_URL_v3,
|
||||||
'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(
|
client.client(
|
||||||
@ -111,8 +110,8 @@ class BaseClientTests(base.BaseTestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
self.assertTrue(mocked.called)
|
self.assertTrue(mocked.called)
|
||||||
self.assertEqual(mocked.call_args[0], expected_args)
|
self.assertEqual(expected_args, mocked.call_args[0])
|
||||||
self.assertDictEqual(mocked.call_args[1], expected_kwargs)
|
self.assertDictEqual(expected_kwargs, mocked.call_args[1])
|
||||||
|
|
||||||
@mock.patch('keystoneclient.v3.client.Client')
|
@mock.patch('keystoneclient.v3.client.Client')
|
||||||
@mock.patch('mistralclient.api.httpclient.HTTPClient')
|
@mock.patch('mistralclient.api.httpclient.HTTPClient')
|
||||||
@ -126,16 +125,18 @@ class BaseClientTests(base.BaseTestCase):
|
|||||||
|
|
||||||
expected_args = (
|
expected_args = (
|
||||||
MISTRAL_HTTPS_URL,
|
MISTRAL_HTTPS_URL,
|
||||||
keystone_client_instance.auth_token,
|
|
||||||
keystone_client_instance.project_id,
|
|
||||||
keystone_client_instance.user_id
|
|
||||||
)
|
)
|
||||||
|
|
||||||
expected_kwargs = {
|
expected_kwargs = {
|
||||||
|
'mistral_url': MISTRAL_HTTPS_URL,
|
||||||
|
'username': 'mistral',
|
||||||
|
'project_name': 'mistral',
|
||||||
|
'auth_url': AUTH_HTTP_URL_v3,
|
||||||
'cacert': None,
|
'cacert': None,
|
||||||
'insecure': True,
|
'insecure': True,
|
||||||
'target_auth_uri': None,
|
'auth_token': keystone_client_instance.auth_token,
|
||||||
'target_token': None
|
'project_id': keystone_client_instance.project_id,
|
||||||
|
'user_id': keystone_client_instance.user_id
|
||||||
}
|
}
|
||||||
|
|
||||||
client.client(
|
client.client(
|
||||||
@ -148,8 +149,8 @@ class BaseClientTests(base.BaseTestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
self.assertTrue(mocked.called)
|
self.assertTrue(mocked.called)
|
||||||
self.assertEqual(mocked.call_args[0], expected_args)
|
self.assertEqual(expected_args, mocked.call_args[0])
|
||||||
self.assertDictEqual(mocked.call_args[1], expected_kwargs)
|
self.assertDictEqual(expected_kwargs, mocked.call_args[1])
|
||||||
|
|
||||||
@mock.patch('keystoneclient.v3.client.Client')
|
@mock.patch('keystoneclient.v3.client.Client')
|
||||||
@mock.patch('mistralclient.api.httpclient.HTTPClient')
|
@mock.patch('mistralclient.api.httpclient.HTTPClient')
|
||||||
@ -163,16 +164,18 @@ class BaseClientTests(base.BaseTestCase):
|
|||||||
|
|
||||||
expected_args = (
|
expected_args = (
|
||||||
MISTRAL_HTTPS_URL,
|
MISTRAL_HTTPS_URL,
|
||||||
keystone_client_instance.auth_token,
|
|
||||||
keystone_client_instance.project_id,
|
|
||||||
keystone_client_instance.user_id
|
|
||||||
)
|
)
|
||||||
|
|
||||||
expected_kwargs = {
|
expected_kwargs = {
|
||||||
|
'mistral_url': MISTRAL_HTTPS_URL,
|
||||||
|
'username': 'mistral',
|
||||||
|
'project_name': 'mistral',
|
||||||
|
'auth_url': AUTH_HTTP_URL_v3,
|
||||||
'cacert': path,
|
'cacert': path,
|
||||||
'insecure': False,
|
'insecure': False,
|
||||||
'target_auth_uri': None,
|
'auth_token': keystone_client_instance.auth_token,
|
||||||
'target_token': None
|
'project_id': keystone_client_instance.project_id,
|
||||||
|
'user_id': keystone_client_instance.user_id
|
||||||
}
|
}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -189,8 +192,8 @@ class BaseClientTests(base.BaseTestCase):
|
|||||||
os.unlink(path)
|
os.unlink(path)
|
||||||
|
|
||||||
self.assertTrue(mock.called)
|
self.assertTrue(mock.called)
|
||||||
self.assertEqual(mock.call_args[0], expected_args)
|
self.assertEqual(expected_args, mock.call_args[0])
|
||||||
self.assertDictEqual(mock.call_args[1], expected_kwargs)
|
self.assertDictEqual(expected_kwargs, mock.call_args[1])
|
||||||
|
|
||||||
@mock.patch('keystoneclient.v3.client.Client')
|
@mock.patch('keystoneclient.v3.client.Client')
|
||||||
def test_mistral_url_https_bad_cacert(self, keystone_client_mock):
|
def test_mistral_url_https_bad_cacert(self, keystone_client_mock):
|
||||||
@ -248,16 +251,16 @@ class BaseClientTests(base.BaseTestCase):
|
|||||||
|
|
||||||
expected_args = (
|
expected_args = (
|
||||||
MISTRAL_HTTP_URL,
|
MISTRAL_HTTP_URL,
|
||||||
keystone_client_instance.auth_token,
|
|
||||||
keystone_client_instance.project_id,
|
|
||||||
keystone_client_instance.user_id
|
|
||||||
)
|
)
|
||||||
|
|
||||||
expected_kwargs = {
|
expected_kwargs = {
|
||||||
'cacert': None,
|
'username': 'mistral',
|
||||||
'insecure': False,
|
'project_name': 'mistral',
|
||||||
'target_auth_uri': None,
|
'auth_url': AUTH_HTTP_URL_v3,
|
||||||
'target_token': None
|
'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(
|
client.client(
|
||||||
@ -268,8 +271,8 @@ class BaseClientTests(base.BaseTestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
self.assertTrue(mocked.called)
|
self.assertTrue(mocked.called)
|
||||||
self.assertEqual(mocked.call_args[0], expected_args)
|
self.assertEqual(expected_args, mocked.call_args[0])
|
||||||
self.assertDictEqual(mocked.call_args[1], expected_kwargs)
|
self.assertDictEqual(expected_kwargs, mocked.call_args[1])
|
||||||
|
|
||||||
profiler = osprofiler.profiler.get()
|
profiler = osprofiler.profiler.get()
|
||||||
|
|
||||||
|
@ -73,9 +73,9 @@ class HTTPClientTest(base.BaseTestCase):
|
|||||||
osprofiler.profiler.init(None)
|
osprofiler.profiler.init(None)
|
||||||
self.client = httpclient.HTTPClient(
|
self.client = httpclient.HTTPClient(
|
||||||
API_BASE_URL,
|
API_BASE_URL,
|
||||||
AUTH_TOKEN,
|
auth_token=AUTH_TOKEN,
|
||||||
PROJECT_ID,
|
project_id=PROJECT_ID,
|
||||||
USER_ID
|
user_id=USER_ID
|
||||||
)
|
)
|
||||||
|
|
||||||
@mock.patch.object(
|
@mock.patch.object(
|
||||||
@ -133,23 +133,23 @@ class HTTPClientTest(base.BaseTestCase):
|
|||||||
mock.MagicMock(return_value=FakeResponse('get', EXPECTED_URL, 200))
|
mock.MagicMock(return_value=FakeResponse('get', EXPECTED_URL, 200))
|
||||||
)
|
)
|
||||||
def test_get_request_options_with_headers_for_get(self):
|
def test_get_request_options_with_headers_for_get(self):
|
||||||
target_auth_uri = str(uuid.uuid4())
|
target_auth_url = str(uuid.uuid4())
|
||||||
target_token = str(uuid.uuid4())
|
target_auth_token = str(uuid.uuid4())
|
||||||
|
|
||||||
target_client = httpclient.HTTPClient(
|
target_client = httpclient.HTTPClient(
|
||||||
API_BASE_URL,
|
API_BASE_URL,
|
||||||
AUTH_TOKEN,
|
auth_token=AUTH_TOKEN,
|
||||||
PROJECT_ID,
|
project_id=PROJECT_ID,
|
||||||
USER_ID,
|
user_id=USER_ID,
|
||||||
target_auth_uri=target_auth_uri,
|
target_auth_url=target_auth_url,
|
||||||
target_token=target_token
|
target_auth_token=target_auth_token
|
||||||
)
|
)
|
||||||
|
|
||||||
target_client.get(API_URL)
|
target_client.get(API_URL)
|
||||||
|
|
||||||
expected_options = copy.deepcopy(EXPECTED_REQ_OPTIONS)
|
expected_options = copy.deepcopy(EXPECTED_REQ_OPTIONS)
|
||||||
expected_options["headers"]["X-Target-Auth-Uri"] = target_auth_uri
|
expected_options["headers"]["X-Target-Auth-Uri"] = target_auth_url
|
||||||
expected_options["headers"]["X-Target-Auth-Token"] = target_token
|
expected_options["headers"]["X-Target-Auth-Token"] = target_auth_token
|
||||||
|
|
||||||
requests.get.assert_called_with(
|
requests.get.assert_called_with(
|
||||||
EXPECTED_URL,
|
EXPECTED_URL,
|
||||||
|
@ -9,3 +9,4 @@ python-keystoneclient!=2.1.0,>=2.0.0 # Apache-2.0
|
|||||||
PyYAML>=3.1.0 # MIT
|
PyYAML>=3.1.0 # MIT
|
||||||
requests>=2.10.0 # Apache-2.0
|
requests>=2.10.0 # Apache-2.0
|
||||||
six>=1.9.0 # MIT
|
six>=1.9.0 # MIT
|
||||||
|
stevedore>=1.16.0 # Apache-2.0
|
||||||
|
@ -102,6 +102,15 @@ openstack.workflow_engine.v2 =
|
|||||||
resource_member_delete = mistralclient.commands.v2.members:Delete
|
resource_member_delete = mistralclient.commands.v2.members:Delete
|
||||||
resource_member_update = mistralclient.commands.v2.members:Update
|
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]
|
[nosetests]
|
||||||
cover-package = mistralclient
|
cover-package = mistralclient
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user