Skip to content

Commit

Permalink
feat(*): add optional wasm filter config validation
Browse files Browse the repository at this point in the history
  • Loading branch information
flrgh committed Sep 21, 2023
1 parent bfc8ef7 commit a4e646b
Show file tree
Hide file tree
Showing 21 changed files with 1,299 additions and 55 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG/unreleased/kong/wasm-filter-config-schemas.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
message: Add support for optional Wasm filter configuration schemas
type: feature
scope: Core
prs:
- 11568
jiras:
- KAG-662
2 changes: 2 additions & 0 deletions kong-3.5.0-0.rockspec
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ dependencies = {
"lua-resty-session == 4.0.5",
"lua-resty-timer-ng == 0.2.5",
"lpeg == 1.0.2",
"lua-resty-ljsonschema == 1.1.5",
}
build = {
type = "builtin",
Expand Down Expand Up @@ -222,6 +223,7 @@ build = {
["kong.db.schema.entities.clustering_data_planes"] = "kong/db/schema/entities/clustering_data_planes.lua",
["kong.db.schema.entities.parameters"] = "kong/db/schema/entities/parameters.lua",
["kong.db.schema.entities.filter_chains"] = "kong/db/schema/entities/filter_chains.lua",
["kong.db.schema.json"] = "kong/db/schema/json.lua",
["kong.db.schema.others.migrations"] = "kong/db/schema/others/migrations.lua",
["kong.db.schema.others.declarative_config"] = "kong/db/schema/others/declarative_config.lua",
["kong.db.schema.entity"] = "kong/db/schema/entity.lua",
Expand Down
4 changes: 4 additions & 0 deletions kong/constants.lua
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,10 @@ local constants = {
DYN_LOG_LEVEL_TIMEOUT_AT_KEY = "kong:dyn_log_level_timeout_at",

ADMIN_GUI_KCONFIG_CACHE_KEY = "admin:gui:kconfig",

SCHEMA_NAMESPACES = {
PROXY_WASM_FILTERS = "proxy-wasm-filters",
},
}

for _, v in ipairs(constants.CLUSTERING_SYNC_STATUS) do
Expand Down
37 changes: 28 additions & 9 deletions kong/db/schema/entities/filter_chains.lua
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
local typedefs = require "kong.db.schema.typedefs"
local wasm = require "kong.runloop.wasm"
local constants = require "kong.constants"


---@class kong.db.schema.entities.filter_chain : table
Expand All @@ -9,7 +10,6 @@ local wasm = require "kong.runloop.wasm"
---@field enabled boolean
---@field route table|nil
---@field service table|nil
---@field protocols table|nil
---@field created_at number
---@field updated_at number
---@field tags string[]
Expand All @@ -18,22 +18,41 @@ local wasm = require "kong.runloop.wasm"

---@class kong.db.schema.entities.wasm_filter : table
---
---@field name string
---@field enabled boolean
---@field config string|table|nil
---@field name string
---@field enabled boolean
---@field config any|nil
---@field raw_config string|nil


local filter = {
type = "record",
fields = {
{ name = { type = "string", required = true, one_of = wasm.filter_names,
err = "no such filter", }, },
{ config = { type = "string", required = false, }, },
{ enabled = { type = "boolean", default = true, required = true, }, },
{ name = { type = "string", required = true, one_of = wasm.filter_names,
err = "no such filter", }, },
{ raw_config = { type = "string", required = false, }, },
{ enabled = { type = "boolean", default = true, required = true, }, },

{ config = {
type = "json",
required = false,
json_schema = {
parent_subschema_key = "name",
namespace = constants.SCHEMA_NAMESPACES.PROXY_WASM_FILTERS,
optional = true,
},
},
},

},
entity_checks = {
{ mutually_exclusive = {
"config",
"raw_config",
},
},
},
}


return {
name = "filter_chains",
primary_key = { "id" },
Expand Down
49 changes: 48 additions & 1 deletion kong/db/schema/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ local cjson = require "cjson"
local new_tab = require "table.new"
local nkeys = require "table.nkeys"
local is_reference = require "kong.pdk.vault".is_reference
local json = require "kong.db.schema.json"
local cjson_safe = require "cjson.safe"


local setmetatable = setmetatable
Expand All @@ -30,10 +32,12 @@ local find = string.find
local null = ngx.null
local max = math.max
local sub = string.sub
local safe_decode = cjson_safe.decode


local random_string = utils.random_string
local uuid = utils.uuid
local json_validate = json.validate


local Schema = {}
Expand Down Expand Up @@ -115,6 +119,12 @@ local validation_errors = {
SUBSCHEMA_ABSTRACT_FIELD = "error in schema definition: abstract field was not specialized",
-- transformations
TRANSFORMATION_ERROR = "transformation failed: %s",
-- json
JSON_ENCODE_ERROR = "value could not be JSON-encoded: %s",
JSON_DECODE_ERROR = "value could not be JSON-decoded: %s",
JSON_SCHEMA_ERROR = "value failed JSON-schema validation: %s",
JSON_PARENT_KEY_MISSING = "validation of %s depends on the parent attribute %s, but it is not set",
JSON_SCHEMA_NOT_FOUND = "mandatory json schema for field (%s) not found"
}


Expand All @@ -129,6 +139,7 @@ Schema.valid_types = {
map = true,
record = true,
["function"] = true,
json = true,
}


Expand Down Expand Up @@ -1110,7 +1121,35 @@ validate_fields = function(self, input)
for k, v in pairs(input) do
local err
local field = self.fields[tostring(k)]
if field and field.type == "self" then

if field and field.type == "json" then
local json_schema = field.json_schema
local inline_schema = json_schema.inline

if inline_schema then
_, errors[k] = json_validate(v, inline_schema)

else
local parent_key = json_schema.parent_subschema_key
local json_subschema_key = input[parent_key]

if json_subschema_key then
local schema_name = json_schema.namespace .. "/" .. json_subschema_key
inline_schema = json.get_schema(schema_name)

if inline_schema then
_, errors[k] = json_validate(v, inline_schema)

elseif not json_schema.optional then
errors[k] = validation_errors.JSON_SCHEMA_NOT_FOUND:format(schema_name)
end

elseif not json_schema.optional then
errors[k] = validation_errors.JSON_PARENT_KEY_MISSING:format(k, parent_key)
end
end

elseif field and field.type == "self" then
local pok
pok, err, errors[k] = pcall(self.validate_field, self, input, v)
if not pok then
Expand Down Expand Up @@ -2261,6 +2300,14 @@ end


local function run_transformations(self, transformations, input, original_input, context)
if self.type == "json" and context == "select" then
local decoded, err = safe_decode(input)
if err then
return nil, validation_errors.JSON_DECODE_ERROR:format(err)
end
input = decoded
end

local output
for i = 1, #transformations do
local transformation = transformations[i]
Expand Down
193 changes: 193 additions & 0 deletions kong/db/schema/json.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
---
-- JSON schema validation.
--
--
local _M = {}

local lrucache = require "resty.lrucache"
local jsonschema = require "resty.ljsonschema"
local metaschema = require "resty.ljsonschema.metaschema"
local utils = require "kong.tools.utils"

local cjson = require("cjson").new()
cjson.decode_array_with_array_mt(true)
local cjson_safe = require("cjson.safe").new()
cjson_safe.decode_array_with_array_mt(true)

local type = type
local cjson_encode = cjson.encode
local cjson_decode = cjson.decode
local sha256_hex = utils.sha256_hex


---@class kong.db.schema.json.schema_doc : table
---
---@field id string|nil
---@field ["$id"] string|nil
---@field ["$schema"] string|nil
---@field type string


-- The correct identifier for draft-4 is 'http://json-schema.org/draft-04/schema#'
-- with the the fragment (#) intact. Newer editions use an identifier _without_
-- the fragment (e.g. 'https://json-schema.org/draft/2020-12/schema'), so we
-- will be lenient when comparing these strings.
assert(type(metaschema.id) == "string",
"JSON metaschema .id not defined or not a string")
local DRAFT_4_NO_FRAGMENT = metaschema.id:gsub("#$", "")
local DRAFT_4 = DRAFT_4_NO_FRAGMENT .. "#"


---@type table<string, table>
local schemas = {}


-- Creating a json schema validator is somewhat expensive as it requires
-- generating and evaluating some Lua code, so we memoize this step with
-- a local LRU cache.
local cache = lrucache.new(1000)

local schema_cache_key
do
local cache_keys = setmetatable({}, { __mode = "k" })

---
-- Generate a unique cache key for a schema document.
--
---@param schema kong.db.schema.json.schema_doc
---@return string
function schema_cache_key(schema)
local cache_key = cache_keys[schema]

if not cache_key then
cache_key = "hash://" .. sha256_hex(cjson_encode(schema))
cache_keys[schema] = cache_key
end

return cache_key
end
end


---@param id any
---@return boolean
local function is_draft_4(id)
return id
and type(id) == "string"
and (id == DRAFT_4 or id == DRAFT_4_NO_FRAGMENT)
end


---@param id any
---@return boolean
local function is_non_draft_4(id)
return id
and type(id) == "string"
and (id ~= DRAFT_4 and id ~= DRAFT_4_NO_FRAGMENT)
end


---
-- Validate input according to a JSON schema document.
--
---@param input any
---@param schema kong.db.schema.json.schema_doc
---@return boolean? ok
---@return string? error
local function validate(input, schema)
assert(type(schema) == "table")

-- we are validating a JSON schema document and need to ensure that it is
-- not using supported JSON schema draft/version
if is_draft_4(schema.id or schema["$id"])
and is_non_draft_4(input["$schema"])
then
return nil, "unsupported document $schema: '" .. input["$schema"] ..
"', expected: " .. DRAFT_4
end

local cache_key = schema_cache_key(schema)

local validator = cache:get(cache_key)

if not validator then
validator = assert(jsonschema.generate_validator(schema, {
name = cache_key,
-- lua-resty-ljsonschema's default behavior for detecting an array type
-- is to compare its metatable against `cjson.array_mt`. This is
-- efficient, but we can't assume that all inputs will necessarily
-- conform to this, so we opt to use the heuristic approach instead
-- (determining object/array type based on the table contents).
array_mt = false,
}))
cache:set(cache_key, validator)
end

-- FIXME: waiting on https://github.com/Tieske/lua-resty-ljsonschema/pull/22
-- to be merged and released in order to remove this.
--
-- This re-encodes the input to ensure array-like tables have the proper
-- `cjson.array_mt` metatable. See the above explanation re: array detection.
input = cjson_decode(cjson_encode(input))

return validator(input)
end


---@type table
_M.metaschema = metaschema


_M.validate = validate


---
-- Validate a JSON schema document.
--
-- This is primarily for use in `kong.db.schema.metaschema`
--
---@param input kong.db.schema.json.schema_doc
---@return boolean? ok
---@return string? error
function _M.validate_schema(input)
local typ = type(input)

if typ ~= "table" then
return nil, "schema must be a table"
end

return validate(input, _M.metaschema)
end


---
-- Add a JSON schema document to the local registry.
--
---@param name string
---@param schema kong.db.schema.json.schema_doc
function _M.add_schema(name, schema)
schemas[name] = schema
end


---
-- Retrieve a schema from local storage by name.
--
---@param name string
---@return table|nil schema
function _M.get_schema(name)
return schemas[name]
end


---
-- Remove a schema from local storage by name (if it exists).
--
---@param name string
---@return table|nil schema
function _M.remove_schema(name)
schemas[name] = nil
end


return _M
Loading

0 comments on commit a4e646b

Please sign in to comment.