Skip to content

Commit

Permalink
feat(acme): support redis namespace (#10562)
Browse files Browse the repository at this point in the history
* feat(acme): support redis namespace

This PR relies on [fffonion/lua-resty-acme#101](fffonion/lua-resty-acme#101)

`namespace` will be treated as a prefix of key and is default to empty
string `""` for backward compatibility. `namespace` must not be prefixed
with any of the reserverd words.

[KAG-615](https://konghq.atlassian.net/browse/KAG-615)

* add changelog

* fix lint

* fix rockspec

* add changelog and more test cases

* fix lint

* Update CHANGELOG.md

Co-authored-by: Chrono <chrono_cpp@me.com>

---------

Co-authored-by: Chrono <chrono_cpp@me.com>
  • Loading branch information
catbro666 and chronolaw authored Mar 29, 2023
1 parent 5dd9ded commit c36fe3e
Show file tree
Hide file tree
Showing 6 changed files with 268 additions and 4 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,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.
[#10562](https://github.com/Kong/kong/pull/10562)

#### PDK

Expand Down Expand Up @@ -200,6 +203,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",
"lua-resty-session == 4.0.3",
"lua-resty-timer-ng == 0.2.4",
"lpeg == 1.0.2",
Expand Down Expand Up @@ -450,6 +450,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)
if namespace ~= "" then
for _, v in pairs(reserved_words) do
if namespace:sub(1, #v) == v then
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)

1 comment on commit c36fe3e

@khcp-gha-bot
Copy link

Choose a reason for hiding this comment

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

Bazel Build

Docker image available kong/kong:c36fe3e16d29a2adf45e11960502899793b4212a
Artifacts available https://github.com/Kong/kong/actions/runs/4555581251

Please sign in to comment.