You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@apisix.apache.org by sp...@apache.org on 2022/11/29 07:47:22 UTC
[apisix] branch master updated: feat(admin): add kms admin api (#8394)
This is an automated email from the ASF dual-hosted git repository.
spacewander pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/apisix.git
The following commit(s) were added to refs/heads/master by this push:
new de069aaaa feat(admin): add kms admin api (#8394)
de069aaaa is described below
commit de069aaaae45b623e8cf60e864f6b0f7c2d64fa6
Author: jinhua luo <ho...@163.com>
AuthorDate: Tue Nov 29 15:47:17 2022 +0800
feat(admin): add kms admin api (#8394)
---
apisix/admin/init.lua | 1 +
apisix/admin/kms.lua | 203 +++++++++++++++++++++++++++++++++++++++++
apisix/constants.lua | 1 +
apisix/schema_def.lua | 15 +++
docs/en/latest/admin-api.md | 59 ++++++++++++
t/admin/kms.t | 217 ++++++++++++++++++++++++++++++++++++++++++++
6 files changed, 496 insertions(+)
diff --git a/apisix/admin/init.lua b/apisix/admin/init.lua
index 5827f8397..02c8cd9d6 100644
--- a/apisix/admin/init.lua
+++ b/apisix/admin/init.lua
@@ -55,6 +55,7 @@ local resources = {
plugin_metadata = require("apisix.admin.plugin_metadata"),
plugin_configs = require("apisix.admin.plugin_config"),
consumer_groups = require("apisix.admin.consumer_group"),
+ kms = require("apisix.admin.kms"),
}
diff --git a/apisix/admin/kms.lua b/apisix/admin/kms.lua
new file mode 100644
index 000000000..895b60421
--- /dev/null
+++ b/apisix/admin/kms.lua
@@ -0,0 +1,203 @@
+--
+-- Licensed to the Apache Software Foundation (ASF) under one or more
+-- contributor license agreements. See the NOTICE file distributed with
+-- this work for additional information regarding copyright ownership.
+-- The ASF licenses this file to You under the Apache License, Version 2.0
+-- (the "License"); you may not use this file except in compliance with
+-- the License. You may obtain a copy of the License at
+--
+-- http://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing, software
+-- distributed under the License is distributed on an "AS IS" BASIS,
+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+-- See the License for the specific language governing permissions and
+-- limitations under the License.
+--
+local core = require("apisix.core")
+local utils = require("apisix.admin.utils")
+local type = type
+local tostring = tostring
+
+
+local _M = {
+ need_v3_filter = true,
+}
+
+
+local function check_conf(id, conf, need_id, typ)
+ if not conf then
+ return nil, {error_msg = "missing configurations"}
+ end
+
+ id = id or conf.id
+ if need_id and not id then
+ return nil, {error_msg = "missing id"}
+ end
+
+ if not need_id and id then
+ return nil, {error_msg = "wrong id, do not need it"}
+ end
+
+ if need_id and conf.id and tostring(conf.id) ~= tostring(id) then
+ return nil, {error_msg = "wrong id"}
+ end
+
+ conf.id = id
+
+ core.log.info("conf: ", core.json.delay_encode(conf))
+ local ok, err = core.schema.check(core.schema["kms_" .. typ], conf)
+ if not ok then
+ return nil, {error_msg = "invalid configuration: " .. err}
+ end
+
+ return true
+end
+
+
+local function split_typ_and_id(id, sub_path)
+ local uri_segs = core.utils.split_uri(sub_path)
+ local typ = id
+ local id = nil
+ if #uri_segs > 0 then
+ id = uri_segs[1]
+ end
+ return typ, id
+end
+
+
+function _M.put(id, conf, sub_path)
+ local typ, id = split_typ_and_id(id, sub_path)
+ if not id then
+ return 400, {error_msg = "no kms id in uri"}
+ end
+
+ local ok, err = check_conf(typ .. "/" .. id, conf, true, typ)
+ if not ok then
+ return 400, err
+ end
+
+ local key = "/kms/" .. typ .. "/" .. id
+
+ local ok, err = utils.inject_conf_with_prev_conf("kms", key, conf)
+ if not ok then
+ return 503, {error_msg = err}
+ end
+
+ local res, err = core.etcd.set(key, conf)
+ if not res then
+ core.log.error("failed to put kms [", key, "]: ", err)
+ return 503, {error_msg = err}
+ end
+
+ return res.status, res.body
+end
+
+
+function _M.get(id, conf, sub_path)
+ local typ, id = split_typ_and_id(id, sub_path)
+
+ local key = "/kms/"
+ if id then
+ key = key .. typ
+ key = key .. "/" .. id
+ end
+
+ local res, err = core.etcd.get(key, not id)
+ if not res then
+ core.log.error("failed to get kms [", key, "]: ", err)
+ return 503, {error_msg = err}
+ end
+
+ utils.fix_count(res.body, id)
+ return res.status, res.body
+end
+
+
+function _M.delete(id, conf, sub_path)
+ local typ, id = split_typ_and_id(id, sub_path)
+ if not id then
+ return 400, {error_msg = "no kms id in uri"}
+ end
+
+ local key = "/kms/" .. typ .. "/" .. id
+
+ local res, err = core.etcd.delete(key)
+ if not res then
+ core.log.error("failed to delete kms [", key, "]: ", err)
+ return 503, {error_msg = err}
+ end
+
+ return res.status, res.body
+end
+
+
+function _M.patch(id, conf, sub_path)
+ local uri_segs = core.utils.split_uri(sub_path)
+ if #uri_segs < 2 then
+ return 400, {error_msg = "no kms id and/or sub path in uri"}
+ end
+ local typ = id
+ id = uri_segs[1]
+ sub_path = core.table.concat(uri_segs, "/", 2)
+
+ if not id then
+ return 400, {error_msg = "missing kms id"}
+ end
+
+ if not conf then
+ return 400, {error_msg = "missing new configuration"}
+ end
+
+ if not sub_path or sub_path == "" then
+ if type(conf) ~= "table" then
+ return 400, {error_msg = "invalid configuration"}
+ end
+ end
+
+ local key = "/kms/" .. typ .. "/" .. id
+ local res_old, err = core.etcd.get(key)
+ if not res_old then
+ core.log.error("failed to get kms [", key, "]: ", err)
+ return 503, {error_msg = err}
+ end
+
+ if res_old.status ~= 200 then
+ return res_old.status, res_old.body
+ end
+ core.log.info("key: ", key, " old value: ",
+ core.json.delay_encode(res_old, true))
+
+ local node_value = res_old.body.node.value
+ local modified_index = res_old.body.node.modifiedIndex
+
+ if sub_path and sub_path ~= "" then
+ local code, err, node_val = core.table.patch(node_value, sub_path, conf)
+ node_value = node_val
+ if code then
+ return code, err
+ end
+ utils.inject_timestamp(node_value, nil, true)
+ else
+ node_value = core.table.merge(node_value, conf)
+ utils.inject_timestamp(node_value, nil, conf)
+ end
+
+ core.log.info("new conf: ", core.json.delay_encode(node_value, true))
+
+ local ok, err = check_conf(typ .. "/" .. id, node_value, true, typ)
+ if not ok then
+ return 400, {error_msg = err}
+ end
+
+ local res, err = core.etcd.atomic_set(key, node_value, nil, modified_index)
+ if not res then
+ core.log.error("failed to set new kms[", key, "]: ", err)
+ return 503, {error_msg = err}
+ end
+
+ return res.status, res.body
+end
+
+
+return _M
diff --git a/apisix/constants.lua b/apisix/constants.lua
index 9f9b62fc3..40ab71a2b 100644
--- a/apisix/constants.lua
+++ b/apisix/constants.lua
@@ -33,6 +33,7 @@ return {
["/protos"] = true,
["/plugin_configs"] = true,
["/consumer_groups"] = true,
+ ["/kms"] = true,
},
STREAM_ETCD_DIRECTORY = {
["/upstreams"] = true,
diff --git a/apisix/schema_def.lua b/apisix/schema_def.lua
index f7b117af9..0bdb56ecd 100644
--- a/apisix/schema_def.lua
+++ b/apisix/schema_def.lua
@@ -692,6 +692,21 @@ _M.service = {
}
+_M.kms_vault = {
+ type = "object",
+ properties = {
+ uri = _M.uri_def,
+ prefix = {
+ type = "string",
+ },
+ token = {
+ type = "string",
+ },
+ },
+ required = {"uri", "prefix", "token"},
+}
+
+
_M.consumer = {
type = "object",
properties = {
diff --git a/docs/en/latest/admin-api.md b/docs/en/latest/admin-api.md
index 22f5e2819..63d8b8114 100644
--- a/docs/en/latest/admin-api.md
+++ b/docs/en/latest/admin-api.md
@@ -117,6 +117,7 @@ Resources that support paging queries:
- SSL
- Stream Route
- Upstream
+- kms
### Support filtering query
@@ -1119,3 +1120,61 @@ Route used in the [Stream Proxy](./stream-proxy.md).
To learn more about filtering in stream proxies, check [this](./stream-proxy.md#more-route-match-options) document.
[Back to TOC](#table-of-contents)
+
+## kms
+
+**API**: /apisix/admin/kms/{secretmanager}/{id}
+
+kms means `Secrets Management`, which could use any secret manager supported, e.g. `vault`.
+
+### Request Methods
+
+| Method | Request URI | Request Body | Description |
+| ------ | ---------------------------------- | ------------ | ------------------------------------------------- |
+| GET | /apisix/admin/kms | NULL | Fetches a list of all kms. |
+| GET | /apisix/admin/kms/{secretmanager}/{id} | NULL | Fetches specified kms by id. |
+| PUT | /apisix/admin/kms/{secretmanager} | {...} | Create new kms configuration. |
+| DELETE | /apisix/admin/kms/{secretmanager}/{id} | NULL | Removes the kms with the specified id. |
+| PATCH | /apisix/admin/kms/{secretmanager}/{id} | {...} | Updates the selected attributes of the specified, existing kms. To delete an attribute, set value of attribute set to null. |
+| PATCH | /apisix/admin/kms/{secretmanager}/{id}/{path} | {...} | Updates the attribute specified in the path. The values of other attributes remain unchanged. |
+
+### Request Body Parameters
+
+When `{secretmanager}` is `vault`:
+
+| Parameter | Required | Type | Description | Example |
+| ----------- | -------- | ----------- | ------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------ |
+| uri | True | URI | URI of the vault server. | |
+| prefix | True | string | key prefix
+| token | True | string | vault token. | |
+
+Example Configuration:
+
+```shell
+{
+ "uri": "https://localhost/vault",
+ "prefix": "/apisix/kv",
+ "token": "343effad"
+}
+```
+
+Example API usage:
+
+```shell
+$ curl -i http://127.0.0.1:9180/apisix/admin/kms/vault/test2 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d '
+{
+ "uri": "http://xxx/get",
+ "prefix" : "apisix",
+ "token" : "apisix"
+}'
+HTTP/1.1 200 OK
+...
+
+{"key":"\/apisix\/kms\/vault\/test2","value":{"id":"vault\/test2","token":"apisix","prefix":"apisix","update_time":1669625828,"create_time":1669625828,"uri":"http:\/\/xxx\/get"}}
+```
+
+### Response Parameters
+
+Currently, the response is returned from etcd.
+
+[Back to TOC](#table-of-contents)
diff --git a/t/admin/kms.t b/t/admin/kms.t
new file mode 100644
index 000000000..8290f77f5
--- /dev/null
+++ b/t/admin/kms.t
@@ -0,0 +1,217 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+use t::APISIX 'no_plan';
+
+repeat_each(1);
+no_long_string();
+no_root_location();
+no_shuffle();
+log_level("info");
+
+add_block_preprocessor(sub {
+ my ($block) = @_;
+
+ if (!$block->request) {
+ $block->set_value("request", "GET /t");
+ }
+});
+
+run_tests;
+
+__DATA__
+
+=== TEST 1: PUT
+--- config
+ location /t {
+ content_by_lua_block {
+ local t = require("lib.test_admin").test
+ local etcd = require("apisix.core.etcd")
+ local code, body = t('/apisix/admin/kms/vault/test1',
+ ngx.HTTP_PUT,
+ [[{
+ "uri": "http://127.0.0.1:12800/get",
+ "prefix" : "apisix",
+ "token" : "apisix"
+ }]],
+ [[{
+ "value": {
+ "uri": "http://127.0.0.1:12800/get",
+ "prefix" : "apisix",
+ "token" : "apisix"
+ },
+ "key": "/apisix/kms/vault/test1"
+ }]]
+ )
+
+ ngx.status = code
+ ngx.say(body)
+
+ local res = assert(etcd.get('/kms/vault/test1'))
+ local create_time = res.body.node.value.create_time
+ assert(create_time ~= nil, "create_time is nil")
+ local update_time = res.body.node.value.update_time
+ assert(update_time ~= nil, "update_time is nil")
+ }
+ }
+--- response_body
+passed
+
+
+
+=== TEST 2: GET
+--- config
+ location /t {
+ content_by_lua_block {
+ local t = require("lib.test_admin").test
+ local code, body = t('/apisix/admin/kms/vault/test1',
+ ngx.HTTP_GET,
+ nil,
+ [[{
+ "value": {
+ "uri": "http://127.0.0.1:12800/get",
+ "prefix" : "apisix",
+ "token" : "apisix"
+ },
+ "key": "/apisix/kms/vault/test1"
+ }]]
+ )
+
+ ngx.status = code
+ ngx.say(body)
+ }
+ }
+--- response_body
+passed
+
+
+
+=== TEST 3: GET all
+--- config
+ location /t {
+ content_by_lua_block {
+ local t = require("lib.test_admin").test
+ local code, body = t('/apisix/admin/kms',
+ ngx.HTTP_GET,
+ nil,
+ [[{
+ "total": 1,
+ "list": [
+ {
+ "key": "/apisix/kms/vault/test1",
+ "value": {
+ "uri": "http://127.0.0.1:12800/get",
+ "prefix" : "apisix",
+ "token" : "apisix"
+ }
+ }
+ ]
+ }]]
+ )
+
+ ngx.status = code
+ ngx.say(body)
+ }
+ }
+--- response_body
+passed
+
+
+
+=== TEST 4: PATCH
+--- config
+ location /t {
+ content_by_lua_block {
+ local t = require("lib.test_admin").test
+ local etcd = require("apisix.core.etcd")
+ local res = assert(etcd.get('/kms/vault/test1'))
+ local prev_create_time = res.body.node.value.create_time
+ assert(prev_create_time ~= nil, "create_time is nil")
+ local prev_update_time = res.body.node.value.update_time
+ assert(prev_update_time ~= nil, "update_time is nil")
+ ngx.sleep(1)
+
+ local code, body = t('/apisix/admin/kms/vault/test1/token',
+ ngx.HTTP_PATCH,
+ [["unknown"]],
+ [[{
+ "value": {
+ "uri": "http://127.0.0.1:12800/get",
+ "prefix" : "apisix",
+ "token" : "unknown"
+ },
+ "key": "/apisix/kms/vault/test1"
+ }]]
+ )
+
+ ngx.status = code
+ ngx.say(body)
+
+ local res = assert(etcd.get('/kms/vault/test1'))
+ assert(res.body.node.value.token == "unknown")
+ }
+ }
+--- response_body
+passed
+
+
+
+=== TEST 5: DELETE
+--- config
+ location /t {
+ content_by_lua_block {
+ local t = require("lib.test_admin").test
+ local code, body = t('/apisix/admin/kms/vault/test1',
+ ngx.HTTP_DELETE
+ )
+ ngx.say(body)
+ }
+ }
+--- response_body
+passed
+
+
+
+=== TEST 6: PUT with invalid format
+--- config
+ location /t {
+ content_by_lua_block {
+ local t = require("lib.test_admin").test
+ local etcd = require("apisix.core.etcd")
+ local code, body = t('/apisix/admin/kms/vault/test1',
+ ngx.HTTP_PUT,
+ [[{
+ "uri": "/get",
+ "prefix" : "apisix",
+ "token" : "apisix"
+ }]],
+ [[{
+ "value": {
+ "uri": "http://127.0.0.1:12800/get",
+ "prefix" : "apisix",
+ "token" : "apisix"
+ },
+ "key": "/apisix/kms/vault/test1"
+ }]]
+ )
+
+ ngx.status = code
+ ngx.say(body)
+ }
+ }
+--- error_code: 400
+--- response_body eval
+qr/validation failed: failed to match pattern/