From dbf5c610de396ddc12f0c90294b4116699d1bf53 Mon Sep 17 00:00:00 2001 From: Eyal Date: Mon, 31 Jul 2017 16:24:07 +0300 Subject: [PATCH] add some tests for keycloak rename cmd package to cli to prevent module collision when running unittests in pycharm Change-Id: I24625526b593eae3430c1e1320a1693bf590fe79 --- setup.cfg | 8 +- test-requirements.txt | 1 + vitrage/cli/__init__.py | 0 vitrage/{cmd => cli}/api.py | 0 vitrage/{cmd => cli}/collector.py | 0 vitrage/{cmd => cli}/graph.py | 0 vitrage/{cmd => cli}/notifier.py | 0 vitrage/cmd/__init__.py | 15 -- vitrage/middleware/keycloak.py | 6 +- vitrage/tests/functional/api/__init__.py | 2 +- .../tests/functional/api/v1/test_keycloak.py | 139 ++++++++++++++++++ 11 files changed, 149 insertions(+), 22 deletions(-) create mode 100644 vitrage/cli/__init__.py rename vitrage/{cmd => cli}/api.py (100%) rename vitrage/{cmd => cli}/collector.py (100%) rename vitrage/{cmd => cli}/graph.py (100%) rename vitrage/{cmd => cli}/notifier.py (100%) delete mode 100644 vitrage/cmd/__init__.py create mode 100644 vitrage/tests/functional/api/v1/test_keycloak.py diff --git a/setup.cfg b/setup.cfg index 1e7fcc551..dd09701b7 100644 --- a/setup.cfg +++ b/setup.cfg @@ -25,10 +25,10 @@ setup-hooks = [entry_points] console_scripts = - vitrage-api = vitrage.cmd.api:main - vitrage-graph = vitrage.cmd.graph:main - vitrage-notifier = vitrage.cmd.notifier:main - vitrage-collector = vitrage.cmd.collector:main + vitrage-api = vitrage.cli.api:main + vitrage-graph = vitrage.cli.graph:main + vitrage-notifier = vitrage.cli.notifier:main + vitrage-collector = vitrage.cli.collector:main vitrage.entity_graph = networkx = vitrage.graph.driver.networkx_graph:NXGraph diff --git a/test-requirements.txt b/test-requirements.txt index be0eb2f2b..eb17adc5e 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -22,6 +22,7 @@ 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 pecan!=1.0.2,!=1.0.3,!=1.0.4,!=1.2,>=1.0.0 # BSD +requests-mock>=1.1 # Apache-2.0 tempest-lib>=0.14.0 # Apache-2.0 testrepository>=0.0.18 # Apache-2.0/BSD testscenarios>=0.4 # Apache-2.0/BSD diff --git a/vitrage/cli/__init__.py b/vitrage/cli/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/vitrage/cmd/api.py b/vitrage/cli/api.py similarity index 100% rename from vitrage/cmd/api.py rename to vitrage/cli/api.py diff --git a/vitrage/cmd/collector.py b/vitrage/cli/collector.py similarity index 100% rename from vitrage/cmd/collector.py rename to vitrage/cli/collector.py diff --git a/vitrage/cmd/graph.py b/vitrage/cli/graph.py similarity index 100% rename from vitrage/cmd/graph.py rename to vitrage/cli/graph.py diff --git a/vitrage/cmd/notifier.py b/vitrage/cli/notifier.py similarity index 100% rename from vitrage/cmd/notifier.py rename to vitrage/cli/notifier.py diff --git a/vitrage/cmd/__init__.py b/vitrage/cmd/__init__.py deleted file mode 100644 index 7a05fb1f4..000000000 --- a/vitrage/cmd/__init__.py +++ /dev/null @@ -1,15 +0,0 @@ -# Copyright 2015 - Alcatel-Lucent -# -# 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 index f5d51d47e..0097e3d1b 100644 --- a/vitrage/middleware/keycloak.py +++ b/vitrage/middleware/keycloak.py @@ -23,7 +23,7 @@ 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', + cfg.StrOpt('auth_url', default='http://127.0.0.1', help='Keycloak authentication server ip',), cfg.StrOpt('insecure', default=False, help='If True, SSL/TLS certificate verification is disabled'), @@ -46,6 +46,7 @@ class KeycloakAuth(base.ConfigurableMiddleware): @property def roles(self): + decoded = {} try: decoded = jwt.decode(self.token, algorithms=['RS256'], verify=False) @@ -70,7 +71,7 @@ class KeycloakAuth(base.ConfigurableMiddleware): 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} + headers = {'Authorization': 'Bearer %s' % self.token} resp = requests.get(endpoint, headers=headers, verify=not self.insecure) @@ -92,6 +93,7 @@ class KeycloakAuth(base.ConfigurableMiddleware): raise exc.HTTPUnauthorized(body=jsonutils.dumps(body), headers=self.reject_auth_headers, + charset='UTF-8', content_type='application/json') diff --git a/vitrage/tests/functional/api/__init__.py b/vitrage/tests/functional/api/__init__.py index a481d98a5..db718630b 100644 --- a/vitrage/tests/functional/api/__init__.py +++ b/vitrage/tests/functional/api/__init__.py @@ -78,7 +78,7 @@ class FunctionalTest(base.BaseTest): headers=headers, extra_environ=extra_environ, status=status, method='put') - def post_json(self, path, params, expect_errors=False, headers=None, + def post_json(self, path, params=None, expect_errors=False, headers=None, method="post", extra_environ=None, status=None): """Sends simulated HTTP POST request to Pecan test app. diff --git a/vitrage/tests/functional/api/v1/test_keycloak.py b/vitrage/tests/functional/api/v1/test_keycloak.py new file mode 100644 index 000000000..c6fac5d18 --- /dev/null +++ b/vitrage/tests/functional/api/v1/test_keycloak.py @@ -0,0 +1,139 @@ +# Copyright 2017 - Nokia Corporation +# Copyright 2014 OpenStack Foundation +# +# 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. + +# noinspection PyPackageRequirements +from datetime import datetime +from mock import mock +import requests +import requests_mock +from webtest import TestRequest + +from vitrage.middleware.keycloak import KeycloakAuth +from vitrage.tests.functional.api.v1 import FunctionalTest + + +TOKEN = { + "iss": "http://127.0.0.1/auth/realms/my_realm", + "realm_access": { + "roles": ["role1", "role2"] + } +} + +HEADERS = { + 'X-Auth-Token': str(TOKEN), + 'X-Project-Id': 'my_realm' +} + +OPENID_CONNECT_USERINFO = 'http://127.0.0.1/realms/my_realm/protocol/' \ + 'openid-connect/userinfo' + +USER_CLAIMS = { + "sub": "248289761001", + "name": "Jane Doe", + "given_name": "Jane", + "family_name": "Doe", + "preferred_username": "j.doe", + "email": "janedoe@example.com", + "picture": "http://example.com/janedoe/me.jpg" +} + +EVENT_DETAILS = { + 'hostname': 'host123', + 'source': 'sample_monitor', + 'cause': 'another alarm', + 'severity': 'critical', + 'status': 'down', + 'monitor_id': 'sample monitor', + 'monitor_event_id': '456', +} + +NO_TOKEN_ERROR_MSG = {'error': { + 'code': 401, + 'title': 'Unauthorized', + 'message': 'Auth token must be provided in "X-Auth-Token" header.', +}} + + +class KeycloakTest(FunctionalTest): + + def __init__(self, *args, **kwds): + super(KeycloakTest, self).__init__(*args, **kwds) + self.auth = 'keycloak' + + @staticmethod + def _build_request(): + req = TestRequest.blank('/') + req.headers = HEADERS + return req + + @mock.patch('jwt.decode', return_value=TOKEN) + @requests_mock.Mocker() + def test_header_parsing(self, _, req_mock): + + # Imitate success response from KeyCloak. + req_mock.get(OPENID_CONNECT_USERINFO) + + req = self._build_request() + auth = KeycloakAuth(mock.Mock(), self.CONF) + auth.process_request(req) + + self.assertEqual('Confirmed', req.headers['X-Identity-Status']) + self.assertEqual('my_realm', req.headers['X-Project-Id']) + self.assertEqual('role1,role2', req.headers['X-Roles']) + self.assertEqual(1, req_mock.call_count) + + def test_in_keycloak_mode_no_token(self): + resp = self.post_json('/topology/', expect_errors=True) + + self.assertEqual('401 Unauthorized', resp.status) + self.assertEqual(NO_TOKEN_ERROR_MSG, resp.json) + + @mock.patch('jwt.decode', return_value=TOKEN) + @requests_mock.Mocker() + def test_in_keycloak_mode_wrong_token(self, _, req_mock): + + # Imitate failure response from KeyCloak. + req_mock.get( + requests_mock.ANY, + status_code=401, + reason='Access token is invalid' + ) + + self.assertRaises(requests.exceptions.HTTPError, + self.post_json, + '/topology/', + params=None, + headers=HEADERS, + expect_errors=True) + + @mock.patch('jwt.decode', return_value=TOKEN) + @requests_mock.Mocker() + def test_in_keycloak_mode_auth_success(self, _, req_mock): + + # Imitate success response from KeyCloak. + req_mock.get(OPENID_CONNECT_USERINFO, json=USER_CLAIMS) + + with mock.patch('pecan.request') as request: + resp = self.post_json('/event/', + params={ + 'time': datetime.now().isoformat(), + 'type': 'compute.host.down', + 'details': EVENT_DETAILS + }, + headers=HEADERS) + + self.assertEqual(1, request.client.call.call_count) + self.assertEqual('200 OK', resp.status)