From 64488a1d60cd7f64dd489f1220a035558f2b3cc4 Mon Sep 17 00:00:00 2001 From: Eyal Date: Tue, 20 Jun 2017 15:02:40 +0300 Subject: [PATCH] add no authentication for vitrage * added tests to check it * remove some legacy code not needed * new pipeline in api-paste.ini Change-Id: I1134d42629f41407338fdb1e8789d04aac932880 --- etc/vitrage/api-paste.ini | 39 ++++- vitrage/api/__init__.py | 19 +-- vitrage/api/app.py | 61 ++++---- vitrage/api/controllers/root.py | 4 +- vitrage/keystone_client.py | 61 -------- vitrage/service.py | 1 - vitrage/tests/functional/api/__init__.py | 40 +++--- vitrage/tests/functional/api/test_versions.py | 5 + .../{test_authentication.py => test_auth.py} | 22 +-- .../tests/functional/api/v1/test_noauth.py | 135 ++++++++++++++++++ 10 files changed, 235 insertions(+), 152 deletions(-) rename vitrage/tests/functional/api/v1/{test_authentication.py => test_auth.py} (56%) create mode 100644 vitrage/tests/functional/api/v1/test_noauth.py diff --git a/etc/vitrage/api-paste.ini b/etc/vitrage/api-paste.ini index 4c12ff182..0217c6903 100644 --- a/etc/vitrage/api-paste.ini +++ b/etc/vitrage/api-paste.ini @@ -1,14 +1,41 @@ -# Remove keystone_authtoken from the pipeline if you don't want to use keystone authentication -[pipeline:main] -pipeline = cors keystone_authtoken vitrage +[composite:vitrage+noauth] +use = egg:Paste#urlmap +/ = vitrageversions_pipeline +/v1 = vitragev1_noauth_pipeline -[app:vitrage] +[composite:vitrage+keystone] +use = egg:Paste#urlmap +/ = vitrageversions_pipeline +/v1 = vitragev1_keystone_pipeline + +[pipeline:vitrageversions_pipeline] +pipeline = cors http_proxy_to_wsgi vitrageversions + +[app:vitrageversions] paste.app_factory = vitrage.api.app:app_factory +root = vitrage.api.controllers.root.VersionsController -[filter:keystone_authtoken] +[pipeline:vitragev1_keystone_pipeline] +pipeline = cors http_proxy_to_wsgi request_id authtoken vitragev1 + +[pipeline:vitragev1_noauth_pipeline] +pipeline = cors http_proxy_to_wsgi request_id vitragev1 + +[app:vitragev1] +paste.app_factory = vitrage.api.app:app_factory +root = vitrage.api.controllers.v1.root.V1Controller + +[filter:authtoken] paste.filter_factory = keystonemiddleware.auth_token:filter_factory oslo_config_project = vitrage +[filter:request_id] +paste.filter_factory = oslo_middleware:RequestId.factory + [filter:cors] paste.filter_factory = oslo_middleware.cors:filter_factory -oslo_config_project = vitrage \ No newline at end of file +oslo_config_project = vitrage + +[filter:http_proxy_to_wsgi] +paste.filter_factory = oslo_middleware.http_proxy_to_wsgi:HTTPProxyToWSGI.factory +oslo_config_project = vitrage diff --git a/vitrage/api/__init__.py b/vitrage/api/__init__.py index bc319e189..5297d92f3 100644 --- a/vitrage/api/__init__.py +++ b/vitrage/api/__init__.py @@ -17,21 +17,16 @@ from oslo_config import cfg # Register options for the service OPTS = [ - cfg.PortOpt('port', - default=8999, - help='The port for the vitrage API server.', - ), - cfg.StrOpt('host', - default='0.0.0.0', - help='The listen IP for the vitrage API server.', - ), + cfg.PortOpt('port', default=8999, + help='The port for the vitrage API server.',), + cfg.StrOpt('host', default='0.0.0.0', + help='The listen IP for the vitrage API server.',), cfg.StrOpt('paste_config', default='api-paste.ini', help='Configuration file for WSGI definition of API.'), - - cfg.IntOpt('workers', default=1, - min=1, + 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'}, + help='Authentication mode to use.'), ] diff --git a/vitrage/api/app.py b/vitrage/api/app.py index 1091029a5..728226ec8 100644 --- a/vitrage/api/app.py +++ b/vitrage/api/app.py @@ -11,6 +11,8 @@ # under the License. import os +import uuid + import pecan from oslo_config import cfg @@ -20,36 +22,27 @@ from paste import deploy from werkzeug import serving from vitrage.api import hooks -from vitrage import service LOG = log.getLogger(__name__) -PECAN_CONFIG = { - 'app': { - 'root': 'vitrage.api.controllers.root.RootController', - 'modules': ['vitrage.api'], - }, -} +# NOTE(sileht): pastedeploy uses ConfigParser to handle +# global_conf, since python 3 ConfigParser doesn't +# allow storing object as config value, only strings are +# permit, so to be able to pass an object created before paste load +# the app, we store them into a global var. But the each loaded app +# store it's configuration in unique key to be concurrency safe. +global APPCONFIGS +APPCONFIGS = {} -def setup_app(pecan_config=PECAN_CONFIG, conf=None): - if conf is None: - raise RuntimeError('Config is actually mandatory') +def setup_app(root, conf=None): app_hooks = [hooks.ConfigHook(conf), hooks.TranslationHook(), hooks.RPCHook(conf), hooks.ContextHook()] - pecan.configuration.set_config(dict(pecan_config), overwrite=True) - pecan_debug = conf.api.pecan_debug - if conf.api.workers != 1 and pecan_debug: - pecan_debug = False - LOG.warning('pecan_debug cannot be enabled, if workers is > 1, ' - 'the value is overridden with False') - app = pecan.make_app( - pecan_config['app']['root'], - debug=pecan_debug, + root, hooks=app_hooks, guess_content_type_from_ext=False ) @@ -58,18 +51,25 @@ def setup_app(pecan_config=PECAN_CONFIG, conf=None): def load_app(conf): + global APPCONFIGS + # Build the WSGI app - cfg_file = None cfg_path = conf.api.paste_config if not os.path.isabs(cfg_path): - cfg_file = conf.find_file(cfg_path) - elif os.path.exists(cfg_path): - cfg_file = cfg_path + cfg_path = conf.find_file(cfg_path) - if not cfg_file: + if cfg_path is None or not os.path.exists(cfg_path): raise cfg.ConfigFilesNotFoundError([conf.api.paste_config]) - LOG.info('Full WSGI config used: %s', cfg_file) - return deploy.loadapp("config:" + cfg_file) + + config = dict(conf=conf) + configkey = str(uuid.uuid4()) + APPCONFIGS[configkey] = config + + LOG.info('Full WSGI config used: %s', cfg_path) + + appname = "vitrage+" + conf.api.auth_mode + return deploy.loadapp("config:" + cfg_path, name=appname, + global_conf={'configkey': configkey}) def build_server(conf): @@ -93,10 +93,7 @@ def build_server(conf): app, processes=conf.api.workers) -def _app(): - conf = service.prepare_service() - return setup_app(conf=conf) - - def app_factory(global_config, **local_conf): - return _app() + global APPCONFIGS + appconfig = APPCONFIGS.get(global_config.get('configkey')) + return setup_app(root=local_conf.get('root'), **appconfig) diff --git a/vitrage/api/controllers/root.py b/vitrage/api/controllers/root.py index 86d2f0e73..de8896284 100644 --- a/vitrage/api/controllers/root.py +++ b/vitrage/api/controllers/root.py @@ -11,11 +11,9 @@ # under the License. import pecan as pecan -from vitrage.api.controllers.v1 import root as v1 -class RootController(object): - v1 = v1.V1Controller() +class VersionsController(object): @staticmethod @pecan.expose('json') diff --git a/vitrage/keystone_client.py b/vitrage/keystone_client.py index a96a0d8b4..89d459fcc 100644 --- a/vitrage/keystone_client.py +++ b/vitrage/keystone_client.py @@ -17,7 +17,6 @@ import os from keystoneauth1 import exceptions as ka_exception -from keystoneauth1 import identity as ka_identity from keystoneauth1 import loading as ka_loading # noinspection PyPackageRequirements from keystoneclient.v3 import client as ks_client_v3 @@ -110,63 +109,3 @@ def register_keystoneauth_opts(conf): cfg.DeprecatedOpt('os-cacert', group=CFG_GROUP), cfg.DeprecatedOpt('os-cacert', group="DEFAULT")] }) - conf.set_default("auth_type", default="password-vitrage-legacy", - group=CFG_GROUP) - - -def setup_keystoneauth(conf): - if conf[CFG_GROUP].auth_type == "password-vitrage-legacy": - LOG.warning("Value 'password-vitrage-legacy' for '[%s]/auth_type' " - "is deprecated. And will be removed in Vitrage 2.0. " - "Use 'password' instead.", - CFG_GROUP) - ka_loading.load_auth_from_conf_options(conf, CFG_GROUP) - - -# noinspection PyClassHasNoInit -class LegacyVitrageKeystoneLoader(ka_loading.BaseLoader): - @property - def plugin_class(self): - return ka_identity.V2Password - - def get_options(self): - options = super(LegacyVitrageKeystoneLoader, self).get_options() - options.extend([ - ka_loading.Opt( - 'os-username', - default=os.environ.get('OS_USERNAME', 'vitrage'), - help='User name to use for OpenStack service access.'), - ka_loading.Opt( - 'os-password', - secret=True, - default=os.environ.get('OS_PASSWORD', 'admin'), - help='Password to use for OpenStack service access.'), - ka_loading.Opt( - 'os-tenant-id', - default=os.environ.get('OS_TENANT_ID', ''), - help='Tenant ID to use for OpenStack service access.'), - ka_loading.Opt( - 'os-tenant-name', - default=os.environ.get('OS_TENANT_NAME', 'admin'), - help='Tenant name to use for OpenStack service access.'), - ka_loading.Opt( - 'os-auth-url', - default=os.environ.get('OS_AUTH_URL', - 'http://localhost:5000/v2.0'), - help='Auth URL to use for OpenStack service access.'), - ]) - return options - - def load_from_options(self, **kwargs): - options_map = { - 'os_auth_url': 'auth_url', - 'os_username': 'username', - 'os_password': 'password', - 'os_tenant_name': 'tenant_name', - 'os_tenant_id': 'tenant_id', - } - identity_kwargs = dict((options_map[o.dest], - kwargs.get(o.dest) or o.default) - for o in self.get_options() - if o.dest in options_map) - return self.plugin_class(**identity_kwargs) diff --git a/vitrage/service.py b/vitrage/service.py index ecfabeee1..9b80c1add 100644 --- a/vitrage/service.py +++ b/vitrage/service.py @@ -40,7 +40,6 @@ def prepare_service(args=None, conf=None, config_files=None): keystone_client.register_keystoneauth_opts(conf) - keystone_client.setup_keystoneauth(conf) log.setup(conf, 'vitrage') conf.log_opt_values(LOG, log.DEBUG) messaging.setup() diff --git a/vitrage/tests/functional/api/__init__.py b/vitrage/tests/functional/api/__init__.py index 3b877d22a..a481d98a5 100644 --- a/vitrage/tests/functional/api/__init__.py +++ b/vitrage/tests/functional/api/__init__.py @@ -16,12 +16,13 @@ # under the License. """Base classes for API tests. """ - import os -import pecan -import pecan.testing from oslo_config import fixture as fixture_config +import sys +import webtest + +from vitrage.api import app from vitrage import service from vitrage.tests import base @@ -40,29 +41,24 @@ class FunctionalTest(base.BaseTest): def setUp(self): super(FunctionalTest, self).setUp() conf = service.prepare_service(args=[], config_files=[]) - self.conf = self.useFixture(fixture_config.Config(conf)).conf + self.CONF = self.useFixture(fixture_config.Config(conf)).conf - self.conf.set_override('policy_file', - os.path.abspath('etc/vitrage/policy.json'), - group='oslo_policy') - self.app = self._make_app() + vitrage_init_file = sys.modules['vitrage'].__file__ + vitrage_root = os.path.abspath( + os.path.join(os.path.dirname(vitrage_init_file), '..', )) - def _make_app(self): - self.config = { - 'app': { - 'root': 'vitrage.api.controllers.root.RootController', - 'modules': ['vitrage.api'], - }, - 'wsme': { - 'debug': True, - }, - } + self.CONF.set_override('policy_file', os.path.join(vitrage_root, + 'etc', 'vitrage', + 'policy.json'), + group='oslo_policy', enforce_type=True) - return pecan.testing.load_test_app(self.config, conf=self.conf) + self.CONF.set_override('paste_config', os.path.join(vitrage_root, + 'etc', 'vitrage', + 'api-paste.ini'), + group='api', enforce_type=True) - def tearDown(self): - super(FunctionalTest, self).tearDown() - pecan.set_config({}, overwrite=True) + self.CONF.set_override('auth_mode', self.auth, group='api') + self.app = webtest.TestApp(app.load_app(self.CONF)) def put_json(self, path, params, expect_errors=False, headers=None, extra_environ=None, status=None): diff --git a/vitrage/tests/functional/api/test_versions.py b/vitrage/tests/functional/api/test_versions.py index bb95f05a7..44549e157 100644 --- a/vitrage/tests/functional/api/test_versions.py +++ b/vitrage/tests/functional/api/test_versions.py @@ -26,6 +26,11 @@ VERSIONS_RESPONSE = {u'versions': [{u'id': u'v1.0', class TestVersions(api.FunctionalTest): + + def __init__(self, *args, **kwds): + super(TestVersions, self).__init__(*args, **kwds) + self.auth = 'keystone' + def test_versions(self): data = self.get_json('/') self.assertEqual(VERSIONS_RESPONSE, data) diff --git a/vitrage/tests/functional/api/v1/test_authentication.py b/vitrage/tests/functional/api/v1/test_auth.py similarity index 56% rename from vitrage/tests/functional/api/v1/test_authentication.py rename to vitrage/tests/functional/api/v1/test_auth.py index 859adf5ac..0461fb559 100644 --- a/vitrage/tests/functional/api/v1/test_authentication.py +++ b/vitrage/tests/functional/api/v1/test_auth.py @@ -15,23 +15,15 @@ # limitations under the License. # noinspection PyPackageRequirements -import mock -import webtest - -from vitrage.api import app from vitrage.tests.functional.api.v1 import FunctionalTest -class TestAuthentications(FunctionalTest): - def _make_app(self): - file_name = self.path_get('etc/vitrage/api-paste.ini') - self.conf.set_override("paste_config", file_name, "api") - # We need the other call to prepare_service in app.py to return the - # same tweaked conf object. - with mock.patch('vitrage.service.prepare_service') as ps: - ps.return_value = self.conf - return webtest.TestApp(app.load_app(conf=self.conf)) +class AuthTest(FunctionalTest): - def test_not_authenticated(self): + def __init__(self, *args, **kwds): + super(AuthTest, self).__init__(*args, **kwds) + self.auth = 'keystone' + + def test_in_keystone_mode_not_authenticated(self): resp = self.post_json('/topology/', params=None, expect_errors=True) - self.assertEqual(401, resp.status_int) + self.assertEqual('401 Unauthorized', resp.status) diff --git a/vitrage/tests/functional/api/v1/test_noauth.py b/vitrage/tests/functional/api/v1/test_noauth.py new file mode 100644 index 000000000..0da7cad17 --- /dev/null +++ b/vitrage/tests/functional/api/v1/test_noauth.py @@ -0,0 +1,135 @@ +# Copyright 2016 - 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. + + +from datetime import datetime +# noinspection PyPackageRequirements +from mock import mock + +from vitrage.tests.functional.api.v1 import FunctionalTest + + +class NoAuthTest(FunctionalTest): + + def __init__(self, *args, **kwds): + super(NoAuthTest, self).__init__(*args, **kwds) + self.auth = 'noauth' + + def test_noauth_mode_post_event(self): + + with mock.patch('pecan.request') as request: + + details = { + 'hostname': 'host123', + 'source': 'sample_monitor', + 'cause': 'another alarm', + 'severity': 'critical', + 'status': 'down', + 'monitor_id': 'sample monitor', + 'monitor_event_id': '456', + } + event_time = datetime.now().isoformat() + event_type = 'compute.host.down' + + resp = self.post_json('/event/', params={'time': event_time, + 'type': event_type, + 'details': details}) + + self.assertEqual(1, request.client.call.call_count) + self.assertEqual('200 OK', resp.status) + + def test_noauth_mode_get_topology(self): + + with mock.patch('pecan.request') as request: + request.client.call.return_value = '{}' + params = dict(depth=None, graph_type='graph', query=None, + root=None, + all_tenants=False) + resp = self.post_json('/topology/', params=params) + + self.assertEqual(1, request.client.call.call_count) + self.assertEqual('200 OK', resp.status) + self.assertEqual({}, resp.json) + + def test_noauth_mode_list_alarms(self): + + with mock.patch('pecan.request') as request: + request.client.call.return_value = '{"alarms": []}' + params = dict(vitrage_id='all', all_tenants=False) + resp = self.post_json('/alarm/', params=params) + + self.assertEqual(1, request.client.call.call_count) + self.assertEqual('200 OK', resp.status) + self.assertEqual([], resp.json) + + def test_noauth_mode_list_resources(self): + + with mock.patch('pecan.request') as request: + request.client.call.return_value = '{"resources": []}' + params = dict(resource_type='all', all_tenants=False) + data = self.get_json('/resources/', params=params) + + self.assertEqual(1, request.client.call.call_count) + self.assertEqual([], data) + + def test_noauth_mode_show_resource(self): + + with mock.patch('pecan.request') as request: + request.client.call.return_value = '{}' + params = dict(resource_type='all', all_tenants=False) + data = self.get_json('/resources/1234', params=params) + + self.assertEqual(1, request.client.call.call_count) + self.assertEqual({}, data) + + def test_noauth_mode_list_templates(self): + + with mock.patch('pecan.request') as request: + request.client.call.return_value = '{"templates_details": []}' + data = self.get_json('/template/') + + self.assertEqual(1, request.client.call.call_count) + self.assertEqual([], data) + + def test_noauth_mode_show_template(self): + + with mock.patch('pecan.request') as request: + request.client.call.return_value = '{}' + data = self.get_json('/template/1234') + + self.assertEqual(1, request.client.call.call_count) + self.assertEqual({}, data) + + def test_noauth_mode_validate_template(self): + + with mock.patch('pecan.request') as request: + request.client.call.return_value = '{}' + params = {"templates": {}} + resp = self.post_json('/template/', params=params) + + self.assertEqual(1, request.client.call.call_count) + self.assertEqual('200 OK', resp.status) + self.assertEqual({}, resp.json) + + def test_noauth_mode_get_rca(self): + + with mock.patch('pecan.request') as request: + request.client.call.return_value = '{}' + params = dict(all_tenants=False) + data = self.get_json('/rca/1234/', params=params) + + self.assertEqual(1, request.client.call.call_count) + self.assertEqual({}, data)