Improve validation of openstack config requests

* In the JSON schema 'number' type restriction replaced with 'integer'.
* Extend parsing boolean values in URL query

Related-Bug: #1557462
Change-Id: Iaaafb61aaa030b001358cee00e9c6b66c306cd46
This commit is contained in:
Alexander Saprykin 2016-04-04 15:07:13 +02:00
parent 8c535849e2
commit fada90bd10
5 changed files with 67 additions and 15 deletions

View File

@ -19,8 +19,8 @@ _base_config = {
} }
_base_properties = { _base_properties = {
'cluster_id': {'type': 'number', }, 'cluster_id': {'type': 'integer'},
'node_id': {'type': 'number'}, 'node_id': {'type': 'integer'},
'node_role': {'type': 'string'}, 'node_role': {'type': 'string'},
} }
@ -28,7 +28,7 @@ OPENSTACK_CONFIG = {
'title': 'OpenstackConfig', 'title': 'OpenstackConfig',
'description': 'Openstack Configuration', 'description': 'Openstack Configuration',
'properties': { 'properties': {
'id': {'type': 'number'}, 'id': {'type': 'integer'},
'configuration': {'type': 'object'}, 'configuration': {'type': 'object'},
}, },
'required': ['cluster_id', 'configuration'], 'required': ['cluster_id', 'configuration'],
@ -38,7 +38,7 @@ OPENSTACK_CONFIG_EXECUTE = {
'title': 'OpenstackConfig execute', 'title': 'OpenstackConfig execute',
'description': 'Openstack Configuration filters for execute', 'description': 'Openstack Configuration filters for execute',
'properties': { 'properties': {
'id': {'type': 'number'}, 'id': {'type': 'integer'},
'force': {'type': 'boolean'} 'force': {'type': 'boolean'}
}, },
'required': ['cluster_id'], 'required': ['cluster_id'],
@ -48,12 +48,7 @@ OPENSTACK_CONFIG_QUERY = {
'title': 'OpenstackConfig query', 'title': 'OpenstackConfig query',
'description': 'URL query for Openstack Configuration filter', 'description': 'URL query for Openstack Configuration filter',
'properties': { 'properties': {
'is_active': { 'is_active': {'type': 'boolean'}
'type': 'number',
'description': "is_active is a number since GET request"
" query parameters don't carry type information"
" and are parsed as strings"},
'cluster_id': {'type': 'number'},
}, },
'required': ['cluster_id'], 'required': ['cluster_id'],
} }

View File

@ -20,11 +20,11 @@ from nailgun import consts
from nailgun.db.sqlalchemy import models from nailgun.db.sqlalchemy import models
from nailgun.errors import errors from nailgun.errors import errors
from nailgun import objects from nailgun import objects
from nailgun import utils
class OpenstackConfigValidator(BasicValidator): class OpenstackConfigValidator(BasicValidator):
int_fields = frozenset(['cluster_id', 'node_id', 'is_active'])
exclusive_fields = frozenset(['node_id', 'node_role']) exclusive_fields = frozenset(['node_id', 'node_role'])
supported_configs = frozenset([ supported_configs = frozenset([
@ -136,7 +136,7 @@ class OpenstackConfigValidator(BasicValidator):
cls._check_exclusive_fields(data) cls._check_exclusive_fields(data)
cls.validate_schema(data, schema.OPENSTACK_CONFIG_QUERY) cls.validate_schema(data, schema.OPENSTACK_CONFIG_QUERY)
data['is_active'] = bool(data.get('is_active', True)) data.setdefault('is_active', True)
return data return data
@classmethod @classmethod
@ -156,9 +156,21 @@ class OpenstackConfigValidator(BasicValidator):
Schema validation doesn't perform any type conversion, so Schema validation doesn't perform any type conversion, so
it is required to convert them before schema validation. it is required to convert them before schema validation.
""" """
for field in cls.int_fields: for field in ['cluster_id', 'node_id']:
if field in data and data[field] is not None: value = data.get(field, None)
data[field] = int(data[field]) if value is not None:
try:
data[field] = int(value)
except ValueError:
raise errors.InvalidData("Invalid '{0}' value: '{1}'"
.format(field, value))
if 'is_active' in data:
try:
data['is_active'] = utils.parse_bool(data['is_active'])
except ValueError:
raise errors.InvalidData("Invalid 'is_active' value: '{0}'"
.format(data['is_active']))
@classmethod @classmethod
def _check_exclusive_fields(cls, data): def _check_exclusive_fields(cls, data):

View File

@ -202,6 +202,16 @@ class TestOpenstackConfigHandlers(BaseIntegrationTest):
resp.json_body['message'], resp.json_body['message'],
r"Parameter '\w+' conflicts with '\w+(, \w+)*'") r"Parameter '\w+' conflicts with '\w+(, \w+)*'")
def test_openstack_config_list_invalid_params(self):
for param in ['cluster_id', 'node_id', 'is_active']:
url = self._make_filter_url(**{param: 'invalidvalue'})
resp = self.app.get(url, headers=self.default_headers,
expect_errors=True)
self.assertEqual(resp.status_code, 400)
self.assertEqual(
resp.json_body['message'],
"Invalid '{0}' value: 'invalidvalue'".format(param))
def test_openstack_config_get(self): def test_openstack_config_get(self):
resp = self.app.get( resp = self.app.get(
reverse('OpenstackConfigHandler', reverse('OpenstackConfigHandler',

View File

@ -26,6 +26,7 @@ from nailgun.utils import dict_update
from nailgun.utils import flatten from nailgun.utils import flatten
from nailgun.utils import get_lines from nailgun.utils import get_lines
from nailgun.utils import grouper from nailgun.utils import grouper
from nailgun.utils import parse_bool
from nailgun.utils import text_format_safe from nailgun.utils import text_format_safe
from nailgun.utils import traverse from nailgun.utils import traverse
@ -138,6 +139,18 @@ class TestUtils(base.BaseIntegrationTest):
self.assertEqual(get_lines(mixed), ['abc', 'foo', 'bar']) self.assertEqual(get_lines(mixed), ['abc', 'foo', 'bar'])
def test_parse_bool(self):
true_values = ['1', 't', 'T', 'TRUE', 'True', 'true']
false_values = ['0', 'f', 'F', 'FALSE', 'False', 'false']
for value in true_values:
self.assertTrue(parse_bool(value))
for value in false_values:
self.assertFalse(parse_bool(value))
self.assertRaises(ValueError, parse_bool, 'tru')
self.assertRaises(ValueError, parse_bool, 'fals')
class TestTraverse(base.BaseUnitTest): class TestTraverse(base.BaseUnitTest):

View File

@ -302,3 +302,25 @@ def dict_update(target, patch, level=None):
target.setdefault(k, {}).update(v) target.setdefault(k, {}).update(v)
else: else:
target[k] = v target[k] = v
def parse_bool(value):
"""
Returns boolean value from string representation.
Function returns ``True`` for ``"1", "t", "true"`` values
and ``False`` for ``"0", "f", "false"``. String values are
case insensetive.
Otherwise raises ValueError.
:param str value: string representation of boolean value
:rtype: bool
:raises ValueError: if the value cannot be converted to bool
"""
value = value.lower()
if value in ('1', 't', 'true'):
return True
elif value in ('0', 'f', 'false'):
return False
raise ValueError('Invalid value: {0}'.format(value))