From c0f845dfe95f79d3b2ddd35d761d00258700b069 Mon Sep 17 00:00:00 2001 From: Eyal Date: Mon, 10 Jul 2017 11:23:06 +0300 Subject: [PATCH] supprt keycloak first stage Change-Id: Ia0b021ee404ab130c79f4d35b1210d365e9f42e0 --- etc/vitrage/api-paste.ini | 21 ++++++-- releasenotes/source/conf.py | 10 +++- requirements.txt | 3 +- test-requirements.txt | 7 ++- vitrage/api/__init__.py | 6 +-- vitrage/middleware/__init__.py | 15 ++++++ vitrage/middleware/keycloak.py | 98 ++++++++++++++++++++++++++++++++++ 7 files changed, 146 insertions(+), 14 deletions(-) create mode 100644 vitrage/middleware/__init__.py create mode 100644 vitrage/middleware/keycloak.py diff --git a/etc/vitrage/api-paste.ini b/etc/vitrage/api-paste.ini index d20d0aadd..8cabb40e2 100644 --- a/etc/vitrage/api-paste.ini +++ b/etc/vitrage/api-paste.ini @@ -10,6 +10,12 @@ use = egg:Paste#urlmap /v1 = vitragev1_keystone_pipeline /healthcheck = healthcheck +[composite:vitrage+keycloak] +use = egg:Paste#urlmap +/ = vitrageversions_pipeline +/v1 = vitragev1_keycloak_pipeline +/healthcheck = healthcheck + [app:healthcheck] use = egg:oslo.middleware#healthcheck oslo_config_project = vitrage @@ -21,20 +27,27 @@ pipeline = cors http_proxy_to_wsgi vitrageversions paste.app_factory = vitrage.api.app:app_factory root = vitrage.api.controllers.root.VersionsController -[pipeline:vitragev1_keystone_pipeline] -pipeline = cors http_proxy_to_wsgi request_id osprofiler authtoken vitragev1 - [pipeline:vitragev1_noauth_pipeline] pipeline = cors http_proxy_to_wsgi request_id osprofiler vitragev1 +[pipeline:vitragev1_keystone_pipeline] +pipeline = cors http_proxy_to_wsgi request_id osprofiler keystoneauthtoken vitragev1 + +[pipeline:vitragev1_keycloak_pipeline] +pipeline = cors http_proxy_to_wsgi request_id osprofiler keycloakauthtoken vitragev1 + [app:vitragev1] paste.app_factory = vitrage.api.app:app_factory root = vitrage.api.controllers.v1.root.V1Controller -[filter:authtoken] +[filter:keystoneauthtoken] paste.filter_factory = keystonemiddleware.auth_token:filter_factory oslo_config_project = vitrage +[filter:keycloakauthtoken] +paste.filter_factory = vitrage.middleware.keycloak:filter_factory +oslo_config_project = vitrage + [filter:request_id] paste.filter_factory = oslo_middleware:RequestId.factory diff --git a/releasenotes/source/conf.py b/releasenotes/source/conf.py index 66234a380..8febc1666 100755 --- a/releasenotes/source/conf.py +++ b/releasenotes/source/conf.py @@ -24,7 +24,7 @@ extensions = [ 'sphinx.ext.autodoc', # 'sphinx.ext.intersphinx', 'reno.sphinxext', - 'oslosphinx' + 'openstackdocstheme' ] # autodoc generation is a bit aggressive and a nuisance when doing heavy @@ -41,6 +41,12 @@ master_doc = 'index' project = u'vitrage releasenotes' copyright = u'2016, Vitrage developers' +# openstackdocstheme options +repository_name = 'openstack/vitrage' +bug_project = 'vitrage' +bug_tag = '' +html_last_updated_fmt = '%Y-%m-%d %H:%M' + # If true, '()' will be appended to :func: etc. cross-reference text. add_function_parentheses = True @@ -56,7 +62,7 @@ pygments_style = 'sphinx' # The theme to use for HTML and HTML Help pages. Major themes that come with # Sphinx are currently 'default' and 'sphinxdoc'. # html_theme_path = ["."] -# html_theme = '_theme' +html_theme = 'openstackdocs' # html_static_path = ['static'] # Output file base name for HTML help builder. diff --git a/requirements.txt b/requirements.txt index 61f40e7e3..071e358d3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -14,7 +14,7 @@ python-novaclient>=9.0.0 # Apache-2.0 python-heatclient>=1.6.1 # Apache-2.0 pyzabbix>=0.7.4 # LGPL networkx>=1.10 # BSD -oslo.config>=4.0.0 # Apache-2.0 +oslo.config!=4.3.0,!=4.4.0,>=4.0.0 # Apache-2.0 oslo.messaging!=5.25.0,>=5.24.2 # Apache-2.0 oslo.log>=3.22.0 # Apache-2.0 oslo.policy>=1.23.0 # Apache-2.0 @@ -28,5 +28,6 @@ stevedore>=1.20.0 # Apache-2.0 voluptuous>=0.8.9 # BSD License sympy>=0.7.6 # BSD pysnmp>=4.2.3 # BSD +PyJWT>=1.0.1 # MIT osprofiler>=1.4.0 # Apache-2.0 diff --git a/test-requirements.txt b/test-requirements.txt index c10f44ae5..965da87df 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -14,11 +14,10 @@ python-novaclient>=9.0.0 # Apache-2.0 python-heatclient>=1.6.1 # Apache-2.0 python-subunit>=0.0.18 # Apache-2.0/BSD pyzabbix>=0.7.4 # LGPL -sphinx!=1.6.1,>=1.5.1 # BSD oslo.log>=3.22.0 # Apache-2.0 oslotest>=1.10.0 # Apache-2.0 oslo.service>=1.10.0 # Apache-2.0 -oslo.config>=4.0.0 # Apache-2.0 +oslo.config!=4.3.0,!=4.4.0,>=4.0.0 # Apache-2.0 oslo.messaging!=5.25.0,>=5.24.2 # Apache-2.0 oslo.i18n!=3.15.2,>=2.1.0 # Apache-2.0 oslo.policy>=1.23.0 # Apache-2.0 @@ -34,5 +33,5 @@ reno!=2.3.1,>=1.8.0 # Apache-2.0 pysnmp>=4.2.3 # BSD # Doc requirements -openstackdocstheme>=1.5.0 # Apache-2.0 -oslosphinx>=4.7.0 # Apache-2.0 +openstackdocstheme>=1.11.0 # Apache-2.0 +sphinx>=1.6.2 # BSD diff --git a/vitrage/api/__init__.py b/vitrage/api/__init__.py index 5297d92f3..d8000a2a3 100644 --- a/vitrage/api/__init__.py +++ b/vitrage/api/__init__.py @@ -25,8 +25,8 @@ OPTS = [ help='Configuration file for WSGI definition of API.'), cfg.IntOpt('workers', default=1, min=1, help='Number of workers for vitrage API server.'), - cfg.BoolOpt('pecan_debug', default=False, - help='Toggle Pecan Debug Middleware.'), - cfg.StrOpt('auth_mode', default='keystone', choices={'keystone', 'noauth'}, + cfg.StrOpt('auth_mode', default='keystone', choices={'keystone', + 'noauth', + 'keycloak'}, help='Authentication mode to use.'), ] diff --git a/vitrage/middleware/__init__.py b/vitrage/middleware/__init__.py new file mode 100644 index 000000000..d19d99459 --- /dev/null +++ b/vitrage/middleware/__init__.py @@ -0,0 +1,15 @@ +# Copyright 2017 - Nokia +# +# 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. + +__author__ = 'stack' diff --git a/vitrage/middleware/keycloak.py b/vitrage/middleware/keycloak.py new file mode 100644 index 000000000..f5d51d47e --- /dev/null +++ b/vitrage/middleware/keycloak.py @@ -0,0 +1,98 @@ +# Copyright 2017 - Nokia +# +# 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 jwt +import requests + +from oslo_config import cfg +from oslo_middleware import base +from oslo_serialization import jsonutils +from webob import exc + +OPENID_CONNECT_USERINFO = '%s/realms/%s/protocol/openid-connect/userinfo' + +KEYCLOAK_OPTS = [ + cfg.StrOpt('auth_url', default='127.0.0.1', + help='Keycloak authentication server ip',), + cfg.StrOpt('insecure', default=False, + help='If True, SSL/TLS certificate verification is disabled'), +] + + +class KeycloakAuth(base.ConfigurableMiddleware): + + def __init__(self, application, conf=None): + super(KeycloakAuth, self).__init__(application, conf) + + self.oslo_conf.register_opts(KEYCLOAK_OPTS, 'keycloak') + self.auth_url = self._conf_get('auth_url', 'keycloak') + self.insecure = self._conf_get('insecure', 'keycloak') + + @property + def reject_auth_headers(self): + header_val = 'Keycloak uri=\'%s\'' % self.auth_url + return [('WWW-Authenticate', header_val)] + + @property + def roles(self): + try: + decoded = jwt.decode(self.token, algorithms=['RS256'], + verify=False) + except jwt.DecodeError: + message = "Token can't be decoded because of wrong format." + self._unauthorized(message) + + return ','.join(decoded['realm_access']['roles']) \ + if 'realm_access' in decoded else '' + + def process_request(self, req): + self._authenticate(req) + + def _authenticate(self, req): + self.token = req.headers.get('X-Auth-Token') + if self.token: + self._decode(req) + else: + message = 'Auth token must be provided in "X-Auth-Token" header.' + self._unauthorized(message) + + def _decode(self, req): + realm_name = req.headers.get('X-Project-Id') + endpoint = OPENID_CONNECT_USERINFO % (self.auth_url, realm_name) + headers = {'Authorization": "Bearer %s' % self.token} + + resp = requests.get(endpoint, headers=headers, + verify=not self.insecure) + + resp.raise_for_status() + + self._set_req_headers(req) + + def _set_req_headers(self, req): + req.headers['X-Identity-Status'] = 'Confirmed' + req.headers['X-Roles'] = self.roles + + def _unauthorized(self, message): + body = {'error': { + 'code': 401, + 'title': 'Unauthorized', + 'message': message, + }} + + raise exc.HTTPUnauthorized(body=jsonutils.dumps(body), + headers=self.reject_auth_headers, + content_type='application/json') + + +filter_factory = KeycloakAuth.factory