From 044f389848108a63a383ed411bfc8256d783f830 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Tr=C4=99bski?= Date: Sat, 29 Oct 2016 04:34:57 +0200 Subject: [PATCH] Integration with oslo.context Similar to other openstack projects, monasca-api should provide information about request's context in the log information. This is done by: * implementing custom Request that creates context (oslo.context) * changing logging configuration to use ContextLogFormatter Since information like tenant-id can be retrieved from the context, modified resource code to use that. Also moved 'limit' directly to request property. Change-Id: I917fa2cba99dc668842fea0a62cda2cabd796d09 --- .../files/monasca-api/python/api-config.ini | 7 +- .../files/monasca-api/python/api-logging.conf | 13 +-- etc/api-config.ini | 7 +- etc/api-logging.conf | 13 +-- .../{middleware => api/core}/__init__.py | 0 monasca_api/api/core/request.py | 109 ++++++++++++++++++ monasca_api/api/server.py | 4 +- monasca_api/middleware/context.py | 83 ------------- monasca_api/middleware/inspector.py | 49 -------- .../middleware/keystone_context_filter.py | 109 ------------------ monasca_api/middleware/mock_auth_filter.py | 37 ------ monasca_api/tests/base.py | 36 ++++++ monasca_api/tests/test_alarms.py | 4 +- monasca_api/tests/test_request.py | 105 +++++++++++++++++ monasca_api/tests/test_validation.py | 16 +-- monasca_api/v2/reference/alarm_definitions.py | 26 ++--- monasca_api/v2/reference/alarms.py | 46 +++----- monasca_api/v2/reference/helpers.py | 57 ++------- monasca_api/v2/reference/metrics.py | 37 +++--- monasca_api/v2/reference/notifications.py | 23 ++-- monasca_api/v2/reference/notificationstype.py | 3 +- requirements.txt | 1 + 22 files changed, 335 insertions(+), 450 deletions(-) rename monasca_api/{middleware => api/core}/__init__.py (100%) create mode 100644 monasca_api/api/core/request.py delete mode 100644 monasca_api/middleware/context.py delete mode 100644 monasca_api/middleware/inspector.py delete mode 100644 monasca_api/middleware/keystone_context_filter.py delete mode 100644 monasca_api/middleware/mock_auth_filter.py create mode 100644 monasca_api/tests/base.py create mode 100644 monasca_api/tests/test_request.py diff --git a/devstack/files/monasca-api/python/api-config.ini b/devstack/files/monasca-api/python/api-config.ini index 951aeb050..000381eda 100644 --- a/devstack/files/monasca-api/python/api-config.ini +++ b/devstack/files/monasca-api/python/api-config.ini @@ -2,8 +2,7 @@ name = monasca_api [pipeline:main] -# Add validator in the pipeline so the metrics messages can be validated. -pipeline = auth keystonecontext api +pipeline = request_id auth api [app:api] paste.app_factory = monasca_api.api.server:launch @@ -11,8 +10,8 @@ paste.app_factory = monasca_api.api.server:launch [filter:auth] paste.filter_factory = keystonemiddleware.auth_token:filter_factory -[filter:keystonecontext] -paste.filter_factory = monasca_api.middleware.keystone_context_filter:filter_factory +[filter:request_id] +paste.filter_factory = oslo_middleware.request_id:RequestId.factory [server:main] use = egg:gunicorn#main diff --git a/devstack/files/monasca-api/python/api-logging.conf b/devstack/files/monasca-api/python/api-logging.conf index c4e842b83..0c54137d3 100644 --- a/devstack/files/monasca-api/python/api-logging.conf +++ b/devstack/files/monasca-api/python/api-logging.conf @@ -5,11 +5,10 @@ keys = root, sqlalchemy, kafka keys = console, file [formatters] -keys = generic +keys = context [logger_root] level = DEBUG -formatter = default handlers = console, file [logger_sqlalchemy] @@ -18,14 +17,12 @@ qualname = sqlalchemy.engine # "level = DEBUG" logs SQL queries and results. # "level = WARN" logs neither. (Recommended for production systems.) level = DEBUG -formatter = default handlers = console, file propagate=0 [logger_kafka] qualname = kafka level = DEBUG -formatter = default handlers = console, file propagate = 0 @@ -33,14 +30,14 @@ propagate = 0 class = logging.StreamHandler args = (sys.stderr,) level = DEBUG -formatter = generic +formatter = context [handler_file] class = logging.handlers.RotatingFileHandler level = DEBUG -formatter = generic +formatter = context # store up to 5*100MB of logs args = ('/var/log/monasca/api/monasca-api.log', 'a', 104857600, 5) -[formatter_generic] -format = %(asctime)s %(levelname)s [%(name)s][%(threadName)s] %(message)s \ No newline at end of file +[formatter_context] +class = oslo_log.formatters.ContextFormatter \ No newline at end of file diff --git a/etc/api-config.ini b/etc/api-config.ini index 3ad164d71..a0fc2af05 100644 --- a/etc/api-config.ini +++ b/etc/api-config.ini @@ -2,8 +2,7 @@ name = monasca_api [pipeline:main] -# Add validator in the pipeline so the metrics messages can be validated. -pipeline = auth keystonecontext api +pipeline = request_id auth api [app:api] paste.app_factory = monasca_api.api.server:launch @@ -11,8 +10,8 @@ paste.app_factory = monasca_api.api.server:launch [filter:auth] paste.filter_factory = keystonemiddleware.auth_token:filter_factory -[filter:keystonecontext] -paste.filter_factory = monasca_api.middleware.keystone_context_filter:filter_factory +[filter:request_id] +paste.filter_factory = oslo_middleware.request_id:RequestId.factory [server:main] use = egg:gunicorn#main diff --git a/etc/api-logging.conf b/etc/api-logging.conf index c4e842b83..0c54137d3 100644 --- a/etc/api-logging.conf +++ b/etc/api-logging.conf @@ -5,11 +5,10 @@ keys = root, sqlalchemy, kafka keys = console, file [formatters] -keys = generic +keys = context [logger_root] level = DEBUG -formatter = default handlers = console, file [logger_sqlalchemy] @@ -18,14 +17,12 @@ qualname = sqlalchemy.engine # "level = DEBUG" logs SQL queries and results. # "level = WARN" logs neither. (Recommended for production systems.) level = DEBUG -formatter = default handlers = console, file propagate=0 [logger_kafka] qualname = kafka level = DEBUG -formatter = default handlers = console, file propagate = 0 @@ -33,14 +30,14 @@ propagate = 0 class = logging.StreamHandler args = (sys.stderr,) level = DEBUG -formatter = generic +formatter = context [handler_file] class = logging.handlers.RotatingFileHandler level = DEBUG -formatter = generic +formatter = context # store up to 5*100MB of logs args = ('/var/log/monasca/api/monasca-api.log', 'a', 104857600, 5) -[formatter_generic] -format = %(asctime)s %(levelname)s [%(name)s][%(threadName)s] %(message)s \ No newline at end of file +[formatter_context] +class = oslo_log.formatters.ContextFormatter \ No newline at end of file diff --git a/monasca_api/middleware/__init__.py b/monasca_api/api/core/__init__.py similarity index 100% rename from monasca_api/middleware/__init__.py rename to monasca_api/api/core/__init__.py diff --git a/monasca_api/api/core/request.py b/monasca_api/api/core/request.py new file mode 100644 index 000000000..b8ff35746 --- /dev/null +++ b/monasca_api/api/core/request.py @@ -0,0 +1,109 @@ +# Copyright 2016 FUJITSU LIMITED +# +# 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 falcon + +from oslo_context import context + +from monasca_api.common.repositories import constants +from monasca_api.v2.common import exceptions + +_TENANT_ID_PARAM = 'tenant_id' +"""Name of the query-param pointing at project-id (tenant-id)""" + + +class Request(falcon.Request): + """Variation of falcon.Request with context + + Following class enhances :py:class:`falcon.Request` with + :py:class:`context.RequestContext`. + + """ + + def __init__(self, env, options=None): + super(Request, self).__init__(env, options) + self.context = context.RequestContext.from_environ(self.env) + + @property + def project_id(self): + """Returns project-id (tenant-id) + + :return: project-id + :rtype: str + + """ + return self.context.tenant + + @property + def cross_project_id(self): + """Returns project-id (tenant-id) found in query params. + + This particular project-id is later on identified as + cross-project-id + + :return: project-id + :rtype: str + + """ + return self.get_param(_TENANT_ID_PARAM, required=False) + + @property + def user_id(self): + """Returns user-id + + :return: user-id + :rtype: str + + """ + return self.context.user + + @property + def roles(self): + """Returns roles associated with user + + :return: user's roles + :rtype: list + + """ + return self.context.roles + + @property + def limit(self): + """Returns LIMIT query param value. + + 'limit' is not required query param. + In case it is not found, py:data:'.constants.PAGE_LIMIT' + value is returned. + + :return: value of 'limit' query param or default value + :rtype: int + :raise exceptions.HTTPUnprocessableEntityError: if limit is not valid integer + + """ + limit = self.get_param('limit', required=False, default=None) + if limit is not None: + if limit.isdigit(): + limit = int(limit) + if limit > constants.PAGE_LIMIT: + return constants.PAGE_LIMIT + else: + return limit + else: + err_msg = 'Limit parameter must be a positive integer' + raise exceptions.HTTPUnprocessableEntityError('Invalid limit', err_msg) + else: + return constants.PAGE_LIMIT + + def __repr__(self): + return '%s, context=%s' % (self.path, self.context) diff --git a/monasca_api/api/server.py b/monasca_api/api/server.py index c14c6c992..058701ce0 100644 --- a/monasca_api/api/server.py +++ b/monasca_api/api/server.py @@ -22,6 +22,8 @@ from oslo_config import cfg from oslo_log import log import paste.deploy +from monasca_api.api.core import request + dispatcher_opts = [cfg.StrOpt('versions', default=None, help='Versions'), cfg.StrOpt('version_2_0', default=None, @@ -66,7 +68,7 @@ def launch(conf, config_file="/etc/monasca/api-config.conf"): default_config_files=[config_file]) log.setup(cfg.CONF, 'monasca_api') - app = falcon.API() + app = falcon.API(request_type=request.Request) versions = simport.load(cfg.CONF.dispatcher.versions)() app.add_route("/", versions) diff --git a/monasca_api/middleware/context.py b/monasca_api/middleware/context.py deleted file mode 100644 index 5cf4a8745..000000000 --- a/monasca_api/middleware/context.py +++ /dev/null @@ -1,83 +0,0 @@ -# Copyright (c) 2015 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. - -"""RequestContext: context for requests that persist through monasca.""" - -import uuid - -from oslo_log import log -from oslo_utils import timeutils - -LOG = log.getLogger(__name__) - - -class RequestContext(object): - """Security context and request information. - - Represents the user taking a given action within the system. - - """ - - def __init__(self, user_id, project_id, domain_id=None, domain_name=None, - roles=None, timestamp=None, request_id=None, - auth_token=None, user_name=None, project_name=None, - service_catalog=None, user_auth_plugin=None, **kwargs): - """Creates the Keystone Context. Supports additional parameters: - - :param user_auth_plugin: - The auth plugin for the current request's authentication data. - :param kwargs: - Extra arguments that might be present - """ - if kwargs: - LOG.warning( - 'Arguments dropped when creating context: %s') % str(kwargs) - - self._roles = roles or [] - self.timestamp = timeutils.utcnow() - - if not request_id: - request_id = self.generate_request_id() - self._request_id = request_id - self._auth_token = auth_token - - self._service_catalog = service_catalog - - self._domain_id = domain_id - self._domain_name = domain_name - - self._user_id = user_id - self._user_name = user_name - - self._project_id = project_id - self._project_name = project_name - - self._user_auth_plugin = user_auth_plugin - - def to_dict(self): - return {'user_id': self._user_id, - 'project_id': self._project_id, - 'domain_id': self._domain_id, - 'domain_name': self._domain_name, - 'roles': self._roles, - 'timestamp': timeutils.strtime(self._timestamp), - 'request_id': self._request_id, - 'auth_token': self._auth_token, - 'user_name': self._user_name, - 'service_catalog': self._service_catalog, - 'project_name': self._project_name, - 'user': self._user} - - def generate_request_id(self): - return b'req-' + str(uuid.uuid4()).encode('ascii') diff --git a/monasca_api/middleware/inspector.py b/monasca_api/middleware/inspector.py deleted file mode 100644 index 9bfd38dbf..000000000 --- a/monasca_api/middleware/inspector.py +++ /dev/null @@ -1,49 +0,0 @@ -# Copyright 2013 IBM Corp -# -# 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. - - -class Inspector(object): - """The middleware that logs the request body and header - - This is a middleware for debug purposes. To enable this middleware, add - the following lines into the configuration file, for example: - - [pipeline:main] - pipeline = inspector api - - [filter:inspector] - use = egg: monasca_api_server#inspector - """ - def __init__(self, app, conf): - """Inspect each request - - :param app: OptionParser to use. If not sent one will be created. - :param conf: Override sys.argv; used in testing - """ - self.app = app - self.conf = conf - print('Inspect config:', self.conf) - - def __call__(self, environ, start_response): - - print('environ is ', environ) - return self.app(environ, start_response) - - -def filter_factory(global_conf, **local_conf): - - def login_filter(app): - return Inspector(app, local_conf) - - return login_filter diff --git a/monasca_api/middleware/keystone_context_filter.py b/monasca_api/middleware/keystone_context_filter.py deleted file mode 100644 index 19cfb415c..000000000 --- a/monasca_api/middleware/keystone_context_filter.py +++ /dev/null @@ -1,109 +0,0 @@ -# Copyright (c) 2015 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. - -import falcon -from oslo_log import log -from oslo_middleware import request_id -from oslo_serialization import jsonutils - -from monasca_api.middleware import context - -LOG = log.getLogger(__name__) - - -def filter_factory(global_conf, **local_conf): - def validator_filter(app): - return KeystoneContextFilter(app, local_conf) - - return validator_filter - - -class KeystoneContextFilter(object): - """Make a request context from keystone headers.""" - - def __init__(self, app, conf): - self._app = app - self._conf = conf - - def __call__(self, env, start_response): - - LOG.debug("Creating Keystone Context Object.") - - user_id = env.get('HTTP_X_USER_ID', env.get('HTTP_X_USER')) - if user_id is None: - msg = "Neither X_USER_ID nor X_USER found in request" - LOG.error(msg) - raise falcon.HTTPUnauthorized(title='Forbidden', description=msg) - - roles = self._get_roles(env) - - project_id = env.get('HTTP_X_PROJECT_ID') - project_name = env.get('HTTP_X_PROJECT_NAME') - - domain_id = env.get('HTTP_X_DOMAIN_ID') - domain_name = env.get('HTTP_X_DOMAIN_NAME') - - user_name = env.get('HTTP_X_USER_NAME') - - req_id = env.get(request_id.ENV_REQUEST_ID) - - # Get the auth token - auth_token = env.get('HTTP_X_AUTH_TOKEN', - env.get('HTTP_X_STORAGE_TOKEN')) - - service_catalog = None - if env.get('HTTP_X_SERVICE_CATALOG') is not None: - try: - catalog_header = env.get('HTTP_X_SERVICE_CATALOG') - service_catalog = jsonutils.loads(catalog_header) - except ValueError: - msg = "Invalid service catalog json." - LOG.error(msg) - raise falcon.HTTPInternalServerError(msg) - - # NOTE(jamielennox): This is a full auth plugin set by auth_token - # middleware in newer versions. - user_auth_plugin = env.get('keystone.token_auth') - - # Build a context - ctx = context.RequestContext(user_id, - project_id, - user_name=user_name, - project_name=project_name, - domain_id=domain_id, - domain_name=domain_name, - roles=roles, - auth_token=auth_token, - service_catalog=service_catalog, - request_id=req_id, - user_auth_plugin=user_auth_plugin) - - env['monasca.context'] = ctx - - LOG.debug("Keystone Context successfully created.") - - return self._app(env, start_response) - - def _get_roles(self, env): - """Get the list of roles.""" - - if 'HTTP_X_ROLES' in env: - roles = env.get('HTTP_X_ROLES', '') - else: - # Fallback to deprecated role header: - roles = env.get('HTTP_X_ROLE', '') - if roles: - LOG.warning( - 'Sourcing roles from deprecated X-Role HTTP header') - return [r.strip() for r in roles.split(',')] diff --git a/monasca_api/middleware/mock_auth_filter.py b/monasca_api/middleware/mock_auth_filter.py deleted file mode 100644 index 85eb3e27f..000000000 --- a/monasca_api/middleware/mock_auth_filter.py +++ /dev/null @@ -1,37 +0,0 @@ -# Copyright 2014 Hewlett-Packard -# -# 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. - - -class MockAuthFilter(object): - """Authorization filter. - - This authorization filter doesn't do any authentication, it just copies the - auth token to the tenant ID and supplies the 'admin' role and is meant for - testing purposes only. - """ - - def __init__(self, app, conf): - self.app = app - self.conf = conf - - def __call__(self, env, start_response): - env['HTTP_X_TENANT_ID'] = env['HTTP_X_AUTH_TOKEN'] - env['HTTP_X_ROLES'] = 'admin' - return self.app(env, start_response) - - -def filter_factory(global_conf, **local_conf): - def validator_filter(app): - return MockAuthFilter(app, local_conf) - return validator_filter diff --git a/monasca_api/tests/base.py b/monasca_api/tests/base.py new file mode 100644 index 000000000..dc2a2d5e8 --- /dev/null +++ b/monasca_api/tests/base.py @@ -0,0 +1,36 @@ +# Copyright 2015 kornicameister@gmail.com +# Copyright 2015 FUJITSU LIMITED +# +# 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 falcon + +from monasca_api.api.core import request + + +class MockedAPI(falcon.API): + """MockedAPI + + Subclasses :py:class:`falcon.API` in order to overwrite + request_type property with custom :py:class:`request.Request` + + """ + + def __init__(self): + super(MockedAPI, self).__init__( + media_type=falcon.DEFAULT_MEDIA_TYPE, + request_type=request.Request, + response_type=falcon.Response, + middleware=None, + router=None + ) diff --git a/monasca_api/tests/test_alarms.py b/monasca_api/tests/test_alarms.py index dece1c627..7effc5fdd 100644 --- a/monasca_api/tests/test_alarms.py +++ b/monasca_api/tests/test_alarms.py @@ -24,10 +24,10 @@ import fixtures import testtools.matchers as matchers from monasca_api.common.repositories.model import sub_alarm_definition +from monasca_api.tests import base from monasca_api.v2.reference import alarm_definitions from monasca_api.v2.reference import alarms -import oslo_config import oslo_config.fixture import oslotest.base as oslotest @@ -159,6 +159,8 @@ class RESTResponseEquals(object): class AlarmTestBase(falcon.testing.TestBase, oslotest.BaseTestCase): + api_class = base.MockedAPI + def setUp(self): super(AlarmTestBase, self).setUp() diff --git a/monasca_api/tests/test_request.py b/monasca_api/tests/test_request.py new file mode 100644 index 000000000..666958acd --- /dev/null +++ b/monasca_api/tests/test_request.py @@ -0,0 +1,105 @@ +# Copyright 2016 FUJITSU LIMITED +# +# 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 mock import mock +from oslo_config import fixture as oo_cfg +from oslo_context import fixture as oo_ctx + +from falcon import testing + +from monasca_api.api.core import request +from monasca_api.v2.common import exceptions + + +class TestRequest(testing.TestBase): + + def test_use_context_from_request(self): + req = request.Request( + testing.create_environ( + path='/', + headers={ + 'X_AUTH_TOKEN': '111', + 'X_USER_ID': '222', + 'X_PROJECT_ID': '333', + 'X_ROLES': 'terminator,predator' + } + ) + ) + + self.assertEqual('111', req.context.auth_token) + self.assertEqual('222', req.user_id) + self.assertEqual('333', req.project_id) + self.assertEqual(['terminator', 'predator'], req.roles) + + +class TestRequestLimit(testing.TestBase): + def setUp(self): + super(TestRequestLimit, self).setUp() + self.useFixture(oo_cfg.Config()) + self.useFixture(oo_ctx.ClearRequestContext()) + + def test_valid_limit(self): + expected_limit = 10 + req = request.Request( + testing.create_environ( + path='/', + query_string='limit=%d' % expected_limit, + headers={ + 'X_AUTH_TOKEN': '111', + 'X_USER_ID': '222', + 'X_PROJECT_ID': '333', + 'X_ROLES': 'terminator,predator' + } + ) + ) + self.assertEqual(expected_limit, req.limit) + + def test_invalid_limit(self): + req = request.Request( + testing.create_environ( + path='/', + query_string='limit=abc', + headers={ + 'X_AUTH_TOKEN': '111', + 'X_USER_ID': '222', + 'X_PROJECT_ID': '333', + 'X_ROLES': 'terminator,predator' + } + ) + ) + + # note(trebskit) assertRaises fails to call property + # so we need the actual function + def property_wrapper(): + return req.limit + + self.assertRaises( + exceptions.HTTPUnprocessableEntityError, + property_wrapper + ) + + @mock.patch('monasca_api.common.repositories.constants.PAGE_LIMIT') + def test_default_limit(self, page_limit): + req = request.Request( + testing.create_environ( + path='/', + headers={ + 'X_AUTH_TOKEN': '111', + 'X_USER_ID': '222', + 'X_PROJECT_ID': '333', + 'X_ROLES': 'terminator,predator' + } + ) + ) + self.assertEqual(page_limit, req.limit) diff --git a/monasca_api/tests/test_validation.py b/monasca_api/tests/test_validation.py index c727da47f..25617c433 100644 --- a/monasca_api/tests/test_validation.py +++ b/monasca_api/tests/test_validation.py @@ -140,31 +140,31 @@ class TestValueMetaValidation(unittest.TestCase): class TestRoleValidation(unittest.TestCase): def test_role_valid(self): - req_roles = 'role0,rOlE1' + req_roles = 'role0', 'rOlE1' authorized_roles = ['RolE1', 'Role2'] req = mock.Mock() - req.get_header.return_value = req_roles + req.roles = req_roles helpers.validate_authorization(req, authorized_roles) def test_role_invalid(self): - req_roles = 'role2 ,role3' - authorized_roles = ['role0', 'role1', 'role2'] + req_roles = 'role2', 'role3' + authorized_roles = ['role0', 'role1'] req = mock.Mock() - req.get_header.return_value = req_roles + req.roles = req_roles self.assertRaises( falcon.HTTPUnauthorized, helpers.validate_authorization, req, authorized_roles) def test_empty_role_header(self): - req_roles = '' + req_roles = [] authorized_roles = ['Role1', 'Role2'] req = mock.Mock() - req.get_header.return_value = req_roles + req.roles = req_roles self.assertRaises( falcon.HTTPUnauthorized, @@ -175,7 +175,7 @@ class TestRoleValidation(unittest.TestCase): authorized_roles = ['Role1', 'Role2'] req = mock.Mock() - req.get_header.return_value = req_roles + req.roles = req_roles self.assertRaises( falcon.HTTPUnauthorized, diff --git a/monasca_api/v2/reference/alarm_definitions.py b/monasca_api/v2/reference/alarm_definitions.py index a1ea5628e..89681c6be 100644 --- a/monasca_api/v2/reference/alarm_definitions.py +++ b/monasca_api/v2/reference/alarm_definitions.py @@ -60,7 +60,6 @@ class AlarmDefinitions(alarm_definitions_api_v2.AlarmDefinitionsV2API, self._validate_alarm_definition(alarm_definition) - tenant_id = helpers.get_tenant_id(req) name = get_query_alarm_definition_name(alarm_definition) expression = get_query_alarm_definition_expression(alarm_definition) description = get_query_alarm_definition_description(alarm_definition) @@ -72,7 +71,7 @@ class AlarmDefinitions(alarm_definitions_api_v2.AlarmDefinitionsV2API, alarm_definition) ok_actions = get_query_ok_actions(alarm_definition) - result = self._alarm_definition_create(tenant_id, name, expression, + result = self._alarm_definition_create(req.project_id, name, expression, description, severity, match_by, alarm_actions, undetermined_actions, @@ -85,7 +84,6 @@ class AlarmDefinitions(alarm_definitions_api_v2.AlarmDefinitionsV2API, def on_get(self, req, res, alarm_definition_id=None): if alarm_definition_id is None: helpers.validate_authorization(req, self._get_alarmdefs_authorized_roles) - tenant_id = helpers.get_tenant_id(req) name = helpers.get_query_name(req) dimensions = helpers.get_query_dimensions(req) severity = helpers.get_query_param(req, "severity", default_val=None) @@ -108,19 +106,18 @@ class AlarmDefinitions(alarm_definitions_api_v2.AlarmDefinitionsV2API, except Exception: raise HTTPUnprocessableEntityError('Unprocessable Entity', 'Offset value {} must be an integer'.format(offset)) - limit = helpers.get_limit(req) - - result = self._alarm_definition_list(tenant_id, name, dimensions, severity, - req.uri, sort_by, offset, limit) + result = self._alarm_definition_list(req.project_id, name, + dimensions, severity, + req.uri, sort_by, + offset, req.limit) res.body = helpers.dumpit_utf8(result) res.status = falcon.HTTP_200 else: helpers.validate_authorization(req, self._get_alarmdefs_authorized_roles) - tenant_id = helpers.get_tenant_id(req) - result = self._alarm_definition_show(tenant_id, + result = self._alarm_definition_show(req.project_id, alarm_definition_id) helpers.add_links_to_resource(result, @@ -137,8 +134,6 @@ class AlarmDefinitions(alarm_definitions_api_v2.AlarmDefinitionsV2API, self._validate_alarm_definition(alarm_definition, require_all=True) - tenant_id = helpers.get_tenant_id(req) - name = get_query_alarm_definition_name(alarm_definition) expression = get_query_alarm_definition_expression(alarm_definition) actions_enabled = ( @@ -151,7 +146,7 @@ class AlarmDefinitions(alarm_definitions_api_v2.AlarmDefinitionsV2API, match_by = get_query_alarm_definition_match_by(alarm_definition) severity = get_query_alarm_definition_severity(alarm_definition) - result = self._alarm_definition_update_or_patch(tenant_id, + result = self._alarm_definition_update_or_patch(req.project_id, alarm_definition_id, name, expression, @@ -174,8 +169,6 @@ class AlarmDefinitions(alarm_definitions_api_v2.AlarmDefinitionsV2API, alarm_definition = helpers.read_json_msg_body(req) - tenant_id = helpers.get_tenant_id(req) - # Optional args name = get_query_alarm_definition_name(alarm_definition, return_none=True) @@ -197,7 +190,7 @@ class AlarmDefinitions(alarm_definitions_api_v2.AlarmDefinitionsV2API, severity = get_query_alarm_definition_severity(alarm_definition, return_none=True) - result = self._alarm_definition_update_or_patch(tenant_id, + result = self._alarm_definition_update_or_patch(req.project_id, alarm_definition_id, name, expression, @@ -217,8 +210,7 @@ class AlarmDefinitions(alarm_definitions_api_v2.AlarmDefinitionsV2API, def on_delete(self, req, res, alarm_definition_id): helpers.validate_authorization(req, self._default_authorized_roles) - tenant_id = helpers.get_tenant_id(req) - self._alarm_definition_delete(tenant_id, alarm_definition_id) + self._alarm_definition_delete(req.project_id, alarm_definition_id) res.status = falcon.HTTP_204 def _validate_name_not_conflicting(self, tenant_id, name, expected_id=None): diff --git a/monasca_api/v2/reference/alarms.py b/monasca_api/v2/reference/alarms.py index 0895db6cc..2de6dfbd9 100644 --- a/monasca_api/v2/reference/alarms.py +++ b/monasca_api/v2/reference/alarms.py @@ -53,8 +53,6 @@ class Alarms(alarms_api_v2.AlarmsV2API, helpers.validate_authorization(req, self._default_authorized_roles) - tenant_id = helpers.get_tenant_id(req) - alarm = helpers.read_http_resource(req) schema_alarm.validate(alarm) @@ -69,10 +67,10 @@ class Alarms(alarms_api_v2.AlarmsV2API, raise HTTPUnprocessableEntityError('Unprocessable Entity', "Field 'link' is required") - self._alarm_update(tenant_id, alarm_id, alarm['state'], + self._alarm_update(req.project_id, alarm_id, alarm['state'], alarm['lifecycle_state'], alarm['link']) - result = self._alarm_show(req.uri, tenant_id, alarm_id) + result = self._alarm_show(req.uri, req.project_id, alarm_id) res.body = helpers.dumpit_utf8(result) res.status = falcon.HTTP_200 @@ -81,12 +79,10 @@ class Alarms(alarms_api_v2.AlarmsV2API, helpers.validate_authorization(req, self._default_authorized_roles) - tenant_id = helpers.get_tenant_id(req) - alarm = helpers.read_http_resource(req) schema_alarm.validate(alarm) - old_alarm = self._alarms_repo.get_alarm(tenant_id, alarm_id)[0] + old_alarm = self._alarms_repo.get_alarm(req.project_id, alarm_id)[0] # if a field is not present or is None, replace it with the old value if 'state' not in alarm or not alarm['state']: @@ -96,10 +92,10 @@ class Alarms(alarms_api_v2.AlarmsV2API, if 'link' not in alarm or alarm['link'] is None: alarm['link'] = old_alarm['link'] - self._alarm_patch(tenant_id, alarm_id, alarm['state'], + self._alarm_patch(req.project_id, alarm_id, alarm['state'], alarm['lifecycle_state'], alarm['link']) - result = self._alarm_show(req.uri, tenant_id, alarm_id) + result = self._alarm_show(req.uri, req.project_id, alarm_id) res.body = helpers.dumpit_utf8(result) res.status = falcon.HTTP_200 @@ -108,15 +104,12 @@ class Alarms(alarms_api_v2.AlarmsV2API, helpers.validate_authorization(req, self._default_authorized_roles) - tenant_id = helpers.get_tenant_id(req) - - self._alarm_delete(tenant_id, alarm_id) + self._alarm_delete(req.project_id, alarm_id) res.status = falcon.HTTP_204 def on_get(self, req, res, alarm_id=None): helpers.validate_authorization(req, self._get_alarms_authorized_roles) - tenant_id = helpers.get_tenant_id(req) if alarm_id is None: query_parms = falcon.uri.parse_query_string(req.query_string) @@ -155,16 +148,15 @@ class Alarms(alarms_api_v2.AlarmsV2API, raise HTTPUnprocessableEntityError("Unprocessable Entity", "Offset value {} must be an integer".format(offset)) - limit = helpers.get_limit(req) - - result = self._alarm_list(req.uri, tenant_id, query_parms, offset, - limit) + result = self._alarm_list(req.uri, req.project_id, + query_parms, offset, + req.limit) res.body = helpers.dumpit_utf8(result) res.status = falcon.HTTP_200 else: - result = self._alarm_show(req.uri, tenant_id, alarm_id) + result = self._alarm_show(req.uri, req.project_id, alarm_id) res.body = helpers.dumpit_utf8(result) res.status = falcon.HTTP_200 @@ -399,7 +391,6 @@ class AlarmsCount(alarms_api_v2.AlarmsCountV2API, alarming.Alarming): def on_get(self, req, res): helpers.validate_authorization(req, self._get_alarms_authorized_roles) - tenant_id = helpers.get_tenant_id(req) query_parms = falcon.uri.parse_query_string(req.query_string) if 'state' in query_parms: @@ -426,9 +417,7 @@ class AlarmsCount(alarms_api_v2.AlarmsCountV2API, alarming.Alarming): raise HTTPUnprocessableEntityError("Unprocessable Entity", "Offset must be a valid integer, was {}".format(offset)) - limit = helpers.get_limit(req) - - result = self._alarms_count(req.uri, tenant_id, query_parms, offset, limit) + result = self._alarms_count(req.uri, req.project_id, query_parms, offset, req.limit) res.body = helpers.dumpit_utf8(result) res.status = falcon.HTTP_200 @@ -507,28 +496,25 @@ class AlarmsStateHistory(alarms_api_v2.AlarmsStateHistoryV2API, if alarm_id is None: helpers.validate_authorization(req, self._get_alarms_authorized_roles) - tenant_id = helpers.get_tenant_id(req) start_timestamp = helpers.get_query_starttime_timestamp(req, False) end_timestamp = helpers.get_query_endtime_timestamp(req, False) query_parms = falcon.uri.parse_query_string(req.query_string) offset = helpers.get_query_param(req, 'offset') - limit = helpers.get_limit(req) - result = self._alarm_history_list(tenant_id, start_timestamp, + result = self._alarm_history_list(req.project_id, start_timestamp, end_timestamp, query_parms, - req.uri, offset, limit) + req.uri, offset, req.limit) res.body = helpers.dumpit_utf8(result) res.status = falcon.HTTP_200 else: helpers.validate_authorization(req, self._get_alarms_authorized_roles) - tenant_id = helpers.get_tenant_id(req) offset = helpers.get_query_param(req, 'offset') - limit = helpers.get_limit(req) - result = self._alarm_history(tenant_id, [alarm_id], req.uri, - offset, limit) + result = self._alarm_history(req.project_id, [alarm_id], + req.uri, offset, + req.limit) res.body = helpers.dumpit_utf8(result) res.status = falcon.HTTP_200 diff --git a/monasca_api/v2/reference/helpers.py b/monasca_api/v2/reference/helpers.py index d4b93e560..5c4d3416f 100644 --- a/monasca_api/v2/reference/helpers.py +++ b/monasca_api/v2/reference/helpers.py @@ -20,9 +20,9 @@ import falcon from oslo_log import log from oslo_utils import timeutils import simplejson +import six import six.moves.urllib.parse as urlparse -from monasca_api.common.repositories import constants from monasca_api.v2.common.exceptions import HTTPUnprocessableEntityError from monasca_api.v2.common.schemas import dimensions_schema from monasca_api.v2.common.schemas import exceptions as schemas_exceptions @@ -54,24 +54,6 @@ def validate_json_content_type(req): 'application/json') -def is_in_role(req, authorized_roles): - """Is one or more of the X-ROLES in the supplied authorized_roles. - - :param req: HTTP request object. Must contain "X-ROLES" in the HTTP - request header. - :param authorized_roles: List of authorized roles to check against. - :return: Returns True if in the list of authorized roles, otherwise False. - """ - str_roles = req.get_header('X-ROLES') - if str_roles is None: - return False - roles = str_roles.lower().split(',') - for role in roles: - if role in authorized_roles: - return True - return False - - def validate_authorization(req, authorized_roles): """Validates whether one or more X-ROLES in the HTTP header is authorized. @@ -85,15 +67,16 @@ def validate_authorization(req, authorized_roles): :param authorized_roles: List of authorized roles to check against. :raises falcon.HTTPUnauthorized """ - str_roles = req.get_header('X-ROLES') + roles = req.roles challenge = 'Token' - if str_roles is None: + if not roles: raise falcon.HTTPUnauthorized('Forbidden', 'Tenant does not have any roles', challenge) - roles = str_roles.lower().split(',') + roles = roles.split(',') if isinstance(roles, six.string_types) else roles authorized_roles_lower = [r.lower() for r in authorized_roles] for role in roles: + role = role.lower() if role in authorized_roles_lower: return raise falcon.HTTPUnauthorized('Forbidden', @@ -102,14 +85,6 @@ def validate_authorization(req, authorized_roles): challenge) -def get_tenant_id(req): - """Returns the tenant ID in the HTTP request header. - - :param req: HTTP request object. - """ - return req.get_header('X-TENANT-ID') - - def get_x_tenant_or_tenant_id(req, delegate_authorized_roles): """Evaluates whether the tenant ID or cross tenant ID should be returned. @@ -118,12 +93,12 @@ def get_x_tenant_or_tenant_id(req, delegate_authorized_roles): delegate privileges. :returns: Returns the cross tenant or tenant ID. """ - if is_in_role(req, delegate_authorized_roles): + if any(x in set(delegate_authorized_roles) for x in req.roles): params = falcon.uri.parse_query_string(req.query_string) if 'tenant_id' in params: tenant_id = params['tenant_id'] return tenant_id - return get_tenant_id(req) + return req.project_id def get_query_param(req, param_name, required=False, default_val=None): @@ -769,21 +744,3 @@ def dumpit_utf8(thingy): def str_2_bool(s): return s.lower() in ("true") - - -def get_limit(req): - limit = get_query_param(req, 'limit') - - if limit: - if limit.isdigit(): - limit = int(limit) - if limit > constants.PAGE_LIMIT: - return constants.PAGE_LIMIT - else: - return limit - else: - raise HTTPUnprocessableEntityError("Invalid limit", - "Limit parameter must " - "be a positive integer") - else: - return constants.PAGE_LIMIT diff --git a/monasca_api/v2/reference/metrics.py b/monasca_api/v2/reference/metrics.py index 8ed2082af..b950a88f5 100644 --- a/monasca_api/v2/reference/metrics.py +++ b/monasca_api/v2/reference/metrics.py @@ -131,18 +131,17 @@ class Metrics(metrics_api_v2.MetricsV2API): def on_get(self, req, res): helpers.validate_authorization(req, self._get_metrics_authorized_roles) - tenant_id = helpers.get_tenant_id(req) name = helpers.get_query_name(req) helpers.validate_query_name(name) dimensions = helpers.get_query_dimensions(req) helpers.validate_query_dimensions(dimensions) offset = helpers.get_query_param(req, 'offset') - limit = helpers.get_limit(req) start_timestamp = helpers.get_query_starttime_timestamp(req, False) end_timestamp = helpers.get_query_endtime_timestamp(req, False) helpers.validate_start_end_timestamps(start_timestamp, end_timestamp) - result = self._list_metrics(tenant_id, name, dimensions, - req.uri, offset, limit, + result = self._list_metrics(req.project_id, name, + dimensions, req.uri, + offset, req.limit, start_timestamp, end_timestamp) res.body = helpers.dumpit_utf8(result) res.status = falcon.HTTP_200 @@ -171,7 +170,6 @@ class MetricsMeasurements(metrics_api_v2.MetricsMeasurementsV2API): def on_get(self, req, res): helpers.validate_authorization(req, self._get_metrics_authorized_roles) - tenant_id = helpers.get_tenant_id(req) name = helpers.get_query_name(req, True) helpers.validate_query_name(name) dimensions = helpers.get_query_dimensions(req) @@ -180,14 +178,13 @@ class MetricsMeasurements(metrics_api_v2.MetricsMeasurementsV2API): end_timestamp = helpers.get_query_endtime_timestamp(req, False) helpers.validate_start_end_timestamps(start_timestamp, end_timestamp) offset = helpers.get_query_param(req, 'offset') - limit = helpers.get_limit(req) merge_metrics_flag = get_merge_metrics_flag(req) group_by = helpers.get_query_group_by(req) - result = self._measurement_list(tenant_id, name, dimensions, + result = self._measurement_list(req.project_id, name, dimensions, start_timestamp, end_timestamp, req.uri, offset, - limit, merge_metrics_flag, + req.limit, merge_metrics_flag, group_by) res.body = helpers.dumpit_utf8(result) @@ -230,7 +227,6 @@ class MetricsStatistics(metrics_api_v2.MetricsStatisticsV2API): def on_get(self, req, res): helpers.validate_authorization(req, self._get_metrics_authorized_roles) - tenant_id = helpers.get_tenant_id(req) name = helpers.get_query_name(req, True) helpers.validate_query_name(name) dimensions = helpers.get_query_dimensions(req) @@ -241,14 +237,13 @@ class MetricsStatistics(metrics_api_v2.MetricsStatisticsV2API): statistics = helpers.get_query_statistics(req) period = helpers.get_query_period(req) offset = helpers.get_query_param(req, 'offset') - limit = helpers.get_limit(req) merge_metrics_flag = get_merge_metrics_flag(req) group_by = helpers.get_query_group_by(req) - result = self._metric_statistics(tenant_id, name, dimensions, + result = self._metric_statistics(req.project_id, name, dimensions, start_timestamp, end_timestamp, statistics, period, req.uri, - offset, limit, merge_metrics_flag, + offset, req.limit, merge_metrics_flag, group_by) res.body = helpers.dumpit_utf8(result) @@ -292,13 +287,11 @@ class MetricsNames(metrics_api_v2.MetricsNamesV2API): def on_get(self, req, res): helpers.validate_authorization(req, self._get_metrics_authorized_roles) - tenant_id = helpers.get_tenant_id(req) dimensions = helpers.get_query_dimensions(req) helpers.validate_query_dimensions(dimensions) offset = helpers.get_query_param(req, 'offset') - limit = helpers.get_limit(req) - result = self._list_metric_names(tenant_id, dimensions, - req.uri, offset, limit) + result = self._list_metric_names(req.project_id, dimensions, + req.uri, offset, req.limit) res.body = helpers.dumpit_utf8(result) res.status = falcon.HTTP_200 @@ -331,14 +324,12 @@ class DimensionValues(metrics_api_v2.DimensionValuesV2API): def on_get(self, req, res): helpers.validate_authorization(req, self._get_metrics_authorized_roles) - tenant_id = helpers.get_tenant_id(req) metric_name = helpers.get_query_param(req, 'metric_name') dimension_name = helpers.get_query_param(req, 'dimension_name', required=True) offset = helpers.get_query_param(req, 'offset') - limit = helpers.get_limit(req) - result = self._dimension_values(tenant_id, req.uri, metric_name, - dimension_name, offset, limit) + result = self._dimension_values(req.project_id, req.uri, metric_name, + dimension_name, offset, req.limit) res.body = helpers.dumpit_utf8(result) res.status = falcon.HTTP_200 @@ -372,12 +363,10 @@ class DimensionNames(metrics_api_v2.DimensionNamesV2API): def on_get(self, req, res): helpers.validate_authorization(req, self._get_metrics_authorized_roles) - tenant_id = helpers.get_tenant_id(req) metric_name = helpers.get_query_param(req, 'metric_name') offset = helpers.get_query_param(req, 'offset') - limit = helpers.get_limit(req) - result = self._dimension_names(tenant_id, req.uri, metric_name, - offset, limit) + result = self._dimension_names(req.project_id, req.uri, metric_name, + offset, req.limit) res.body = helpers.dumpit_utf8(result) res.status = falcon.HTTP_200 diff --git a/monasca_api/v2/reference/notifications.py b/monasca_api/v2/reference/notifications.py index ed0e307f2..1153404b2 100644 --- a/monasca_api/v2/reference/notifications.py +++ b/monasca_api/v2/reference/notifications.py @@ -202,8 +202,7 @@ class Notifications(notifications_api_v2.NotificationsV2API): helpers.validate_authorization(req, self._default_authorized_roles) notification = helpers.read_http_resource(req) self._parse_and_validate_notification(notification) - tenant_id = helpers.get_tenant_id(req) - result = self._create_notification(tenant_id, notification, req.uri) + result = self._create_notification(req.project_id, notification, req.uri) res.body = helpers.dumpit_utf8(result) res.status = falcon.HTTP_201 @@ -211,7 +210,6 @@ class Notifications(notifications_api_v2.NotificationsV2API): if notification_method_id is None: helpers.validate_authorization(req, self._get_notifications_authorized_roles) - tenant_id = helpers.get_tenant_id(req) sort_by = helpers.get_query_param(req, 'sort_by', default_val=None) if sort_by is not None: if isinstance(sort_by, basestring): @@ -223,16 +221,14 @@ class Notifications(notifications_api_v2.NotificationsV2API): validation.validate_sort_by(sort_by, allowed_sort_by) offset = helpers.get_query_param(req, 'offset') - limit = helpers.get_limit(req) - result = self._list_notifications(tenant_id, req.uri, sort_by, - offset, limit) + result = self._list_notifications(req.project_id, req.uri, sort_by, + offset, req.limit) res.body = helpers.dumpit_utf8(result) res.status = falcon.HTTP_200 else: helpers.validate_authorization(req, self._get_notifications_authorized_roles) - tenant_id = helpers.get_tenant_id(req) - result = self._list_notification(tenant_id, + result = self._list_notification(req.project_id, notification_method_id, req.uri) res.body = helpers.dumpit_utf8(result) @@ -240,8 +236,7 @@ class Notifications(notifications_api_v2.NotificationsV2API): def on_delete(self, req, res, notification_method_id): helpers.validate_authorization(req, self._default_authorized_roles) - tenant_id = helpers.get_tenant_id(req) - self._delete_notification(tenant_id, notification_method_id) + self._delete_notification(req.project_id, notification_method_id) res.status = falcon.HTTP_204 def on_put(self, req, res, notification_method_id): @@ -249,8 +244,7 @@ class Notifications(notifications_api_v2.NotificationsV2API): helpers.validate_authorization(req, self._default_authorized_roles) notification = helpers.read_http_resource(req) self._parse_and_validate_notification(notification, require_all=True) - tenant_id = helpers.get_tenant_id(req) - result = self._update_notification(notification_method_id, tenant_id, + result = self._update_notification(notification_method_id, req.project_id, notification, req.uri) res.body = helpers.dumpit_utf8(result) res.status = falcon.HTTP_200 @@ -259,10 +253,9 @@ class Notifications(notifications_api_v2.NotificationsV2API): helpers.validate_json_content_type(req) helpers.validate_authorization(req, self._default_authorized_roles) notification = helpers.read_http_resource(req) - tenant_id = helpers.get_tenant_id(req) - self._patch_get_notification(tenant_id, notification_method_id, notification) + self._patch_get_notification(req.project_id, notification_method_id, notification) self._parse_and_validate_notification(notification, require_all=True) - result = self._update_notification(notification_method_id, tenant_id, + result = self._update_notification(notification_method_id, req.project_id, notification, req.uri) res.body = helpers.dumpit_utf8(result) res.status = falcon.HTTP_200 diff --git a/monasca_api/v2/reference/notificationstype.py b/monasca_api/v2/reference/notificationstype.py index 9851ce4ee..bd42a6e76 100644 --- a/monasca_api/v2/reference/notificationstype.py +++ b/monasca_api/v2/reference/notificationstype.py @@ -37,8 +37,7 @@ class NotificationsType(notificationstype_api_v2.NotificationsTypeV2API): # This is to provide consistency. Pagination is not really supported here as there # are not that many rows - limit = helpers.get_limit(req) - result = self._list_notifications(req.uri, limit) + result = self._list_notifications(req.uri, req.limit) res.body = helpers.dumpit_utf8(result) res.status = falcon.HTTP_200 diff --git a/requirements.txt b/requirements.txt index 77e494129..4c5bfcc1a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,6 +2,7 @@ # of appearance. Changing the order has an impact on the overall integration # process, which may cause wedges in the gate later. oslo.config!=3.18.0,>=3.14.0 # Apache-2.0 +oslo.context>=2.9.0 # Apache-2.0 oslo.log>=3.11.0 # Apache-2.0 oslo.middleware>=3.0.0 # Apache-2.0 oslo.serialization>=1.10.0 # Apache-2.0