Merge "Abstract authentication function"
This commit is contained in:
@@ -12,58 +12,15 @@
|
|||||||
# 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):
|
||||||
if mistral_version.find("v2") != -1:
|
if mistral_version.find("v2") != -1:
|
||||||
return 2
|
return 2
|
||||||
|
|
||||||
raise RuntimeError("Can not determine mistral API version")
|
raise RuntimeError("Cannot determine mistral API 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,119 +16,147 @@ 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):
|
|
||||||
"""Performs authentication using Keycloak OpenID Protocol.
|
|
||||||
|
|
||||||
:param auth_url: Base authentication url of KeyCloak server (e.g.
|
def authenticate(self, req):
|
||||||
"https://my.keycloak:8443/auth"
|
"""Performs authentication using Keycloak OpenID Protocol.
|
||||||
: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).
|
|
||||||
|
|
||||||
"""
|
:param req: Request dict containing list of parameters required
|
||||||
if not auth_url:
|
for Keycloak authentication.
|
||||||
raise ValueError('Base authentication url is not provided.')
|
|
||||||
|
|
||||||
if not client_id:
|
auth_url: Base authentication url of KeyCloak server (e.g.
|
||||||
raise ValueError('Client ID is not provided.')
|
"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:
|
auth_url = req.get('auth_url')
|
||||||
raise ValueError('Project(realm) name is not provided.')
|
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:
|
if not auth_url:
|
||||||
raise ValueError(
|
raise ValueError('Base authentication url is not provided.')
|
||||||
"User name and access token can't be provided at the same time."
|
|
||||||
|
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:
|
client_auth = (client_id, client_secret)
|
||||||
return _authenticate_with_token(
|
|
||||||
auth_url,
|
body = {
|
||||||
client_id,
|
'grant_type': 'password',
|
||||||
client_secret,
|
'username': username,
|
||||||
access_token,
|
'password': password,
|
||||||
cacert,
|
'scope': 'profile'
|
||||||
insecure
|
}
|
||||||
|
|
||||||
|
resp = requests.post(
|
||||||
|
access_token_endpoint,
|
||||||
|
auth=client_auth,
|
||||||
|
data=body,
|
||||||
|
verify=not insecure
|
||||||
)
|
)
|
||||||
|
|
||||||
if not username:
|
try:
|
||||||
raise ValueError('Either user name or access token must be provided.')
|
resp.raise_for_status()
|
||||||
|
except Exception as e:
|
||||||
|
raise Exception("Failed to get access token:\n %s" % str(e))
|
||||||
|
|
||||||
return _authenticate_with_password(
|
LOG.debug(
|
||||||
auth_url,
|
"HTTP response from OIDC provider: %s" %
|
||||||
client_id,
|
pprint.pformat(resp.json())
|
||||||
client_secret,
|
)
|
||||||
realm_name,
|
|
||||||
username,
|
|
||||||
password,
|
|
||||||
cacert,
|
|
||||||
insecure
|
|
||||||
)
|
|
||||||
|
|
||||||
|
return resp.json()['access_token']
|
||||||
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']
|
|
||||||
|
|
||||||
|
|
||||||
# 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,60 +12,115 @@
|
|||||||
# 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,
|
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
def _get_keystone_client(auth_url):
|
def _get_keystone_client(auth_url):
|
||||||
if "v2.0" in auth_url:
|
if 'v2.0' in auth_url:
|
||||||
from keystoneclient.v2_0 import client
|
from keystoneclient.v2_0 import client
|
||||||
else:
|
else:
|
||||||
from keystoneclient.v3 import client
|
from keystoneclient.v3 import client
|
||||||
|
|
||||||
return 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
|
||||||
|
}
|
||||||
|
@@ -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
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user