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,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
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user