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/