Added validations

- Empty events list
- Missing content-type or wrong content-type
- Empty body

Change-Id: I5848dd018aee6b9d95bff7be52eece0ac97b2a49
Story: 2003955
Task: 27036
This commit is contained in:
Martin Chacon Piza 2018-10-10 11:12:07 +02:00
parent 3c5e504ede
commit 8c0ddb4f4d
9 changed files with 103 additions and 100 deletions

View File

@ -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

View File

@ -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)

View File

@ -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):

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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.'

View File

@ -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)