diff --git a/mistral/auth/__init__.py b/mistral/auth/__init__.py new file mode 100644 index 000000000..9c301e26b --- /dev/null +++ b/mistral/auth/__init__.py @@ -0,0 +1,53 @@ +# 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 + +from oslo_config import cfg +from oslo_log import log as logging +import six +from stevedore import driver + +from mistral import exceptions as exc + + +LOG = logging.getLogger(__name__) + +_IMPL_AUTH_HANDLER = None + + +def get_auth_handler(): + auth_type = cfg.CONF.auth_type + + global _IMPL_AUTH_HANDLER + + if not _IMPL_AUTH_HANDLER: + mgr = driver.DriverManager( + 'mistral.auth', + auth_type, + invoke_on_load=True + ) + + _IMPL_AUTH_HANDLER = mgr.driver + + return _IMPL_AUTH_HANDLER + + +@six.add_metaclass(abc.ABCMeta) +class AuthHandler(object): + """Abstract base class for an authentication plugin.""" + + @abc.abstractmethod + def authenticate(self, req): + raise exc.UnauthorizedException() diff --git a/mistral/auth/keycloak.py b/mistral/auth/keycloak.py new file mode 100644 index 000000000..de07f0871 --- /dev/null +++ b/mistral/auth/keycloak.py @@ -0,0 +1,56 @@ +# 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. + +from oslo_config import cfg +from oslo_log import log as logging +import pprint +import requests + +from mistral import auth + + +LOG = logging.getLogger(__name__) + +CONF = cfg.CONF + + +class KeycloakAuthHandler(auth.AuthHandler): + + def authenticate(self, req): + realm_name = req.headers.get('X-Project-Id') + + # NOTE(rakhmerov): There's a special endpoint for introspecting + # access tokens described in OpenID Connect specification but it's + # available in KeyCloak starting only with version 1.8.Final so we have + # to use user info endpoint which also takes exactly one parameter + # (access token) and replies with error if token is invalid. + user_info_endpoint = ( + "%s/realms/%s/protocol/openid-connect/userinfo" % + (CONF.keycloak_oidc.auth_url, realm_name) + ) + + access_token = req.headers.get('X-Auth-Token') + + resp = requests.get( + user_info_endpoint, + headers={"Authorization": "Bearer %s" % access_token}, + verify=not CONF.keycloak_oidc.insecure + ) + + resp.raise_for_status() + + LOG.debug( + "HTTP response from OIDC provider: %s" % + pprint.pformat(resp.json()) + ) diff --git a/mistral/auth/keystone.py b/mistral/auth/keystone.py new file mode 100644 index 000000000..f8737b7a8 --- /dev/null +++ b/mistral/auth/keystone.py @@ -0,0 +1,45 @@ +# 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. + +from oslo_config import cfg +from oslo_log import log as logging + +from mistral import auth +from mistral import exceptions as exc + + +LOG = logging.getLogger(__name__) + +CONF = cfg.CONF + + +class KeystoneAuthHandler(auth.AuthHandler): + + def authenticate(self, req): + # Note(nmakhotkin): Since we have deferred authentication, + # need to check for auth manually (check for corresponding + # headers according to keystonemiddleware docs. + identity_status = req.headers.get('X-Identity-Status') + service_identity_status = req.headers.get('X-Service-Identity-Status') + + if (identity_status == 'Confirmed' or + service_identity_status == 'Confirmed'): + return + + if req.headers.get('X-Auth-Token'): + msg = 'Auth token is invalid: %s' % req.headers['X-Auth-Token'] + else: + msg = 'Authentication required' + + raise exc.UnauthorizedException(msg) diff --git a/mistral/context.py b/mistral/context.py index f3b4bede0..241aa7428 100644 --- a/mistral/context.py +++ b/mistral/context.py @@ -22,9 +22,8 @@ from oslo_serialization import jsonutils from osprofiler import profiler import pecan from pecan import hooks -import pprint -import requests +from mistral import auth from mistral import exceptions as exc from mistral import utils @@ -260,10 +259,8 @@ class AuthHook(hooks.PecanHook): return try: - if CONF.auth_type == 'keystone': - authenticate_with_keystone(state.request) - elif CONF.auth_type == 'keycloak-oidc': - authenticate_with_keycloak(state.request) + auth_handler = auth.get_auth_handler() + auth_handler.authenticate(state.request) except Exception as e: msg = "Failed to validate access token: %s" % str(e) @@ -274,54 +271,6 @@ class AuthHook(hooks.PecanHook): ) -def authenticate_with_keystone(req): - # Note(nmakhotkin): Since we have deferred authentication, - # need to check for auth manually (check for corresponding - # headers according to keystonemiddleware docs. - identity_status = req.headers.get('X-Identity-Status') - service_identity_status = req.headers.get('X-Service-Identity-Status') - - if (identity_status == 'Confirmed' or - service_identity_status == 'Confirmed'): - return - - if req.headers.get('X-Auth-Token'): - msg = 'Auth token is invalid: %s' % req.headers['X-Auth-Token'] - else: - msg = 'Authentication required' - - raise exc.UnauthorizedException(msg) - - -def authenticate_with_keycloak(req): - realm_name = req.headers.get('X-Project-Id') - - # NOTE(rakhmerov): There's a special endpoint for introspecting - # access tokens described in OpenID Connect specification but it's - # available in KeyCloak starting only with version 1.8.Final so we have - # to use user info endpoint which also takes exactly one parameter - # (access token) and replies with error if token is invalid. - user_info_endpoint = ( - "%s/realms/%s/protocol/openid-connect/userinfo" % - (CONF.keycloak_oidc.auth_url, realm_name) - ) - - access_token = req.headers.get('X-Auth-Token') - - resp = requests.get( - user_info_endpoint, - headers={"Authorization": "Bearer %s" % access_token}, - verify=not CONF.keycloak_oidc.insecure - ) - - resp.raise_for_status() - - LOG.debug( - "HTTP response from OIDC provider: %s" % - pprint.pformat(resp.json()) - ) - - class ContextHook(hooks.PecanHook): def before(self, state): set_ctx(context_from_headers_and_env( diff --git a/setup.cfg b/setup.cfg index 085655ace..9e9b662e8 100644 --- a/setup.cfg +++ b/setup.cfg @@ -75,3 +75,7 @@ mistral.yaql_functions = execution = mistral.utils.yaql_utils:execution_ env = mistral.utils.yaql_utils:env_ uuid = mistral.utils.yaql_utils:uuid_ + +mistral.auth = + keystone = mistral.auth.keystone:KeystoneAuthHandler + keycloak-oidc = mistral.auth.keycloak:KeycloakAuthHandler