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:
parent
3c5e504ede
commit
8c0ddb4f4d
@ -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
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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):
|
||||
|
@ -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)
|
||||
|
@ -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
|
@ -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)
|
||||
|
@ -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.'
|
||||
|
@ -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)
|
Loading…
Reference in New Issue
Block a user