Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(*): add optional wasm filter config validation #11568

Merged
merged 2 commits into from
Oct 2, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions changelog/unreleased/kong/wasm-filter-config-schemas.yml
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.6",
}
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 @@ -249,6 +249,10 @@ local constants = {

REQUEST_DEBUG_TOKEN_FILE = ".request_debug_token",
REQUEST_DEBUG_LOG_PREFIX = "[request-debug]",

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 string|nil
---@field json_config any|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", }, },
{ config = { type = "string", required = false, }, },
{ enabled = { type = "boolean", default = true, required = true, }, },

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

},
entity_checks = {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason we don't need at_least_one_of for config, json_config?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There can be filters that take no config.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, configuration needs to remain optional, as it's perfectly valid to have a proxy-wasm filter that requires no configuration to function.

Specifying a schema for your filter's json_config using this new my-filter.meta.json method is one means of making it effectively required though.

{ mutually_exclusive = {
"config",
"json_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
181 changes: 181 additions & 0 deletions kong/db/schema/json.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
---
-- 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"

local type = type
local cjson_encode = cjson.encode
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

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