diff --git a/kong/dao/schemas_validation.lua b/kong/dao/schemas_validation.lua index 27d697ffd18..e4169b48381 100644 --- a/kong/dao/schemas_validation.lua +++ b/kong/dao/schemas_validation.lua @@ -1,71 +1,85 @@ local utils = require "kong.tools.utils" +local stringy = require "stringy" local constants = require "kong.constants" -local LUA_TYPES = { - boolean = true, +local POSSIBLE_TYPES = { + id = true, + table = true, + array = true, string = true, number = true, - table = true + boolean = true, + timestamp = true } -local LUA_TYPE_ALIASES = { - [constants.DATABASE_TYPES.ID] = "string", - [constants.DATABASE_TYPES.TIMESTAMP] = "number" +local types_validation = { + [constants.DATABASE_TYPES.ID] = function(v) return type(v) == "string" end, + [constants.DATABASE_TYPES.TIMESTAMP] = function(v) return type(v) == "number" end, + ["array"] = function(v) return utils.is_array(v) end } -local _M = {} - --- Returns the proper Lua type from a schema type, handling aliases --- @param {string} type_val The type of the schema property --- @return {string} A valid Lua type -function _M.get_type(type_val) - local alias = LUA_TYPE_ALIASES[type_val] - return alias and alias or type_val +local function validate_type(field_type, value) + if types_validation[field_type] then + return types_validation[field_type](value) + end + return type(value) == field_type end +local _M = {} + -- Validate a table against a given schema --- @param {table} t Table to validate --- @param {table} schema Schema against which to validate the table --- @param {boolean} is_update For an entity update, we might want a slightly different behaviour --- @return {boolean} Success of validation --- @return {table} A list of encountered errors during the validation +-- @param `t` Entity to validate, as a table. +-- @param `schema` Schema against which to validate the entity. +-- @param `is_update` For an entity update, check immutable fields. Set to true. +-- @return `valid` Success of validation. True or false. +-- @return `errors` A list of encountered errors during the validation. function _M.validate(t, schema, is_update) local errors -- Check the given table against a given schema for column, v in pairs(schema) do - -- Set default value for the field if given + -- [DEFAULT] Set default value for the field if given if t[column] == nil and v.default ~= nil then if type(v.default) == "function" then t[column] = v.default(t) else t[column] = v.default end + -- [IMMUTABLE] check immutability of a field if updating elseif is_update and t[column] ~= nil and v.immutable and not v.required then - -- is_update check immutability of a field errors = utils.add_error(errors, column, column.." cannot be updated") end - -- Check if type is valid boolean and numbers as strings are accepted and converted + -- [TYPE] Check if type is valid. Boolean and Numbers as strings are accepted and converted if v.type ~= nil and t[column] ~= nil then - local valid - if _M.get_type(v.type) == "number" and type(t[column]) == "string" then -- a number can also be sent as a string - t[column] = tonumber(t[column]) - valid = t[column] ~= nil - elseif _M.get_type(v.type) == "boolean" and type(t[column]) == "string" then - local bool = t[column]:lower() - valid = bool == "true" or bool == "false" - t[column] = bool == "true" + local is_valid_type + + -- ALIASES: number, boolean and array can be passed as strings and will be converted + if type(t[column]) == "string" then + if v.type == "number" then + t[column] = tonumber(t[column]) + is_valid_type = t[column] ~= nil + elseif v.type == "boolean" then + local bool = t[column]:lower() + is_valid_type = bool == "true" or bool == "false" + t[column] = bool == "true" + elseif v.type == "array" then + t[column] = stringy.split(t[column], ",") + is_valid_type = validate_type(v.type, t[column]) + else -- if string + is_valid_type = validate_type(v.type, t[column]) + end else - valid = type(t[column]) == _M.get_type(v.type) + is_valid_type = validate_type(v.type, t[column]) end - if not valid and LUA_TYPES[v.type] then + + if not is_valid_type and POSSIBLE_TYPES[v.type] then errors = utils.add_error(errors, column, column.." is not a "..v.type) end end - -- Check type if value is allowed in the enum + -- [ENUM] Check if the value is allowed in the enum. if v.enum and t[column] ~= nil then local found = false for _, allowed in ipairs(v.enum) do @@ -80,14 +94,14 @@ function _M.validate(t, schema, is_update) end end - -- Check field against a regex if specified + -- [REGEX] Check field against a regex if specified if t[column] ~= nil and v.regex then if not ngx.re.match(t[column], v.regex) then errors = utils.add_error(errors, column, column.." has an invalid value") end end - -- validate a subschema + -- [SCHEMA] Validate a sub-schema from a table or retrived by a function if v.schema then local sub_schema, err if type(v.schema) == "function" then @@ -102,19 +116,19 @@ function _M.validate(t, schema, is_update) end if sub_schema then - -- Check for sub-schema defaults and required properties + -- Check for sub-schema defaults and required properties in advance for sub_field_k, sub_field in pairs(sub_schema) do if t[column] == nil then - if sub_field.default then + if sub_field.default then -- Sub-value has a default, be polite and pre-assign the sub-value t[column] = {} - elseif sub_field.required then -- only check required if field doesn't have a default + elseif sub_field.required then -- Only check required if field doesn't have a default errors = utils.add_error(errors, column, column.."."..sub_field_k.." is required") end end end if t[column] and type(t[column]) == "table" then - -- validating subschema + -- Actually validating the sub-schema local s_ok, s_errors = _M.validate(t[column], sub_schema, is_update) if not s_ok then for s_k, s_v in pairs(s_errors) do @@ -125,12 +139,13 @@ function _M.validate(t, schema, is_update) end end - -- Check required fields are set + -- [REQUIRED] Check that required fields are set. Now that default and most other checks + -- have been run. if v.required and (t[column] == nil or t[column] == "") then errors = utils.add_error(errors, column, column.." is required") end - -- Check field against a custom function only if there is no error on that field already + -- [FUNC] Check field against a custom function only if there is no error on that field already if v.func and type(v.func) == "function" and (errors == nil or errors[column] == nil) then local ok, err, new_fields = v.func(t[column], t) if not ok and err then diff --git a/kong/plugins/keyauth/schema.lua b/kong/plugins/keyauth/schema.lua index 1fb3b55341f..b80553551e2 100644 --- a/kong/plugins/keyauth/schema.lua +++ b/kong/plugins/keyauth/schema.lua @@ -6,17 +6,7 @@ local function default_key_names(t) end end -local function validate_key_names(t) - if type(t) == "table" and not utils.is_array(t) then - local printable_mt = require "kong.tools.printable" - setmetatable(t, printable_mt) - return false, "key_names must be an array. '"..t.."' is a table. Lua tables must have integer indexes starting at 1." - end - - return true -end - return { - key_names = { required = true, type = "table", default = default_key_names, func = validate_key_names }, + key_names = { required = true, type = "array", default = default_key_names }, hide_credentials = { type = "boolean", default = false } } diff --git a/kong/plugins/request_transformer/access.lua b/kong/plugins/request_transformer/access.lua index 3b377444fb8..a9f92ca0b1a 100644 --- a/kong/plugins/request_transformer/access.lua +++ b/kong/plugins/request_transformer/access.lua @@ -23,7 +23,6 @@ local function get_content_type(request) if header_value then return stringy.strip(header_value) end - return nil end function _M.execute(conf) diff --git a/kong/plugins/request_transformer/schema.lua b/kong/plugins/request_transformer/schema.lua index e563c96bb68..25c8fc34205 100644 --- a/kong/plugins/request_transformer/schema.lua +++ b/kong/plugins/request_transformer/schema.lua @@ -1,14 +1,16 @@ return { - add = { type = "table", schema = { - form = { type = "table" }, - headers = { type = "table" }, - querystring = { type = "table" } + add = { type = "table", + schema = { + form = { type = "array" }, + headers = { type = "array" }, + querystring = { type = "array" } } }, - remove = { type = "table", schema = { - form = { type = "table" }, - headers = { type = "table" }, - querystring = { type = "table" } + remove = { type = "table", + schema = { + form = { type = "array" }, + headers = { type = "array" }, + querystring = { type = "array" } } } } diff --git a/spec/unit/schemas_spec.lua b/spec/unit/schemas_spec.lua index 2a4b932df06..56dacbcfb5b 100644 --- a/spec/unit/schemas_spec.lua +++ b/spec/unit/schemas_spec.lua @@ -6,15 +6,6 @@ require "kong.tools.ngx_stub" describe("Schemas", function() - it("should alias lua types to database types", function() - assert.are.same("number", schemas.get_type("number")) - assert.are.same("string", schemas.get_type("string")) - assert.are.same("boolean", schemas.get_type("boolean")) - assert.are.same("table", schemas.get_type("table")) - assert.are.same("string", schemas.get_type(constants.DATABASE_TYPES.ID)) - assert.are.same("number", schemas.get_type(constants.DATABASE_TYPES.TIMESTAMP)) - end) - -- Ok kids, today we're gonna test a custom validation schema, -- grab a pair of glasses, this stuff can literally explode. describe("#validate()", function() @@ -51,16 +42,19 @@ describe("Schemas", function() assert.truthy(valid) end) - it("should invalidate entity if required property is missing", function() - local values = { url = "mockbin.com" } + describe("[required]", function() + it("should invalidate entity if required property is missing", function() + local values = { url = "mockbin.com" } - local valid, err = validate(values, schema) - assert.falsy(valid) - assert.truthy(err) - assert.are.same("string is required", err.string) + local valid, err = validate(values, schema) + assert.falsy(valid) + assert.truthy(err) + assert.are.same("string is required", err.string) + end) end) - it("should validate the type of a property if it has a type field", function() + describe("[type]", function() + it("should validate the type of a property if it has a type field", function() -- Failure local values = { string = "foo", table = "bar" } @@ -140,154 +134,195 @@ describe("Schemas", function() assert.truthy(valid) end) - it("should not return an error when a number is passed as a string", function() - -- Success - local values = { string = "test", number = "10" } + it("should consider `id` and `timestamp` as types", function() + local s = { id = { type = "id" } } - local valid, err = validate(values, schema) + local values = { id = "123" } + + local valid, err = validate(values, s) assert.falsy(err) assert.truthy(valid) - assert.are.same("number", type(values.number)) end) - it("should not return an error when a boolean is passed as a string", function() + it("should consider `array` as a type", function() + local s = { array = { type = "array" } } + -- Success - local values = { string = "test", boolean_val = "false" } + local values = { array = {"hello", "world"} } - local valid, err = validate(values, schema) + local valid, err = validate(values, s) + assert.True(valid) assert.falsy(err) - assert.truthy(valid) - assert.are.same("boolean", type(values.boolean_val)) - end) - - it("should consider id and timestampd as valid types", function() - local s = { id = { type = "id" } } - local values = { id = 123 } + -- Failure + local values = { array = {hello="world"} } local valid, err = validate(values, s) - assert.falsy(err) - assert.truthy(valid) + assert.False(valid) + assert.truthy(err) + assert.equal("array is not a array", err.array) end) - it("should set default values if those are variables or functions specified in the validator", function() - -- Variables - local values = { string = "mockbin entity", url = "mockbin.com" } + describe("[aliases]", function() + it("should not return an error when a `number` is passed as a string", function() + local values = { string = "test", number = "10" } - local valid, err = validate(values, schema) - assert.falsy(err) - assert.truthy(valid) - assert.are.same(123456, values.date) + local valid, err = validate(values, schema) + assert.falsy(err) + assert.truthy(valid) + assert.same("number", type(values.number)) + end) - -- Functions - local values = { string = "mockbin entity", url = "mockbin.com" } + it("should not return an error when a `boolean` is passed as a string", function() + local values = { string = "test", boolean_val = "false" } - local valid, err = validate(values, schema) - assert.falsy(err) - assert.truthy(valid) - assert.are.same("default", values.default) + local valid, err = validate(values, schema) + assert.falsy(err) + assert.truthy(valid) + assert.same("boolean", type(values.boolean_val)) + end) + + it("should alias a string to `array`", function() + local s = { array = { type = "array" } } + + local values = { array = "hello,world" } + + local valid, err = validate(values, s) + assert.True(valid) + assert.falsy(err) + assert.same({"hello", "world"}, values.array) + end) end) + end) - it("should override default values if specified", function() - -- Variables - local values = { string = "mockbin entity", url = "mockbin.com", date = 654321 } + describe("[default]", function() + it("should set default values if those are variables or functions specified in the validator", function() + -- Variables + local values = { string = "mockbin entity", url = "mockbin.com" } - local valid, err = validate(values, schema) - assert.falsy(err) - assert.truthy(valid) - assert.are.same(654321, values.date) + local valid, err = validate(values, schema) + assert.falsy(err) + assert.truthy(valid) + assert.are.same(123456, values.date) - -- Functions - local values = { string = "mockbin entity", url = "mockbin.com", default = "abcdef" } + -- Functions + local values = { string = "mockbin entity", url = "mockbin.com" } - local valid, err = validate(values, schema) - assert.falsy(err) - assert.truthy(valid) - assert.are.same("abcdef", values.default) - end) + local valid, err = validate(values, schema) + assert.falsy(err) + assert.truthy(valid) + assert.are.same("default", values.default) + end) - it("should validate a field against a regex", function() - local values = { string = "mockbin entity", url = "mockbin_!" } + it("should override default values if specified", function() + -- Variables + local values = { string = "mockbin entity", url = "mockbin.com", date = 654321 } - local valid, err = validate(values, schema) - assert.falsy(valid) - assert.truthy(err) - assert.are.same("url has an invalid value", err.url) + local valid, err = validate(values, schema) + assert.falsy(err) + assert.truthy(valid) + assert.are.same(654321, values.date) + + -- Functions + local values = { string = "mockbin entity", url = "mockbin.com", default = "abcdef" } + + local valid, err = validate(values, schema) + assert.falsy(err) + assert.truthy(valid) + assert.are.same("abcdef", values.default) + end) end) - it("should return error when unexpected values are included in the schema", function() - local values = { string = "mockbin entity", url = "mockbin.com", unexpected = "abcdef" } + describe("[regex]", function() + it("should validate a field against a regex", function() + local values = { string = "mockbin entity", url = "mockbin_!" } - local valid, err = validate(values, schema) - assert.falsy(valid) - assert.truthy(err) + local valid, err = validate(values, schema) + assert.falsy(valid) + assert.truthy(err) + assert.are.same("url has an invalid value", err.url) + end) end) - it("should be able to return multiple errors at once", function() - local values = { url = "mockbin.com", unexpected = "abcdef" } + describe("[enum]", function() + it("should validate a field against an enum", function() + -- Success + local values = { string = "somestring", allowed = "hello" } - local valid, err = validate(values, schema) - assert.falsy(valid) - assert.truthy(err) - assert.are.same("string is required", err.string) - assert.are.same("unexpected is an unknown field", err.unexpected) + local valid, err = validate(values, schema) + assert.falsy(err) + assert.truthy(valid) + + -- Failure + local values = { string = "somestring", allowed = "hello123" } + + local valid, err = validate(values, schema) + assert.falsy(valid) + assert.truthy(err) + assert.are.same("\"hello123\" is not allowed. Allowed values are: \"hello\", \"world\"", err.allowed) + end) end) - it("should validate a field against an enum", function() - -- Success - local values = { string = "somestring", allowed = "hello" } + describe("[func]", function() + it("should validate a field against a custom function", function() + -- Success + local values = { string = "somestring", custom = true, default = "test_custom_func" } - local valid, err = validate(values, schema) - assert.falsy(err) - assert.truthy(valid) + local valid, err = validate(values, schema) + assert.falsy(err) + assert.truthy(valid) - -- Failure - local values = { string = "somestring", allowed = "hello123" } + -- Failure + local values = { string = "somestring", custom = true, default = "not the default :O" } - local valid, err = validate(values, schema) - assert.falsy(valid) - assert.truthy(err) - assert.are.same("\"hello123\" is not allowed. Allowed values are: \"hello\", \"world\"", err.allowed) + local valid, err = validate(values, schema) + assert.falsy(valid) + assert.truthy(err) + assert.are.same("Nah", err.custom) + end) end) - it("should validate a field against a custom function", function() - -- Success - local values = { string = "somestring", custom = true, default = "test_custom_func" } + describe("[immutable]", function() + it("should prevent immutable properties to be changed if validating a schema that will be updated", function() + -- Success + local values = { string = "somestring", date = 1234 } - local valid, err = validate(values, schema) - assert.falsy(err) - assert.truthy(valid) + local valid, err = validate(values, schema) + assert.falsy(err) + assert.truthy(valid) - -- Failure - local values = { string = "somestring", custom = true, default = "not the default :O" } + -- Failure + local valid, err = validate(values, schema, true) + assert.falsy(valid) + assert.truthy(err) + assert.are.same("date cannot be updated", err.date) + end) - local valid, err = validate(values, schema) - assert.falsy(valid) - assert.truthy(err) - assert.are.same("Nah", err.custom) + it("should ignore required properties if they are immutable and we are updating", function() + local values = { string = "somestring" } + + local valid, err = validate(values, schema, true) + assert.falsy(err) + assert.truthy(valid) + end) end) - it("should prevent immutable properties to be changed if validating a schema that will be updated", function() - -- Success - local values = { string = "somestring", date = 1234 } + it("should return error when unexpected values are included in the schema", function() + local values = { string = "mockbin entity", url = "mockbin.com", unexpected = "abcdef" } local valid, err = validate(values, schema) - assert.falsy(err) - assert.truthy(valid) - - -- Failure - local valid, err = validate(values, schema, true) assert.falsy(valid) assert.truthy(err) - assert.are.same("date cannot be updated", err.date) end) - it("should ignore required properties if they are immutable and we are updating", function() - local values = { string = "somestring" } + it("should be able to return multiple errors at once", function() + local values = { url = "mockbin.com", unexpected = "abcdef" } - local valid, err = validate(values, schema, true) - assert.falsy(err) - assert.truthy(valid) + local valid, err = validate(values, schema) + assert.falsy(valid) + assert.truthy(err) + assert.are.same("string is required", err.string) + assert.are.same("unexpected is an unknown field", err.unexpected) end) it("should not check a custom function if a `required` condition is false already", function()