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
etc/monasca
monasca_events_api
app
middleware
tests/unit
@ -22,7 +22,7 @@ use = egg:Paste#urlmap
|
|||||||
/healthcheck: events_healthcheck
|
/healthcheck: events_healthcheck
|
||||||
|
|
||||||
[pipeline:events_api_v1]
|
[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:events_version]
|
||||||
pipeline = error_trap versionapp
|
pipeline = error_trap versionapp
|
||||||
@ -48,9 +48,6 @@ paste.filter_factory = oslo_middleware.catch_errors:CatchErrors.factory
|
|||||||
[filter:request_id]
|
[filter:request_id]
|
||||||
paste.filter_factory = oslo_middleware.request_id:RequestId.factory
|
paste.filter_factory = oslo_middleware.request_id:RequestId.factory
|
||||||
|
|
||||||
[filter:middleware]
|
|
||||||
paste.filter_factory = monasca_events_api.middleware.validation_middleware:ValidationMiddleware.factory
|
|
||||||
|
|
||||||
[filter:sizelimit]
|
[filter:sizelimit]
|
||||||
use = egg:oslo.middleware#sizelimit
|
use = egg:oslo.middleware#sizelimit
|
||||||
|
|
||||||
|
@ -14,8 +14,12 @@
|
|||||||
|
|
||||||
import six
|
import six
|
||||||
|
|
||||||
|
from falcon import HTTPUnprocessableEntity
|
||||||
from oslo_log import log
|
from oslo_log import log
|
||||||
|
from voluptuous import All
|
||||||
from voluptuous import Any
|
from voluptuous import Any
|
||||||
|
from voluptuous import Length
|
||||||
|
from voluptuous import MultipleInvalid
|
||||||
from voluptuous import Required
|
from voluptuous import Required
|
||||||
from voluptuous import Schema
|
from voluptuous import Schema
|
||||||
|
|
||||||
@ -23,7 +27,8 @@ from voluptuous import Schema
|
|||||||
LOG = log.getLogger(__name__)
|
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})
|
Required("timestamp"): six.text_type})
|
||||||
|
|
||||||
|
|
||||||
@ -33,7 +38,10 @@ def validate_body(request_body):
|
|||||||
Method validate if body contain all required fields,
|
Method validate if body contain all required fields,
|
||||||
and check if all value have correct type.
|
and check if all value have correct type.
|
||||||
|
|
||||||
|
|
||||||
:param request_body: body
|
:param request_body: body
|
||||||
"""
|
"""
|
||||||
|
try:
|
||||||
default_schema(request_body)
|
default_schema(request_body)
|
||||||
|
except MultipleInvalid as ex:
|
||||||
|
LOG.exception(ex)
|
||||||
|
raise HTTPUnprocessableEntity(description=ex.error_message)
|
||||||
|
@ -13,13 +13,12 @@
|
|||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
import falcon
|
import falcon
|
||||||
from oslo_log import log
|
|
||||||
from voluptuous import MultipleInvalid
|
|
||||||
|
|
||||||
from monasca_events_api.app.common import helpers
|
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 body_validation
|
||||||
from monasca_events_api.app.controller.v1 import bulk_processor
|
from monasca_events_api.app.controller.v1 import bulk_processor
|
||||||
from monasca_events_api.app.core.model import prepare_message_to_sent
|
from monasca_events_api.app.core.model import prepare_message_to_sent
|
||||||
|
from oslo_log import log
|
||||||
|
|
||||||
LOG = log.getLogger(__name__)
|
LOG = log.getLogger(__name__)
|
||||||
|
|
||||||
@ -51,6 +50,7 @@ class Events(object):
|
|||||||
policy_action = 'events_api:agent_required'
|
policy_action = 'events_api:agent_required'
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
req.validate(self.SUPPORTED_CONTENT_TYPES)
|
||||||
request_body = helpers.read_json_msg_body(req)
|
request_body = helpers.read_json_msg_body(req)
|
||||||
req.can(policy_action)
|
req.can(policy_action)
|
||||||
project_id = req.project_id
|
project_id = req.project_id
|
||||||
@ -58,14 +58,22 @@ class Events(object):
|
|||||||
messages = prepare_message_to_sent(request_body)
|
messages = prepare_message_to_sent(request_body)
|
||||||
self._processor.send_message(messages, event_project_id=project_id)
|
self._processor.send_message(messages, event_project_id=project_id)
|
||||||
res.status = falcon.HTTP_200
|
res.status = falcon.HTTP_200
|
||||||
except MultipleInvalid as ex:
|
except falcon.HTTPUnprocessableEntity as ex:
|
||||||
LOG.error('Entire bulk package was rejected, unsupported body')
|
LOG.error('Entire bulk package was rejected, unsupported body')
|
||||||
LOG.exception(ex)
|
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:
|
except Exception as ex:
|
||||||
LOG.error('Entire bulk package was rejected')
|
LOG.error('Entire bulk package was rejected')
|
||||||
LOG.exception(ex)
|
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
|
@property
|
||||||
def version(self):
|
def version(self):
|
||||||
|
@ -17,6 +17,7 @@ from monasca_common.policy import policy_engine as policy
|
|||||||
from oslo_log import log
|
from oslo_log import log
|
||||||
|
|
||||||
from monasca_events_api.app.core import request_contex
|
from monasca_events_api.app.core import request_contex
|
||||||
|
from monasca_events_api.app.core import validation
|
||||||
from monasca_events_api import policies
|
from monasca_events_api import policies
|
||||||
|
|
||||||
LOG = log.getLogger(__name__)
|
LOG = log.getLogger(__name__)
|
||||||
@ -38,5 +39,19 @@ class Request(falcon.Request):
|
|||||||
self.is_admin = policy.check_is_admin(self.context)
|
self.is_admin = policy.check_is_admin(self.context)
|
||||||
self.project_id = self.context.project_id
|
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):
|
def can(self, action, target=None):
|
||||||
return self.context.can(action, target)
|
return self.context.can(action, target)
|
||||||
|
47
monasca_events_api/middleware/validation_middleware.py → monasca_events_api/app/core/validation.py
47
monasca_events_api/middleware/validation_middleware.py → 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
|
# 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
|
# not use this file except in compliance with the License. You may obtain
|
||||||
@ -14,54 +14,37 @@
|
|||||||
|
|
||||||
import falcon
|
import falcon
|
||||||
from oslo_log import log
|
from oslo_log import log
|
||||||
from oslo_middleware import base
|
|
||||||
|
|
||||||
from monasca_events_api import config
|
|
||||||
|
|
||||||
CONF = config.CONF
|
|
||||||
LOG = log.getLogger(__name__)
|
LOG = log.getLogger(__name__)
|
||||||
|
|
||||||
SUPPORTED_CONTENT_TYPES = ('application/json',)
|
|
||||||
|
|
||||||
|
def validate_content_type(req, allowed):
|
||||||
|
"""Validates content type.
|
||||||
|
|
||||||
def _validate_content_type(req):
|
Method validates request against correct
|
||||||
"""Validate content type.
|
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.
|
:py:class:`falcon.HTTPMissingHeader` is thrown.
|
||||||
If Content-Type is not **application/json**(supported contents
|
If content-type is not **application/json** or **text/plain**,
|
||||||
|
|
||||||
types are define in SUPPORTED_CONTENT_TYPES variable),
|
|
||||||
:py:class:`falcon.HTTPUnsupportedMediaType` is thrown.
|
:py:class:`falcon.HTTPUnsupportedMediaType` is thrown.
|
||||||
|
|
||||||
|
|
||||||
:param falcon.Request req: current request
|
:param falcon.Request req: current request
|
||||||
|
:param iterable allowed: allowed content type
|
||||||
|
|
||||||
:exception: :py:class:`falcon.HTTPMissingHeader`
|
:exception: :py:class:`falcon.HTTPMissingHeader`
|
||||||
:exception: :py:class:`falcon.HTTPUnsupportedMediaType`
|
:exception: :py:class:`falcon.HTTPUnsupportedMediaType`
|
||||||
"""
|
"""
|
||||||
content_type = req.content_type
|
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:
|
if content_type is None or len(content_type) == 0:
|
||||||
raise falcon.HTTPMissingHeader('Content-Type')
|
raise falcon.HTTPMissingHeader('Content-Type')
|
||||||
|
|
||||||
if content_type not in SUPPORTED_CONTENT_TYPES:
|
if content_type not in allowed:
|
||||||
types = ','.join(SUPPORTED_CONTENT_TYPES)
|
sup_types = ', '.join(allowed)
|
||||||
details = ('Only [{0}] are accepted as events representation'.
|
details = ('Only [%s] are accepted as logs representations'
|
||||||
format(types))
|
% str(sup_types))
|
||||||
raise falcon.HTTPUnsupportedMediaType(description=details)
|
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
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# 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.app.controller.v1.body_validation import validate_body
|
||||||
from monasca_events_api.tests.unit import base
|
from monasca_events_api.tests.unit import base
|
||||||
@ -22,26 +22,36 @@ class TestBodyValidation(base.BaseTestCase):
|
|||||||
|
|
||||||
def test_missing_events_filed(self):
|
def test_missing_events_filed(self):
|
||||||
body = {'timestamp': '2012-10-29T13:42:11Z+0200'}
|
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):
|
def test_missing_timestamp_field(self):
|
||||||
body = {'events': []}
|
body = {'events': [{'event': {'payload': 'test'}}]}
|
||||||
self.assertRaises(MultipleInvalid, validate_body, body)
|
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):
|
def test_empty_body(self):
|
||||||
body = {}
|
body = {}
|
||||||
self.assertRaises(MultipleInvalid, validate_body, body)
|
self.assertRaises(HTTPUnprocessableEntity, validate_body, body)
|
||||||
|
|
||||||
def test_incorrect_timestamp_type(self):
|
def test_incorrect_timestamp_type(self):
|
||||||
body = {'events': [], 'timestamp': 9000}
|
body = {'events': [], 'timestamp': 9000}
|
||||||
self.assertRaises(MultipleInvalid, validate_body, body)
|
self.assertRaises(HTTPUnprocessableEntity, validate_body, body)
|
||||||
|
|
||||||
def test_incorrect_events_type(self):
|
def test_incorrect_events_type(self):
|
||||||
body = {'events': 'over9000', 'timestamp': '2012-10-29T13:42:11Z+0200'}
|
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):
|
def test_correct_body(self):
|
||||||
body = [{'events': [], 'timestamp': u'2012-10-29T13:42:11Z+0200'},
|
body = [{'events': [{'event': {'payload': 'test'}}],
|
||||||
{'events': {}, 'timestamp': u'2012-10-29T13:42:11Z+0200'}]
|
'timestamp': u'2012-10-29T13:42:11Z+0200'},
|
||||||
|
{'events': {'event': {'payload': 'test'}},
|
||||||
|
'timestamp': u'2012-10-29T13:42:11Z+0200'}]
|
||||||
for b in body:
|
for b in body:
|
||||||
validate_body(b)
|
validate_body(b)
|
||||||
|
@ -125,6 +125,35 @@ class TestEventsApi(base.BaseApiTestCase):
|
|||||||
)
|
)
|
||||||
self.assertEqual(falcon.HTTP_422, self.srmock.status)
|
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):
|
class TestApiEventsVersion(base.BaseApiTestCase):
|
||||||
@mock.patch('monasca_events_api.app.controller.v1.'
|
@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…
x
Reference in New Issue
Block a user