From 08fc1b3b474a094b8bc135c9d1023e892d2d4e7d Mon Sep 17 00:00:00 2001 From: bugy Date: Fri, 28 Dec 2018 18:39:24 +0100 Subject: [PATCH] #136 added IP parameter type + fixed some small issues --- samples/configs/parameterized.json | 16 +++ src/model/script_configs.py | 23 ++++- src/tests/script_config_test.py | 56 ++++++++++- web-src/js/components/file_upload.vue | 10 -- web-src/js/components/textfield.vue | 105 +++++++++++++++++-- web-src/js/script/script-controller.js | 16 ++- web-src/js/script/script-view.vue | 15 +-- web-src/tests/textfield_test.js | 133 ++++++++++++++++++++++++- 8 files changed, 334 insertions(+), 40 deletions(-) diff --git a/samples/configs/parameterized.json b/samples/configs/parameterized.json index 6d16ff45..7b84326a 100644 --- a/samples/configs/parameterized.json +++ b/samples/configs/parameterized.json @@ -183,6 +183,22 @@ "constant": true, "param": "--audit_name", "default": "${auth.audit_name}" + }, + { + "name": "Any IP", + "param": "--any_ip", + "type": "ip" + }, + { + "name": "IP v4", + "param": "--ip4", + "type": "ip4", + "default": "127.0.0.1" + }, + { + "name": "IP v6", + "param": "--ip6", + "type": "ip6" } ] } \ No newline at end of file diff --git a/src/model/script_configs.py b/src/model/script_configs.py index 0777271b..a88083b2 100644 --- a/src/model/script_configs.py +++ b/src/model/script_configs.py @@ -2,6 +2,7 @@ import logging import os import re +from ipaddress import ip_address, IPv4Address, IPv6Address from auth.authorization import ANY_USER from config.script.list_values import ConstValuesProvider, ScriptValuesProvider, EmptyValuesProvider, \ @@ -270,7 +271,7 @@ def _reload(self): self.separator = config.get('separator', ',') self.multiple_arguments = read_boolean('multiple_arguments', config, default=False) self.default = _resolve_default(config.get('default'), self._username, self._audit_name) - self.type = config.get('type', 'text') + self.type = self._read_type(config) constant = read_boolean('constant', config, False) if constant and not self.default: @@ -288,6 +289,14 @@ def _reload(self): self._values_provider = values_provider self._reload_values() + def _read_type(self, config): + type = config.get('type', 'text') + + if type.lower() in ('ip', 'ip4', 'ip6', 'ipv4', 'ipv6'): + type = type.lower().replace('v', '') + + return type + def _param_values_observer(self, key, old_value, new_value): values_provider = self._values_provider if values_provider is None: @@ -380,6 +389,18 @@ def validate_value(self, value, *, ignore_required=False): + value_string + ' < ' + str(self.min) + ')' return None + if self.type in ('ip', 'ip4', 'ip6'): + try: + address = ip_address(value.strip()) + if self.type == 'ip4': + if not isinstance(address, IPv4Address): + return value_string + ' is not an IPv4 address' + elif self.type == 'ip6': + if not isinstance(address, IPv6Address): + return value_string + ' is not an IPv6 address' + except ValueError: + return 'wrong IP address ' + value_string + allowed_values = self.values if self.type == 'list': diff --git a/src/tests/script_config_test.py b/src/tests/script_config_test.py index c8248bc4..5ad5872e 100644 --- a/src/tests/script_config_test.py +++ b/src/tests/script_config_test.py @@ -262,6 +262,18 @@ def test_allowed_values_for_non_list(self): 'values': {'script': 'echo "123\n" "456"'}}) self.assertEqual(None, parameter_model.values) + def test_ip_uppercase(self): + parameter_model = _create_parameter_model({ + 'name': 'def_param', + 'type': 'IP'}) + self.assertEqual('ip', parameter_model.type) + + def test_ip_with_v(self): + parameter_model = _create_parameter_model({ + 'name': 'def_param', + 'type': 'Ipv6'}) + self.assertEqual('ip6', parameter_model.type) + class ParameterModelDependantValuesTest(unittest.TestCase): def test_get_parameter_values_simple(self): @@ -866,9 +878,9 @@ def test_multiselect_when_single_not_matching_element(self): self.assert_error(error) def test_list_with_script_when_matches(self): - parameter = create_parameter_model('param', type=list, values_script="echo '123\n' 'abc'") + parameter = create_parameter_model('param', type='list', values_script="echo '123\n' 'abc'") - error = parameter.validate_value('abc') + error = parameter.validate_value('123') self.assertIsNone(error) def test_list_with_dependency_when_matches(self): @@ -886,6 +898,46 @@ def test_list_with_dependency_when_matches(self): error = parameter.validate_value(' _abc_') self.assertIsNone(error) + def test_any_ip_when_ip4(self): + parameter = create_parameter_model('param', type='ip') + error = parameter.validate_value('127.0.0.1') + self.assertIsNone(error) + + def test_any_ip_when_ip6(self): + parameter = create_parameter_model('param', type='ip') + error = parameter.validate_value('ABCD::6789') + self.assertIsNone(error) + + def test_any_ip_when_wrong(self): + parameter = create_parameter_model('param', type='ip') + error = parameter.validate_value('127.abcd.1') + self.assert_error(error) + + def test_ip4_when_valid(self): + parameter = create_parameter_model('param', type='ip4') + error = parameter.validate_value('192.168.0.13') + self.assertIsNone(error) + + def test_ip4_when_ip6(self): + parameter = create_parameter_model('param', type='ip4') + error = parameter.validate_value('ABCD::1234') + self.assert_error(error) + + def test_ip6_when_valid(self): + parameter = create_parameter_model('param', type='ip6') + error = parameter.validate_value('1:2:3:4:5:6:7:8') + self.assertIsNone(error) + + def test_ip6_when_ip4(self): + parameter = create_parameter_model('param', type='ip6') + error = parameter.validate_value('172.13.0.15') + self.assert_error(error) + + def test_ip6_when_complex_valid(self): + parameter = create_parameter_model('param', type='ip6') + error = parameter.validate_value('AbC:0::13:127.0.0.1') + self.assertIsNone(error) + def assert_error(self, error): self.assertFalse(is_blank(error), 'Expected validation error, but validation passed') diff --git a/web-src/js/components/file_upload.vue b/web-src/js/components/file_upload.vue index cf58a7b4..40ab6ebc 100644 --- a/web-src/js/components/file_upload.vue +++ b/web-src/js/components/file_upload.vue @@ -42,16 +42,6 @@ return this.value.name; }, - - fieldType() { - if (this.config.type === 'int') { - return 'number'; - } else if (this.config.secure) { - return 'password'; - } - - return 'text'; - } }, mounted: function () { diff --git a/web-src/js/components/textfield.vue b/web-src/js/components/textfield.vue index bebb2d4b..648cf30c 100644 --- a/web-src/js/components/textfield.vue +++ b/web-src/js/components/textfield.vue @@ -85,7 +85,7 @@ var empty = isEmptyString(value) || isEmptyString(value.trim()); if ((textField.validity.badInput)) { - return getInvalidTypeError(this.type); + return getInvalidTypeError(this.config.type); } if (this.config.required && empty) { @@ -114,7 +114,7 @@ function getValidByTypeError(value, type, min, max) { if (type === 'int') { - var isInteger = /^(((\-?[1-9])(\d*))|0)$/.test(value); + const isInteger = /^(((-?[1-9])(\d*))|0)$/.test(value); if (!isInteger) { return getInvalidTypeError(type); } @@ -122,9 +122,9 @@ var intValue = parseInt(value); var minMaxValid = true; - var minMaxError = ""; + var minMaxError = ''; if (!isNull(min)) { - minMaxError += "min: " + min; + minMaxError += 'min: ' + min; if (intValue < parseInt(min)) { minMaxValid = false; @@ -137,27 +137,112 @@ } if (!isEmptyString(minMaxError)) { - minMaxError += ", "; + minMaxError += ', '; } - minMaxError += "max: " + max; + minMaxError += 'max: ' + max; } if (!minMaxValid) { return minMaxError; } - return ""; + return ''; + + } else if (type === 'ip') { + if (isEmptyString(validateIp4(value)) || isEmptyString(validateIp6(value))) { + return '' + } + + return 'IPv4 or IPv6 expected'; + + } else if (type === 'ip4') { + return validateIp4(value); + + } else if (type === 'ip6') { + return validateIp6(value); + } + + return ''; + } + + function validateIp4(value) { + const ipElements = value.trim().split('.'); + if (ipElements.length !== 4) { + return 'IPv4 expected' + } + + for (const element of ipElements) { + if (isEmptyString(element)) { + return 'Empty IP block' + } + + if (!/^[12]?[0-9]{1,2}$/.test(element)) { + return 'Invalid block ' + element; + } + + const elementNumeric = parseInt(element, 10); + if (elementNumeric > 255) { + return 'Out of range ' + elementNumeric; + } + } + + return ''; + } + + function validateIp6(value) { + const chunks = value.trim().split('::'); + if (chunks.length > 2) { + return ':: allowed only once'; + } + + const elements = []; + + elements.push(...chunks[0].split(':')); + if (chunks.length === 2) { + elements.push('::'); + elements.push(...chunks[1].split(':')) + } + + const hasCompressZeroes = chunks.length === 2; + let afterDoubleColon = false; + let hasIp4 = false; + let count = 0; + + for (const element of elements) { + if (hasIp4) { + return 'IPv4 should be the last'; + } + + if (element === '::') { + afterDoubleColon = true; + + } else if (element.includes('.') && ((afterDoubleColon || count >= 6))) { + if (!isEmptyString(validateIp4(element))) { + return 'Invalid IPv4 block ' + element; + } + hasIp4 = true; + count++; + + } else if (!/^[A-F0-9]{0,4}$/.test(element.toUpperCase())) { + return 'Invalid block ' + element; + } + + count++; + } + + if (((count < 8) && (!hasCompressZeroes)) || (count > 8)) { + return 'Should be 8 blocks'; } - return ""; + return ''; } function getInvalidTypeError(type) { if (type === 'int') { - return "integer expected"; + return 'integer expected'; } - return type + " expected"; + return type + ' expected'; } \ No newline at end of file diff --git a/web-src/js/script/script-controller.js b/web-src/js/script/script-controller.js index 8d0a312d..d3536eb5 100644 --- a/web-src/js/script/script-controller.js +++ b/web-src/js/script/script-controller.js @@ -301,6 +301,7 @@ ScriptController.prototype._initStore = function () { parameterValues: {}, parameterErrors: {}, executing: false, + showLog: false, downloadableFiles: [], inputPromptText: null, sentValues: {}, @@ -355,9 +356,14 @@ ScriptController.prototype._initStore = function () { }); controller.scriptView.$nextTick(function () { - var parameterValues = state.parameterValues; + const parameterValues = state.parameterValues; + const parameterErrors = state.parameterErrors; + forEachKeyValue(parameterValues, function (key, value) { - controller._sendCurrentValue(key, value); + const errorMessage = parameterErrors[key]; + const valueToSend = isEmptyString(errorMessage) ? value : null; + + controller._sendCurrentValue(key, valueToSend); }); }); }, @@ -447,6 +453,10 @@ ScriptController.prototype._initStore = function () { [SET_EXECUTING](state, executing) { state.executing = executing; + + if (executing) { + state.showLog = true; + } }, [SET_INPUT_PROMPT](state, promptText) { @@ -505,7 +515,7 @@ ScriptController.prototype._initStore = function () { if (!(error instanceof HttpUnauthorizedError)) { logError(error); - commit.commit(APPEND_LOG_CHUNK, {text: '\n\n' + error.message}); + commit(APPEND_LOG_CHUNK, {text: '\n\n' + error.message}); } } diff --git a/web-src/js/script/script-view.vue b/web-src/js/script/script-view.vue index d3285dba..a34b0f7a 100644 --- a/web-src/js/script/script-view.vue +++ b/web-src/js/script/script-view.vue @@ -16,7 +16,7 @@ Stop - +
Validation failed. Errors list:
    @@ -74,6 +74,7 @@ ...mapState({ scriptDescription: state => state.scriptConfig ? state.scriptConfig.description : '', executing: 'executing', + showLog: 'showLog', downloadableFiles: 'downloadableFiles', inputPromptText: 'inputPromptText', logChunks: 'logChunks' @@ -176,18 +177,6 @@ } }, - executing: function (value) { - const newExecuting = toBoolean(value); - - if (newExecuting) { - this.errors = []; - - if (!this.everStarted) { - this.everStarted = true; - } - } - }, - logChunks: { handler(newValue, oldValue) { if (isNull(newValue)) { diff --git a/web-src/tests/textfield_test.js b/web-src/tests/textfield_test.js index 41e26c16..3ed0730d 100644 --- a/web-src/tests/textfield_test.js +++ b/web-src/tests/textfield_test.js @@ -2,7 +2,7 @@ import {mount} from '@vue/test-utils'; import {assert, config as chaiConfig} from 'chai'; -import {setInputValue} from '../js/common'; +import {isEmptyString, setInputValue} from '../js/common'; import Textfield from '../js/components/textfield' import {mergeDeepProps, setDeepProp, vueTicks, wrapVModel} from './test_utils'; @@ -191,4 +191,135 @@ describe('Test TextField', function () { assert.equal('integer expected', this.textfield.currentError); }); }); + + describe('Test IP validaton', function () { + + async function testValidation(textfield, type, value, expectedError) { + setDeepProp(textfield, 'config.type', type); + textfield.setProps({value: value}); + await vueTicks(); + + if (isEmptyString(expectedError)) { + assert.equal(expectedError, textfield.currentError); + } else { + assert.include(textfield.currentError, expectedError); + } + } + + it('Test IPv4 127.0.0.1', async function () { + await testValidation(this.textfield, 'ip4', '127.0.0.1', '') + }); + + it('Test IPv4 255.255.255.255', async function () { + await testValidation(this.textfield, 'ip4', '255.255.255.255', '') + }); + + it('Test IPv4 valid with trim', async function () { + await testValidation(this.textfield, 'ip4', ' 192.168.0.1\n', '') + }); + + it('Test IPv4 invalid block count', async function () { + await testValidation(this.textfield, 'ip4', '127.0.1', 'IPv4 expected') + }); + + it('Test IPv4 empty block', async function () { + await testValidation(this.textfield, 'ip4', '127..0.1', 'Empty IP block') + }); + + it('Test IPv4 invalid block', async function () { + await testValidation(this.textfield, 'ip4', '127.wrong.0.1', 'Invalid block wrong') + }); + + it('Test IPv4 large number', async function () { + await testValidation(this.textfield, 'ip4', '192.168.256.0', 'Out of range') + }); + + it('Test IPv6 ::', async function () { + await testValidation(this.textfield, 'ip6', '::', '') + }); + + it('Test IPv6 ::0', async function () { + await testValidation(this.textfield, 'ip6', '::0', '') + }); + + it('Test IPv6 ABCD::0', async function () { + await testValidation(this.textfield, 'ip6', 'ABCD::0', '') + }); + + it('Test IPv6 ABCD::192.168.2.12', async function () { + await testValidation(this.textfield, 'ip6', 'ABCD::192.168.2.12', '') + }); + + it('Test IPv6 ABCD:0123::4567:192.168.2.12', async function () { + await testValidation(this.textfield, 'ip6', 'ABCD:0123::4567:192.168.2.12', '') + }); + + it('Test IPv6 valid with trim', async function () { + await testValidation(this.textfield, 'ip6', ' ABCD::0123 ', '') + }); + + it('Test IPv6 valid with different cases', async function () { + await testValidation(this.textfield, 'ip6', 'AbCd::123:dEf', '') + }); + + it('Test IPv6 valid blocks count', async function () { + await testValidation(this.textfield, 'ip6', '1:2:3:4:5:6:7:8', '') + }); + + it('Test IPv6 valid blocks count with ip4', async function () { + await testValidation(this.textfield, 'ip6', '1:2:3:4:5:6:127.0.0.1', '') + }); + + it('Test IPv6 too much blocks', async function () { + await testValidation(this.textfield, 'ip6', '1:2:3:4:5:6:7:8:9', 'Should be 8 blocks') + }); + + it('Test IPv6 too much blocks with zero compression', async function () { + await testValidation(this.textfield, 'ip6', '1:2:3::4:5:6:7:8:9', 'Should be 8 blocks') + }); + + it('Test IPv6 too little blocks', async function () { + await testValidation(this.textfield, 'ip6', '1:2:3:4:5:6:7', 'Should be 8 blocks') + }); + + it('Test IPv6 double ::', async function () { + await testValidation(this.textfield, 'ip6', '1::2::3', 'allowed only once') + }); + + it('Test IPv6 invalid long block', async function () { + await testValidation(this.textfield, 'ip6', '1::ABCDE:3', 'Invalid block ABCDE') + }); + + it('Test IPv6 invalid character', async function () { + await testValidation(this.textfield, 'ip6', '1::ABCG:3', 'Invalid block ABCG') + }); + + it('Test IPv6 when ip4 not last', async function () { + await testValidation(this.textfield, 'ip6', '1::127.0.0.1:AB', 'should be the last') + }); + + it('Test IPv6 when ip4 wrong', async function () { + await testValidation(this.textfield, 'ip6', '1::127..1', 'Invalid IPv4 block 127..1') + }); + + it('Test IPv6 when ip4 too early', async function () { + await testValidation(this.textfield, 'ip6', '1:2:3:4:5:127.0.0.1', 'Invalid block 127.0.0.1') + }); + + it('Test Any IP when correct ip4', async function () { + await testValidation(this.textfield, 'ip', '127.0.0.1', '') + }); + + it('Test Any IP when wrong ip4', async function () { + await testValidation(this.textfield, 'ip', '127.0..1', 'IPv4 or IPv6 expected') + }); + + it('Test Any IP when correct ip6', async function () { + await testValidation(this.textfield, 'ip', 'ABCD::0', '') + }); + + it('Test Any IP when wrong ip6', async function () { + await testValidation(this.textfield, 'ip', 'ABCX::0', 'IPv4 or IPv6 expected') + }); + }); }); \ No newline at end of file