376 lines
10 KiB
Lua
376 lines
10 KiB
Lua
local helpers = require "spec.helpers"
|
|
|
|
-- using the full path so that we don't have to modify package.path in
|
|
-- this context
|
|
local test_vault = require "spec.fixtures.custom_vaults.kong.vaults.test"
|
|
|
|
local CUSTOM_VAULTS = "./spec/fixtures/custom_vaults"
|
|
local CUSTOM_PLUGINS = "./spec/fixtures/custom_plugins"
|
|
|
|
local LUA_PATH = CUSTOM_VAULTS .. "/?.lua;" ..
|
|
CUSTOM_VAULTS .. "/?/init.lua;" ..
|
|
CUSTOM_PLUGINS .. "/?.lua;" ..
|
|
CUSTOM_PLUGINS .. "/?/init.lua;;"
|
|
|
|
local DUMMY_HEADER = "Dummy-Plugin"
|
|
local fmt = string.format
|
|
|
|
local json = require "cjson"
|
|
|
|
|
|
|
|
--- A vault test harness is a driver for vault backends, which implements
|
|
--- all the necessary glue for initializing a vault backend and performing
|
|
--- secret read/write operations.
|
|
---
|
|
--- All functions defined here are called as "methods" (e.g. harness:fn()), so
|
|
--- it is permitted to keep state on the harness object (self).
|
|
---
|
|
---@class vault_test_harness
|
|
---
|
|
---@field name string
|
|
---
|
|
--- this table is passed directly to kong.db.vaults:insert()
|
|
---@field config table
|
|
---
|
|
--- create_secret() is called once per test run for a given secret
|
|
---@field create_secret fun(self: vault_test_harness, secret: string, value: string, opts?: table)
|
|
---
|
|
--- update_secret() may be called more than once per test run for a given secret
|
|
---@field update_secret fun(self: vault_test_harness, secret: string, value: string, opts?: table)
|
|
---
|
|
--- setup() is called before kong is started and before any DB entities
|
|
--- have been created and is best used for things like validating backend
|
|
--- credentials and establishing a connection to a backend
|
|
---@field setup fun(self: vault_test_harness)
|
|
---
|
|
--- teardown() is exactly what you'd expect
|
|
---@field teardown fun(self: vault_test_harness)
|
|
---
|
|
--- fixtures() output is passed directly to `helpers.start_kong()`
|
|
---@field fixtures fun(self: vault_test_harness):table|nil
|
|
---
|
|
--- pause() is exactly what you'd expect
|
|
---@field pause fun(self: vault_test_harness)
|
|
---
|
|
---
|
|
---@field prefix string # generated by the test suite
|
|
---@field host string # generated by the test suite
|
|
|
|
|
|
---@type vault_test_harness[]
|
|
local VAULTS = {
|
|
{
|
|
name = "test",
|
|
|
|
config = {
|
|
default_value = "DEFAULT",
|
|
default_value_ttl = 1,
|
|
},
|
|
|
|
create_secret = function(self, _, value)
|
|
-- Currently, create_secret is called _before_ starting Kong.
|
|
--
|
|
-- This means our backend won't be available yet because it is
|
|
-- piggy-backing on Kong as an HTTP mock fixture.
|
|
--
|
|
-- We can, however, inject a default value into our configuration.
|
|
self.config.default_value = value
|
|
end,
|
|
|
|
update_secret = function(_, secret, value, opts)
|
|
return test_vault.client.put(secret, value, opts)
|
|
end,
|
|
|
|
delete_secret = function(_, secret)
|
|
return test_vault.client.delete(secret)
|
|
end,
|
|
|
|
fixtures = function()
|
|
return {
|
|
http_mock = {
|
|
test_vault = test_vault.http_mock,
|
|
}
|
|
}
|
|
end,
|
|
|
|
pause = function(_)
|
|
return test_vault.client.pause()
|
|
end,
|
|
},
|
|
}
|
|
|
|
|
|
local noop = function(...) end
|
|
|
|
for _, vault in ipairs(VAULTS) do
|
|
-- fill out some values that we'll use in route/service/plugin config
|
|
vault.prefix = vault.name .. "-ttl-test"
|
|
vault.host = vault.name .. ".vault-ttl.test"
|
|
|
|
-- ...and fill out non-required methods
|
|
vault.setup = vault.setup or noop
|
|
vault.teardown = vault.teardown or noop
|
|
vault.fixtures = vault.fixtures or noop
|
|
vault.pause = vault.pause or noop
|
|
end
|
|
|
|
|
|
for _, strategy in helpers.each_strategy() do
|
|
for _, vault in ipairs(VAULTS) do
|
|
|
|
|
|
describe("vault resurrect_ttl and rotation (#" .. strategy .. ") #" .. vault.name, function()
|
|
local client
|
|
local secret = "my-secret"
|
|
|
|
|
|
local function http_get(path)
|
|
path = path or "/"
|
|
|
|
local res = client:get(path, {
|
|
headers = {
|
|
host = assert(vault.host),
|
|
},
|
|
})
|
|
|
|
assert.response(res).has.status(200)
|
|
|
|
return res
|
|
end
|
|
|
|
|
|
lazy_setup(function()
|
|
helpers.setenv("KONG_LUA_PATH_OVERRIDE", LUA_PATH)
|
|
helpers.setenv("KONG_VAULT_ROTATION_INTERVAL", "1")
|
|
|
|
vault:setup()
|
|
vault:create_secret(secret, "init")
|
|
|
|
local bp = helpers.get_db_utils(strategy,
|
|
{ "vaults", "routes", "services", "plugins" },
|
|
{ "dummy" },
|
|
{ vault.name })
|
|
|
|
|
|
assert(bp.vaults:insert({
|
|
name = vault.name,
|
|
prefix = vault.prefix,
|
|
config = vault.config,
|
|
}))
|
|
|
|
local route = assert(bp.routes:insert({
|
|
name = vault.host,
|
|
hosts = { vault.host },
|
|
paths = { "/" },
|
|
service = assert(bp.services:insert()),
|
|
}))
|
|
|
|
|
|
-- used by the plugin config test case
|
|
assert(bp.plugins:insert({
|
|
name = "dummy",
|
|
config = {
|
|
resp_header_value = fmt("{vault://%s/%s?ttl=%d&resurrect_ttl=%d}",
|
|
vault.prefix, secret, 2, 2),
|
|
},
|
|
route = { id = route.id },
|
|
}))
|
|
|
|
assert(helpers.start_kong({
|
|
database = strategy,
|
|
nginx_conf = "spec/fixtures/custom_nginx.template",
|
|
vaults = vault.name,
|
|
plugins = "dummy",
|
|
log_level = "info",
|
|
}, nil, nil, vault:fixtures() ))
|
|
|
|
client = helpers.proxy_client()
|
|
end)
|
|
|
|
|
|
lazy_teardown(function()
|
|
if client then
|
|
client:close()
|
|
end
|
|
|
|
helpers.stop_kong()
|
|
vault:teardown()
|
|
|
|
helpers.unsetenv("KONG_LUA_PATH_OVERRIDE")
|
|
end)
|
|
|
|
|
|
it("resurrects plugin config references when secret is deleted (backend: #" .. vault.name .. ")", function()
|
|
local function check_plugin_secret(expect, ttl, leeway)
|
|
leeway = leeway or 0.25 -- 25%
|
|
|
|
local timeout = ttl + (ttl * leeway)
|
|
|
|
assert
|
|
.with_timeout(timeout)
|
|
.with_step(0.5)
|
|
.eventually(function()
|
|
local res = http_get("/")
|
|
local value
|
|
if expect == "" then
|
|
value = res.headers[DUMMY_HEADER] or ""
|
|
if value == "" then
|
|
return true
|
|
end
|
|
|
|
else
|
|
value = assert.response(res).has.header(DUMMY_HEADER)
|
|
if value == expect then
|
|
return true
|
|
end
|
|
end
|
|
|
|
return nil, { expected = expect, got = value }
|
|
end)
|
|
.is_truthy("expected plugin secret to be updated to '" .. tostring(expect) .. "' "
|
|
.. "within " .. tostring(timeout) .. " seconds")
|
|
end
|
|
|
|
vault:update_secret(secret, "old", { ttl = 2, resurrect_ttl = 2 })
|
|
check_plugin_secret("old", 5)
|
|
vault:delete_secret(secret)
|
|
ngx.sleep(2.5)
|
|
check_plugin_secret("old", 5)
|
|
check_plugin_secret("", 5)
|
|
end)
|
|
end)
|
|
|
|
|
|
describe("#multiworker vault resurrect_ttl and rotation (#" .. strategy .. ") #" .. vault.name, function()
|
|
local client, admin_client
|
|
local secret = "my-secret"
|
|
|
|
lazy_setup(function()
|
|
helpers.setenv("KONG_LUA_PATH_OVERRIDE", LUA_PATH)
|
|
helpers.setenv("KONG_VAULT_ROTATION_INTERVAL", "1")
|
|
|
|
vault:setup()
|
|
vault:create_secret(secret, "init")
|
|
|
|
local bp = helpers.get_db_utils(strategy,
|
|
{ "vaults", "routes", "services", "plugins" },
|
|
{ "dummy" },
|
|
{ vault.name })
|
|
|
|
|
|
assert(bp.vaults:insert({
|
|
name = vault.name,
|
|
prefix = vault.prefix,
|
|
config = vault.config,
|
|
}))
|
|
|
|
local route = assert(bp.routes:insert({
|
|
name = vault.host,
|
|
hosts = { vault.host },
|
|
paths = { "/" },
|
|
service = assert(bp.services:insert()),
|
|
}))
|
|
|
|
|
|
assert(bp.plugins:insert({
|
|
name = "post-function",
|
|
config = {
|
|
access = {fmt([[
|
|
local value, err = kong.vault.get("{vault://%s/%s?ttl=%d&resurrect_ttl=%d}")
|
|
if value then
|
|
kong.response.exit(200, {["value"]=value, ["pid"]=ngx.worker.pid()}, {["Content-Type"]="application/json"})
|
|
end
|
|
]], vault.prefix, secret, 2, 5),}
|
|
},
|
|
route = { id = route.id },
|
|
}))
|
|
|
|
assert(helpers.start_kong({
|
|
database = strategy,
|
|
nginx_conf = "spec/fixtures/custom_nginx.template",
|
|
vaults = vault.name,
|
|
plugins = "post-function",
|
|
log_level = "debug",
|
|
dedicated_config_processing = false,
|
|
-- nginx_worker_processes = 2,
|
|
nginx_main_worker_processes = 2,
|
|
}, nil, nil, vault:fixtures() ))
|
|
|
|
client = helpers.proxy_client()
|
|
admin_client = helpers.admin_client()
|
|
end)
|
|
|
|
|
|
lazy_teardown(function()
|
|
if client then
|
|
client:close()
|
|
end
|
|
|
|
if admin_client then
|
|
admin_client:close()
|
|
end
|
|
|
|
helpers.stop_kong()
|
|
vault:teardown()
|
|
|
|
helpers.unsetenv("KONG_LUA_PATH_OVERRIDE")
|
|
end)
|
|
|
|
|
|
it("resurrects secret value from shared dict when secret is deleted (backend: #" .. vault.name .. ")", function()
|
|
-- fetch all worker pids
|
|
local status_ret = admin_client:get("/")
|
|
local body = assert.res_status(200, status_ret)
|
|
local json_body = json.decode(body)
|
|
assert.truthy(json_body)
|
|
local worker_pids = json_body.pids.workers
|
|
assert.truthy(#worker_pids == 2)
|
|
|
|
local worker_secret_hits = {}
|
|
for _, worker_pid in ipairs(worker_pids) do
|
|
worker_secret_hits[tostring(worker_pid)] = false
|
|
end
|
|
|
|
vault:update_secret(secret, "old", { ttl = 2, resurrect_ttl = 5 })
|
|
|
|
-- trigger post-function in one of the workers
|
|
local res = client:get("/", {headers = {host = assert(vault.host)}})
|
|
local body = assert.res_status(200, res)
|
|
local json_body = json.decode(body)
|
|
assert.same("old", json_body.value)
|
|
worker_secret_hits[tostring(json_body.pid)] = true
|
|
|
|
vault:pause()
|
|
|
|
-- let ttl pass and try to trigger post-function in all workers
|
|
-- check all of them can resurrect the secret from shared dict
|
|
ngx.sleep(3)
|
|
|
|
assert.with_timeout(5).with_step(0.1).eventually(
|
|
function()
|
|
-- avoid connection reuse so that we can hit all workers
|
|
local new_client = helpers.proxy_client()
|
|
local res = new_client:get("/", {headers = {host = assert(vault.host)}})
|
|
local body = assert.res_status(200, res)
|
|
local json_body = json.decode(body)
|
|
new_client:close()
|
|
assert.same("old", json_body.value)
|
|
worker_secret_hits[tostring(json_body.pid)] = true
|
|
|
|
for k, v in pairs(worker_secret_hits) do
|
|
if not v then
|
|
return false, "worker pid " .. k .. " did not hit the secret"
|
|
end
|
|
end
|
|
|
|
return true
|
|
end
|
|
).is_truthy("expected all workers to resurrect the secret from shared dict")
|
|
end)
|
|
end)
|
|
|
|
|
|
end -- each vault backend
|
|
end -- each strategy
|