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(acme): support redis namespace #10562

Merged
merged 7 commits into from
Mar 29, 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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,9 @@
[#10533](https://github.com/Kong/kong/pull/10533)
- **Zipkin&Opentelemetry**: convert traceid in http response headers to hex format
[#10534](https://github.com/Kong/kong/pull/10534)
- **ACME**: acme plugin now supports configuring `namespace` for redis storage
which is default to empty string for backward compatibility.
catbro666 marked this conversation as resolved.
Show resolved Hide resolved
[#10562](https://github.com/Kong/kong/pull/10562)

#### PDK

Expand Down Expand Up @@ -184,6 +187,8 @@
[#10547](https://github.com/Kong/kong/pull/10547)
- Bumped LuaSec from 1.2.0 to 1.3.1
[#10528](https://github.com/Kong/kong/pull/10528)
- Bumped lua-resty-acme from 0.10.1 to 0.11.0
[#10562](https://github.com/Kong/kong/pull/10562)

## 3.2.0

Expand Down
3 changes: 2 additions & 1 deletion kong-3.2.1-0.rockspec
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ dependencies = {
"lua-resty-openssl == 0.8.20",
"lua-resty-counter == 0.2.1",
"lua-resty-ipmatcher == 0.6.1",
"lua-resty-acme == 0.10.1",
"lua-resty-acme == 0.11.0",
catbro666 marked this conversation as resolved.
Show resolved Hide resolved
"lua-resty-session == 4.0.3",
"lua-resty-timer-ng == 0.2.4",
"lpeg == 1.0.2",
Expand Down Expand Up @@ -446,6 +446,7 @@ build = {
["kong.plugins.acme.migrations"] = "kong/plugins/acme/migrations/init.lua",
["kong.plugins.acme.schema"] = "kong/plugins/acme/schema.lua",
["kong.plugins.acme.storage.kong"] = "kong/plugins/acme/storage/kong.lua",
["kong.plugins.acme.reserved_words"] = "kong/plugins/acme/reserved_words.lua",

["kong.plugins.prometheus.api"] = "kong/plugins/prometheus/api.lua",
["kong.plugins.prometheus.status_api"] = "kong/plugins/prometheus/status_api.lua",
Expand Down
7 changes: 4 additions & 3 deletions kong/plugins/acme/client.lua
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
local acme = require "resty.acme.client"
local util = require "resty.acme.util"
local x509 = require "resty.openssl.x509"
local reserved_words = require "kong.plugins.acme.reserved_words"

local cjson = require "cjson"
local ngx_ssl = require "ngx.ssl"
Expand All @@ -21,9 +22,9 @@ local dbless = kong.configuration.database == "off"
local hybrid_mode = kong.configuration.role == "control_plane" or
kong.configuration.role == "data_plane"

local RENEW_KEY_PREFIX = "kong_acme:renew_config:"
local RENEW_LAST_RUN_KEY = "kong_acme:renew_last_run"
local CERTKEY_KEY_PREFIX = "kong_acme:cert_key:"
local RENEW_KEY_PREFIX = reserved_words.RENEW_KEY_PREFIX
local RENEW_LAST_RUN_KEY = reserved_words.RENEW_LAST_RUN_KEY
local CERTKEY_KEY_PREFIX = reserved_words.CERTKEY_KEY_PREFIX

local DAY_SECONDS = 86400 -- one day in seconds

Expand Down
7 changes: 7 additions & 0 deletions kong/plugins/acme/reserved_words.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
local reserved_words = {
RENEW_KEY_PREFIX = "kong_acme:renew_config:",
RENEW_LAST_RUN_KEY = "kong_acme:renew_last_run",
CERTKEY_KEY_PREFIX = "kong_acme:cert_key:",
}

return reserved_words
15 changes: 15 additions & 0 deletions kong/plugins/acme/schema.lua
Original file line number Diff line number Diff line change
@@ -1,11 +1,24 @@
local typedefs = require "kong.db.schema.typedefs"
local reserved_words = require "kong.plugins.acme.reserved_words"

local CERT_TYPES = { "rsa", "ecc" }

local RSA_KEY_SIZES = { 2048, 3072, 4096 }

local STORAGE_TYPES = { "kong", "shm", "redis", "consul", "vault" }

local function validate_namespace(namespace)
Copy link
Contributor

@fffonion fffonion Mar 27, 2023

Choose a reason for hiding this comment

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

let's make sure to communicate with koko team on this constraint to ensure it doesn't get missed

Copy link
Contributor Author

Choose a reason for hiding this comment

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

OK, will inform the koko team on the JIRA ticket.

if namespace ~= "" then
for _, v in pairs(reserved_words) do
if namespace:sub(1, #v) == v then
chronolaw marked this conversation as resolved.
Show resolved Hide resolved
return nil, "namespace can't be prefixed with reserved word: " .. v
end
end
end

return true
end

local SHM_STORAGE_SCHEMA = {
{ shm_name = {
type = "string",
Expand All @@ -26,6 +39,8 @@ local REDIS_STORAGE_SCHEMA = {
{ ssl = { type = "boolean", required = true, default = false } },
{ ssl_verify = { type = "boolean", required = true, default = false } },
{ ssl_server_name = typedefs.sni { required = false } },
{ namespace = { type = "string", required = true, default = "", len_min = 0,
custom_validator = validate_namespace } },
}

local CONSUL_STORAGE_SCHEMA = {
Expand Down
235 changes: 235 additions & 0 deletions spec/03-plugins/29-acme/05-redis_storage_spec.lua
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
local redis_storage = require("resty.acme.storage.redis")
local reserved_words = require "kong.plugins.acme.reserved_words"

local helpers = require "spec.helpers"

Expand All @@ -22,4 +23,238 @@ describe("Plugin: acme (storage.redis)", function()
assert.is_nil(err)
assert.equal("bar", value)
end)

describe("redis namespace", function()
local config = {
host = helpers.redis_host,
port = helpers.redis_port,
database = 0,
namespace = "test",
}
local storage, err = redis_storage.new(config)
assert.is_nil(err)
assert.not_nil(storage)

it("redis namespace set", function()
local err = storage:set("key1", "1", 10)
assert.is_nil(err)
local err = storage:set("key1", "new value", 10)
assert.is_nil(err)
end)

it("redis namespace get", function()
local err = storage:set("key2", "2", 10)
assert.is_nil(err)
local value, err = storage:get("key2")
assert.is_nil(err)
assert.equal("2", value)
end)

it("redis namespace delete", function()
local err = storage:set("key3", "3", 10)
assert.is_nil(err)
local value, err = storage:get("key3")
assert.is_nil(err)
assert.equal("3", value)
local err = storage:delete("key3")
assert.is_nil(err)

-- now the key should be deleted
local value, err = storage:get("key3")
assert.is_nil(err)
assert.is_nil(value)

-- delete again with no error
local err = storage:delete("key3")
assert.is_nil(err)
end)

it("redis namespace add", function()
local err = storage:set("key4", "4", 10)
assert.is_nil(err)
local err = storage:add("key4", "5", 10)
assert.equal("exists", err)
local value, err = storage:get("key4")
assert.is_nil(err)
assert.equal("4", value)

local err = storage:add("key5", "5", 10)
assert.is_nil(err)
local value, err = storage:get("key5")
assert.is_nil(err)
assert.equal("5", value)
end)

it("redis namespace list", function()
local err = storage:set("prefix1", "bb--", 10)
assert.is_nil(err)
local err = storage:set("pref-x2", "aa--", 10)
assert.is_nil(err)
local err = storage:set("prefix3", "aa--", 10)
assert.is_nil(err)
local keys, err = storage:list("prefix")
assert.is_nil(err)
assert.is_table(keys)
assert.equal(2, #keys)
table.sort(keys)
assert.equal("prefix1", keys[1])
assert.equal("prefix3", keys[2])

local keys, err = storage:list("nonexistent")
assert.is_nil(err)
assert.is_table(keys)
assert.equal(0, #keys)
end)

it("redis namespace isolation", function()
local config0 = {
host = helpers.redis_host,
port = helpers.redis_port,
database = 0,
}
local config1 = {
host = helpers.redis_host,
port = helpers.redis_port,
database = 0,
namespace = "namespace1",
}
local config2 = {
host = helpers.redis_host,
port = helpers.redis_port,
database = 0,
namespace = "namespace2",
}
local storage0, err = redis_storage.new(config0)
assert.is_nil(err)
assert.not_nil(storage0)
local storage1, err = redis_storage.new(config1)
assert.is_nil(err)
assert.not_nil(storage1)
local storage2, err = redis_storage.new(config2)
assert.is_nil(err)
assert.not_nil(storage2)
local err = storage0:set("isolation", "0", 10)
assert.is_nil(err)
local value, err = storage0:get("isolation")
assert.is_nil(err)
assert.equal("0", value)
local value, err = storage1:get("isolation")
assert.is_nil(err)
assert.is_nil(value)
local value, err = storage2:get("isolation")
assert.is_nil(err)
assert.is_nil(value)

local err = storage1:set("isolation", "1", 10)
assert.is_nil(err)
local value, err = storage0:get("isolation")
assert.is_nil(err)
assert.equal("0", value)
local value, err = storage1:get("isolation")
assert.is_nil(err)
assert.equal("1", value)
local value, err = storage2:get("isolation")
assert.is_nil(err)
assert.is_nil(value)

local err = storage2:set("isolation", "2", 10)
assert.is_nil(err)
local value, err = storage0:get("isolation")
assert.is_nil(err)
assert.equal("0", value)
local value, err = storage1:get("isolation")
assert.is_nil(err)
assert.equal("1", value)
local value, err = storage2:get("isolation")
assert.is_nil(err)
assert.equal("2", value)
end)

-- irrelevant to db, just test one
describe("validate redis namespace #postgres", function()
local client
local strategy = "postgres"

lazy_setup(function()
helpers.get_db_utils(strategy, {
"plugins",
}, {
"acme"
})

assert(helpers.start_kong({
database = strategy,
}))
end)

lazy_teardown(function()
helpers.stop_kong(nil, true)
end)

before_each(function()
client = helpers.admin_client()
end)

after_each(function()
if client then
client:close()
end
end)

it("successfully create acme plugin with valid namespace", function()
local res = assert(client:send {
method = "POST",
path = "/plugins",
headers = { ["Content-Type"] = "application/json" },
body = {
name = "acme",
config = {
account_email = "test@test.com",
api_uri = "https://api.acme.org",
storage = "redis",
preferred_chain = "test",
storage_config = {
redis = {
host = helpers.redis_host,
port = helpers.redis_port,
auth = "null",
namespace = "namespace1:",
},
},
},
},
})
assert.res_status(201, res)
end)

it("fail to create acme plugin with invalid namespace", function()
for _, v in pairs(reserved_words) do
local res = assert(client:send {
method = "POST",
path = "/plugins",
headers = { ["Content-Type"] = "application/json" },
body = {
name = "acme",
config = {
account_email = "test@test.com",
api_uri = "https://api.acme.org",
storage = "redis",
preferred_chain = "test",
storage_config = {
redis = {
host = helpers.redis_host,
port = helpers.redis_port,
auth = "null",
namespace = v,
},
},
},
},
})
local body = assert.res_status(400, res)
assert.matches("namespace can't be prefixed with reserved word: " .. v, body)
end
end)
end)
end)
end)