From 8c0ddb4f4d5753b6ecab0ffff429ad75ace863bc Mon Sep 17 00:00:00 2001 From: Martin Chacon Piza Date: Wed, 10 Oct 2018 11:12:07 +0200 Subject: [PATCH] Added validations - Empty events list - Missing content-type or wrong content-type - Empty body Change-Id: I5848dd018aee6b9d95bff7be52eece0ac97b2a49 Story: 2003955 Task: 27036 --- etc/monasca/events-api-paste.ini | 5 +- .../app/controller/v1/body_validation.py | 14 ++++-- .../app/controller/v1/events.py | 18 +++++-- monasca_events_api/app/core/request.py | 15 ++++++ .../core/validation.py} | 47 ++++++------------- monasca_events_api/middleware/__init__.py | 0 .../tests/unit/test_body_validation.py | 28 +++++++---- .../tests/unit/test_events_v1.py | 29 ++++++++++++ .../tests/unit/test_validation_middleware.py | 47 ------------------- 9 files changed, 103 insertions(+), 100 deletions(-) rename monasca_events_api/{middleware/validation_middleware.py => app/core/validation.py} (53%) delete mode 100644 monasca_events_api/middleware/__init__.py delete mode 100644 monasca_events_api/tests/unit/test_validation_middleware.py diff --git a/etc/monasca/events-api-paste.ini b/etc/monasca/events-api-paste.ini index 492f325..0d9813f 100644 --- a/etc/monasca/events-api-paste.ini +++ b/etc/monasca/events-api-paste.ini @@ -22,7 +22,7 @@ use = egg:Paste#urlmap /healthcheck: events_healthcheck [pipeline:events_api_v1] -pipeline = error_trap request_id auth sizelimit middleware api_v1_app +pipeline = error_trap request_id auth sizelimit api_v1_app [pipeline:events_version] pipeline = error_trap versionapp @@ -48,9 +48,6 @@ paste.filter_factory = oslo_middleware.catch_errors:CatchErrors.factory [filter:request_id] paste.filter_factory = oslo_middleware.request_id:RequestId.factory -[filter:middleware] -paste.filter_factory = monasca_events_api.middleware.validation_middleware:ValidationMiddleware.factory - [filter:sizelimit] use = egg:oslo.middleware#sizelimit diff --git a/monasca_events_api/app/controller/v1/body_validation.py b/monasca_events_api/app/controller/v1/body_validation.py index 2012109..75012cb 100644 --- a/monasca_events_api/app/controller/v1/body_validation.py +++ b/monasca_events_api/app/controller/v1/body_validation.py @@ -14,8 +14,12 @@ import six +from falcon import HTTPUnprocessableEntity from oslo_log import log +from voluptuous import All from voluptuous import Any +from voluptuous import Length +from voluptuous import MultipleInvalid from voluptuous import Required from voluptuous import Schema @@ -23,7 +27,8 @@ from voluptuous import Schema LOG = log.getLogger(__name__) -default_schema = Schema({Required("events"): Any(list, dict), +default_schema = Schema({Required("events"): All(Any(list, dict), + Length(min=1)), Required("timestamp"): six.text_type}) @@ -33,7 +38,10 @@ def validate_body(request_body): Method validate if body contain all required fields, and check if all value have correct type. - :param request_body: body """ - default_schema(request_body) + try: + default_schema(request_body) + except MultipleInvalid as ex: + LOG.exception(ex) + raise HTTPUnprocessableEntity(description=ex.error_message) diff --git a/monasca_events_api/app/controller/v1/events.py b/monasca_events_api/app/controller/v1/events.py index 5cf22c0..5955030 100644 --- a/monasca_events_api/app/controller/v1/events.py +++ b/monasca_events_api/app/controller/v1/events.py @@ -13,13 +13,12 @@ # under the License. import falcon -from oslo_log import log -from voluptuous import MultipleInvalid from monasca_events_api.app.common import helpers from monasca_events_api.app.controller.v1 import body_validation from monasca_events_api.app.controller.v1 import bulk_processor from monasca_events_api.app.core.model import prepare_message_to_sent +from oslo_log import log LOG = log.getLogger(__name__) @@ -51,6 +50,7 @@ class Events(object): policy_action = 'events_api:agent_required' try: + req.validate(self.SUPPORTED_CONTENT_TYPES) request_body = helpers.read_json_msg_body(req) req.can(policy_action) project_id = req.project_id @@ -58,14 +58,22 @@ class Events(object): messages = prepare_message_to_sent(request_body) self._processor.send_message(messages, event_project_id=project_id) res.status = falcon.HTTP_200 - except MultipleInvalid as ex: + except falcon.HTTPUnprocessableEntity as ex: LOG.error('Entire bulk package was rejected, unsupported body') LOG.exception(ex) - res.status = falcon.HTTP_422 + raise ex + except falcon.HTTPUnsupportedMediaType as ex: + LOG.error('Entire bulk package was rejected, ' + 'unsupported media type') + LOG.exception(ex) + raise ex except Exception as ex: LOG.error('Entire bulk package was rejected') LOG.exception(ex) - res.status = falcon.HTTP_400 + _title = ex.title if hasattr(ex, 'title') else None + _descr = ex.description if hasattr(ex, 'description') else None + raise falcon.HTTPError(falcon.HTTP_400, + title=_title, description=_descr) @property def version(self): diff --git a/monasca_events_api/app/core/request.py b/monasca_events_api/app/core/request.py index ca7b2e7..be638fa 100644 --- a/monasca_events_api/app/core/request.py +++ b/monasca_events_api/app/core/request.py @@ -17,6 +17,7 @@ from monasca_common.policy import policy_engine as policy from oslo_log import log from monasca_events_api.app.core import request_contex +from monasca_events_api.app.core import validation from monasca_events_api import policies LOG = log.getLogger(__name__) @@ -38,5 +39,19 @@ class Request(falcon.Request): self.is_admin = policy.check_is_admin(self.context) self.project_id = self.context.project_id + def validate(self, content_types): + """Performs common request validation + + Validation checklist (in that order): + + * :py:func:`validation.validate_content_type` + + :param content_types: allowed content-types handler supports + :type content_types: list + :raises Exception: if any of the validation fails + + """ + validation.validate_content_type(self, content_types) + def can(self, action, target=None): return self.context.can(action, target) diff --git a/monasca_events_api/middleware/validation_middleware.py b/monasca_events_api/app/core/validation.py similarity index 53% rename from monasca_events_api/middleware/validation_middleware.py rename to monasca_events_api/app/core/validation.py index a6755db..7662044 100644 --- a/monasca_events_api/middleware/validation_middleware.py +++ b/monasca_events_api/app/core/validation.py @@ -1,4 +1,4 @@ -# Copyright 2017 FUJITSU LIMITED +# Copyright 2018 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 @@ -14,54 +14,37 @@ import falcon from oslo_log import log -from oslo_middleware import base -from monasca_events_api import config - -CONF = config.CONF LOG = log.getLogger(__name__) -SUPPORTED_CONTENT_TYPES = ('application/json',) +def validate_content_type(req, allowed): + """Validates content type. -def _validate_content_type(req): - """Validate content type. + Method validates request against correct + content type. - Function validates request against correct content type. - - If Content-Type cannot be established (i.e. header is missing), + If content-type cannot be established (i.e. header is missing), :py:class:`falcon.HTTPMissingHeader` is thrown. - If Content-Type is not **application/json**(supported contents - - types are define in SUPPORTED_CONTENT_TYPES variable), + If content-type is not **application/json** or **text/plain**, :py:class:`falcon.HTTPUnsupportedMediaType` is thrown. + :param falcon.Request req: current request + :param iterable allowed: allowed content type :exception: :py:class:`falcon.HTTPMissingHeader` :exception: :py:class:`falcon.HTTPUnsupportedMediaType` """ content_type = req.content_type - LOG.debug('Content-type is {0}'.format(content_type)) + + LOG.debug('Content-Type is %s', content_type) if content_type is None or len(content_type) == 0: raise falcon.HTTPMissingHeader('Content-Type') - if content_type not in SUPPORTED_CONTENT_TYPES: - types = ','.join(SUPPORTED_CONTENT_TYPES) - details = ('Only [{0}] are accepted as events representation'. - format(types)) + if content_type not in allowed: + sup_types = ', '.join(allowed) + details = ('Only [%s] are accepted as logs representations' + % str(sup_types)) raise falcon.HTTPUnsupportedMediaType(description=details) - - -class ValidationMiddleware(base.ConfigurableMiddleware): - """Middleware that validates request content. - - """ - - @staticmethod - def process_request(req): - - _validate_content_type(req) - - return diff --git a/monasca_events_api/middleware/__init__.py b/monasca_events_api/middleware/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/monasca_events_api/tests/unit/test_body_validation.py b/monasca_events_api/tests/unit/test_body_validation.py index c07e5fb..2f9c3a4 100644 --- a/monasca_events_api/tests/unit/test_body_validation.py +++ b/monasca_events_api/tests/unit/test_body_validation.py @@ -12,7 +12,7 @@ # License for the specific language governing permissions and limitations # under the License. -from voluptuous import MultipleInvalid +from falcon.errors import HTTPUnprocessableEntity from monasca_events_api.app.controller.v1.body_validation import validate_body from monasca_events_api.tests.unit import base @@ -22,26 +22,36 @@ class TestBodyValidation(base.BaseTestCase): def test_missing_events_filed(self): body = {'timestamp': '2012-10-29T13:42:11Z+0200'} - self.assertRaises(MultipleInvalid, validate_body, body) + self.assertRaises(HTTPUnprocessableEntity, validate_body, body) def test_missing_timestamp_field(self): - body = {'events': []} - self.assertRaises(MultipleInvalid, validate_body, body) + body = {'events': [{'event': {'payload': 'test'}}]} + self.assertRaises(HTTPUnprocessableEntity, validate_body, body) + + def test_empty_events_as_list(self): + body = {'events': [], 'timestamp': u'2012-10-29T13:42:11Z+0200'} + self.assertRaises(HTTPUnprocessableEntity, validate_body, body) + + def test_empty_events_as_dict(self): + body = {'events': {}, 'timestamp': u'2012-10-29T13:42:11Z+0200'} + self.assertRaises(HTTPUnprocessableEntity, validate_body, body) def test_empty_body(self): body = {} - self.assertRaises(MultipleInvalid, validate_body, body) + self.assertRaises(HTTPUnprocessableEntity, validate_body, body) def test_incorrect_timestamp_type(self): body = {'events': [], 'timestamp': 9000} - self.assertRaises(MultipleInvalid, validate_body, body) + self.assertRaises(HTTPUnprocessableEntity, validate_body, body) def test_incorrect_events_type(self): body = {'events': 'over9000', 'timestamp': '2012-10-29T13:42:11Z+0200'} - self.assertRaises(MultipleInvalid, validate_body, body) + self.assertRaises(HTTPUnprocessableEntity, validate_body, body) def test_correct_body(self): - body = [{'events': [], 'timestamp': u'2012-10-29T13:42:11Z+0200'}, - {'events': {}, 'timestamp': u'2012-10-29T13:42:11Z+0200'}] + body = [{'events': [{'event': {'payload': 'test'}}], + 'timestamp': u'2012-10-29T13:42:11Z+0200'}, + {'events': {'event': {'payload': 'test'}}, + 'timestamp': u'2012-10-29T13:42:11Z+0200'}] for b in body: validate_body(b) diff --git a/monasca_events_api/tests/unit/test_events_v1.py b/monasca_events_api/tests/unit/test_events_v1.py index c39e0df..8b22050 100644 --- a/monasca_events_api/tests/unit/test_events_v1.py +++ b/monasca_events_api/tests/unit/test_events_v1.py @@ -125,6 +125,35 @@ class TestEventsApi(base.BaseApiTestCase): ) self.assertEqual(falcon.HTTP_422, self.srmock.status) + def test_should_fail_missing_content_type(self, bulk_processor): + events_resource = _init_resource(self) + events_resource._processor = bulk_processor + body = {'timestamp': '2012-10-29T13:42:11Z+0200'} + self.simulate_request( + path=ENDPOINT, + method='POST', + headers={ + 'X_ROLES': 'monasca-user' + }, + body=json.dumps(body) + ) + self.assertEqual(falcon.HTTP_400, self.srmock.status) + + def test_should_fail_wrong_content_type(self, bulk_processor): + events_resource = _init_resource(self) + events_resource._processor = bulk_processor + body = {'timestamp': '2012-10-29T13:42:11Z+0200'} + self.simulate_request( + path=ENDPOINT, + method='POST', + headers={ + 'Content-Type': 'text/plain', + 'X_ROLES': 'monasca-user' + }, + body=json.dumps(body) + ) + self.assertEqual(falcon.HTTP_415, self.srmock.status) + class TestApiEventsVersion(base.BaseApiTestCase): @mock.patch('monasca_events_api.app.controller.v1.' diff --git a/monasca_events_api/tests/unit/test_validation_middleware.py b/monasca_events_api/tests/unit/test_validation_middleware.py deleted file mode 100644 index 50fea8c..0000000 --- a/monasca_events_api/tests/unit/test_validation_middleware.py +++ /dev/null @@ -1,47 +0,0 @@ -# Copyright 2017 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_events_api.middleware import validation_middleware as vm -from monasca_events_api.tests.unit import base - - -class FakeRequest(object): - def __init__(self, content=None, length=0): - self.content_type = content if content else None - self.content_length = (length if length is not None and length > 0 - else None) - - -class TestValidation(base.BaseTestCase): - - def setUp(self): - super(TestValidation, self).setUp() - - def test_should_validate_right_content_type(self): - req = FakeRequest('application/json') - vm._validate_content_type(req) - - def test_should_fail_missing_content_type(self): - req = FakeRequest() - self.assertRaises(falcon.HTTPMissingHeader, - vm._validate_content_type, - req) - - def test_should_fail_unsupported_content_type(self): - req = FakeRequest('test/plain') - self.assertRaises(falcon.HTTPUnsupportedMediaType, - vm._validate_content_type, - req)