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

Revert "Revert "feat(plugins): standard-webhooks"" #12962

Merged
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
4 changes: 4 additions & 0 deletions .github/labeler.yml
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,10 @@ plugins/opentelemetry:
- changed-files:
- any-glob-to-any-file: kong/plugins/opentelemetry/**/*

plugins/standard-webhooks:
- changed-files:
- any-glob-to-any-file: kong/plugins/standard-webhooks/**/*

schema-change-noteworthy:
- changed-files:
- any-glob-to-any-file: ['kong/db/schema/**/*.lua', 'kong/**/schema.lua', 'kong/plugins/**/daos.lua', 'plugins-ee/**/daos.lua', 'plugins-ee/**/schema.lua', 'kong/db/dao/*.lua', 'kong/enterprise_edition/redis/init.lua']
Expand Down
4 changes: 4 additions & 0 deletions changelog/unreleased/kong/plugins-add-standard-webhooks.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
message: |
Add standard webhooks plugin
type: feature
scope: Plugin
4 changes: 4 additions & 0 deletions kong-3.7.0-0.rockspec
Original file line number Diff line number Diff line change
Expand Up @@ -609,6 +609,10 @@ build = {
["kong.plugins.ai-prompt-guard.handler"] = "kong/plugins/ai-prompt-guard/handler.lua",
["kong.plugins.ai-prompt-guard.schema"] = "kong/plugins/ai-prompt-guard/schema.lua",

["kong.plugins.standard-webhooks.handler"] = "kong/plugins/standard-webhooks/handler.lua",
["kong.plugins.standard-webhooks.internal"] = "kong/plugins/standard-webhooks/internal.lua",
["kong.plugins.standard-webhooks.schema"] = "kong/plugins/standard-webhooks/schema.lua",

["kong.vaults.env"] = "kong/vaults/env/init.lua",
["kong.vaults.env.schema"] = "kong/vaults/env/schema.lua",

Expand Down
1 change: 1 addition & 0 deletions kong/constants.lua
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ local plugins = {
"ai-prompt-guard",
"ai-request-transformer",
"ai-response-transformer",
"standard-webhooks",
}

local plugin_map = {}
Expand Down
12 changes: 12 additions & 0 deletions kong/plugins/standard-webhooks/handler.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
local plugin = require "kong.plugins.standard-webhooks.internal"

local StandardWebhooks = {
VERSION = require("kong.meta").version,
PRIORITY = 760
}

function StandardWebhooks:access(conf)
plugin.access(conf)
end

return StandardWebhooks
78 changes: 78 additions & 0 deletions kong/plugins/standard-webhooks/internal.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
local kong = kong
local mac = require "resty.openssl.mac"
local tonumber = tonumber
local ngx = ngx
local type = type

local HEADER_WEBHOOK_ID = "webhook-id"
local HEADER_WEBHOOK_SIGN = "webhook-signature"
local HEADER_WEBHOOK_TS = "webhook-timestamp"

local function getHeader(input)
if type(input) == "table" then
return input[1]
else
return input
end
zekth marked this conversation as resolved.
Show resolved Hide resolved
end

local function sign(secret, id, ts, payload)
local d, err = mac.new(secret, "HMAC", nil, "sha256")
if err then
kong.log.error(err)
return kong.response.error(500)
end
local r, err = d:final(id .. "." .. ts .. "." .. payload)
if err then
kong.log.error(err)
return kong.response.error(500)
end
return "v1," .. ngx.encode_base64(r)
end

local function extract_webhook()
local headers = kong.request.get_headers()

local id = getHeader(headers[HEADER_WEBHOOK_ID])
local signature = getHeader(headers[HEADER_WEBHOOK_SIGN])
local ts = getHeader(headers[HEADER_WEBHOOK_TS])
if not id or not signature or not ts then
kong.log.debug("missing required headers")
return kong.response.error(400)
end

ts = tonumber(ts) or 0 -- if parse fails we inject 0, which will fail on clock-skew check
samugi marked this conversation as resolved.
Show resolved Hide resolved

return id, signature, ts
end


local function access(config)
local id, signature, ts = extract_webhook()

if ngx.now() - ts > config.tolerance_second then
kong.log.debug("timestamp tolerance exceeded")
return kong.response.error(400)
end

local body = kong.request.get_raw_body()

if not body or body == "" then
kong.log.debug("missing required body")
return kong.response.error(400)
end

local expected_signature = sign(config.secret_v1, id, ts, body)

if signature == expected_signature then
return
end

kong.log.debug("signature not matched")
return kong.response.error(400)
zekth marked this conversation as resolved.
Show resolved Hide resolved
end

return {
access = access,
sign = sign
}
38 changes: 38 additions & 0 deletions kong/plugins/standard-webhooks/schema.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
local typedefs = require "kong.db.schema.typedefs"

local PLUGIN_NAME = "standard-webhooks"

local schema = {
name = PLUGIN_NAME,
fields = {
{ consumer = typedefs.no_consumer },
{ protocols = typedefs.protocols_http },
{
config = {
type = "record",
fields = {
{
secret_v1 = {
type = "string",
required = true,
description = "Webhook secret",
referenceable = true,
encrypted = true,
},
},
{
tolerance_second = {
description = "Tolerance of the webhook timestamp in seconds. If the webhook timestamp is older than this number of seconds, it will be rejected with a '400' response.",
type = "integer",
required = true,
gt = -1,
default = 5 * 60
}
}
}
}
}
}
}

return schema
1 change: 1 addition & 0 deletions spec/01-unit/12-plugins_order_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ describe("Plugins", function()
"ai-prompt-guard",
"ai-proxy",
"ai-response-transformer",
"standard-webhooks",
"aws-lambda",
"azure-functions",
"proxy-cache",
Expand Down
140 changes: 140 additions & 0 deletions spec/03-plugins/44-standard-webhooks/01-unit_spec.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
local PLUGIN_NAME = "standard-webhooks"


-- helper function to validate data against a schema
local validate do
local validate_entity = require("spec.helpers").validate_plugin_config_schema
local plugin_schema = require("kong.plugins."..PLUGIN_NAME..".schema")

function validate(data)
return validate_entity(data, plugin_schema)
end
end


describe(PLUGIN_NAME .. ": (schema)", function()


it("accepts a valid config", function()
local ok, err = validate({
secret_v1 = "abc123",
tolerance_second = 5*60,
})
assert.is_nil(err)
assert.is_truthy(ok)
end)


describe("secret", function()

it("must be set", function()
local ok, err = validate({
secret_v1 = nil,
tolerance_second = 5*60,
})

assert.is_same({
["config"] = {
["secret_v1"] = 'required field missing',
}
}, err)
assert.is_falsy(ok)
end)


it("is not nullable", function()
local ok, err = validate({
secret_v1 = assert(ngx.null),
tolerance_second = 5*60,
})

assert.is_same({
["config"] = {
["secret_v1"] = 'required field missing',
}
}, err)
assert.is_falsy(ok)
end)


it("must be a string", function()
local ok, err = validate({
secret_v1 = 123,
tolerance_second = 5*60,
})

assert.is_same({
["config"] = {
["secret_v1"] = 'expected a string',
}
}, err)
assert.is_falsy(ok)
end)

end)



describe("tolerance_second", function()

it("gets a default", function()
local ok, err = validate({
secret_v1 = "abc123",
tolerance_second = nil,
})

assert.is_nil(err)
assert.are.same(ok.config, {
secret_v1 = "abc123",
tolerance_second = 5*60,
})
end)


it("is not nullable", function()
local ok, err = validate({
secret_v1 = "abc123",
tolerance_second = assert(ngx.null),
})

assert.is_same({
["config"] = {
["tolerance_second"] = 'required field missing',
}
}, err)
assert.is_falsy(ok)
end)


it("must be an integer", function()
local ok, err = validate({
secret_v1 = "abc123",
tolerance_second = 5.67,
})

assert.is_same({
["config"] = {
["tolerance_second"] = 'expected an integer',
}
}, err)
assert.is_falsy(ok)
end)


it("must be >= 0", function()
local ok, err = validate({
secret_v1 = "abc123",
tolerance_second = -1,
})

assert.is_same({
["config"] = {
["tolerance_second"] = 'value must be greater than -1',
}
}, err)
assert.is_falsy(ok)
end)

end)

end)
Loading
Loading