From d46737fe70b6ce332146a9eb322e76997c8fa8ba Mon Sep 17 00:00:00 2001 From: Shreemaan Abhishek Date: Tue, 17 Sep 2024 10:08:58 +0545 Subject: [PATCH 1/7] feat: ai-proxy plugin (#11499) --- Makefile | 6 + apisix/cli/config.lua | 1 + apisix/core/request.lua | 16 + apisix/plugins/ai-proxy.lua | 138 ++++ apisix/plugins/ai-proxy/drivers/openai.lua | 85 +++ apisix/plugins/ai-proxy/schema.lua | 154 +++++ ci/common.sh | 21 + ci/linux_openresty_common_runner.sh | 2 + ci/redhat-ci.sh | 2 + conf/config.yaml.example | 1 + docs/en/latest/config.json | 3 +- docs/en/latest/plugins/ai-proxy.md | 144 +++++ t/admin/plugins.t | 1 + t/assets/ai-proxy-response.json | 15 + t/plugin/ai-proxy.t | 693 +++++++++++++++++++++ t/plugin/ai-proxy2.t | 200 ++++++ t/sse_server_example/go.mod | 3 + t/sse_server_example/main.go | 58 ++ 18 files changed, 1542 insertions(+), 1 deletion(-) create mode 100644 apisix/plugins/ai-proxy.lua create mode 100644 apisix/plugins/ai-proxy/drivers/openai.lua create mode 100644 apisix/plugins/ai-proxy/schema.lua create mode 100644 docs/en/latest/plugins/ai-proxy.md create mode 100644 t/assets/ai-proxy-response.json create mode 100644 t/plugin/ai-proxy.t create mode 100644 t/plugin/ai-proxy2.t create mode 100644 t/sse_server_example/go.mod create mode 100644 t/sse_server_example/main.go diff --git a/Makefile b/Makefile index 21a2389633b3..545a21e4f29f 100644 --- a/Makefile +++ b/Makefile @@ -374,6 +374,12 @@ install: runtime $(ENV_INSTALL) -d $(ENV_INST_LUADIR)/apisix/utils $(ENV_INSTALL) apisix/utils/*.lua $(ENV_INST_LUADIR)/apisix/utils/ + $(ENV_INSTALL) -d $(ENV_INST_LUADIR)/apisix/plugins/ai-proxy + $(ENV_INSTALL) apisix/plugins/ai-proxy/*.lua $(ENV_INST_LUADIR)/apisix/plugins/ai-proxy + + $(ENV_INSTALL) -d $(ENV_INST_LUADIR)/apisix/plugins/ai-proxy/drivers + $(ENV_INSTALL) apisix/plugins/ai-proxy/drivers/*.lua $(ENV_INST_LUADIR)/apisix/plugins/ai-proxy/drivers + $(ENV_INSTALL) bin/apisix $(ENV_INST_BINDIR)/apisix diff --git a/apisix/cli/config.lua b/apisix/cli/config.lua index 6ab10c9256cd..f5c5d8dcaf94 100644 --- a/apisix/cli/config.lua +++ b/apisix/cli/config.lua @@ -219,6 +219,7 @@ local _M = { "proxy-rewrite", "workflow", "api-breaker", + "ai-proxy", "limit-conn", "limit-count", "limit-req", diff --git a/apisix/core/request.lua b/apisix/core/request.lua index c5278b6b8072..fef4bf17e3f7 100644 --- a/apisix/core/request.lua +++ b/apisix/core/request.lua @@ -21,6 +21,7 @@ local lfs = require("lfs") local log = require("apisix.core.log") +local json = require("apisix.core.json") local io = require("apisix.core.io") local req_add_header if ngx.config.subsystem == "http" then @@ -334,6 +335,21 @@ function _M.get_body(max_size, ctx) end +function _M.get_json_request_body_table() + local body, err = _M.get_body() + if not body then + return nil, { message = "could not get body: " .. (err or "request body is empty") } + end + + local body_tab, err = json.decode(body) + if not body_tab then + return nil, { message = "could not get parse JSON request body: " .. err } + end + + return body_tab +end + + function _M.get_scheme(ctx) if not ctx then ctx = ngx.ctx.api_ctx diff --git a/apisix/plugins/ai-proxy.lua b/apisix/plugins/ai-proxy.lua new file mode 100644 index 000000000000..8a0d8fa970d4 --- /dev/null +++ b/apisix/plugins/ai-proxy.lua @@ -0,0 +1,138 @@ +-- +-- 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 schema = require("apisix.plugins.ai-proxy.schema") +local require = require +local pcall = pcall +local internal_server_error = ngx.HTTP_INTERNAL_SERVER_ERROR +local bad_request = ngx.HTTP_BAD_REQUEST +local ngx_req = ngx.req +local ngx_print = ngx.print +local ngx_flush = ngx.flush + +local plugin_name = "ai-proxy" +local _M = { + version = 0.5, + priority = 999, + name = plugin_name, + schema = schema, +} + + +function _M.check_schema(conf) + local ai_driver = pcall(require, "apisix.plugins.ai-proxy.drivers." .. conf.model.provider) + if not ai_driver then + return false, "provider: " .. conf.model.provider .. " is not supported." + end + return core.schema.check(schema.plugin_schema, conf) +end + + +local CONTENT_TYPE_JSON = "application/json" + + +local function keepalive_or_close(conf, httpc) + if conf.set_keepalive then + httpc:set_keepalive(10000, 100) + return + end + httpc:close() +end + + +function _M.access(conf, ctx) + local ct = core.request.header(ctx, "Content-Type") or CONTENT_TYPE_JSON + if not core.string.has_prefix(ct, CONTENT_TYPE_JSON) then + return bad_request, "unsupported content-type: " .. ct + end + + local request_table, err = core.request.get_json_request_body_table() + if not request_table then + return bad_request, err + end + + local ok, err = core.schema.check(schema.chat_request_schema, request_table) + if not ok then + return bad_request, "request format doesn't match schema: " .. err + end + + if conf.model.name then + request_table.model = conf.model.name + end + + if core.table.try_read_attr(conf, "model", "options", "stream") then + request_table.stream = true + end + + local ai_driver = require("apisix.plugins.ai-proxy.drivers." .. conf.model.provider) + local res, err, httpc = ai_driver.request(conf, request_table, ctx) + if not res then + core.log.error("failed to send request to LLM service: ", err) + return internal_server_error + end + + local body_reader = res.body_reader + if not body_reader then + core.log.error("LLM sent no response body") + return internal_server_error + end + + if conf.passthrough then + ngx_req.init_body() + while true do + local chunk, err = body_reader() -- will read chunk by chunk + if err then + core.log.error("failed to read response chunk: ", err) + break + end + if not chunk then + break + end + ngx_req.append_body(chunk) + end + ngx_req.finish_body() + keepalive_or_close(conf, httpc) + return + end + + if request_table.stream then + while true do + local chunk, err = body_reader() -- will read chunk by chunk + if err then + core.log.error("failed to read response chunk: ", err) + break + end + if not chunk then + break + end + ngx_print(chunk) + ngx_flush(true) + end + keepalive_or_close(conf, httpc) + return + else + local res_body, err = res:read_body() + if not res_body then + core.log.error("failed to read response body: ", err) + return internal_server_error + end + keepalive_or_close(conf, httpc) + return res.status, res_body + end +end + +return _M diff --git a/apisix/plugins/ai-proxy/drivers/openai.lua b/apisix/plugins/ai-proxy/drivers/openai.lua new file mode 100644 index 000000000000..c8f7f4b6223f --- /dev/null +++ b/apisix/plugins/ai-proxy/drivers/openai.lua @@ -0,0 +1,85 @@ +-- +-- 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 _M = {} + +local core = require("apisix.core") +local http = require("resty.http") +local url = require("socket.url") + +local pairs = pairs + +-- globals +local DEFAULT_HOST = "api.openai.com" +local DEFAULT_PORT = 443 +local DEFAULT_PATH = "/v1/chat/completions" + + +function _M.request(conf, request_table, ctx) + local httpc, err = http.new() + if not httpc then + return nil, "failed to create http client to send request to LLM server: " .. err + end + httpc:set_timeout(conf.timeout) + + local endpoint = core.table.try_read_attr(conf, "override", "endpoint") + local parsed_url + if endpoint then + parsed_url = url.parse(endpoint) + end + + local ok, err = httpc:connect({ + scheme = parsed_url.scheme or "https", + host = parsed_url.host or DEFAULT_HOST, + port = parsed_url.port or DEFAULT_PORT, + ssl_verify = conf.ssl_verify, + ssl_server_name = parsed_url.host or DEFAULT_HOST, + pool_size = conf.keepalive and conf.keepalive_pool, + }) + + if not ok then + return nil, "failed to connect to LLM server: " .. err + end + + local path = (parsed_url.path or DEFAULT_PATH) + + local headers = (conf.auth.header or {}) + headers["Content-Type"] = "application/json" + local params = { + method = "POST", + headers = headers, + keepalive = conf.keepalive, + ssl_verify = conf.ssl_verify, + path = path, + query = conf.auth.query + } + + if conf.model.options then + for opt, val in pairs(conf.model.options) do + request_table[opt] = val + end + end + params.body = core.json.encode(request_table) + + local res, err = httpc:request(params) + if not res then + return nil, err + end + + return res, nil, httpc +end + +return _M diff --git a/apisix/plugins/ai-proxy/schema.lua b/apisix/plugins/ai-proxy/schema.lua new file mode 100644 index 000000000000..382644dc2147 --- /dev/null +++ b/apisix/plugins/ai-proxy/schema.lua @@ -0,0 +1,154 @@ +-- +-- 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 _M = {} + +local auth_item_schema = { + type = "object", + patternProperties = { + ["^[a-zA-Z0-9._-]+$"] = { + type = "string" + } + } +} + +local auth_schema = { + type = "object", + patternProperties = { + header = auth_item_schema, + query = auth_item_schema, + }, + additionalProperties = false, +} + +local model_options_schema = { + description = "Key/value settings for the model", + type = "object", + properties = { + max_tokens = { + type = "integer", + description = "Defines the max_tokens, if using chat or completion models.", + default = 256 + + }, + input_cost = { + type = "number", + description = "Defines the cost per 1M tokens in your prompt.", + minimum = 0 + + }, + output_cost = { + type = "number", + description = "Defines the cost per 1M tokens in the output of the AI.", + minimum = 0 + + }, + temperature = { + type = "number", + description = "Defines the matching temperature, if using chat or completion models.", + minimum = 0.0, + maximum = 5.0, + + }, + top_p = { + type = "number", + description = "Defines the top-p probability mass, if supported.", + minimum = 0, + maximum = 1, + + }, + stream = { + description = "Stream response by SSE", + type = "boolean", + default = false, + } + } +} + +local model_schema = { + type = "object", + properties = { + provider = { + type = "string", + description = "Name of the AI service provider.", + oneOf = { "openai" }, -- add more providers later + + }, + name = { + type = "string", + description = "Model name to execute.", + }, + options = model_options_schema, + override = { + type = "object", + properties = { + endpoint = { + type = "string", + description = "To be specified to override the host of the AI provider", + }, + } + } + }, + required = {"provider", "name"} +} + +_M.plugin_schema = { + type = "object", + properties = { + auth = auth_schema, + model = model_schema, + passthrough = { type = "boolean", default = false }, + timeout = { + type = "integer", + minimum = 1, + maximum = 60000, + default = 3000, + description = "timeout in milliseconds", + }, + keepalive = {type = "boolean", default = true}, + keepalive_timeout = {type = "integer", minimum = 1000, default = 60000}, + keepalive_pool = {type = "integer", minimum = 1, default = 30}, + ssl_verify = {type = "boolean", default = true }, + }, + required = {"model", "auth"} +} + +_M.chat_request_schema = { + type = "object", + properties = { + messages = { + type = "array", + minItems = 1, + items = { + properties = { + role = { + type = "string", + enum = {"system", "user", "assistant"} + }, + content = { + type = "string", + minLength = "1", + }, + }, + additionalProperties = false, + required = {"role", "content"}, + }, + } + }, + required = {"messages"} +} + +return _M diff --git a/ci/common.sh b/ci/common.sh index 146b7aa5080a..ae5d12b2b7c6 100644 --- a/ci/common.sh +++ b/ci/common.sh @@ -203,3 +203,24 @@ function start_grpc_server_example() { ss -lntp | grep 10051 | grep grpc_server && break done } + + +function start_sse_server_example() { + # build sse_server_example + pushd t/sse_server_example + go build + ./sse_server_example 7737 2>&1 & + + for (( i = 0; i <= 10; i++ )); do + sleep 0.5 + SSE_PROC=`ps -ef | grep sse_server_example | grep -v grep || echo "none"` + if [[ $SSE_PROC == "none" || "$i" -eq 10 ]]; then + echo "failed to start sse_server_example" + ss -antp | grep 7737 || echo "no proc listen port 7737" + exit 1 + else + break + fi + done + popd +} diff --git a/ci/linux_openresty_common_runner.sh b/ci/linux_openresty_common_runner.sh index ea2e8b41c8bb..1b73ceec92c6 100755 --- a/ci/linux_openresty_common_runner.sh +++ b/ci/linux_openresty_common_runner.sh @@ -77,6 +77,8 @@ script() { start_grpc_server_example + start_sse_server_example + # APISIX_ENABLE_LUACOV=1 PERL5LIB=.:$PERL5LIB prove -Itest-nginx/lib -r t FLUSH_ETCD=1 TEST_EVENTS_MODULE=$TEST_EVENTS_MODULE prove --timer -Itest-nginx/lib -I./ -r $TEST_FILE_SUB_DIR | tee /tmp/test.result rerun_flaky_tests /tmp/test.result diff --git a/ci/redhat-ci.sh b/ci/redhat-ci.sh index 3cad10b5992b..da9839d4e699 100755 --- a/ci/redhat-ci.sh +++ b/ci/redhat-ci.sh @@ -77,6 +77,8 @@ install_dependencies() { yum install -y iproute procps start_grpc_server_example + start_sse_server_example + # installing grpcurl install_grpcurl diff --git a/conf/config.yaml.example b/conf/config.yaml.example index da125f77daa2..bd741b2f767b 100644 --- a/conf/config.yaml.example +++ b/conf/config.yaml.example @@ -486,6 +486,7 @@ plugins: # plugin list (sorted by priority) - limit-count # priority: 1002 - limit-req # priority: 1001 #- node-status # priority: 1000 + - ai-proxy # priority: 999 #- brotli # priority: 996 - gzip # priority: 995 - server-info # priority: 990 diff --git a/docs/en/latest/config.json b/docs/en/latest/config.json index 2195688a365c..ad9c1e051523 100644 --- a/docs/en/latest/config.json +++ b/docs/en/latest/config.json @@ -96,7 +96,8 @@ "plugins/fault-injection", "plugins/mocking", "plugins/degraphql", - "plugins/body-transformer" + "plugins/body-transformer", + "plugins/ai-proxy" ] }, { diff --git a/docs/en/latest/plugins/ai-proxy.md b/docs/en/latest/plugins/ai-proxy.md new file mode 100644 index 000000000000..a6a4e35426eb --- /dev/null +++ b/docs/en/latest/plugins/ai-proxy.md @@ -0,0 +1,144 @@ +--- +title: ai-proxy +keywords: + - Apache APISIX + - API Gateway + - Plugin + - ai-proxy +description: This document contains information about the Apache APISIX ai-proxy Plugin. +--- + + + +## Description + +The `ai-proxy` plugin simplifies access to LLM providers and models by defining a standard request format +that allows key fields in plugin configuration to be embedded into the request. + +Proxying requests to OpenAI is supported now. Other LLM services will be supported soon. + +## Request Format + +### OpenAI + +- Chat API + +| Name | Type | Required | Description | +| ------------------ | ------ | -------- | --------------------------------------------------- | +| `messages` | Array | Yes | An array of message objects | +| `messages.role` | String | Yes | Role of the message (`system`, `user`, `assistant`) | +| `messages.content` | String | Yes | Content of the message | + +## Plugin Attributes + +| **Field** | **Required** | **Type** | **Description** | +| ------------------------- | ------------ | -------- | ------------------------------------------------------------------------------------ | +| auth | Yes | Object | Authentication configuration | +| auth.header | No | Object | Authentication headers. Key must match pattern `^[a-zA-Z0-9._-]+$`. | +| auth.query | No | Object | Authentication query parameters. Key must match pattern `^[a-zA-Z0-9._-]+$`. | +| model.provider | Yes | String | Name of the AI service provider (`openai`). | +| model.name | Yes | String | Model name to execute. | +| model.options | No | Object | Key/value settings for the model | +| model.options.max_tokens | No | Integer | Defines the max tokens if using chat or completion models. Default: 256 | +| model.options.input_cost | No | Number | Cost per 1M tokens in your prompt. Minimum: 0 | +| model.options.output_cost | No | Number | Cost per 1M tokens in the output of the AI. Minimum: 0 | +| model.options.temperature | No | Number | Matching temperature for models. Range: 0.0 - 5.0 | +| model.options.top_p | No | Number | Top-p probability mass. Range: 0 - 1 | +| model.options.stream | No | Boolean | Stream response by SSE. Default: false | +| model.override.endpoint | No | String | Override the endpoint of the AI provider | +| passthrough | No | Boolean | If enabled, the response from LLM will be sent to the upstream. Default: false | +| timeout | No | Integer | Timeout in milliseconds for requests to LLM. Range: 1 - 60000. Default: 3000 | +| keepalive | No | Boolean | Enable keepalive for requests to LLM. Default: true | +| keepalive_timeout | No | Integer | Keepalive timeout in milliseconds for requests to LLM. Minimum: 1000. Default: 60000 | +| keepalive_pool | No | Integer | Keepalive pool size for requests to LLM. Minimum: 1. Default: 30 | +| ssl_verify | No | Boolean | SSL verification for requests to LLM. Default: true | + +## Example usage + +Create a route with the `ai-proxy` plugin like so: + +```shell +curl "http://127.0.0.1:9180/apisix/admin/routes/1" -X PUT \ + -H "X-API-KEY: ${ADMIN_API_KEY}" \ + -d '{ + "uri": "/anything", + "plugins": { + "ai-proxy": { + "auth": { + "header": { + "Authorization": "Bearer " + } + }, + "model": { + "provider": "openai", + "name": "gpt-4", + "options": { + "max_tokens": 512, + "temperature": 1.0 + } + } + } + }, + "upstream": { + "type": "roundrobin", + "nodes": { + "somerandom.com:443": 1 + }, + "scheme": "https", + "pass_host": "node" + } + }' +``` + +Since `passthrough` is not enabled upstream node can be any arbitrary value because it won't be contacted. + +Now send a request: + +```shell +curl http://127.0.0.1:9080/anything -i -XPOST -H 'Content-Type: application/json' -d '{ + "messages": [ + { "role": "system", "content": "You are a mathematician" }, + { "role": "user", "a": 1, "content": "What is 1+1?" } + ] + }' +``` + +You will receive a response like this: + +```json +{ + "choices": [ + { + "finish_reason": "stop", + "index": 0, + "message": { + "content": "The sum of \\(1 + 1\\) is \\(2\\).", + "role": "assistant" + } + } + ], + "created": 1723777034, + "id": "chatcmpl-9whRKFodKl5sGhOgHIjWltdeB8sr7", + "model": "gpt-4o-2024-05-13", + "object": "chat.completion", + "system_fingerprint": "fp_abc28019ad", + "usage": { "completion_tokens": 15, "prompt_tokens": 23, "total_tokens": 38 } +} +``` diff --git a/t/admin/plugins.t b/t/admin/plugins.t index ef43ea9f3965..bf3d485e8b31 100644 --- a/t/admin/plugins.t +++ b/t/admin/plugins.t @@ -102,6 +102,7 @@ api-breaker limit-conn limit-count limit-req +ai-proxy gzip server-info traffic-split diff --git a/t/assets/ai-proxy-response.json b/t/assets/ai-proxy-response.json new file mode 100644 index 000000000000..94665e5eaea9 --- /dev/null +++ b/t/assets/ai-proxy-response.json @@ -0,0 +1,15 @@ +{ + "choices": [ + { + "finish_reason": "stop", + "index": 0, + "message": { "content": "1 + 1 = 2.", "role": "assistant" } + } + ], + "created": 1723780938, + "id": "chatcmpl-9wiSIg5LYrrpxwsr2PubSQnbtod1P", + "model": "gpt-4o-2024-05-13", + "object": "chat.completion", + "system_fingerprint": "fp_abc28019ad", + "usage": { "completion_tokens": 8, "prompt_tokens": 23, "total_tokens": 31 } +} diff --git a/t/plugin/ai-proxy.t b/t/plugin/ai-proxy.t new file mode 100644 index 000000000000..445e406f60ab --- /dev/null +++ b/t/plugin/ai-proxy.t @@ -0,0 +1,693 @@ +# +# 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'; + +log_level("info"); +repeat_each(1); +no_long_string(); +no_root_location(); + + +my $resp_file = 't/assets/ai-proxy-response.json'; +open(my $fh, '<', $resp_file) or die "Could not open file '$resp_file' $!"; +my $resp = do { local $/; <$fh> }; +close($fh); + +print "Hello, World!\n"; +print $resp; + + +add_block_preprocessor(sub { + my ($block) = @_; + + if (!defined $block->request) { + $block->set_value("request", "GET /t"); + } + + my $http_config = $block->http_config // <<_EOC_; + server { + server_name openai; + listen 6724; + + default_type 'application/json'; + + location /anything { + content_by_lua_block { + local json = require("cjson.safe") + + if ngx.req.get_method() ~= "POST" then + ngx.status = 400 + ngx.say("Unsupported request method: ", ngx.req.get_method()) + end + ngx.req.read_body() + local body = ngx.req.get_body_data() + + if body ~= "SELECT * FROM STUDENTS" then + ngx.status = 503 + ngx.say("passthrough doesn't work") + return + end + ngx.say('{"foo", "bar"}') + } + } + + location /v1/chat/completions { + content_by_lua_block { + local json = require("cjson.safe") + + if ngx.req.get_method() ~= "POST" then + ngx.status = 400 + ngx.say("Unsupported request method: ", ngx.req.get_method()) + end + ngx.req.read_body() + local body, err = ngx.req.get_body_data() + body, err = json.decode(body) + + local test_type = ngx.req.get_headers()["test-type"] + if test_type == "options" then + if body.foo == "bar" then + ngx.status = 200 + ngx.say("options works") + else + ngx.status = 500 + ngx.say("model options feature doesn't work") + end + return + end + + local header_auth = ngx.req.get_headers()["authorization"] + local query_auth = ngx.req.get_uri_args()["apikey"] + + if header_auth ~= "Bearer token" and query_auth ~= "apikey" then + ngx.status = 401 + ngx.say("Unauthorized") + return + end + + if header_auth == "Bearer token" or query_auth == "apikey" then + ngx.req.read_body() + local body, err = ngx.req.get_body_data() + body, err = json.decode(body) + + if not body.messages or #body.messages < 1 then + ngx.status = 400 + ngx.say([[{ "error": "bad request"}]]) + return + end + + if body.messages[1].content == "write an SQL query to get all rows from student table" then + ngx.print("SELECT * FROM STUDENTS") + return + end + + ngx.status = 200 + ngx.say([[$resp]]) + return + end + + + ngx.status = 503 + ngx.say("reached the end of the test suite") + } + } + + location /random { + content_by_lua_block { + ngx.say("path override works") + } + } + } +_EOC_ + + $block->set_value("http_config", $http_config); +}); + +run_tests(); + +__DATA__ + +=== TEST 1: minimal viable configuration +--- config + location /t { + content_by_lua_block { + local plugin = require("apisix.plugins.ai-proxy") + local ok, err = plugin.check_schema({ + model = { + provider = "openai", + name = "gpt-4", + }, + auth = { + header = { + some_header = "some_value" + } + } + }) + + if not ok then + ngx.say(err) + else + ngx.say("passed") + end + } + } +--- response_body +passed + + + +=== TEST 2: unsupported provider +--- config + location /t { + content_by_lua_block { + local plugin = require("apisix.plugins.ai-proxy") + local ok, err = plugin.check_schema({ + model = { + provider = "some-unique", + name = "gpt-4", + }, + auth = { + header = { + some_header = "some_value" + } + } + }) + + if not ok then + ngx.say(err) + else + ngx.say("passed") + end + } + } +--- response_body eval +qr/.*provider: some-unique is not supported.*/ + + + +=== TEST 3: set route with wrong auth header +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "uri": "/anything", + "plugins": { + "ai-proxy": { + "auth": { + "header": { + "Authorization": "Bearer wrongtoken" + } + }, + "model": { + "provider": "openai", + "name": "gpt-35-turbo-instruct", + "options": { + "max_tokens": 512, + "temperature": 1.0 + } + }, + "override": { + "endpoint": "http://localhost:6724" + }, + "ssl_verify": false + } + }, + "upstream": { + "type": "roundrobin", + "nodes": { + "canbeanything.com": 1 + } + } + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- response_body +passed + + + +=== TEST 4: send request +--- request +POST /anything +{ "messages": [ { "role": "system", "content": "You are a mathematician" }, { "role": "user", "content": "What is 1+1?"} ] } +--- error_code: 401 +--- response_body +Unauthorized + + + +=== TEST 5: set route with right auth header +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "uri": "/anything", + "plugins": { + "ai-proxy": { + "auth": { + "header": { + "Authorization": "Bearer token" + } + }, + "model": { + "provider": "openai", + "name": "gpt-35-turbo-instruct", + "options": { + "max_tokens": 512, + "temperature": 1.0 + } + }, + "override": { + "endpoint": "http://localhost:6724" + }, + "ssl_verify": false + } + }, + "upstream": { + "type": "roundrobin", + "nodes": { + "canbeanything.com": 1 + } + } + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- response_body +passed + + + +=== TEST 6: send request +--- request +POST /anything +{ "messages": [ { "role": "system", "content": "You are a mathematician" }, { "role": "user", "content": "What is 1+1?"} ] } +--- more_headers +Authorization: Bearer token +--- error_code: 200 +--- response_body eval +qr/\{ "content": "1 \+ 1 = 2\.", "role": "assistant" \}/ + + + +=== TEST 7: send request with empty body +--- request +POST /anything +--- more_headers +Authorization: Bearer token +--- error_code: 400 +--- response_body_chomp +failed to get request body: request body is empty + + + +=== TEST 8: send request with wrong method (GET) should work +--- request +GET /anything +{ "messages": [ { "role": "system", "content": "You are a mathematician" }, { "role": "user", "content": "What is 1+1?"} ] } +--- more_headers +Authorization: Bearer token +--- error_code: 200 +--- response_body eval +qr/\{ "content": "1 \+ 1 = 2\.", "role": "assistant" \}/ + + + +=== TEST 9: wrong JSON in request body should give error +--- request +GET /anything +{}"messages": [ { "role": "system", "cont +--- error_code: 400 +--- response_body +{"message":"could not get parse JSON request body: Expected the end but found T_STRING at character 3"} + + + +=== TEST 10: content-type should be JSON +--- request +POST /anything +prompt%3Dwhat%2520is%25201%2520%252B%25201 +--- more_headers +Content-Type: application/x-www-form-urlencoded +--- error_code: 400 +--- response_body chomp +unsupported content-type: application/x-www-form-urlencoded + + + +=== TEST 11: request schema validity check +--- request +POST /anything +{ "messages-missing": [ { "role": "system", "content": "xyz" } ] } +--- more_headers +Authorization: Bearer token +--- error_code: 400 +--- response_body chomp +request format doesn't match schema: property "messages" is required + + + +=== TEST 12: model options being merged to request body +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "uri": "/anything", + "plugins": { + "ai-proxy": { + "auth": { + "header": { + "Authorization": "Bearer token" + } + }, + "model": { + "provider": "openai", + "name": "some-model", + "options": { + "foo": "bar", + "temperature": 1.0 + } + }, + "override": { + "endpoint": "http://localhost:6724" + }, + "ssl_verify": false + } + }, + "upstream": { + "type": "roundrobin", + "nodes": { + "canbeanything.com": 1 + } + } + }]] + ) + + if code >= 300 then + ngx.status = code + ngx.say(body) + return + end + + local code, body, actual_body = t("/anything", + ngx.HTTP_POST, + [[{ + "messages": [ + { "role": "system", "content": "You are a mathematician" }, + { "role": "user", "content": "What is 1+1?" } + ] + }]], + nil, + { + ["test-type"] = "options", + ["Content-Type"] = "application/json", + } + ) + + ngx.status = code + ngx.say(actual_body) + + } + } +--- error_code: 200 +--- response_body_chomp +options_works + + + +=== TEST 13: override path +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "uri": "/anything", + "plugins": { + "ai-proxy": { + "auth": { + "header": { + "Authorization": "Bearer token" + } + }, + "model": { + "provider": "openai", + "name": "some-model", + "options": { + "foo": "bar", + "temperature": 1.0 + } + }, + "override": { + "endpoint": "http://localhost:6724/random" + }, + "ssl_verify": false + } + }, + "upstream": { + "type": "roundrobin", + "nodes": { + "canbeanything.com": 1 + } + } + }]] + ) + + if code >= 300 then + ngx.status = code + ngx.say(body) + return + end + + local code, body, actual_body = t("/anything", + ngx.HTTP_POST, + [[{ + "messages": [ + { "role": "system", "content": "You are a mathematician" }, + { "role": "user", "content": "What is 1+1?" } + ] + }]], + nil, + { + ["test-type"] = "path", + ["Content-Type"] = "application/json", + } + ) + + ngx.status = code + ngx.say(actual_body) + + } + } +--- response_body_chomp +path override works + + + +=== TEST 14: set route with right auth header +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "uri": "/anything", + "plugins": { + "ai-proxy": { + "auth": { + "header": { + "Authorization": "Bearer token" + } + }, + "model": { + "provider": "openai", + "name": "gpt-35-turbo-instruct", + "options": { + "max_tokens": 512, + "temperature": 1.0 + } + }, + "override": { + "endpoint": "http://localhost:6724" + }, + "ssl_verify": false, + "passthrough": true + } + }, + "upstream": { + "type": "roundrobin", + "nodes": { + "127.0.0.1:6724": 1 + } + } + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- response_body +passed + + + +=== TEST 15: send request with wrong method should work +--- request +POST /anything +{ "messages": [ { "role": "user", "content": "write an SQL query to get all rows from student table" } ] } +--- more_headers +Authorization: Bearer token +--- error_code: 200 +--- response_body +{"foo", "bar"} + + + +=== TEST 16: set route with stream = true (SSE) +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "uri": "/anything", + "plugins": { + "ai-proxy": { + "auth": { + "header": { + "Authorization": "Bearer token" + } + }, + "model": { + "provider": "openai", + "name": "gpt-35-turbo-instruct", + "options": { + "max_tokens": 512, + "temperature": 1.0, + "stream": true + } + }, + "override": { + "endpoint": "http://localhost:7737" + }, + "ssl_verify": false + } + }, + "upstream": { + "type": "roundrobin", + "nodes": { + "canbeanything.com": 1 + } + } + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- response_body +passed + + + +=== TEST 17: test is SSE works as expected +--- config + location /t { + content_by_lua_block { + local http = require("resty.http") + local httpc = http.new() + local core = require("apisix.core") + + local ok, err = httpc:connect({ + scheme = "http", + host = "localhost", + port = ngx.var.server_port, + }) + + if not ok then + ngx.status = 500 + ngx.say(err) + return + end + + local params = { + method = "POST", + headers = { + ["Content-Type"] = "application/json", + }, + path = "/anything", + body = [[{ + "messages": [ + { "role": "system", "content": "some content" } + ] + }]], + } + + local res, err = httpc:request(params) + if not res then + ngx.status = 500 + ngx.say(err) + return + end + + local final_res = {} + while true do + local chunk, err = res.body_reader() -- will read chunk by chunk + if err then + core.log.error("failed to read response chunk: ", err) + break + end + if not chunk then + break + end + core.table.insert_tail(final_res, chunk) + end + + ngx.print(#final_res .. final_res[6]) + } + } +--- response_body_like eval +qr/6data: \[DONE\]\n\n/ diff --git a/t/plugin/ai-proxy2.t b/t/plugin/ai-proxy2.t new file mode 100644 index 000000000000..6e398e5665a4 --- /dev/null +++ b/t/plugin/ai-proxy2.t @@ -0,0 +1,200 @@ +# +# 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'; + +log_level("info"); +repeat_each(1); +no_long_string(); +no_root_location(); + + +my $resp_file = 't/assets/ai-proxy-response.json'; +open(my $fh, '<', $resp_file) or die "Could not open file '$resp_file' $!"; +my $resp = do { local $/; <$fh> }; +close($fh); + +print "Hello, World!\n"; +print $resp; + + +add_block_preprocessor(sub { + my ($block) = @_; + + if (!defined $block->request) { + $block->set_value("request", "GET /t"); + } + + my $http_config = $block->http_config // <<_EOC_; + server { + server_name openai; + listen 6724; + + default_type 'application/json'; + + location /v1/chat/completions { + content_by_lua_block { + local json = require("cjson.safe") + + if ngx.req.get_method() ~= "POST" then + ngx.status = 400 + ngx.say("Unsupported request method: ", ngx.req.get_method()) + end + ngx.req.read_body() + local body, err = ngx.req.get_body_data() + body, err = json.decode(body) + + local query_auth = ngx.req.get_uri_args()["api_key"] + + if query_auth ~= "apikey" then + ngx.status = 401 + ngx.say("Unauthorized") + return + end + + + ngx.status = 200 + ngx.say("passed") + } + } + } +_EOC_ + + $block->set_value("http_config", $http_config); +}); + +run_tests(); + +__DATA__ + +=== TEST 1: set route with wrong query param +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "uri": "/anything", + "plugins": { + "ai-proxy": { + "auth": { + "query": { + "api_key": "wrong_key" + } + }, + "model": { + "provider": "openai", + "name": "gpt-35-turbo-instruct", + "options": { + "max_tokens": 512, + "temperature": 1.0 + } + }, + "override": { + "endpoint": "http://localhost:6724" + }, + "ssl_verify": false + } + }, + "upstream": { + "type": "roundrobin", + "nodes": { + "canbeanything.com": 1 + } + } + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- response_body +passed + + + +=== TEST 2: send request +--- request +POST /anything +{ "messages": [ { "role": "system", "content": "You are a mathematician" }, { "role": "user", "content": "What is 1+1?"} ] } +--- error_code: 401 +--- response_body +Unauthorized + + + +=== TEST 3: set route with right query param +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "uri": "/anything", + "plugins": { + "ai-proxy": { + "auth": { + "query": { + "api_key": "apikey" + } + }, + "model": { + "provider": "openai", + "name": "gpt-35-turbo-instruct", + "options": { + "max_tokens": 512, + "temperature": 1.0 + } + }, + "override": { + "endpoint": "http://localhost:6724" + }, + "ssl_verify": false + } + }, + "upstream": { + "type": "roundrobin", + "nodes": { + "canbeanything.com": 1 + } + } + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- response_body +passed + + + +=== TEST 4: send request +--- request +POST /anything +{ "messages": [ { "role": "system", "content": "You are a mathematician" }, { "role": "user", "content": "What is 1+1?"} ] } +--- error_code: 200 +--- response_body +passed diff --git a/t/sse_server_example/go.mod b/t/sse_server_example/go.mod new file mode 100644 index 000000000000..9cc909d0338e --- /dev/null +++ b/t/sse_server_example/go.mod @@ -0,0 +1,3 @@ +module foo.bar/apache/sse_server_example + +go 1.17 diff --git a/t/sse_server_example/main.go b/t/sse_server_example/main.go new file mode 100644 index 000000000000..ab976c86094a --- /dev/null +++ b/t/sse_server_example/main.go @@ -0,0 +1,58 @@ +/* + * 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. + */ + +package main + +import ( + "fmt" + "log" + "net/http" + "os" + "time" +) + +func sseHandler(w http.ResponseWriter, r *http.Request) { + // Set the headers for SSE + w.Header().Set("Content-Type", "text/event-stream") + w.Header().Set("Cache-Control", "no-cache") + w.Header().Set("Connection", "keep-alive") + + f, ok := w.(http.Flusher); + if !ok { + fmt.Fprintf(w, "[ERROR]") + return + } + // A simple loop that sends a message every 500ms + for i := 0; i < 5; i++ { + // Create a message to send to the client + fmt.Fprintf(w, "data: %s\n\n", time.Now().Format(time.RFC3339)) + + // Flush the data immediately to the client + f.Flush() + time.Sleep(500 * time.Millisecond) + } + fmt.Fprintf(w, "data: %s\n\n", "[DONE]") +} + +func main() { + // Create a simple route + http.HandleFunc("/v1/chat/completions", sseHandler) + port := os.Args[1] + // Start the server + log.Println("Starting server on :", port) + log.Fatal(http.ListenAndServe(":" + port, nil)) +} From a3933201658ef9d147c82fdad8e228908c7fc884 Mon Sep 17 00:00:00 2001 From: Ashish Tiwari Date: Thu, 19 Sep 2024 12:31:30 +0530 Subject: [PATCH 2/7] feat: rewrite hmac-auth plugin for usability (#11581) --- apisix/plugins/hmac-auth.lua | 364 ++++++---------- t/node/consumer-plugin.t | 21 +- t/plugin/consumer-restriction.t | 122 ++---- t/plugin/hmac-auth-custom.t | 357 --------------- t/plugin/hmac-auth.t | 731 +++++++++++++++++++++---------- t/plugin/hmac-auth2.t | 748 +------------------------------- t/plugin/hmac-auth3.t | 543 ++--------------------- t/plugin/hmac-auth4.t | 42 +- 8 files changed, 753 insertions(+), 2175 deletions(-) delete mode 100644 t/plugin/hmac-auth-custom.t diff --git a/apisix/plugins/hmac-auth.lua b/apisix/plugins/hmac-auth.lua index 94916e975216..88aafaf7e833 100644 --- a/apisix/plugins/hmac-auth.lua +++ b/apisix/plugins/hmac-auth.lua @@ -15,51 +15,37 @@ -- limitations under the License. -- local ngx = ngx -local type = type local abs = math.abs local ngx_time = ngx.time local ngx_re = require("ngx.re") -local pairs = pairs local ipairs = ipairs local hmac_sha1 = ngx.hmac_sha1 -local escape_uri = ngx.escape_uri local core = require("apisix.core") local hmac = require("resty.hmac") local consumer = require("apisix.consumer") -local plugin = require("apisix.plugin") local ngx_decode_base64 = ngx.decode_base64 local ngx_encode_base64 = ngx.encode_base64 - -local BODY_DIGEST_KEY = "X-HMAC-DIGEST" -local SIGNATURE_KEY = "X-HMAC-SIGNATURE" -local ALGORITHM_KEY = "X-HMAC-ALGORITHM" -local DATE_KEY = "Date" -local ACCESS_KEY = "X-HMAC-ACCESS-KEY" -local SIGNED_HEADERS_KEY = "X-HMAC-SIGNED-HEADERS" local plugin_name = "hmac-auth" -local MAX_REQ_BODY = 1024 * 512 - +local ALLOWED_ALGORITHMS = {"hmac-sha1", "hmac-sha256", "hmac-sha512"} +local resty_sha256 = require("resty.sha256") local schema = { type = "object", title = "work with route or service object", - properties = {}, -} - -local consumer_schema = { - type = "object", - title = "work with consumer object", properties = { - access_key = {type = "string", minLength = 1, maxLength = 256}, - secret_key = {type = "string", minLength = 1, maxLength = 256}, - algorithm = { - type = "string", - enum = {"hmac-sha1", "hmac-sha256", "hmac-sha512"}, - default = "hmac-sha256" + allowed_algorithms = { + type = "array", + minItems = 1, + items = { + type = "string", + enum = ALLOWED_ALGORITHMS + }, + default = ALLOWED_ALGORITHMS, }, clock_skew = { type = "integer", - default = 0 + default = 300, + minimum = 1 }, signed_headers = { type = "array", @@ -69,29 +55,24 @@ local consumer_schema = { maxLength = 50, } }, - keep_headers = { - type = "boolean", - title = "whether to keep the http request header", - default = false, - }, - encode_uri_params = { - type = "boolean", - title = "Whether to escape the uri parameter", - default = true, - }, validate_request_body = { type = "boolean", title = "A boolean value telling the plugin to enable body validation", default = false, }, - max_req_body = { - type = "integer", - title = "Max request body size", - default = MAX_REQ_BODY, - }, + hide_credentials = {type = "boolean", default = false}, + }, +} + +local consumer_schema = { + type = "object", + title = "work with consumer object", + properties = { + key_id = {type = "string", minLength = 1, maxLength = 256}, + secret_key = {type = "string", minLength = 1, maxLength = 256}, }, encrypt_fields = {"secret_key"}, - required = {"access_key", "secret_key"}, + required = {"key_id", "secret_key"}, } local _M = { @@ -126,17 +107,6 @@ local function array_to_map(arr) end -local function remove_headers(ctx, ...) - local headers = { ... } - if headers and #headers > 0 then - for _, header in ipairs(headers) do - core.log.info("remove_header: ", header) - core.request.set_header(ctx, header, nil) - end - end -end - - function _M.check_schema(conf, schema_type) core.log.info("input conf: ", core.json.delay_encode(conf)) @@ -148,9 +118,9 @@ function _M.check_schema(conf, schema_type) end -local function get_consumer(access_key) - if not access_key then - return nil, "missing access key" +local function get_consumer(key_id) + if not key_id then + return nil, "missing key_id" end local consumer_conf = consumer.plugin(plugin_name) @@ -158,10 +128,10 @@ local function get_consumer(access_key) return nil, "Missing related consumer" end - local consumers = consumer.consumers_kv(plugin_name, consumer_conf, "access_key") - local consumer = consumers[access_key] + local consumers = consumer.consumers_kv(plugin_name, consumer_conf, "key_id") + local consumer = consumers[key_id] if not consumer then - return nil, "Invalid access key" + return nil, "Invalid key_id" end core.log.info("consumer: ", core.json.delay_encode(consumer)) @@ -169,130 +139,90 @@ local function get_consumer(access_key) end -local function get_conf_field(access_key, field_name) - local consumer, err = get_consumer(access_key) - if err then - return false, err - end - - return consumer.auth_conf[field_name] -end - - -local function do_nothing(v) - return v -end - local function generate_signature(ctx, secret_key, params) - local canonical_uri = ctx.var.uri - local canonical_query_string = "" + local uri = ctx.var.request_uri local request_method = core.request.get_method() - local args = core.request.get_uri_args(ctx) - if canonical_uri == "" then - canonical_uri = "/" + if uri == "" then + uri = "/" end - if type(args) == "table" then - local keys = {} - local query_tab = {} - - for k, v in pairs(args) do - core.table.insert(keys, k) - end - core.table.sort(keys) - - local field_val = get_conf_field(params.access_key, "encode_uri_params") - core.log.info("encode_uri_params: ", field_val) - - local encode_or_not = do_nothing - if field_val then - encode_or_not = escape_uri - end - - for _, key in pairs(keys) do - local param = args[key] - -- when args without `=`, value is treated as true. - -- In order to be compatible with args lacking `=`, - -- we need to replace true with an empty string. - if type(param) == "boolean" then - param = "" - end - - -- whether to encode the uri parameters - if type(param) == "table" then - local vals = {} - for _, val in pairs(param) do - if type(val) == "boolean" then - val = "" - end - core.table.insert(vals, val) - end - core.table.sort(vals) - - for _, val in pairs(vals) do - core.table.insert(query_tab, encode_or_not(key) .. "=" .. encode_or_not(val)) - end - else - core.table.insert(query_tab, encode_or_not(key) .. "=" .. encode_or_not(param)) - end - end - canonical_query_string = core.table.concat(query_tab, "&") - end - - core.log.info("all headers: ", - core.json.delay_encode(core.request.headers(ctx), true)) - local signing_string_items = { - request_method, - canonical_uri, - canonical_query_string, - params.access_key, - params.date, + params.keyId, } - if params.signed_headers then - for _, h in ipairs(params.signed_headers) do - local canonical_header = core.request.header(ctx, h) or "" - core.table.insert(signing_string_items, - h .. ":" .. canonical_header) - core.log.info("canonical_header name:", core.json.delay_encode(h)) - core.log.info("canonical_header value: ", - core.json.delay_encode(canonical_header)) + if params.headers then + for _, h in ipairs(params.headers) do + local canonical_header = core.request.header(ctx, h) + if not canonical_header then + if h == "@request-target" then + local request_target = request_method .. " " .. uri + core.table.insert(signing_string_items, request_target) + core.log.info("canonical_header name:", core.json.delay_encode(h)) + core.log.info("canonical_header value: ", + core.json.delay_encode(request_target)) + end + else + core.table.insert(signing_string_items, + h .. ": " .. canonical_header) + core.log.info("canonical_header name:", core.json.delay_encode(h)) + core.log.info("canonical_header value: ", + core.json.delay_encode(canonical_header)) + end end end local signing_string = core.table.concat(signing_string_items, "\n") .. "\n" + return hmac_funcs[params.algorithm](secret_key, signing_string) +end - core.log.info("signing_string: ", signing_string, - " params.signed_headers:", - core.json.delay_encode(params.signed_headers)) - return hmac_funcs[params.algorithm](secret_key, signing_string) +local function sha256(key) + local hash = resty_sha256:new() + hash:update(key) + local digest = hash:final() + return digest end -local function validate(ctx, params) - if not params.access_key or not params.signature then - return nil, "access key or signature missing" +local function validate(ctx, conf, params) + if not params.keyId or not params.signature then + return nil, "keyId or signature missing" end if not params.algorithm then return nil, "algorithm missing" end - local consumer, err = get_consumer(params.access_key) + local consumer, err = get_consumer(params.keyId) if err then return nil, err end - local conf = consumer.auth_conf - if conf.algorithm ~= params.algorithm then - return nil, "algorithm " .. params.algorithm .. " not supported" + local consumer_conf = consumer.auth_conf + local found_algorithm = false + -- check supported algorithm used + if not conf.allowed_algorithms then + conf.allowed_algorithms = ALLOWED_ALGORITHMS + end + + for _, algo in ipairs(conf.allowed_algorithms) do + if algo == params.algorithm then + found_algorithm = true + break + end + end + + if not found_algorithm then + return nil, "Invalid algorithm" end core.log.info("clock_skew: ", conf.clock_skew) if conf.clock_skew and conf.clock_skew > 0 then + if not params.date then + return nil, "Date header missing. failed to validate clock skew" + end + local time = ngx.parse_http_time(params.date) core.log.info("params.date: ", params.date, " time: ", time) if not time then @@ -300,52 +230,51 @@ local function validate(ctx, params) end local diff = abs(ngx_time() - time) - core.log.info("gmt diff: ", diff) + if diff > conf.clock_skew then return nil, "Clock skew exceeded" end end -- validate headers + -- All headers passed in route conf.signed_headers must be used in signing(params.headers) if conf.signed_headers and #conf.signed_headers >= 1 then - local headers_map = array_to_map(conf.signed_headers) - if params.signed_headers then - for _, header in ipairs(params.signed_headers) do - if not headers_map[header] then - return nil, "Invalid signed header " .. header + if not params.headers then + return nil, "headers missing" + end + local params_headers_map = array_to_map(params.headers) + if params_headers_map then + for _, header in ipairs(conf.signed_headers) do + if not params_headers_map[header] then + return nil, [[expected header "]] .. header .. [[" missing in signing]] end end end end - local secret_key = conf and conf.secret_key + local secret_key = consumer_conf and consumer_conf.secret_key local request_signature = ngx_decode_base64(params.signature) local generated_signature = generate_signature(ctx, secret_key, params) - - core.log.info("request_signature: ", request_signature, - " generated_signature: ", generated_signature) - if request_signature ~= generated_signature then return nil, "Invalid signature" end - local validate_request_body = get_conf_field(params.access_key, "validate_request_body") + local validate_request_body = conf.validate_request_body if validate_request_body then local digest_header = params.body_digest if not digest_header then return nil, "Invalid digest" end - local max_req_body = get_conf_field(params.access_key, "max_req_body") - local req_body, err = core.request.get_body(max_req_body, ctx) + local req_body, err = core.request.get_body() if err then - return nil, "Exceed body limit size" + return nil, err end req_body = req_body or "" - local request_body_hash = ngx_encode_base64( - hmac_funcs[params.algorithm](secret_key, req_body)) - if request_body_hash ~= digest_header then + local digest_created = "SHA-256" .. "=" .. + ngx_encode_base64(sha256(req_body)) + if digest_created ~= digest_header then return nil, "Invalid digest" end end @@ -354,78 +283,55 @@ local function validate(ctx, params) end -local function get_params(ctx) - local params = {} - local access_key = ACCESS_KEY - local signature_key = SIGNATURE_KEY - local algorithm_key = ALGORITHM_KEY - local date_key = DATE_KEY - local signed_headers_key = SIGNED_HEADERS_KEY - local body_digest_key = BODY_DIGEST_KEY - - - local attr = plugin.plugin_attr(plugin_name) - if attr then - access_key = attr.access_key or access_key - signature_key = attr.signature_key or signature_key - algorithm_key = attr.algorithm_key or algorithm_key - date_key = attr.date_key or date_key - signed_headers_key = attr.signed_headers_key or signed_headers_key - body_digest_key = attr.body_digest_key or body_digest_key +local function retrieve_hmac_fields(ctx) + local hmac_params = {} + local auth_string = core.request.header(ctx, "Authorization") + if not auth_string then + return nil, "missing Authorization header" end - local app_key = core.request.header(ctx, access_key) - local signature = core.request.header(ctx, signature_key) - local algorithm = core.request.header(ctx, algorithm_key) - local date = core.request.header(ctx, date_key) - local signed_headers = core.request.header(ctx, signed_headers_key) - local body_digest = core.request.header(ctx, body_digest_key) - core.log.info("signature_key: ", signature_key) - - -- get params from header `Authorization` - if not app_key then - local auth_string = core.request.header(ctx, "Authorization") - if not auth_string then - return params - end - - local auth_data = ngx_re.split(auth_string, "#") - core.log.info("auth_string: ", auth_string, " #auth_data: ", - #auth_data, " auth_data: ", - core.json.delay_encode(auth_data)) - - if #auth_data == 6 and auth_data[1] == "hmac-auth-v1" then - app_key = auth_data[2] - signature = auth_data[3] - algorithm = auth_data[4] - date = auth_data[5] - signed_headers = auth_data[6] - end + if not core.string.has_prefix(auth_string, "Signature") then + return nil, "Authorization header does not start with 'Signature'" end - params.access_key = app_key - params.algorithm = algorithm - params.signature = signature - params.date = date or "" - params.signed_headers = signed_headers and ngx_re.split(signed_headers, ";") - params.body_digest = body_digest + local signature_fields = auth_string:sub(10):gmatch('[^,]+') + + for field in signature_fields do + local key, value = field:match('%s*(%w+)="(.-)"') + if key and value then + if key == "keyId" or key == "algorithm" or key == "signature" then + hmac_params[key] = value - local keep_headers = get_conf_field(params.access_key, "keep_headers") - core.log.info("keep_headers: ", keep_headers) + elseif key == "headers" then + hmac_params.headers = ngx_re.split(value, " ") + end + end + end - if not keep_headers then - remove_headers(ctx, signature_key, algorithm_key, signed_headers_key) + -- will be required to check clock skew + if core.request.header(ctx, "Date") then + hmac_params.date = core.request.header(ctx, "Date") end - core.log.info("params: ", core.json.delay_encode(params)) + if core.request.header(ctx, "Digest") then + hmac_params.body_digest = core.request.header(ctx, "Digest") + end - return params + return hmac_params end function _M.rewrite(conf, ctx) - local params = get_params(ctx) - local validated_consumer, err = validate(ctx, params) + local params,err = retrieve_hmac_fields(ctx) + if err then + core.log.warn("client request can't be validated: ", err) + return 401, {message = "client request can't be validated: " .. err} + end + + if conf.hide_credentials then + core.request.set_header("Authorization", nil) + end + local validated_consumer, err = validate(ctx, conf, params) if not validated_consumer then core.log.warn("client request can't be validated: ", err or "Invalid signature") return 401, {message = "client request can't be validated"} diff --git a/t/node/consumer-plugin.t b/t/node/consumer-plugin.t index 89cdf68cc6ab..76e3f25bb634 100644 --- a/t/node/consumer-plugin.t +++ b/t/node/consumer-plugin.t @@ -237,7 +237,7 @@ GET /t "key": "consumer-plugin-John_Doe" }, "hmac-auth": { - "access_key": "my-access-key", + "key_id": "my-access-key", "secret_key": "my-secret-key", "clock_skew": 1 } @@ -341,18 +341,16 @@ location /t { local secret_key = "my-secret-key" local timestamp = ngx_time() local gmt = ngx_http_time(timestamp) - local access_key = "my-access-key" + local key_id = "my-access-key" local custom_header_a = "asld$%dfasf" local custom_header_b = "23879fmsldfk" local signing_string = { - "GET", - "/status", - "", - access_key, - gmt, - "x-custom-header-a:" .. custom_header_a, - "x-custom-header-b:" .. custom_header_b + key_id, + "GET /status", + "date: " .. gmt, + "x-custom-header-a: " .. custom_header_a, + "x-custom-header-b: " .. custom_header_b } signing_string = core.table.concat(signing_string, "\n") .. "\n" core.log.info("signing_string:", signing_string) @@ -360,11 +358,8 @@ location /t { local signature = hmac:new(secret_key, hmac.ALGOS.SHA256):final(signing_string) core.log.info("signature:", ngx_encode_base64(signature)) local headers = {} - headers["X-HMAC-SIGNATURE"] = ngx_encode_base64(signature) - headers["X-HMAC-ALGORITHM"] = "hmac-sha256" headers["Date"] = gmt - headers["X-HMAC-ACCESS-KEY"] = access_key - headers["X-HMAC-SIGNED-HEADERS"] = "x-custom-header-a;x-custom-header-b" + headers["Authorization"] = "Signature keyId=\"" .. key_id .. "\",algorithm=\"hmac-sha256\"" .. ",headers=\"@request-target date x-custom-header-a x-custom-header-b\",signature=\"" .. ngx_encode_base64(signature) .. "\"" headers["x-custom-header-a"] = custom_header_a headers["x-custom-header-b"] = custom_header_b diff --git a/t/plugin/consumer-restriction.t b/t/plugin/consumer-restriction.t index cc86aacdcd7d..9ba590d0530e 100644 --- a/t/plugin/consumer-restriction.t +++ b/t/plugin/consumer-restriction.t @@ -738,7 +738,7 @@ passed "username": "jack", "plugins": { "hmac-auth": { - "access_key": "my-access-key", + "key_id": "my-access-key", "secret_key": "my-secret-key" }, "consumer-restriction": { @@ -811,29 +811,24 @@ location /t { local secret_key = "my-secret-key" local timestamp = ngx_time() local gmt = ngx_http_time(timestamp) - local access_key = "my-access-key" + local key_id = "my-access-key" local custom_header_a = "asld$%dfasf" local custom_header_b = "23879fmsldfk" local signing_string = { - "GET", - "/hello", - "", - access_key, - gmt, - "x-custom-header-a:" .. custom_header_a, - "x-custom-header-b:" .. custom_header_b + key_id, + "GET /hello", + "date: " .. gmt, + "x-custom-header-a: " .. custom_header_a, + "x-custom-header-b: " .. custom_header_b } signing_string = core.table.concat(signing_string, "\n") .. "\n" local signature = hmac:new(secret_key, hmac.ALGOS.SHA256):final(signing_string) core.log.info("signature:", ngx_encode_base64(signature)) local headers = {} - headers["X-HMAC-SIGNATURE"] = ngx_encode_base64(signature) - headers["X-HMAC-ALGORITHM"] = "hmac-sha256" headers["Date"] = gmt - headers["X-HMAC-ACCESS-KEY"] = access_key - headers["X-HMAC-SIGNED-HEADERS"] = "x-custom-header-a;x-custom-header-b" + headers["Authorization"] = "Signature keyId=\"" .. key_id .. "\",algorithm=\"hmac-sha256\"" .. ",headers=\"@request-target date x-custom-header-a x-custom-header-b\",signature=\"" .. ngx_encode_base64(signature) .. "\"" headers["x-custom-header-a"] = custom_header_a headers["x-custom-header-b"] = custom_header_b @@ -934,29 +929,24 @@ location /t { local secret_key = "my-secret-key" local timestamp = ngx_time() local gmt = ngx_http_time(timestamp) - local access_key = "my-access-key" + local key_id = "my-access-key" local custom_header_a = "asld$%dfasf" local custom_header_b = "23879fmsldfk" local signing_string = { - "GET", - "/hello", - "", - access_key, - gmt, - "x-custom-header-a:" .. custom_header_a, - "x-custom-header-b:" .. custom_header_b + key_id, + "GET /hello", + "date: " .. gmt, + "x-custom-header-a: " .. custom_header_a, + "x-custom-header-b: " .. custom_header_b } signing_string = core.table.concat(signing_string, "\n") .. "\n" local signature = hmac:new(secret_key, hmac.ALGOS.SHA256):final(signing_string) core.log.info("signature:", ngx_encode_base64(signature)) local headers = {} - headers["X-HMAC-SIGNATURE"] = ngx_encode_base64(signature) - headers["X-HMAC-ALGORITHM"] = "hmac-sha256" headers["Date"] = gmt - headers["X-HMAC-ACCESS-KEY"] = access_key - headers["X-HMAC-SIGNED-HEADERS"] = "x-custom-header-a;x-custom-header-b" + headers["Authorization"] = "Signature keyId=\"" .. key_id .. "\",algorithm=\"hmac-sha256\"" .. ",headers=\"@request-target date x-custom-header-a x-custom-header-b\",signature=\"" .. ngx_encode_base64(signature) .. "\"" headers["x-custom-header-a"] = custom_header_a headers["x-custom-header-b"] = custom_header_b @@ -992,7 +982,7 @@ qr/\{"message":"The service_id is forbidden."\}/ "username": "jack", "plugins": { "hmac-auth": { - "access_key": "my-access-key", + "key_id": "my-access-key", "secret_key": "my-secret-key" }, "consumer-restriction": { @@ -1064,29 +1054,24 @@ location /t { local secret_key = "my-secret-key" local timestamp = ngx_time() local gmt = ngx_http_time(timestamp) - local access_key = "my-access-key" + local key_id = "my-access-key" local custom_header_a = "asld$%dfasf" local custom_header_b = "23879fmsldfk" local signing_string = { - "GET", - "/hello", - "", - access_key, - gmt, - "x-custom-header-a:" .. custom_header_a, - "x-custom-header-b:" .. custom_header_b + key_id, + "GET /hello", + "date: " .. gmt, + "x-custom-header-a: " .. custom_header_a, + "x-custom-header-b: " .. custom_header_b } signing_string = core.table.concat(signing_string, "\n") .. "\n" local signature = hmac:new(secret_key, hmac.ALGOS.SHA256):final(signing_string) core.log.info("signature:", ngx_encode_base64(signature)) local headers = {} - headers["X-HMAC-SIGNATURE"] = ngx_encode_base64(signature) - headers["X-HMAC-ALGORITHM"] = "hmac-sha256" headers["Date"] = gmt - headers["X-HMAC-ACCESS-KEY"] = access_key - headers["X-HMAC-SIGNED-HEADERS"] = "x-custom-header-a;x-custom-header-b" + headers["Authorization"] = "Signature keyId=\"" .. key_id .. "\",algorithm=\"hmac-sha256\"" .. ",headers=\"@request-target date x-custom-header-a x-custom-header-b\",signature=\"" .. ngx_encode_base64(signature) .. "\"" headers["x-custom-header-a"] = custom_header_a headers["x-custom-header-b"] = custom_header_b @@ -1158,29 +1143,24 @@ location /t { local secret_key = "my-secret-key" local timestamp = ngx_time() local gmt = ngx_http_time(timestamp) - local access_key = "my-access-key" + local key_id = "my-access-key" local custom_header_a = "asld$%dfasf" local custom_header_b = "23879fmsldfk" local signing_string = { - "GET", - "/hello", - "", - access_key, - gmt, - "x-custom-header-a:" .. custom_header_a, - "x-custom-header-b:" .. custom_header_b + key_id, + "GET /hello", + "date: " .. gmt, + "x-custom-header-a: " .. custom_header_a, + "x-custom-header-b: " .. custom_header_b } signing_string = core.table.concat(signing_string, "\n") .. "\n" local signature = hmac:new(secret_key, hmac.ALGOS.SHA256):final(signing_string) core.log.info("signature:", ngx_encode_base64(signature)) local headers = {} - headers["X-HMAC-SIGNATURE"] = ngx_encode_base64(signature) - headers["X-HMAC-ALGORITHM"] = "hmac-sha256" headers["Date"] = gmt - headers["X-HMAC-ACCESS-KEY"] = access_key - headers["X-HMAC-SIGNED-HEADERS"] = "x-custom-header-a;x-custom-header-b" + headers["Authorization"] = "Signature keyId=\"" .. key_id .. "\",algorithm=\"hmac-sha256\"" .. ",headers=\"@request-target date x-custom-header-a x-custom-header-b\",signature=\"" .. ngx_encode_base64(signature) .. "\"" headers["x-custom-header-a"] = custom_header_a headers["x-custom-header-b"] = custom_header_b @@ -1213,7 +1193,7 @@ passed "username": "jack", "plugins": { "hmac-auth": { - "access_key": "my-access-key", + "key_id": "my-access-key", "secret_key": "my-secret-key" }, "consumer-restriction": { @@ -1248,29 +1228,24 @@ location /t { local ngx_encode_base64 = ngx.encode_base64 local secret_key = "my-secret-key" local gmt = ngx_http_time(ngx_time) - local access_key = "my-access-key" + local key_id = "my-access-key" local custom_header_a = "asld$%dfasf" local custom_header_b = "23879fmsldfk" local signing_string = { - "GET", - "/hello", - "", - access_key, - gmt, - "x-custom-header-a:" .. custom_header_a, - "x-custom-header-b:" .. custom_header_b + key_id, + "GET /hello", + "date: " .. gmt, + "x-custom-header-a: " .. custom_header_a, + "x-custom-header-b: " .. custom_header_b } signing_string = core.table.concat(signing_string, "\n") .. "\n" local signature = hmac:new(secret_key, hmac.ALGOS.SHA256):final(signing_string) core.log.info("signature:", ngx_encode_base64(signature)) local headers = {} - headers["X-HMAC-SIGNATURE"] = ngx_encode_base64(signature) - headers["X-HMAC-ALGORITHM"] = "hmac-sha256" headers["Date"] = gmt - headers["X-HMAC-ACCESS-KEY"] = access_key - headers["X-HMAC-SIGNED-HEADERS"] = "x-custom-header-a;x-custom-header-b" + headers["Authorization"] = "Signature keyId=\"" .. key_id .. "\",algorithm=\"hmac-sha256\"" .. ",headers=\"@request-target date x-custom-header-a x-custom-header-b\",signature=\"" .. ngx_encode_base64(signature) .. "\"" headers["x-custom-header-a"] = custom_header_a headers["x-custom-header-b"] = custom_header_b @@ -1303,7 +1278,7 @@ passed "username": "jack", "plugins": { "hmac-auth": { - "access_key": "my-access-key", + "key_id": "my-access-key", "secret_key": "my-secret-key" }, "consumer-restriction": { @@ -1340,29 +1315,24 @@ location /t { local secret_key = "my-secret-key" local timestamp = ngx_time() local gmt = ngx_http_time(timestamp) - local access_key = "my-access-key" + local key_id = "my-access-key" local custom_header_a = "asld$%dfasf" local custom_header_b = "23879fmsldfk" local signing_string = { - "GET", - "/hello", - "", - access_key, - gmt, - "x-custom-header-a:" .. custom_header_a, - "x-custom-header-b:" .. custom_header_b + key_id, + "GET /hello", + "date: " .. gmt, + "x-custom-header-a: " .. custom_header_a, + "x-custom-header-b: " .. custom_header_b } signing_string = core.table.concat(signing_string, "\n") .. "\n" local signature = hmac:new(secret_key, hmac.ALGOS.SHA256):final(signing_string) core.log.info("signature:", ngx_encode_base64(signature)) local headers = {} - headers["X-HMAC-SIGNATURE"] = ngx_encode_base64(signature) - headers["X-HMAC-ALGORITHM"] = "hmac-sha256" headers["Date"] = gmt - headers["X-HMAC-ACCESS-KEY"] = access_key - headers["X-HMAC-SIGNED-HEADERS"] = "x-custom-header-a;x-custom-header-b" + headers["Authorization"] = "Signature keyId=\"" .. key_id .. "\",algorithm=\"hmac-sha256\"" .. ",headers=\"@request-target date x-custom-header-a x-custom-header-b\",signature=\"" .. ngx_encode_base64(signature) .. "\"" headers["x-custom-header-a"] = custom_header_a headers["x-custom-header-b"] = custom_header_b diff --git a/t/plugin/hmac-auth-custom.t b/t/plugin/hmac-auth-custom.t deleted file mode 100644 index 6e463c9401ff..000000000000 --- a/t/plugin/hmac-auth-custom.t +++ /dev/null @@ -1,357 +0,0 @@ -# -# 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(2); -no_long_string(); -no_root_location(); -no_shuffle(); - -add_block_preprocessor(sub { - my ($block) = @_; - - if (!$block->request) { - $block->set_value("request", "GET /t"); - } - - my $extra_yaml_config = <<_EOC_; -plugin_attr: - hmac-auth: - signature_key: X-APISIX-HMAC-SIGNATURE - algorithm_key: X-APISIX-HMAC-ALGORITHM - date_key: X-APISIX-DATE - access_key: X-APISIX-HMAC-ACCESS-KEY - signed_headers_key: X-APISIX-HMAC-SIGNED-HEADERS -_EOC_ - - $block->set_value("extra_yaml_config", $extra_yaml_config); -}); - -run_tests; - -__DATA__ - -=== TEST 1: add consumer with username and plugins ---- config - location /t { - content_by_lua_block { - local t = require("lib.test_admin").test - local code, body = t('/apisix/admin/consumers', - ngx.HTTP_PUT, - [[{ - "username": "jack", - "plugins": { - "hmac-auth": { - "access_key": "my-access-key", - "secret_key": "my-secret-key", - "clock_skew": 10 - } - } - }]] - ) - - if code >= 300 then - ngx.status = code - end - ngx.say(body) - } - } ---- response_body -passed - - - -=== TEST 2: add consumer with plugin hmac-auth - missing secret key ---- config - location /t { - content_by_lua_block { - local t = require("lib.test_admin").test - local code, body = t('/apisix/admin/consumers', - ngx.HTTP_PUT, - [[{ - "username": "jack", - "plugins": { - "hmac-auth": { - "access_key": "user-key" - } - } - }]]) - - ngx.status = code - ngx.say(body) - } - } ---- error_code: 400 - - - -=== TEST 3: add consumer with plugin hmac-auth - missing access key ---- config - location /t { - content_by_lua_block { - local t = require("lib.test_admin").test - local code, body = t('/apisix/admin/consumers', - ngx.HTTP_PUT, - [[{ - "username": "jack", - "plugins": { - "hmac-auth": { - "secret_key": "skey" - } - } - }]]) - - ngx.status = code - ngx.say(body) - } - } ---- error_code: 400 - - - -=== TEST 4: enable hmac auth plugin using admin api ---- config - location /t { - content_by_lua_block { - local t = require("lib.test_admin").test - local code, body = t('/apisix/admin/routes/1', - ngx.HTTP_PUT, - [[{ - "plugins": { - "hmac-auth": {} - }, - "upstream": { - "nodes": { - "127.0.0.1:1980": 1 - }, - "type": "roundrobin" - }, - "uri": "/hello" - }]] - ) - - if code >= 300 then - ngx.status = code - end - ngx.say(body) - } - } ---- response_body -passed - - - -=== TEST 5: verify, missing signature ---- request -GET /hello ---- error_code: 401 ---- grep_error_log eval -qr/client request can't be validated: [^,]+/ ---- grep_error_log_out -client request can't be validated: access key or signature missing ---- response_body eval -qr/\{"message":"client request can't be validated"\}/ - - - -=== TEST 6: verify: invalid access key ---- request -GET /hello ---- more_headers -X-APISIX-HMAC-SIGNATURE: asdf -X-APISIX-HMAC-ALGORITHM: hmac-sha256 -X-APISIX-Date: Thu, 24 Sep 2020 06:39:52 GMT -X-APISIX-HMAC-ACCESS-KEY: sdf ---- error_code: 401 ---- grep_error_log eval -qr/client request can't be validated: [^,]+/ ---- grep_error_log_out -client request can't be validated: Invalid access key ---- response_body eval -qr/\{"message":"client request can't be validated"\}/ - - - -=== TEST 7: verify: invalid algorithm ---- request -GET /hello ---- more_headers -X-APISIX-HMAC-SIGNATURE: asdf -X-APISIX-HMAC-ALGORITHM: ljlj -X-APISIX-Date: Thu, 24 Sep 2020 06:39:52 GMT -X-APISIX-HMAC-ACCESS-KEY: sdf ---- error_code: 401 ---- grep_error_log eval -qr/client request can't be validated: [^,]+/ ---- grep_error_log_out -client request can't be validated: Invalid access key ---- response_body eval -qr/\{"message":"client request can't be validated"\}/ - - - -=== TEST 8: verify: Invalid GMT format time ---- request -GET /hello ---- more_headers -X-APISIX-HMAC-SIGNATURE: asdf -X-APISIX-HMAC-ALGORITHM: hmac-sha256 -X-APISIX-Date: adfa -X-APISIX-HMAC-ACCESS-KEY: my-access-key ---- error_code: 401 ---- grep_error_log eval -qr/client request can't be validated: [^,]+/ ---- grep_error_log_out -client request can't be validated: Invalid GMT format time ---- response_body eval -qr/\{"message":"client request can't be validated"\}/ - - - -=== TEST 9: verify: ok ---- config -location /t { - content_by_lua_block { - local ngx_time = ngx.time - local ngx_http_time = ngx.http_time - local core = require("apisix.core") - local t = require("lib.test_admin") - local hmac = require("resty.hmac") - local ngx_encode_base64 = ngx.encode_base64 - - local secret_key = "my-secret-key" - local time = ngx_time() - local gmt = ngx_http_time(time) - local access_key = "my-access-key" - local custom_header_a = "asld$%dfasf" - local custom_header_b = "23879fmsldfk" - - local signing_string = { - "GET", - "/hello", - "", - access_key, - gmt, - "x-custom-header-a:" .. custom_header_a, - "x-custom-header-b:" .. custom_header_b - } - signing_string = core.table.concat(signing_string, "\n") .. "\n" - - local signature = hmac:new(secret_key, hmac.ALGOS.SHA256):final(signing_string) - core.log.info("signature:", ngx_encode_base64(signature)) - local headers = {} - headers["X-APISIX-HMAC-SIGNATURE"] = ngx_encode_base64(signature) - headers["X-APISIX-HMAC-ALGORITHM"] = "hmac-sha256" - headers["X-APISIX-DATE"] = gmt - headers["X-APISIX-HMAC-ACCESS-KEY"] = access_key - headers["X-APISIX-HMAC-SIGNED-HEADERS"] = "x-custom-header-a;x-custom-header-b" - headers["x-custom-header-a"] = custom_header_a - headers["x-custom-header-b"] = custom_header_b - - local code, body = t.test('/hello', - ngx.HTTP_GET, - "", - nil, - headers - ) - - ngx.status = code - ngx.say(body) - } -} ---- response_body -passed - - - -=== TEST 10: update consumer with clock skew ---- config - location /t { - content_by_lua_block { - local t = require("lib.test_admin").test - local code, body = t('/apisix/admin/consumers', - ngx.HTTP_PUT, - [[{ - "username": "pony", - "plugins": { - "hmac-auth": { - "access_key": "my-access-key2", - "secret_key": "my-secret-key2", - "clock_skew": 1 - } - } - }]] - ) - if code >= 300 then - ngx.status = code - end - ngx.say(body) - } - } ---- response_body -passed - - - -=== TEST 11: verify: Clock skew exceeded ---- config -location /t { - content_by_lua_block { - local ngx_time = ngx.time - local ngx_http_time = ngx.http_time - local core = require("apisix.core") - local t = require("lib.test_admin") - local hmac = require("resty.hmac") - local ngx_encode_base64 = ngx.encode_base64 - - local secret_key = "my-secret-key2" - local time = ngx_time() - local gmt = ngx_http_time(time) - local access_key = "my-access-key2" - local custom_header_a = "asld$%dfasf" - local custom_header_b = "23879fmsldfk" - local signing_string = "GET" .. "/hello" .. "" .. - access_key .. gmt .. custom_header_a .. custom_header_b - - ngx.sleep(2) - - local signature = hmac:new(secret_key, hmac.ALGOS.SHA256):final(signing_string) - core.log.info("signature:", ngx_encode_base64(signature)) - local headers = {} - headers["X-APISIX-HMAC-SIGNATURE"] = ngx_encode_base64(signature) - headers["X-APISIX-HMAC-ALGORITHM"] = "hmac-sha256" - headers["X-APISIX-DATE"] = gmt - headers["X-APISIX-HMAC-ACCESS-KEY"] = access_key - - local code, body = t.test('/hello', - ngx.HTTP_GET, - core.json.encode(data), - nil, - headers - ) - - ngx.status = code - ngx.say(body) - } -} ---- error_code: 401 ---- grep_error_log eval -qr/client request can't be validated: [^,]+/ ---- grep_error_log_out -client request can't be validated: Clock skew exceeded ---- response_body eval -qr/\{"message":"client request can't be validated"\}/ diff --git a/t/plugin/hmac-auth.t b/t/plugin/hmac-auth.t index 4efdae88f1b3..68029b3821b2 100644 --- a/t/plugin/hmac-auth.t +++ b/t/plugin/hmac-auth.t @@ -35,9 +35,8 @@ __DATA__ "username": "jack", "plugins": { "hmac-auth": { - "access_key": "my-access-key", - "secret_key": "my-secret-key", - "clock_skew": 10 + "key_id": "my-access-key", + "secret_key": "my-secret-key" } } }]] @@ -67,7 +66,7 @@ passed "username": "foo", "plugins": { "hmac-auth": { - "access_key": "user-key" + "key_id": "user-key" } } }]]) @@ -84,7 +83,7 @@ qr/\{"error_msg":"invalid plugins configuration: failed to check the configurati -=== TEST 3: add consumer with plugin hmac-auth - missing access key +=== TEST 3: add consumer with plugin hmac-auth - missing key_id --- config location /t { content_by_lua_block { @@ -108,11 +107,11 @@ qr/\{"error_msg":"invalid plugins configuration: failed to check the configurati GET /t --- error_code: 400 --- response_body eval -qr/\{"error_msg":"invalid plugins configuration: failed to check the configuration of plugin hmac-auth err: property \\"access_key\\" is required"\}/ +qr/\{"error_msg":"invalid plugins configuration: failed to check the configuration of plugin hmac-auth err: property \\"key_id\\" is required"\}/ -=== TEST 4: add consumer with plugin hmac-auth - access key exceeds the length limit +=== TEST 4: add consumer with plugin hmac-auth - key id exceeds the length limit --- config location /t { content_by_lua_block { @@ -123,7 +122,7 @@ qr/\{"error_msg":"invalid plugins configuration: failed to check the configurati "username": "li", "plugins": { "hmac-auth": { - "access_key": "akeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakey", + "key_id": "akeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakey", "secret_key": "skey" } } @@ -137,11 +136,11 @@ qr/\{"error_msg":"invalid plugins configuration: failed to check the configurati GET /t --- error_code: 400 --- response_body eval -qr/\{"error_msg":"invalid plugins configuration: failed to check the configuration of plugin hmac-auth err: property \\"access_key\\" validation failed: string too long, expected at most 256, got 320"\}/ +qr/\{"error_msg":"invalid plugins configuration: failed to check the configuration of plugin hmac-auth err: property \\"key_id\\" validation failed: string too long, expected at most 256, got 320"\}/ -=== TEST 5: add consumer with plugin hmac-auth - access key exceeds the length limit +=== TEST 5: add consumer with plugin hmac-auth - secret key exceeds the length limit --- config location /t { content_by_lua_block { @@ -152,7 +151,7 @@ qr/\{"error_msg":"invalid plugins configuration: failed to check the configurati "username": "zhang", "plugins": { "hmac-auth": { - "access_key": "akey", + "key_id": "akey", "secret_key": "skeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskey" } } @@ -204,16 +203,16 @@ passed -=== TEST 7: verify, missing signature +=== TEST 7: verify,missing Authorization header --- request GET /hello --- error_code: 401 --- response_body -{"message":"client request can't be validated"} +{"message":"client request can't be validated: missing Authorization header"} --- grep_error_log eval qr/client request can't be validated: [^,]+/ --- grep_error_log_out -client request can't be validated: access key or signature missing +client request can't be validated: missing Authorization header @@ -221,34 +220,31 @@ client request can't be validated: access key or signature missing --- request GET /hello --- more_headers -X-HMAC-SIGNATURE: asdf +Authorization: Signature keyId="my-access-key",headers="@request-target date" ,signature="asdf" Date: Thu, 24 Sep 2020 06:39:52 GMT -X-HMAC-ACCESS-KEY: my-access-key --- error_code: 401 --- response_body {"message":"client request can't be validated"} --- grep_error_log eval -qr/client request can't be validated: [^,]+/ +qr/client request can't be validated[^,]+/ --- grep_error_log_out client request can't be validated: algorithm missing -=== TEST 9: verify: invalid access key +=== TEST 9: verify: invalid key_id --- request GET /hello --- more_headers -X-HMAC-SIGNATURE: asdf -X-HMAC-ALGORITHM: hmac-sha256 +Authorization: Signature keyId="sdf",algorithm="hmac-sha256",headers="@request-target date",signature="asdf" Date: Thu, 24 Sep 2020 06:39:52 GMT -X-HMAC-ACCESS-KEY: sdf --- error_code: 401 --- response_body {"message":"client request can't be validated"} --- grep_error_log eval qr/client request can't be validated: [^,]+/ --- grep_error_log_out -client request can't be validated: Invalid access key +client request can't be validated: Invalid key_id @@ -256,17 +252,15 @@ client request can't be validated: Invalid access key --- request GET /hello --- more_headers -X-HMAC-SIGNATURE: asdf -X-HMAC-ALGORITHM: ljlj +Authorization: Signature keyId="my-access-key",algorithm="ljlj",headers="@request-target date",signature="asdf" Date: Thu, 24 Sep 2020 06:39:52 GMT -X-HMAC-ACCESS-KEY: my-access-key --- error_code: 401 --- response_body {"message":"client request can't be validated"} --- grep_error_log eval qr/client request can't be validated: [^,]+/ --- grep_error_log_out -client request can't be validated: algorithm ljlj not supported +client request can't be validated: Invalid algorithm @@ -274,10 +268,8 @@ client request can't be validated: algorithm ljlj not supported --- request GET /hello --- more_headers -X-HMAC-SIGNATURE: asdf -X-HMAC-ALGORITHM: hmac-sha256 +Authorization: Signature keyId="my-access-key",algorithm="hmac-sha256",headers="@request-target date",signature="asdf" Date: Thu, 24 Sep 2020 06:39:52 GMT -X-HMAC-ACCESS-KEY: my-access-key --- error_code: 401 --- response_body {"message":"client request can't be validated"} @@ -292,16 +284,14 @@ client request can't be validated: Clock skew exceeded --- request GET /hello --- more_headers -X-HMAC-SIGNATURE: asdf -X-HMAC-ALGORITHM: hmac-sha256 -X-HMAC-ACCESS-KEY: my-access-key +Authorization: Signature keyId="my-access-key",algorithm="hmac-sha256",headers="@request-target date",signature="asdf" --- error_code: 401 --- response_body {"message":"client request can't be validated"} --- grep_error_log eval -qr/client request can't be validated: [^,]+/ +qr/client request can't be validated: Date header missing/ --- grep_error_log_out -client request can't be validated: Invalid GMT format time +client request can't be validated: Date header missing @@ -309,10 +299,8 @@ client request can't be validated: Invalid GMT format time --- request GET /hello --- more_headers -X-HMAC-SIGNATURE: asdf -X-HMAC-ALGORITHM: hmac-sha256 +Authorization: Signature keyId="my-access-key",algorithm="hmac-sha256",headers="@request-target date",signature="asdf" Date: adfsdf -X-HMAC-ACCESS-KEY: my-access-key --- error_code: 401 --- response_body {"message":"client request can't be validated"} @@ -337,18 +325,16 @@ location /t { local secret_key = "my-secret-key" local timestamp = ngx_time() local gmt = ngx_http_time(timestamp) - local access_key = "my-access-key" + local key_id = "my-access-key" local custom_header_a = "asld$%dfasf" local custom_header_b = "23879fmsldfk" local signing_string = { - "GET", - "/hello", - "", - access_key, - gmt, - "x-custom-header-a:" .. custom_header_a, - "x-custom-header-b:" .. custom_header_b + key_id, + "GET /hello", + "date: " .. gmt, + "x-custom-header-a: " .. custom_header_a, + "x-custom-header-b: " .. custom_header_b } signing_string = core.table.concat(signing_string, "\n") .. "\n" core.log.info("signing_string:", signing_string) @@ -356,11 +342,8 @@ location /t { local signature = hmac:new(secret_key, hmac.ALGOS.SHA256):final(signing_string) core.log.info("signature:", ngx_encode_base64(signature)) local headers = {} - headers["X-HMAC-SIGNATURE"] = ngx_encode_base64(signature) - headers["X-HMAC-ALGORITHM"] = "hmac-sha256" headers["Date"] = gmt - headers["X-HMAC-ACCESS-KEY"] = access_key - headers["X-HMAC-SIGNED-HEADERS"] = "x-custom-header-a;x-custom-header-b" + headers["Authorization"] = "Signature algorithm=\"hmac-sha256\"" .. ",keyId=\"" .. key_id .. "\",headers=\"@request-target date x-custom-header-a x-custom-header-b\",signature=\"" .. ngx_encode_base64(signature) .. "\"" headers["x-custom-header-a"] = custom_header_a headers["x-custom-header-b"] = custom_header_b @@ -382,88 +365,68 @@ passed -=== TEST 15: verify: ok (multiple duplicates X-HMAC-SIGNATURE header) +=== TEST 15: add route with 0 clock skew --- config -location /t { - content_by_lua_block { - local ngx_time = ngx.time - local ngx_http_time = ngx.http_time - local core = require("apisix.core") - local t = require("lib.test_admin") - local hmac = require("resty.hmac") - local ngx_encode_base64 = ngx.encode_base64 - - local secret_key = "my-secret-key" - local timestamp = ngx_time() - local gmt = ngx_http_time(timestamp) - local access_key = "my-access-key" - local custom_header_a = "asld$%dfasf" - local custom_header_b = "23879fmsldfk" - - local signing_string = { - "GET", - "/hello", - "", - access_key, - gmt, - "x-custom-header-a:" .. custom_header_a, - "x-custom-header-b:" .. custom_header_b + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "plugins": { + "hmac-auth": { + "clock_skew": 0 + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + }, + "uri": "/hello" + }]] + ) + if code == 400 then + ngx.say(body) + end } - signing_string = core.table.concat(signing_string, "\n") .. "\n" - core.log.info("signing_string:", signing_string) - - local signature = hmac:new(secret_key, hmac.ALGOS.SHA256):final(signing_string) - core.log.info("signature:", ngx_encode_base64(signature)) - local headers = {} - local encoded_signature = ngx_encode_base64(signature) - headers["X-HMAC-SIGNATURE"] = {encoded_signature, "another-signature"} - headers["X-HMAC-ALGORITHM"] = "hmac-sha256" - headers["Date"] = gmt - headers["X-HMAC-ACCESS-KEY"] = access_key - headers["X-HMAC-SIGNED-HEADERS"] = "x-custom-header-a;x-custom-header-b" - headers["x-custom-header-a"] = custom_header_a - headers["x-custom-header-b"] = custom_header_b - - local code, body = t.test('/hello', - ngx.HTTP_GET, - "", - nil, - headers - ) - - ngx.status = code - ngx.say(body) } -} --- request GET /t ---- response_body -passed +-- error_code: 400 +--- response_body eval +qr/.*failed to check the configuration of plugin hmac-auth err.*/ -=== TEST 16: add consumer with 0 clock skew +=== TEST 16: add route with valid clock skew --- config location /t { content_by_lua_block { local t = require("lib.test_admin").test - local code, body = t('/apisix/admin/consumers', + local code, body = t('/apisix/admin/routes/1', ngx.HTTP_PUT, [[{ - "username": "robin", "plugins": { "hmac-auth": { - "access_key": "my-access-key3", + "key_id": "my-access-key3", "secret_key": "my-secret-key3", - "clock_skew": 0 + "clock_skew": 1000000000000 } - } + }, + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + }, + "uri": "/hello" }]] ) - if code >= 300 then - ngx.status = code + if code == 200 then + ngx.say(body) end - ngx.say(body) } } --- request @@ -477,11 +440,8 @@ passed --- request GET /hello --- more_headers -X-HMAC-SIGNATURE: asdf -X-HMAC-SIGNATURE: asdf -X-HMAC-ALGORITHM: hmac-sha256 +Authorization: Signature keyId="my-access-key",algorithm="hmac-sha256",headers="@request-target date",signature="asdf" Date: Thu, 24 Sep 2020 06:39:52 GMT -X-HMAC-ACCESS-KEY: my-access-key3 --- error_code: 401 --- response_body {"message":"client request can't be validated"} @@ -492,29 +452,47 @@ client request can't be validated: Invalid signature -=== TEST 18: add consumer with 1 clock skew +=== TEST 18: verify: invalid signature +--- request +GET /hello +--- more_headers +Authorization: Signature keyId="my-access-key",algorithm="hmac-sha256",headers="@request-target date",signature="asdf" +Date: Thu, 24 Sep 2020 06:39:52 GMT +--- error_code: 401 +--- response_body +{"message":"client request can't be validated"} +--- grep_error_log eval +qr/client request can't be validated: [^,]+/ +--- grep_error_log_out +client request can't be validated: Invalid signature + + + +=== TEST 19: add route with 1 clock skew --- config location /t { content_by_lua_block { local t = require("lib.test_admin").test - local code, body = t('/apisix/admin/consumers', + local code, body = t('/apisix/admin/routes/1', ngx.HTTP_PUT, [[{ - "username": "pony", "plugins": { "hmac-auth": { - "access_key": "my-access-key2", - "secret_key": "my-secret-key2", - "clock_skew": 1 + "clock_skew": 1 } - } + }, + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + }, + "uri": "/hello" }]] ) - - if code >= 300 then - ngx.status = code + if code == 200 then + ngx.say(body) end - ngx.say(body) } } --- request @@ -524,7 +502,7 @@ passed -=== TEST 19: verify: Invalid GMT format time +=== TEST 20: verify: Invalid GMT format time --- config location /t { content_by_lua_block { @@ -535,26 +513,23 @@ location /t { local hmac = require("resty.hmac") local ngx_encode_base64 = ngx.encode_base64 - local secret_key = "my-secret-key2" + local secret_key = "my-secret-key" local timestamp = ngx_time() local gmt = ngx_http_time(timestamp) - local access_key = "my-access-key2" + local key_id = "my-access-key" local custom_header_a = "asld$%dfasf" local custom_header_b = "23879fmsldfk" ngx.sleep(2) local signing_string = "GET" .. "/hello" .. "" .. - access_key .. gmt .. custom_header_a .. custom_header_b + key_id .. gmt .. custom_header_a .. custom_header_b local signature = hmac:new(secret_key, hmac.ALGOS.SHA256):final(signing_string) core.log.info("signature:", ngx_encode_base64(signature)) local headers = {} - headers["X-HMAC-SIGNATURE"] = ngx_encode_base64(signature) - headers["X-HMAC-ALGORITHM"] = "hmac-sha256" headers["Date"] = gmt - headers["X-HMAC-ACCESS-KEY"] = access_key - headers["X-HMAC-SIGNED-HEADERS"] = "x-custom-header-a;x-custom-header-b" + headers["Authorization"] = "Signature keyId=\"" .. key_id .. "\",algorithm=\"hmac-sha256\"" .. ",headers=\"@request-target date x-custom-header-a x-custom-header-b\",signature=\"" .. ngx_encode_base64(signature) .. "\"" headers["x-custom-header-a"] = custom_header_a headers["x-custom-header-b"] = custom_header_b @@ -581,7 +556,39 @@ client request can't be validated: Clock skew exceeded -=== TEST 20: verify: put ok +=== TEST 21: update route with default clock skew +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "plugins": { + "hmac-auth": {} + }, + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + }, + "uri": "/hello" + }]] + ) + if code == 200 then + ngx.say(body) + end + } + } +--- request +GET /t +--- response_body +passed + + + +=== TEST 22: verify: put ok --- config location /t { content_by_lua_block { @@ -599,18 +606,16 @@ location /t { local secret_key = "my-secret-key" local timestamp = ngx_time() local gmt = ngx_http_time(timestamp) - local access_key = "my-access-key" + local key_id = "my-access-key" local custom_header_a = "asld$%dfasf" local custom_header_b = "23879fmsldfk" local signing_string = { - "PUT", - "/hello", - "", - access_key, - gmt, - "x-custom-header-a:" .. custom_header_a, - "x-custom-header-b:" .. custom_header_b + key_id, + "PUT /hello", + "date: " .. gmt, + "x-custom-header-a: " .. custom_header_a, + "x-custom-header-b: " .. custom_header_b } signing_string = core.table.concat(signing_string, "\n") .. "\n" core.log.info("signing_string:", signing_string) @@ -618,11 +623,8 @@ location /t { local signature = hmac:new(secret_key, hmac.ALGOS.SHA256):final(signing_string) core.log.info("signature:", ngx_encode_base64(signature)) local headers = {} - headers["X-HMAC-SIGNATURE"] = ngx_encode_base64(signature) - headers["X-HMAC-ALGORITHM"] = "hmac-sha256" headers["Date"] = gmt - headers["X-HMAC-ACCESS-KEY"] = access_key - headers["X-HMAC-SIGNED-HEADERS"] = "x-custom-header-a;x-custom-header-b" + headers["Authorization"] = "Signature keyId=\"" .. key_id .. "\",algorithm=\"hmac-sha256\"" .. ",headers=\"@request-target date x-custom-header-a x-custom-header-b\",signature=\"" .. ngx_encode_base64(signature) .. "\"" headers["x-custom-header-a"] = custom_header_a headers["x-custom-header-b"] = custom_header_b @@ -644,53 +646,131 @@ passed -=== TEST 21: verify: put ok (pass auth data by header `Authorization`) +=== TEST 23: update route with signed_headers +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "plugins": { + "hmac-auth": { + "signed_headers": ["date","x-custom-header-a", "x-custom-header-b"] + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + }, + "uri": "/hello" + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- request +GET /t +--- response_body +passed + + + +=== TEST 24: verify with invalid signed header --- config location /t { content_by_lua_block { local ngx_time = ngx.time - local ngx_http_time = ngx.http_time + local ngx_http_time = ngx.http_time local core = require("apisix.core") local t = require("lib.test_admin") local hmac = require("resty.hmac") local ngx_encode_base64 = ngx.encode_base64 - local data = {cert = "ssl_cert", key = "ssl_key", sni = "test.com"} - local req_body = core.json.encode(data) - req_body = req_body or "" + local secret_key = "my-secret-key" + local timestamp = ngx_time() + local gmt = ngx_http_time(timestamp) + local key_id = "my-access-key" + local custom_header_a = "asld$%dfasf" + local custom_header_c = "23879fmsldfk" + + local signing_string = "GET" .. "/hello" .. "" .. + key_id .. gmt .. custom_header_a .. custom_header_c + + local signature = hmac:new(secret_key, hmac.ALGOS.SHA256):final(signing_string) + core.log.info("signature:", ngx_encode_base64(signature)) + local headers = {} + headers["Date"] = gmt + headers["Authorization"] = "Signature keyId=\"" .. key_id .. "\",algorithm=\"hmac-sha256\"" .. ",headers=\"@request-target date x-custom-header-a x-custom-header-c\",signature=\"" .. ngx_encode_base64(signature) .. "\"" + headers["x-custom-header-a"] = custom_header_a + headers["x-custom-header-c"] = custom_header_c + + local code, body = t.test('/hello', + ngx.HTTP_GET, + "", + nil, + headers + ) + + ngx.status = code + ngx.say(body) + } +} +--- request +GET /t +--- error_code: 401 +--- response_body eval +qr/{"message":"client request can't be validated"}/ +--- grep_error_log eval +qr/client request can't be validated: [^,]+/ +--- grep_error_log_out +client request can't be validated: expected header "x-custom-header-b" missing in signing + + + +=== TEST 25: verify ok with signed headers +--- config +location /t { + content_by_lua_block { + local ngx_time = ngx.time + local ngx_http_time = ngx.http_time + local core = require("apisix.core") + local t = require("lib.test_admin") + local hmac = require("resty.hmac") + local ngx_encode_base64 = ngx.encode_base64 local secret_key = "my-secret-key" local timestamp = ngx_time() local gmt = ngx_http_time(timestamp) - local access_key = "my-access-key" + local key_id = "my-access-key" local custom_header_a = "asld$%dfasf" - local custom_header_b = "23879fmsldfk" + local custom_header_b = "asld$%dfasf" local signing_string = { - "PUT", - "/hello", - "", - access_key, - gmt, - "x-custom-header-a:" .. custom_header_a, - "x-custom-header-b:" .. custom_header_b + key_id, + "GET /hello", + "date: " .. gmt, + "x-custom-header-a: " .. custom_header_a, + "x-custom-header-b: " .. custom_header_b } signing_string = core.table.concat(signing_string, "\n") .. "\n" - core.log.info("signing_string:", signing_string) local signature = hmac:new(secret_key, hmac.ALGOS.SHA256):final(signing_string) core.log.info("signature:", ngx_encode_base64(signature)) - local auth_string = "hmac-auth-v1#" .. access_key .. "#" .. ngx_encode_base64(signature) .. "#" .. - "hmac-sha256#" .. gmt .. "#x-custom-header-a;x-custom-header-b" - local headers = {} - headers["Authorization"] = auth_string + headers["date"] = gmt + headers["Authorization"] = "Signature keyId=\"" .. key_id .. "\",algorithm=\"hmac-sha256\"" .. ",headers=\"@request-target date x-custom-header-a x-custom-header-b\",signature=\"" .. ngx_encode_base64(signature) .. "\"" headers["x-custom-header-a"] = custom_header_a headers["x-custom-header-b"] = custom_header_b - local code, body = t.test('/hello', - ngx.HTTP_PUT, - req_body, + ngx.HTTP_GET, + "", nil, headers ) @@ -706,35 +786,174 @@ passed -=== TEST 22: hit route without auth info +=== TEST 26: add consumer with plugin hmac-auth - empty configuration +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/consumers', + ngx.HTTP_PUT, + [[{ + "username": "foo", + "plugins": { + "hmac-auth": { + } + } + }]]) + + ngx.status = code + ngx.say(body) + } + } --- request -GET /hello ---- error_code: 401 +GET /t +--- error_code: 400 +--- response_body eval +qr/\{"error_msg":"invalid plugins configuration: failed to check the configuration of plugin hmac-auth err: property \\"(key_id|secret_key)\\" is required"\}/ + + + +=== TEST 27: add route with no allowed algorithms +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "plugins": { + "hmac-auth": { + "allowed_algorithms": [] + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + }, + "uri": "/hello" + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- request +GET /t +--- error_code: 400 +--- response_body eval +qr/validation failed: expect array to have at least 1 items/ + + + +=== TEST 28: update route with signed_headers +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "plugins": { + "hmac-auth": { + "hide_credentials": true + } + }, + "upstream": { + "nodes": { + "httpbin.org:80": 1 + }, + "type": "roundrobin" + }, + "uri": "/headers" + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- request +GET /t --- response_body -{"message":"client request can't be validated"} ---- grep_error_log eval -qr/client request can't be validated: [^,]+/ ---- grep_error_log_out -client request can't be validated: access key or signature missing +passed + + + +=== TEST 29: verify Authorization header missing +--- config +location /t { + content_by_lua_block { + local ngx_time = ngx.time + local ngx_http_time = ngx.http_time + local core = require("apisix.core") + local t = require("lib.test_admin") + local hmac = require("resty.hmac") + local ngx_encode_base64 = ngx.encode_base64 + + local secret_key = "my-secret-key" + local timestamp = ngx_time() + local gmt = ngx_http_time(timestamp) + local key_id = "my-access-key" + + local signing_string = { + key_id, + "GET /headers", + } + signing_string = core.table.concat(signing_string, "\n") .. "\n" + + local signature = hmac:new(secret_key, hmac.ALGOS.SHA256):final(signing_string) + core.log.info("signature:", ngx_encode_base64(signature)) + local headers = {} + headers["date"] = gmt + headers["Authorization"] = "Signature keyId=\"" .. key_id .. "\",algorithm=\"hmac-sha256\"" .. ",headers=\"@request-target\",signature=\"" .. ngx_encode_base64(signature) .. "\"" + local code, _, body = t.test('/headers', + ngx.HTTP_GET, + "", + nil, + headers + ) + if string.find(body,"Authorization") then + ngx.say("failed") + else + ngx.say("passed") + end + } +} +--- request +GET /t +--- response_body +passed -=== TEST 23: add consumer with signed_headers + +=== TEST 30 : update route with signed_headers --- config location /t { content_by_lua_block { local t = require("lib.test_admin").test - local code, body = t('/apisix/admin/consumers', + local code, body = t('/apisix/admin/routes/1', ngx.HTTP_PUT, [[{ - "username": "cook", "plugins": { "hmac-auth": { - "access_key": "my-access-key5", - "secret_key": "my-secret-key5", - "signed_headers": ["x-custom-header-a", "x-custom-header-b"] + "signed_headers": ["date","x-custom-header-a", "x-custom-header-b"] } - } + }, + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + }, + "uri": "/hello" }]] ) @@ -751,7 +970,7 @@ passed -=== TEST 24: verify with invalid signed header +=== TEST 31: verify error with the client only sends one in the request, but there are two in the signature --- config location /t { content_by_lua_block { @@ -762,27 +981,28 @@ location /t { local hmac = require("resty.hmac") local ngx_encode_base64 = ngx.encode_base64 - local secret_key = "my-secret-key5" + local secret_key = "my-secret-key" local timestamp = ngx_time() local gmt = ngx_http_time(timestamp) - local access_key = "my-access-key5" + local key_id = "my-access-key" local custom_header_a = "asld$%dfasf" - local custom_header_c = "23879fmsldfk" + local custom_header_b = "asld$%dfasf" - local signing_string = "GET" .. "/hello" .. "" .. - access_key .. gmt .. custom_header_a .. custom_header_c + local signing_string = { + key_id, + "GET /hello", + "date: " .. gmt, + "x-custom-header-a: " .. custom_header_a, + "x-custom-header-b: " .. custom_header_b + } + signing_string = core.table.concat(signing_string, "\n") .. "\n" local signature = hmac:new(secret_key, hmac.ALGOS.SHA256):final(signing_string) core.log.info("signature:", ngx_encode_base64(signature)) local headers = {} - headers["X-HMAC-SIGNATURE"] = ngx_encode_base64(signature) - headers["X-HMAC-ALGORITHM"] = "hmac-sha256" - headers["Date"] = gmt - headers["X-HMAC-ACCESS-KEY"] = access_key - headers["X-HMAC-SIGNED-HEADERS"] = "x-custom-header-a;x-custom-header-c" + headers["date"] = gmt + headers["Authorization"] = "Signature keyId=\"" .. key_id .. "\",algorithm=\"hmac-sha256\"" .. ",headers=\"@request-target date x-custom-header-a x-custom-header-b\",signature=\"" .. ngx_encode_base64(signature) .. "\"" headers["x-custom-header-a"] = custom_header_a - headers["x-custom-header-c"] = custom_header_c - local code, body = t.test('/hello', ngx.HTTP_GET, "", @@ -798,15 +1018,15 @@ location /t { GET /t --- error_code: 401 --- response_body eval -qr/{"message":"client request can't be validated"}/ +qr/client request can't be validated/ --- grep_error_log eval qr/client request can't be validated: [^,]+/ --- grep_error_log_out -client request can't be validated: Invalid signed header x-custom-header-c +client request can't be validated: Invalid signature -=== TEST 25: verify ok with signed headers +=== TEST 32: verify error with the client sends two in the request, but there is only one in the signature --- config location /t { content_by_lua_block { @@ -817,32 +1037,28 @@ location /t { local hmac = require("resty.hmac") local ngx_encode_base64 = ngx.encode_base64 - local secret_key = "my-secret-key5" + local secret_key = "my-secret-key" local timestamp = ngx_time() local gmt = ngx_http_time(timestamp) - local access_key = "my-access-key5" + local key_id = "my-access-key" local custom_header_a = "asld$%dfasf" + local custom_header_b = "asld$%dfasf" local signing_string = { - "GET", - "/hello", - "", - access_key, - gmt, - "x-custom-header-a:" .. custom_header_a + key_id, + "GET /hello", + "date: " .. gmt, + "x-custom-header-a: " .. custom_header_a } signing_string = core.table.concat(signing_string, "\n") .. "\n" local signature = hmac:new(secret_key, hmac.ALGOS.SHA256):final(signing_string) core.log.info("signature:", ngx_encode_base64(signature)) local headers = {} - headers["X-HMAC-SIGNATURE"] = ngx_encode_base64(signature) - headers["X-HMAC-ALGORITHM"] = "hmac-sha256" headers["date"] = gmt - headers["X-HMAC-ACCESS-KEY"] = access_key - headers["X-HMAC-SIGNED-HEADERS"] = "x-custom-header-a" + headers["Authorization"] = "Signature keyId=\"" .. key_id .. "\",algorithm=\"hmac-sha256\"" .. ",headers=\"@request-target date x-custom-header-a x-custom-header-b\",signature=\"" .. ngx_encode_base64(signature) .. "\"" headers["x-custom-header-a"] = custom_header_a - + headers["x-custom-header-b"] = custom_header_b local code, body = t.test('/hello', ngx.HTTP_GET, "", @@ -856,32 +1072,103 @@ location /t { } --- request GET /t ---- response_body -passed +--- error_code: 401 +--- response_body eval +qr/client request can't be validated/ +--- grep_error_log eval +qr/client request can't be validated: [^,]+/ +--- grep_error_log_out +client request can't be validated: Invalid signature -=== TEST 26: add consumer with plugin hmac-auth - empty configuration +=== TEST 33 : update route with allowed_algorithms --- config location /t { content_by_lua_block { local t = require("lib.test_admin").test - local code, body = t('/apisix/admin/consumers', + local code, body = t('/apisix/admin/routes/1', ngx.HTTP_PUT, [[{ - "username": "foo", "plugins": { "hmac-auth": { + "allowed_algorithms": ["hmac-sha256"] } - } - }]]) + }, + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + }, + "uri": "/hello" + }]] + ) - ngx.status = code + if code >= 300 then + ngx.status = code + end ngx.say(body) } } --- request GET /t ---- error_code: 400 +--- response_body +passed + + + +=== TEST 34: verify with hmac-sha1 algorithm, not part of allowed_algorithms +--- config +location /t { + content_by_lua_block { + local ngx_time = ngx.time + local ngx_http_time = ngx.http_time + local core = require("apisix.core") + local t = require("lib.test_admin") + local hmac = require("resty.hmac") + local ngx_encode_base64 = ngx.encode_base64 + + local secret_key = "my-secret-key" + local timestamp = ngx_time() + local gmt = ngx_http_time(timestamp) + local key_id = "my-access-key" + local custom_header_a = "asld$%dfasf" + local custom_header_b = "asld$%dfasf" + + local signing_string = { + key_id, + "GET /hello", + "date: " .. gmt, + "x-custom-header-a: " .. custom_header_a, + "x-custom-header-b: " .. custom_header_b + } + signing_string = core.table.concat(signing_string, "\n") .. "\n" + + local signature = hmac:new(secret_key, hmac.ALGOS.SHA1):final(signing_string) + core.log.info("signature:", ngx_encode_base64(signature)) + local headers = {} + headers["date"] = gmt + headers["Authorization"] = "Signature keyId=\"" .. key_id .. "\",algorithm=\"hmac-sha1\"" .. ",headers=\"@request-target date x-custom-header-a x-custom-header-b\",signature=\"" .. ngx_encode_base64(signature) .. "\"" + headers["x-custom-header-a"] = custom_header_a + headers["x-custom-header-b"] = custom_header_b + local code, body = t.test('/hello', + ngx.HTTP_GET, + "", + nil, + headers + ) + + ngx.status = code + ngx.say(body) + } +} +--- request +GET /t +--- error_code: 401 --- response_body eval -qr/\{"error_msg":"invalid plugins configuration: failed to check the configuration of plugin hmac-auth err: property \\"(access|secret)_key\\" is required"\}/ +qr/client request can't be validated/ +--- grep_error_log eval +qr/client request can't be validated: [^,]+/ +--- grep_error_log_out +client request can't be validated: Invalid algorithm diff --git a/t/plugin/hmac-auth2.t b/t/plugin/hmac-auth2.t index 9b78c9075ca5..7d6e8600cbc6 100644 --- a/t/plugin/hmac-auth2.t +++ b/t/plugin/hmac-auth2.t @@ -65,329 +65,7 @@ passed -=== TEST 2: keep_headers field is empty ---- config - location /t { - content_by_lua_block { - local t = require("lib.test_admin").test - local code, body = t('/apisix/admin/consumers', - ngx.HTTP_PUT, - [[{ - "username": "james", - "plugins": { - "hmac-auth": { - "access_key": "my-access-key4", - "secret_key": "my-secret-key4" - } - } - }]] - ) - - if code >= 300 then - ngx.status = code - end - ngx.say(body) - } - } ---- response_body -passed - - - -=== TEST 3: verify pass(keep_headers field is empty), remove http request header ---- config -location /t { - content_by_lua_block { - local ngx_time = ngx.time - local ngx_http_time = ngx.http_time - local core = require("apisix.core") - local t = require("lib.test_admin") - local hmac = require("resty.hmac") - local ngx_re = require("ngx.re") - local ngx_encode_base64 = ngx.encode_base64 - - local data = {cert = "ssl_cert", key = "ssl_key", sni = "test.com"} - local req_body = core.json.encode(data) - req_body = req_body or "" - - local secret_key = "my-secret-key4" - local timestamp = ngx_time() - local gmt = ngx_http_time(timestamp) - local access_key = "my-access-key4" - local custom_header_a = "asld$%dfasf" - local custom_header_b = "23879fmsldfk" - - local signing_string = { - "PUT", - "/uri", - "", - access_key, - gmt, - "x-custom-header-a:" .. custom_header_a, - "x-custom-header-b:" .. custom_header_b - } - signing_string = core.table.concat(signing_string, "\n") .. "\n" - core.log.info("signing_string:", signing_string) - - local signature = hmac:new(secret_key, hmac.ALGOS.SHA256):final(signing_string) - core.log.info("signature:", ngx_encode_base64(signature)) - local headers = {} - headers["X-HMAC-SIGNATURE"] = ngx_encode_base64(signature) - headers["X-HMAC-ALGORITHM"] = "hmac-sha256" - headers["Date"] = gmt - headers["X-HMAC-ACCESS-KEY"] = access_key - headers["X-HMAC-SIGNED-HEADERS"] = "x-custom-header-a;x-custom-header-b" - headers["x-custom-header-a"] = custom_header_a - headers["x-custom-header-b"] = custom_header_b - - local code, _, body = t.test('/uri', - ngx.HTTP_PUT, - req_body, - nil, - headers - ) - - if code >= 300 then - ngx.status = code - end - - local headers_arr = ngx_re.split(body, "\n") - for i, v in ipairs(headers_arr) do - if i ~= 4 and i ~= 6 then -- skip date and user-agent field - ngx.say(v) - end - end - } -} ---- response_body -uri: /uri -content-length: 52 -content-type: application/x-www-form-urlencoded -host: 127.0.0.1:1984 -x-custom-header-a: asld$%dfasf -x-custom-header-b: 23879fmsldfk -x-hmac-access-key: my-access-key4 -x-real-ip: 127.0.0.1 - - - -=== TEST 4: keep_headers field is false ---- config - location /t { - content_by_lua_block { - local t = require("lib.test_admin").test - local code, body = t('/apisix/admin/consumers', - ngx.HTTP_PUT, - [[{ - "username": "james", - "plugins": { - "hmac-auth": { - "access_key": "my-access-key4", - "secret_key": "my-secret-key4", - "keep_headers": false - } - } - }]] - ) - - if code >= 300 then - ngx.status = code - end - ngx.say(body) - } - } ---- response_body -passed - - - -=== TEST 5: verify pass(keep_headers field is false), remove http request header ---- config -location /t { - content_by_lua_block { - local ngx_time = ngx.time - local ngx_http_time = ngx.http_time - local core = require("apisix.core") - local t = require("lib.test_admin") - local hmac = require("resty.hmac") - local ngx_re = require("ngx.re") - local ngx_encode_base64 = ngx.encode_base64 - - local data = {cert = "ssl_cert", key = "ssl_key", sni = "test.com"} - local req_body = core.json.encode(data) - req_body = req_body or "" - - local secret_key = "my-secret-key4" - local timestamp = ngx_time() - local gmt = ngx_http_time(timestamp) - local access_key = "my-access-key4" - local custom_header_a = "asld$%dfasf" - local custom_header_b = "23879fmsldfk" - - local signing_string = { - "PUT", - "/uri", - "", - access_key, - gmt, - "x-custom-header-a:" .. custom_header_a, - "x-custom-header-b:" .. custom_header_b - } - signing_string = core.table.concat(signing_string, "\n") .. "\n" - core.log.info("signing_string:", signing_string) - - local signature = hmac:new(secret_key, hmac.ALGOS.SHA256):final(signing_string) - core.log.info("signature:", ngx_encode_base64(signature)) - local headers = {} - headers["X-HMAC-SIGNATURE"] = ngx_encode_base64(signature) - headers["X-HMAC-ALGORITHM"] = "hmac-sha256" - headers["Date"] = gmt - headers["X-HMAC-ACCESS-KEY"] = access_key - headers["X-HMAC-SIGNED-HEADERS"] = "x-custom-header-a;x-custom-header-b" - headers["x-custom-header-a"] = custom_header_a - headers["x-custom-header-b"] = custom_header_b - - local code, _, body = t.test('/uri', - ngx.HTTP_PUT, - req_body, - nil, - headers - ) - - if code >= 300 then - ngx.status = code - end - - local headers_arr = ngx_re.split(body, "\n") - for i, v in ipairs(headers_arr) do - if i ~= 4 and i ~= 6 then -- skip date and user-agent field - ngx.say(v) - end - end - } -} ---- response_body -uri: /uri -content-length: 52 -content-type: application/x-www-form-urlencoded -host: 127.0.0.1:1984 -x-custom-header-a: asld$%dfasf -x-custom-header-b: 23879fmsldfk -x-hmac-access-key: my-access-key4 -x-real-ip: 127.0.0.1 - - - -=== TEST 6: keep_headers field is true ---- config - location /t { - content_by_lua_block { - local t = require("lib.test_admin").test - local code, body = t('/apisix/admin/consumers', - ngx.HTTP_PUT, - [[{ - "username": "james", - "plugins": { - "hmac-auth": { - "access_key": "my-access-key4", - "secret_key": "my-secret-key4", - "keep_headers": true - } - } - }]] - ) - - if code >= 300 then - ngx.status = code - end - ngx.say(body) - } - } ---- response_body -passed - - - -=== TEST 7: verify pass(keep_headers field is true), keep http request header ---- config -location /t { - content_by_lua_block { - local ngx_time = ngx.time - local ngx_http_time = ngx.http_time - local core = require("apisix.core") - local t = require("lib.test_admin") - local hmac = require("resty.hmac") - local ngx_re = require("ngx.re") - local ngx_encode_base64 = ngx.encode_base64 - - local data = {cert = "ssl_cert", key = "ssl_key", sni = "test.com"} - local req_body = core.json.encode(data) - req_body = req_body or "" - - local secret_key = "my-secret-key4" - local timestamp = ngx_time() - local gmt = ngx_http_time(timestamp) - local access_key = "my-access-key4" - local custom_header_a = "asld$%dfasf" - local custom_header_b = "23879fmsldfk" - - local signing_string = { - "PUT", - "/uri", - "", - access_key, - gmt, - "x-custom-header-a:" .. custom_header_a, - "x-custom-header-b:" .. custom_header_b - } - signing_string = core.table.concat(signing_string, "\n") .. "\n" - core.log.info("signing_string:", signing_string) - - local signature = hmac:new(secret_key, hmac.ALGOS.SHA256):final(signing_string) - core.log.info("signature:", ngx_encode_base64(signature)) - local headers = {} - headers["X-HMAC-SIGNATURE"] = ngx_encode_base64(signature) - headers["X-HMAC-ALGORITHM"] = "hmac-sha256" - headers["Date"] = gmt - headers["X-HMAC-ACCESS-KEY"] = access_key - headers["X-HMAC-SIGNED-HEADERS"] = "x-custom-header-a;x-custom-header-b" - headers["x-custom-header-a"] = custom_header_a - headers["x-custom-header-b"] = custom_header_b - - local code, _, body = t.test('/uri', - ngx.HTTP_PUT, - req_body, - nil, - headers - ) - - if code >= 300 then - ngx.status = code - end - - local headers_arr = ngx_re.split(body, "\n") - for i, v in ipairs(headers_arr) do - if i ~= 4 and i ~= 6 and i ~= 11 then -- skip date, user-agent and x-hmac-signature field - ngx.say(v) - end - end - } -} ---- response_body -uri: /uri -content-length: 52 -content-type: application/x-www-form-urlencoded -host: 127.0.0.1:1984 -x-custom-header-a: asld$%dfasf -x-custom-header-b: 23879fmsldfk -x-hmac-access-key: my-access-key4 -x-hmac-algorithm: hmac-sha256 -x-hmac-signed-headers: x-custom-header-a;x-custom-header-b -x-real-ip: 127.0.0.1 - - - -=== TEST 8: get the default schema +=== TEST 2: get the default schema --- config location /t { content_by_lua_block { @@ -396,7 +74,7 @@ x-real-ip: 127.0.0.1 ngx.HTTP_GET, nil, [[ -{"properties":{},"title":"work with route or service object","type":"object"} +{"type":"object","$comment":"this is a mark for our injected plugin schema","title":"work with route or service object","properties":{"allowed_algorithms":{"type":"array","default":["hmac-sha1","hmac-sha256","hmac-sha512"],"items":{"type":"string","enum":["hmac-sha1","hmac-sha256","hmac-sha512"]},"minItems":1},"_meta":{"type":"object","properties":{"filter":{"description":"filter determines whether the plugin needs to be executed at runtime","type":"array"},"error_response":{"oneOf":[{"type":"string"},{"type":"object"}]},"disable":{"type":"boolean"},"priority":{"description":"priority of plugins by customized order","type":"integer"}}},"clock_skew":{"type":"integer","default":300,"minimum":1},"signed_headers":{"type":"array","items":{"type":"string","minLength":1,"maxLength":50}},"hide_credentials":{"type":"boolean","default":false},"validate_request_body":{"type":"boolean","default":false,"title":"A boolean value telling the plugin to enable body validation"}}} ]] ) ngx.status = code @@ -405,7 +83,7 @@ x-real-ip: 127.0.0.1 -=== TEST 9: get the schema by schema_type +=== TEST 3: get the schema by schema_type --- config location /t { content_by_lua_block { @@ -414,7 +92,7 @@ x-real-ip: 127.0.0.1 ngx.HTTP_GET, nil, [[ -{"title":"work with consumer object","required":["access_key","secret_key"],"properties":{"clock_skew":{"default":0,"type":"integer"},"encode_uri_params":{"title":"Whether to escape the uri parameter","default":true,"type":"boolean"},"keep_headers":{"title":"whether to keep the http request header","default":false,"type":"boolean"},"secret_key":{"minLength":1,"maxLength":256,"type":"string"},"algorithm":{"type":"string","default":"hmac-sha256","enum":["hmac-sha1","hmac-sha256","hmac-sha512"]},"signed_headers":{"items":{"minLength":1,"maxLength":50,"type":"string"},"type":"array"},"access_key":{"minLength":1,"maxLength":256,"type":"string"}},"type":"object"} +{"title":"work with consumer object","required":["key_id","secret_key"],"properties":{"secret_key":{"minLength":1,"maxLength":256,"type":"string"},"key_id":{"minLength":1,"maxLength":256,"type":"string"}},"type":"object"} ]] ) ngx.status = code @@ -423,7 +101,7 @@ x-real-ip: 127.0.0.1 -=== TEST 10: get the schema by error schema_type +=== TEST 4: get the schema by error schema_type --- config location /t { content_by_lua_block { @@ -441,7 +119,7 @@ x-real-ip: 127.0.0.1 -=== TEST 11: enable hmac auth plugin using admin api +=== TEST 5: enable hmac auth plugin using admin api --- config location /t { content_by_lua_block { @@ -470,417 +148,3 @@ x-real-ip: 127.0.0.1 } --- response_body passed - - - -=== TEST 12: encode_uri_params field is true, the signature of uri enables escaping ---- config - location /t { - content_by_lua_block { - local t = require("lib.test_admin").test - local code, body = t('/apisix/admin/consumers', - ngx.HTTP_PUT, - [[{ - "username": "james", - "plugins": { - "hmac-auth": { - "access_key": "my-access-key6", - "secret_key": "my-secret-key6" - } - } - }]] - ) - - if code >= 300 then - ngx.status = code - end - ngx.say(body) - } - } ---- response_body -passed - - - -=== TEST 13: verify: invalid signature (Lowercase letters of escape characters are converted to uppercase.) ---- config -location /t { - content_by_lua_block { - local ngx_time = ngx.time - local ngx_http_time = ngx.http_time - local core = require("apisix.core") - local t = require("lib.test_admin") - local hmac = require("resty.hmac") - local ngx_encode_base64 = ngx.encode_base64 - - local secret_key = "my-secret-key6" - local timestamp = ngx_time() - local gmt = ngx_http_time(timestamp) - local access_key = "my-access-key6" - local custom_header_a = "asld$%dfasf" - local custom_header_b = "23879fmsldfk" - - local signing_string = { - "GET", - "/hello", - "name=LeBron%2Cjames&name2=%2c%3e", - access_key, - gmt, - "x-custom-header-a:" .. custom_header_a, - "x-custom-header-b:" .. custom_header_b - } - signing_string = core.table.concat(signing_string, "\n") .. "\n" - core.log.info("signing_string:", signing_string) - - local signature = hmac:new(secret_key, hmac.ALGOS.SHA256):final(signing_string) - core.log.info("signature:", ngx_encode_base64(signature)) - local headers = {} - headers["X-HMAC-SIGNATURE"] = ngx_encode_base64(signature) - headers["X-HMAC-ALGORITHM"] = "hmac-sha256" - headers["Date"] = gmt - headers["X-HMAC-ACCESS-KEY"] = access_key - headers["X-HMAC-SIGNED-HEADERS"] = "x-custom-header-a;x-custom-header-b" - headers["x-custom-header-a"] = custom_header_a - headers["x-custom-header-b"] = custom_header_b - - local code, body = t.test('/hello?name=LeBron%2Cjames&name2=%2c%3e', - ngx.HTTP_GET, - "", - nil, - headers - ) - - ngx.status = code - ngx.say(body) - } -} ---- error_code: 401 ---- grep_error_log eval -qr/client request can't be validated: [^,]+/ ---- grep_error_log_out -client request can't be validated: Invalid signature ---- response_body eval -qr/\{"message":"client request can't be validated"\}/ ---- error_log eval -qr/name=LeBron\%2Cjames\&name2=\%2C\%3E/ - - - -=== TEST 14: verify: ok (The letters in the escape character are all uppercase.) ---- config -location /t { - content_by_lua_block { - local ngx_time = ngx.time - local ngx_http_time = ngx.http_time - local core = require("apisix.core") - local t = require("lib.test_admin") - local hmac = require("resty.hmac") - local ngx_encode_base64 = ngx.encode_base64 - - local secret_key = "my-secret-key6" - local timestamp = ngx_time() - local gmt = ngx_http_time(timestamp) - local access_key = "my-access-key6" - local custom_header_a = "asld$%dfasf" - local custom_header_b = "23879fmsldfk" - - local signing_string = { - "GET", - "/hello", - "name=LeBron%2Cjames&name2=%2C%3E", - access_key, - gmt, - "x-custom-header-a:" .. custom_header_a, - "x-custom-header-b:" .. custom_header_b - } - signing_string = core.table.concat(signing_string, "\n") .. "\n" - core.log.info("signing_string:", signing_string) - - local signature = hmac:new(secret_key, hmac.ALGOS.SHA256):final(signing_string) - core.log.info("signature:", ngx_encode_base64(signature)) - local headers = {} - headers["X-HMAC-SIGNATURE"] = ngx_encode_base64(signature) - headers["X-HMAC-ALGORITHM"] = "hmac-sha256" - headers["Date"] = gmt - headers["X-HMAC-ACCESS-KEY"] = access_key - headers["X-HMAC-SIGNED-HEADERS"] = "x-custom-header-a;x-custom-header-b" - headers["x-custom-header-a"] = custom_header_a - headers["x-custom-header-b"] = custom_header_b - - local code, body = t.test('/hello?name=LeBron%2Cjames&name2=%2C%3E', - ngx.HTTP_GET, - "", - nil, - headers - ) - - ngx.status = code - ngx.say(body) - } -} ---- response_body -passed ---- no_error_log - - - -=== TEST 15: encode_uri_params field is false, uri’s signature is enabled for escaping ---- config - location /t { - content_by_lua_block { - local t = require("lib.test_admin").test - local code, body = t('/apisix/admin/consumers', - ngx.HTTP_PUT, - [[{ - "username": "james", - "plugins": { - "hmac-auth": { - "access_key": "my-access-key6", - "secret_key": "my-secret-key6", - "encode_uri_params": false - } - } - }]] - ) - - if code >= 300 then - ngx.status = code - end - ngx.say(body) - } - } ---- response_body -passed - - - -=== TEST 16: verify: invalid signature (uri’s signature is enabled for escaping) ---- config -location /t { - content_by_lua_block { - local ngx_time = ngx.time - local ngx_http_time = ngx.http_time - local core = require("apisix.core") - local t = require("lib.test_admin") - local hmac = require("resty.hmac") - local ngx_encode_base64 = ngx.encode_base64 - - local secret_key = "my-secret-key6" - local timestamp = ngx_time() - local gmt = ngx_http_time(timestamp) - local access_key = "my-access-key6" - local custom_header_a = "asld$%dfasf" - local custom_header_b = "23879fmsldfk" - - local signing_string = { - "GET", - "/hello", - "name=LeBron%2Cjames&name2=%2c%3e", - access_key, - gmt, - "x-custom-header-a:" .. custom_header_a, - "x-custom-header-b:" .. custom_header_b - } - signing_string = core.table.concat(signing_string, "\n") .. "\n" - core.log.info("signing_string:", signing_string) - - local signature = hmac:new(secret_key, hmac.ALGOS.SHA256):final(signing_string) - core.log.info("signature:", ngx_encode_base64(signature)) - local headers = {} - headers["X-HMAC-SIGNATURE"] = ngx_encode_base64(signature) - headers["X-HMAC-ALGORITHM"] = "hmac-sha256" - headers["Date"] = gmt - headers["X-HMAC-ACCESS-KEY"] = access_key - headers["X-HMAC-SIGNED-HEADERS"] = "x-custom-header-a;x-custom-header-b" - headers["x-custom-header-a"] = custom_header_a - headers["x-custom-header-b"] = custom_header_b - - local code, body = t.test('/hello?name=LeBron%2Cjames&name2=%2c%3e', - ngx.HTTP_GET, - "", - nil, - headers - ) - - ngx.status = code - ngx.say(body) - } -} ---- error_code: 401 ---- grep_error_log eval -qr/client request can't be validated: [^,]+/ ---- grep_error_log_out -client request can't be validated: Invalid signature ---- response_body eval -qr/\{"message":"client request can't be validated"\}/ - - - -=== TEST 17: verify: ok ---- config -location /t { - content_by_lua_block { - local ngx_time = ngx.time - local ngx_http_time = ngx.http_time - local core = require("apisix.core") - local t = require("lib.test_admin") - local hmac = require("resty.hmac") - local ngx_encode_base64 = ngx.encode_base64 - - local secret_key = "my-secret-key6" - local timestamp = ngx_time() - local gmt = ngx_http_time(timestamp) - local access_key = "my-access-key6" - local custom_header_a = "asld$%dfasf" - local custom_header_b = "23879fmsldfk" - - local signing_string = { - "GET", - "/hello", - "name=LeBron,james&name2=,>", - access_key, - gmt, - "x-custom-header-a:" .. custom_header_a, - "x-custom-header-b:" .. custom_header_b - } - signing_string = core.table.concat(signing_string, "\n") .. "\n" - core.log.info("signing_string:", signing_string) - - local signature = hmac:new(secret_key, hmac.ALGOS.SHA256):final(signing_string) - core.log.info("signature:", ngx_encode_base64(signature)) - local headers = {} - headers["X-HMAC-SIGNATURE"] = ngx_encode_base64(signature) - headers["X-HMAC-ALGORITHM"] = "hmac-sha256" - headers["Date"] = gmt - headers["X-HMAC-ACCESS-KEY"] = access_key - headers["X-HMAC-SIGNED-HEADERS"] = "x-custom-header-a;x-custom-header-b" - headers["x-custom-header-a"] = custom_header_a - headers["x-custom-header-b"] = custom_header_b - - local code, body = t.test('/hello?name=LeBron%2Cjames&name2=%2c%3e', - ngx.HTTP_GET, - "", - nil, - headers - ) - - ngx.status = code - ngx.say(body) - } -} ---- response_body -passed - - - -=== TEST 18: verify: ok, the request parameter is missing `=`. ---- config -location /t { - content_by_lua_block { - local ngx_time = ngx.time - local ngx_http_time = ngx.http_time - local core = require("apisix.core") - local t = require("lib.test_admin") - local hmac = require("resty.hmac") - local ngx_encode_base64 = ngx.encode_base64 - - local secret_key = "my-secret-key6" - local timestamp = ngx_time() - local gmt = ngx_http_time(timestamp) - local access_key = "my-access-key6" - local custom_header_a = "asld$%dfasf" - local custom_header_b = "23879fmsldfk" - - local signing_string = { - "GET", - "/hello", - "age=&name=jack", - access_key, - gmt, - "x-custom-header-a:" .. custom_header_a, - "x-custom-header-b:" .. custom_header_b - } - signing_string = core.table.concat(signing_string, "\n") .. "\n" - core.log.info("signing_string:", signing_string) - - local signature = hmac:new(secret_key, hmac.ALGOS.SHA256):final(signing_string) - core.log.info("signature:", ngx_encode_base64(signature)) - local headers = {} - headers["X-HMAC-SIGNATURE"] = ngx_encode_base64(signature) - headers["X-HMAC-ALGORITHM"] = "hmac-sha256" - headers["Date"] = gmt - headers["X-HMAC-ACCESS-KEY"] = access_key - headers["X-HMAC-SIGNED-HEADERS"] = "x-custom-header-a;x-custom-header-b" - headers["x-custom-header-a"] = custom_header_a - headers["x-custom-header-b"] = custom_header_b - - local code, body = t.test('/hello?name=jack&age', - ngx.HTTP_GET, - "", - nil, - headers - ) - - ngx.status = code - ngx.say(body) - } -} ---- response_body -passed - - - -=== TEST 19: verify: ok, the value of the request parameter is true. ---- config -location /t { - content_by_lua_block { - local ngx_time = ngx.time - local ngx_http_time = ngx.http_time - local core = require("apisix.core") - local t = require("lib.test_admin") - local hmac = require("resty.hmac") - local ngx_encode_base64 = ngx.encode_base64 - - local secret_key = "my-secret-key6" - local timestamp = ngx_time() - local gmt = ngx_http_time(timestamp) - local access_key = "my-access-key6" - local custom_header_a = "asld$%dfasf" - local custom_header_b = "23879fmsldfk" - - local signing_string = { - "GET", - "/hello", - "age=true&name=jack", - access_key, - gmt, - "x-custom-header-a:" .. custom_header_a, - "x-custom-header-b:" .. custom_header_b - } - signing_string = core.table.concat(signing_string, "\n") .. "\n" - core.log.info("signing_string:", signing_string) - - local signature = hmac:new(secret_key, hmac.ALGOS.SHA256):final(signing_string) - core.log.info("signature:", ngx_encode_base64(signature)) - local headers = {} - headers["X-HMAC-SIGNATURE"] = ngx_encode_base64(signature) - headers["X-HMAC-ALGORITHM"] = "hmac-sha256" - headers["Date"] = gmt - headers["X-HMAC-ACCESS-KEY"] = access_key - headers["X-HMAC-SIGNED-HEADERS"] = "x-custom-header-a;x-custom-header-b" - headers["x-custom-header-a"] = custom_header_a - headers["x-custom-header-b"] = custom_header_b - - local code, body = t.test('/hello?name=jack&age=true', - ngx.HTTP_GET, - "", - nil, - headers - ) - - ngx.status = code - ngx.say(body) - } -} ---- response_body -passed diff --git a/t/plugin/hmac-auth3.t b/t/plugin/hmac-auth3.t index 70c5048c89dd..faeebad99b4d 100644 --- a/t/plugin/hmac-auth3.t +++ b/t/plugin/hmac-auth3.t @@ -49,9 +49,8 @@ __DATA__ "username": "robin", "plugins": { "hmac-auth": { - "access_key": "my-access-key", - "secret_key": "my-secret-key", - "validate_request_body": true + "key_id": "my-access-key", + "secret_key": "my-secret-key" } } }]] @@ -76,7 +75,9 @@ passed ngx.HTTP_PUT, [[{ "plugins": { - "hmac-auth": {} + "hmac-auth": { + "validate_request_body": true + } }, "upstream": { "nodes": { @@ -113,19 +114,17 @@ passed local secret_key = "my-secret-key" local timestamp = ngx_time() local gmt = ngx_http_time(timestamp) - local access_key = "my-access-key" + local key_id = "my-access-key" local custom_header_a = "asld$%dfasf" local custom_header_b = "23879fmsldfk" local body = "{\"name\": \"world\"}" local signing_string = { - "POST", - "/hello", - "", - access_key, - gmt, - "x-custom-header-a:" .. custom_header_a, - "x-custom-header-b:" .. custom_header_b + key_id, + "POST /hello", + "date: " .. gmt, + "x-custom-header-a: " .. custom_header_a, + "x-custom-header-b: " .. custom_header_b } signing_string = core.table.concat(signing_string, "\n") .. "\n" core.log.info("signing_string:", signing_string) @@ -134,11 +133,8 @@ passed core.log.info("signature:", ngx_encode_base64(signature)) local headers = {} - headers["X-HMAC-SIGNATURE"] = ngx_encode_base64(signature) - headers["X-HMAC-ALGORITHM"] = "hmac-sha256" headers["Date"] = gmt - headers["X-HMAC-ACCESS-KEY"] = access_key - headers["X-HMAC-SIGNED-HEADERS"] = "x-custom-header-a;x-custom-header-b" + headers["Authorization"] = "Signature keyId=\"" .. key_id .. "\",algorithm=\"hmac-sha256\"" .. ",headers=\"@request-target date x-custom-header-a x-custom-header-b\",signature=\"" .. ngx_encode_base64(signature) .. "\"" headers["x-custom-header-a"] = custom_header_a headers["x-custom-header-b"] = custom_header_b @@ -177,19 +173,17 @@ qr/\{"message":"client request can't be validated"\}/ local secret_key = "my-secret-key" local timestamp = ngx_time() local gmt = ngx_http_time(timestamp) - local access_key = "my-access-key" + local key_id = "my-access-key" local custom_header_a = "asld$%dfasf" local custom_header_b = "23879fmsldfk" local body = "{\"name\": \"world\"}" local signing_string = { - "POST", - "/hello", - "", - access_key, - gmt, - "x-custom-header-a:" .. custom_header_a, - "x-custom-header-b:" .. custom_header_b + key_id, + "POST /hello", + "date: " .. gmt, + "x-custom-header-a: " .. custom_header_a, + "x-custom-header-b: " .. custom_header_b } signing_string = core.table.concat(signing_string, "\n") .. "\n" core.log.info("signing_string:", signing_string) @@ -198,12 +192,9 @@ qr/\{"message":"client request can't be validated"\}/ core.log.info("signature:", ngx_encode_base64(signature)) local headers = {} - headers["X-HMAC-SIGNATURE"] = ngx_encode_base64(signature) - headers["X-HMAC-ALGORITHM"] = "hmac-sha256" headers["Date"] = gmt - headers["X-HMAC-DIGEST"] = "hello" - headers["X-HMAC-ACCESS-KEY"] = access_key - headers["X-HMAC-SIGNED-HEADERS"] = "x-custom-header-a;x-custom-header-b" + headers["Authorization"] ="Signature keyId=\"" .. key_id .. "\",algorithm=\"hmac-sha256\"" .. ",headers=\"@request-target date x-custom-header-a x-custom-header-b\",signature=\"" .. ngx_encode_base64(signature) .. "\"" + headers["Digest"] = "hello" headers["x-custom-header-a"] = custom_header_a headers["x-custom-header-b"] = custom_header_b @@ -242,264 +233,35 @@ qr/\{"message":"client request can't be validated"\}/ local secret_key = "my-secret-key" local timestamp = ngx_time() local gmt = ngx_http_time(timestamp) - local access_key = "my-access-key" + local key_id = "my-access-key" local custom_header_a = "asld$%dfasf" local custom_header_b = "23879fmsldfk" local body = "{\"name\": \"world\"}" local signing_string = { - "POST", - "/hello", - "", - access_key, - gmt, - "x-custom-header-a:" .. custom_header_a, - "x-custom-header-b:" .. custom_header_b - } - signing_string = core.table.concat(signing_string, "\n") .. "\n" - core.log.info("signing_string:", signing_string) - - local signature = hmac:new(secret_key, hmac.ALGOS.SHA256):final(signing_string) - local body_digest = hmac:new(secret_key, hmac.ALGOS.SHA256):final(body) - - core.log.info("signature:", ngx_encode_base64(signature)) - local headers = {} - headers["X-HMAC-SIGNATURE"] = ngx_encode_base64(signature) - headers["X-HMAC-ALGORITHM"] = "hmac-sha256" - headers["Date"] = gmt - headers["X-HMAC-DIGEST"] = ngx_encode_base64(body_digest) - headers["X-HMAC-ACCESS-KEY"] = access_key - headers["X-HMAC-SIGNED-HEADERS"] = "x-custom-header-a;x-custom-header-b" - headers["x-custom-header-a"] = custom_header_a - headers["x-custom-header-b"] = custom_header_b - - local code, body = t.test('/hello', - ngx.HTTP_POST, - body, - nil, - headers - ) - - ngx.status = code - ngx.say(body) - } - } ---- response_body -passed - - - -=== TEST 6: add consumer with max_req_body ---- config - location /t { - content_by_lua_block { - local t = require("lib.test_admin").test - local code, body = t('/apisix/admin/consumers', - ngx.HTTP_PUT, - [[{ - "username": "robin", - "plugins": { - "hmac-auth": { - "access_key": "my-access-key", - "secret_key": "my-secret-key", - "validate_request_body": true, - "max_req_body": 1024 - } - } - }]] - ) - - ngx.status = code - ngx.say(body) - } - } ---- response_body -passed - - - -=== TEST 7: Exceed body limit size ---- config - location /t { - content_by_lua_block { - local ngx_time = ngx.time - local ngx_http_time = ngx.http_time - local core = require("apisix.core") - local t = require("lib.test_admin") - local hmac = require("resty.hmac") - local ngx_encode_base64 = ngx.encode_base64 - - local secret_key = "my-secret-key" - local timestamp = ngx_time() - local gmt = ngx_http_time(timestamp) - local access_key = "my-access-key" - local custom_header_a = "asld$%dfasf" - local custom_header_b = "23879fmsldfk" - local body = ("-1Aa#"):rep(205) - - local signing_string = { - "POST", - "/hello", - "", - access_key, - gmt, - "x-custom-header-a:" .. custom_header_a, - "x-custom-header-b:" .. custom_header_b + key_id, + "POST /hello", + "date: " .. gmt, + "x-custom-header-a: " .. custom_header_a, + "x-custom-header-b: " .. custom_header_b } signing_string = core.table.concat(signing_string, "\n") .. "\n" core.log.info("signing_string:", signing_string) local signature = hmac:new(secret_key, hmac.ALGOS.SHA256):final(signing_string) - local body_digest = hmac:new(secret_key, hmac.ALGOS.SHA256):final(body) - - core.log.info("signature:", ngx_encode_base64(signature)) - local headers = {} - headers["X-HMAC-SIGNATURE"] = ngx_encode_base64(signature) - headers["X-HMAC-ALGORITHM"] = "hmac-sha256" - headers["Date"] = gmt - headers["X-HMAC-DIGEST"] = ngx_encode_base64(body_digest) - headers["X-HMAC-ACCESS-KEY"] = access_key - headers["X-HMAC-SIGNED-HEADERS"] = "x-custom-header-a;x-custom-header-b" - headers["x-custom-header-a"] = custom_header_a - headers["x-custom-header-b"] = custom_header_b - - local code, body = t.test('/hello', - ngx.HTTP_POST, - body, - nil, - headers - ) - - ngx.status = code - ngx.say(body) - } - } ---- error_code: 401 ---- grep_error_log eval -qr/client request can't be validated: [^,]+/ ---- grep_error_log_out -client request can't be validated: Exceed body limit size ---- response_body eval -qr/\{"message":"client request can't be validated"}/ - - - -=== TEST 8: Test custom request body digest header name with mismatched header. ---- yaml_config -plugin_attr: - hmac-auth: - body_digest_key: "X-Digest-Custom" ---- config - location /t { - content_by_lua_block { - local ngx_time = ngx.time - local ngx_http_time = ngx.http_time - local core = require("apisix.core") - local t = require("lib.test_admin") - local hmac = require("resty.hmac") local ngx_encode_base64 = ngx.encode_base64 - local secret_key = "my-secret-key" - local timestamp = ngx_time() - local gmt = ngx_http_time(timestamp) - local access_key = "my-access-key" - local custom_header_a = "asld$%dfasf" - local custom_header_b = "23879fmsldfk" - local body = "{\"name\": \"world\"}" - - local signing_string = { - "POST", - "/hello", - "", - access_key, - gmt, - "x-custom-header-a:" .. custom_header_a, - "x-custom-header-b:" .. custom_header_b - } - signing_string = core.table.concat(signing_string, "\n") .. "\n" - core.log.info("signing_string:", signing_string) - - local signature = hmac:new(secret_key, hmac.ALGOS.SHA256):final(signing_string) - local body_digest = hmac:new(secret_key, hmac.ALGOS.SHA256):final(body) + local resty_sha256 = require("resty.sha256") + local hash = resty_sha256:new() + hash:update(body) + local digest = hash:final() + local body_digest = ngx_encode_base64(digest) core.log.info("signature:", ngx_encode_base64(signature)) local headers = {} - headers["X-HMAC-SIGNATURE"] = ngx_encode_base64(signature) - headers["X-HMAC-ALGORITHM"] = "hmac-sha256" headers["Date"] = gmt - headers["X-HMAC-DIGEST"] = ngx_encode_base64(body_digest) - headers["X-HMAC-ACCESS-KEY"] = access_key - headers["X-HMAC-SIGNED-HEADERS"] = "x-custom-header-a;x-custom-header-b" - headers["x-custom-header-a"] = custom_header_a - headers["x-custom-header-b"] = custom_header_b - - local code, body = t.test('/hello', - ngx.HTTP_POST, - body, - nil, - headers - ) - - ngx.status = code - ngx.say(body) - } - } ---- error_code: 401 ---- grep_error_log eval -qr/client request can't be validated: [^,]+/ ---- grep_error_log_out -client request can't be validated: Invalid digest ---- response_body eval -qr/\{"message":"client request can't be validated"\}/ - - - -=== TEST 9: Test custom request body digest header name. ---- yaml_config -plugin_attr: - hmac-auth: - body_digest_key: "X-Digest-Custom" ---- config - location /t { - content_by_lua_block { - local ngx_time = ngx.time - local ngx_http_time = ngx.http_time - local core = require("apisix.core") - local t = require("lib.test_admin") - local hmac = require("resty.hmac") - local ngx_encode_base64 = ngx.encode_base64 - - local secret_key = "my-secret-key" - local timestamp = ngx_time() - local gmt = ngx_http_time(timestamp) - local access_key = "my-access-key" - local custom_header_a = "asld$%dfasf" - local custom_header_b = "23879fmsldfk" - local body = "{\"name\": \"world\"}" - - local signing_string = { - "POST", - "/hello", - "", - access_key, - gmt, - "x-custom-header-a:" .. custom_header_a, - "x-custom-header-b:" .. custom_header_b - } - signing_string = core.table.concat(signing_string, "\n") .. "\n" - core.log.info("signing_string:", signing_string) - - local signature = hmac:new(secret_key, hmac.ALGOS.SHA256):final(signing_string) - local body_digest = hmac:new(secret_key, hmac.ALGOS.SHA256):final(body) - - core.log.info("signature:", ngx_encode_base64(signature)) - local headers = {} - headers["X-HMAC-SIGNATURE"] = ngx_encode_base64(signature) - headers["X-HMAC-ALGORITHM"] = "hmac-sha256" - headers["Date"] = gmt - headers["X-Digest-Custom"] = ngx_encode_base64(body_digest) - headers["X-HMAC-ACCESS-KEY"] = access_key - headers["X-HMAC-SIGNED-HEADERS"] = "x-custom-header-a;x-custom-header-b" + headers["Digest"] = "SHA-256=" .. body_digest + headers["Authorization"] = "Signature keyId=\"" .. key_id .. "\",algorithm=\"hmac-sha256\"" .. ",headers=\"@request-target date x-custom-header-a x-custom-header-b\",signature=\"" .. ngx_encode_base64(signature) .. "\"" headers["x-custom-header-a"] = custom_header_a headers["x-custom-header-b"] = custom_header_b @@ -516,242 +278,3 @@ plugin_attr: } --- response_body passed - - - -=== TEST 10: Test sort table param. ---- config - location /t { - content_by_lua_block { - local ngx_time = ngx.time - local ngx_http_time = ngx.http_time - local core = require("apisix.core") - local t = require("lib.test_admin") - local hmac = require("resty.hmac") - local ngx_encode_base64 = ngx.encode_base64 - - local secret_key = "my-secret-key" - local timestamp = ngx_time() - local gmt = ngx_http_time(timestamp) - local access_key = "my-access-key" - local custom_header_a = "asld$%dfasf" - local custom_header_b = "23879fmsldfk" - local body = "{\"name\": \"world\"}" - - local signing_string = { - "POST", - "/hello", - "a=&a=1&a=2&a1a=123&c=&name=123", - access_key, - gmt, - "x-custom-header-a:" .. custom_header_a, - "x-custom-header-b:" .. custom_header_b - } - signing_string = core.table.concat(signing_string, "\n") .. "\n" - core.log.info("signing_string:", signing_string) - - local signature = hmac:new(secret_key, hmac.ALGOS.SHA256):final(signing_string) - local body_digest = hmac:new(secret_key, hmac.ALGOS.SHA256):final(body) - - core.log.info("signature:", ngx_encode_base64(signature)) - local headers = {} - headers["X-HMAC-SIGNATURE"] = ngx_encode_base64(signature) - headers["X-HMAC-ALGORITHM"] = "hmac-sha256" - headers["Date"] = gmt - headers["X-HMAC-DIGEST"] = ngx_encode_base64(body_digest) - headers["X-HMAC-ACCESS-KEY"] = access_key - headers["X-HMAC-SIGNED-HEADERS"] = "x-custom-header-a;x-custom-header-b" - headers["x-custom-header-a"] = custom_header_a - headers["x-custom-header-b"] = custom_header_b - - local code, body = t.test('/hello?c=&a1a=123&name=123&a&a=2&a=1', - ngx.HTTP_POST, - body, - nil, - headers - ) - - ngx.status = code - ngx.say(body) - } - } ---- response_body -passed - - - -=== TEST 11: update consumer ---- config - location /t { - content_by_lua_block { - local t = require("lib.test_admin").test - local code, body = t('/apisix/admin/consumers', - ngx.HTTP_PUT, - [[{ - "username": "robin", - "plugins": { - "hmac-auth": { - "access_key": "my-access-key", - "secret_key": "my-secret-key", - "clock_skew": 10 - } - } - }]] - ) - if code >= 300 then - ngx.status = code - end - ngx.say(body) - } - } ---- response_body -passed - - - -=== TEST 12: verify that uri args are greater than 100 is ok ---- config -location /t { - content_by_lua_block { - local ngx_time = ngx.time - local ngx_http_time = ngx.http_time - local core = require("apisix.core") - local t = require("lib.test_admin") - local hmac = require("resty.hmac") - local ngx_encode_base64 = ngx.encode_base64 - - local secret_key = "my-secret-key" - local timestamp = ngx_time() - local gmt = ngx_http_time(timestamp) - local access_key = "my-access-key" - local custom_header_a = "asld$%dfasf" - local custom_header_b = "23879fmsldfk" - - local uri_args = {} - for i = 1, 101 do - uri_args["arg_" .. tostring(i)] = "val_" .. tostring(i) - end - local keys = {} - local query_tab = {} - - for k, v in pairs(uri_args) do - core.table.insert(keys, k) - end - core.table.sort(keys) - - local args_str = "" - for _, key in pairs(keys) do - args_str = args_str .. key .. "=" .. uri_args[key] .. "&" - end - -- remove the last '&' - args_str = args_str:sub(1, -2) - - local signing_string = { - "GET", - "/hello", - args_str, - access_key, - gmt, - "x-custom-header-a:" .. custom_header_a, - "x-custom-header-b:" .. custom_header_b - } - signing_string = core.table.concat(signing_string, "\n") .. "\n" - core.log.info("signing_string:", signing_string) - - local signature = hmac:new(secret_key, hmac.ALGOS.SHA256):final(signing_string) - core.log.info("signature:", ngx_encode_base64(signature)) - local headers = {} - headers["X-HMAC-SIGNATURE"] = ngx_encode_base64(signature) - headers["X-HMAC-ALGORITHM"] = "hmac-sha256" - headers["Date"] = gmt - headers["X-HMAC-ACCESS-KEY"] = access_key - headers["X-HMAC-SIGNED-HEADERS"] = "x-custom-header-a;x-custom-header-b" - headers["x-custom-header-a"] = custom_header_a - headers["x-custom-header-b"] = custom_header_b - - local code, body = t.test('/hello' .. '?' .. args_str, - ngx.HTTP_GET, - "", - nil, - headers - ) - - ngx.status = code - ngx.say(body) - } -} ---- response_body -passed - - - -=== TEST 13: delete exist consumers ---- config - location /t { - content_by_lua_block { - local t = require("lib.test_admin").test - -- delete exist consumers - local code, body = t('/apisix/admin/consumers/robin', ngx.HTTP_DELETE) - ngx.say(body) - } - } ---- response_body -passed - - - -=== TEST 14: data encryption for secret_key ---- yaml_config -apisix: - data_encryption: - enable_encrypt_fields: true - keyring: - - edd1c9f0985e76a2 ---- config - location /t { - content_by_lua_block { - local json = require("toolkit.json") - local t = require("lib.test_admin").test - local code, body = t('/apisix/admin/consumers', - ngx.HTTP_PUT, - [[{ - "username": "jack", - "plugins": { - "hmac-auth": { - "access_key": "my-access-key", - "secret_key": "my-secret-key", - "clock_skew": 10 - } - } - }]] - ) - - if code >= 300 then - ngx.status = code - ngx.say(body) - return - end - ngx.sleep(0.1) - - -- get plugin conf from admin api, password is decrypted - local code, message, res = t('/apisix/admin/consumers/jack', - ngx.HTTP_GET - ) - res = json.decode(res) - if code >= 300 then - ngx.status = code - ngx.say(message) - return - end - - ngx.say(res.value.plugins["hmac-auth"].secret_key) - - -- get plugin conf from etcd, password is encrypted - local etcd = require("apisix.core.etcd") - local res = assert(etcd.get('/consumers/jack')) - ngx.say(res.body.node.value.plugins["hmac-auth"].secret_key) - } - } ---- response_body -my-secret-key -IRWpPjbDq5BCgHyIllnOMA== diff --git a/t/plugin/hmac-auth4.t b/t/plugin/hmac-auth4.t index 78b89b58c7ce..1bdd4707a492 100644 --- a/t/plugin/hmac-auth4.t +++ b/t/plugin/hmac-auth4.t @@ -68,7 +68,7 @@ __DATA__ "username": "jack", "plugins": { "hmac-auth": { - "access_key": "my-access-key", + "key_id": "my-access-key", "secret_key": "$secret://vault/test1/jack/secret_key" } } @@ -129,18 +129,16 @@ location /t { local secret_key = "my-secret-key" local timestamp = ngx_time() local gmt = ngx_http_time(timestamp) - local access_key = "my-access-key" + local key_id = "my-access-key" local custom_header_a = "asld$%dfasf" local custom_header_b = "23879fmsldfk" local signing_string = { - "GET", - "/hello", - "", - access_key, - gmt, - "x-custom-header-a:" .. custom_header_a, - "x-custom-header-b:" .. custom_header_b + key_id, + "GET /hello", + "date: " .. gmt, + "x-custom-header-a: " .. custom_header_a, + "x-custom-header-b: " .. custom_header_b } signing_string = core.table.concat(signing_string, "\n") .. "\n" core.log.info("signing_string:", signing_string) @@ -148,11 +146,8 @@ location /t { local signature = hmac:new(secret_key, hmac.ALGOS.SHA256):final(signing_string) core.log.info("signature:", ngx_encode_base64(signature)) local headers = {} - headers["X-HMAC-SIGNATURE"] = ngx_encode_base64(signature) - headers["X-HMAC-ALGORITHM"] = "hmac-sha256" headers["Date"] = gmt - headers["X-HMAC-ACCESS-KEY"] = access_key - headers["X-HMAC-SIGNED-HEADERS"] = "x-custom-header-a;x-custom-header-b" + headers["Authorization"] = "Signature keyId=\"" .. key_id .. "\",algorithm=\"hmac-sha256\"" .. ",headers=\"@request-target date x-custom-header-a x-custom-header-b\",signature=\"" .. ngx_encode_base64(signature) .. "\"" headers["x-custom-header-a"] = custom_header_a headers["x-custom-header-b"] = custom_header_b @@ -197,7 +192,7 @@ passed "username": "jack", "plugins": { "hmac-auth": { - "access_key": "my-access-key", + "key_id": "my-access-key", "secret_key": "$secret://vault/test1/jack/secret_key" } } @@ -248,18 +243,16 @@ location /t { local secret_key = "my-secret-key" local timestamp = ngx_time() local gmt = ngx_http_time(timestamp) - local access_key = "my-access-key" + local key_id = "my-access-key" local custom_header_a = "asld$%dfasf" local custom_header_b = "23879fmsldfk" local signing_string = { - "GET", - "/hello", - "", - access_key, - gmt, - "x-custom-header-a:" .. custom_header_a, - "x-custom-header-b:" .. custom_header_b + key_id, + "GET /hello", + "date: " .. gmt, + "x-custom-header-a: " .. custom_header_a, + "x-custom-header-b: " .. custom_header_b } signing_string = core.table.concat(signing_string, "\n") .. "\n" core.log.info("signing_string:", signing_string) @@ -267,11 +260,8 @@ location /t { local signature = hmac:new(secret_key, hmac.ALGOS.SHA256):final(signing_string) core.log.info("signature:", ngx_encode_base64(signature)) local headers = {} - headers["X-HMAC-SIGNATURE"] = ngx_encode_base64(signature) - headers["X-HMAC-ALGORITHM"] = "hmac-sha256" headers["Date"] = gmt - headers["X-HMAC-ACCESS-KEY"] = access_key - headers["X-HMAC-SIGNED-HEADERS"] = "x-custom-header-a;x-custom-header-b" + headers["Authorization"] = "Signature keyId=\"" .. key_id .. "\",algorithm=\"hmac-sha256\"" .. ",headers=\"@request-target date x-custom-header-a x-custom-header-b\",signature=\"" .. ngx_encode_base64(signature) .. "\"" headers["x-custom-header-a"] = custom_header_a headers["x-custom-header-b"] = custom_header_b From 2fcfbd83e22301aea4f027738d628f19c262a458 Mon Sep 17 00:00:00 2001 From: HuanXin-Chen <111850224+HuanXin-Chen@users.noreply.github.com> Date: Sun, 22 Sep 2024 11:53:48 +0800 Subject: [PATCH 3/7] feat: support gcp secret manager (#11436) --- apisix/secret/gcp.lua | 202 ++++++++ apisix/utils/google-cloud-oauth.lua | 130 +++++ docs/en/latest/terminology/secret.md | 54 ++ docs/zh/latest/terminology/secret.md | 56 +- t/lib/server.lua | 186 +++++++ t/secret/conf/error.json | 9 + t/secret/conf/success.json | 10 + t/secret/gcp.t | 737 +++++++++++++++++++++++++++ 8 files changed, 1383 insertions(+), 1 deletion(-) create mode 100644 apisix/secret/gcp.lua create mode 100644 apisix/utils/google-cloud-oauth.lua create mode 100644 t/secret/conf/error.json create mode 100644 t/secret/conf/success.json create mode 100644 t/secret/gcp.t diff --git a/apisix/secret/gcp.lua b/apisix/secret/gcp.lua new file mode 100644 index 000000000000..6b6e661c4ff2 --- /dev/null +++ b/apisix/secret/gcp.lua @@ -0,0 +1,202 @@ +-- +-- 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. +-- + +--- GCP Tools. +local core = require("apisix.core") +local http = require("resty.http") +local google_oauth = require("apisix.utils.google-cloud-oauth") + +local str_sub = core.string.sub +local str_find = core.string.find +local decode_base64 = ngx.decode_base64 + +local lrucache = core.lrucache.new({ ttl = 300, count = 8 }) + +local schema = { + type = "object", + properties = { + auth_config = { + type = "object", + properties = { + client_email = { type = "string" }, + private_key = { type = "string" }, + project_id = { type = "string" }, + token_uri = { + type = "string", + default = "https://oauth2.googleapis.com/token" + }, + scope = { + type = "array", + items = { + type = "string" + }, + default = { + "https://www.googleapis.com/auth/cloud-platform" + } + }, + entries_uri = { + type = "string", + default = "https://secretmanager.googleapis.com/v1" + }, + }, + required = { "client_email", "private_key", "project_id" } + }, + ssl_verify = { + type = "boolean", + default = true + }, + auth_file = { type = "string" }, + }, + oneOf = { + { required = { "auth_config" } }, + { required = { "auth_file" } }, + }, +} + +local _M = { + schema = schema +} + +local function fetch_oauth_conf(conf) + if conf.auth_config then + return conf.auth_config + end + + local file_content, err = core.io.get_file(conf.auth_file) + if not file_content then + return nil, "failed to read configuration, file: " .. conf.auth_file .. ", err: " .. err + end + + local config_tab, err = core.json.decode(file_content) + if not config_tab then + return nil, "config parse failure, data: " .. file_content .. ", err: " .. err + end + + local config = { + auth_config = { + client_email = config_tab.client_email, + private_key = config_tab.private_key, + project_id = config_tab.project_id + } + } + + local ok, err = core.schema.check(schema, config) + if not ok then + return nil, "config parse failure, file: " .. conf.auth_file .. ", err: " .. err + end + + return config_tab +end + + +local function get_secret(oauth, secrets_id) + local httpc = http.new() + + local access_token = oauth:generate_access_token() + if not access_token then + return nil, "failed to get google oauth token" + end + + local entries_uri = oauth.entries_uri .. "/projects/" .. oauth.project_id + .. "/secrets/" .. secrets_id .. "/versions/latest:access" + + local res, err = httpc:request_uri(entries_uri, { + ssl_verify = oauth.ssl_verify, + method = "GET", + headers = { + ["Content-Type"] = "application/json", + ["Authorization"] = (oauth.access_token_type or "Bearer") .. " " .. access_token, + }, + }) + + if not res then + return nil, err + end + + if res.status ~= 200 then + return nil, res.body + end + + local body, err = core.json.decode(res.body) + if not body then + return nil, "failed to parse response data, " .. err + end + + local payload = body.payload + if not payload then + return nil, "invalid payload" + end + + return decode_base64(payload.data) +end + + +local function make_request_to_gcp(conf, secrets_id) + local auth_config, err = fetch_oauth_conf(conf) + if not auth_config then + return nil, err + end + + local lru_key = auth_config.client_email .. "#" .. auth_config.project_id + + local oauth, err = lrucache(lru_key, "gcp", google_oauth.new, auth_config, conf.ssl_verify) + if not oauth then + return nil, "failed to create oauth object, " .. err + end + + local secret, err = get_secret(oauth, secrets_id) + if not secret then + return nil, err + end + + return secret +end + + +function _M.get(conf, key) + core.log.info("fetching data from gcp for key: ", key) + + local idx = str_find(key, '/') + + local main_key = idx and str_sub(key, 1, idx - 1) or key + if main_key == "" then + return nil, "can't find main key, key: " .. key + end + + local sub_key = idx and str_sub(key, idx + 1) + + core.log.info("main: ", main_key, sub_key and ", sub: " .. sub_key or "") + + local res, err = make_request_to_gcp(conf, main_key) + if not res then + return nil, "failed to retrtive data from gcp secret manager: " .. err + end + + if not sub_key then + return res + end + + local data, err = core.json.decode(res) + if not data then + return nil, "failed to decode result, err: " .. err + end + + return data[sub_key] +end + + +return _M diff --git a/apisix/utils/google-cloud-oauth.lua b/apisix/utils/google-cloud-oauth.lua new file mode 100644 index 000000000000..6cb352848bad --- /dev/null +++ b/apisix/utils/google-cloud-oauth.lua @@ -0,0 +1,130 @@ +-- +-- 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 type = type +local setmetatable = setmetatable + +local ngx_update_time = ngx.update_time +local ngx_time = ngx.time +local ngx_encode_args = ngx.encode_args + +local http = require("resty.http") +local jwt = require("resty.jwt") + + +local function get_timestamp() + ngx_update_time() + return ngx_time() +end + + +local _M = {} + + +function _M.generate_access_token(self) + if not self.access_token or get_timestamp() > self.access_token_expire_time - 60 then + self:refresh_access_token() + end + return self.access_token +end + + +function _M.refresh_access_token(self) + local http_new = http.new() + local res, err = http_new:request_uri(self.token_uri, { + ssl_verify = self.ssl_verify, + method = "POST", + body = ngx_encode_args({ + grant_type = "urn:ietf:params:oauth:grant-type:jwt-bearer", + assertion = self:generate_jwt_token() + }), + headers = { + ["Content-Type"] = "application/x-www-form-urlencoded", + }, + }) + + if not res then + core.log.error("failed to refresh google oauth access token, ", err) + return + end + + if res.status ~= 200 then + core.log.error("failed to refresh google oauth access token: ", res.body) + return + end + + res, err = core.json.decode(res.body) + if not res then + core.log.error("failed to parse google oauth response data: ", err) + return + end + + self.access_token = res.access_token + self.access_token_type = res.token_type + self.access_token_expire_time = get_timestamp() + res.expires_in +end + + +function _M.generate_jwt_token(self) + local payload = core.json.encode({ + iss = self.client_email, + aud = self.token_uri, + scope = self.scope, + iat = get_timestamp(), + exp = get_timestamp() + (60 * 60) + }) + + local jwt_token = jwt:sign(self.private_key, { + header = { alg = "RS256", typ = "JWT" }, + payload = payload, + }) + + return jwt_token +end + + +function _M.new(config, ssl_verify) + local oauth = { + client_email = config.client_email, + private_key = config.private_key, + project_id = config.project_id, + token_uri = config.token_uri or "https://oauth2.googleapis.com/token", + auth_uri = config.auth_uri or "https://accounts.google.com/o/oauth2/auth", + entries_uri = config.entries_uri, + access_token = nil, + access_token_type = nil, + access_token_expire_time = 0, + } + + oauth.ssl_verify = ssl_verify + + if config.scope then + if type(config.scope) == "string" then + oauth.scope = config.scope + end + + if type(config.scope) == "table" then + oauth.scope = core.table.concat(config.scope, " ") + end + end + + return setmetatable(oauth, { __index = _M }) +end + + +return _M diff --git a/docs/en/latest/terminology/secret.md b/docs/en/latest/terminology/secret.md index e27ee79fc1ac..94c1b8843b0a 100644 --- a/docs/en/latest/terminology/secret.md +++ b/docs/en/latest/terminology/secret.md @@ -40,6 +40,7 @@ APISIX currently supports storing secrets in the following ways: - [Environment Variables](#use-environment-variables-to-manage-secrets) - [HashiCorp Vault](#use-hashicorp-vault-to-manage-secrets) - [AWS Secrets Manager](#use-aws-secrets-manager-to-manage-secrets) +- [GCP Secrets Manager](#use-gcp-secrets-manager-to-manage-secrets) You can use APISIX Secret functions by specifying format variables in the consumer configuration of the following plugins, such as `key-auth`. @@ -293,3 +294,56 @@ curl -i http://127.0.0.1:9080/your_route -H 'apikey: value' ``` This will verify whether the `key-auth` plugin is correctly using the key from AWS Secrets Manager. + +## Use GCP Secrets Manager to manage secrets + +Using the GCP Secrets Manager to manage secrets means you can store the secret information in the GCP service, and reference it using a specific format of variables when configuring plugins. APISIX currently supports integration with the GCP Secrets Manager, and the supported authentication method is [OAuth 2.0](https://developers.google.com/identity/protocols/oauth2). + +### Reference Format + +``` +$secret://$manager/$id/$secret_name/$key +``` + +The reference format is the same as before: + +- manager: secrets management service, could be the HashiCorp Vault, AWS, GCP etc. +- id: APISIX Secrets resource ID, which needs to be consistent with the one specified when adding the APISIX Secrets resource +- secret_name: the secret name in the secrets management service +- key: get the value of a property when the value of the secret is a JSON string + +### Required Parameters + +| Name | Required | Default | Description | +|-------------------------|----------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| auth_config | True | | Either `auth_config` or `auth_file` must be provided. | +| auth_config.client_email | True | | Email address of the Google Cloud service account. | +| auth_config.private_key | True | | Private key of the Google Cloud service account. | +| auth_config.project_id | True | | Project ID in the Google Cloud service account. | +| auth_config.token_uri | False | https://oauth2.googleapis.com/token | Token URI of the Google Cloud service account. | +| auth_config.entries_uri | False | https://secretmanager.googleapis.com/v1 | The API access endpoint for the Google Secrets Manager. | +| auth_config.scope | False | https://www.googleapis.com/auth/cloud-platform | Access scopes of the Google Cloud service account. See [OAuth 2.0 Scopes for Google APIs](https://developers.google.com/identity/protocols/oauth2/scopes) | +| auth_file | True | | Path to the Google Cloud service account authentication JSON file. Either `auth_config` or `auth_file` must be provided. | +| ssl_verify | False | true | When set to `true`, enables SSL verification as mentioned in [OpenResty docs](https://github.com/openresty/lua-nginx-module#tcpsocksslhandshake). | + +You need to configure the corresponding authentication parameters, or specify the authentication file through auth_file, where the content of auth_file is in JSON format. + +### Example + +Here is a correct configuration example: + +``` +curl http://127.0.0.1:9180/apisix/admin/secrets/gcp/1 \ +-H "X-API-KEY: $admin_key" -X PUT -d ' +{ + "auth_config" : { + "client_email": "email@apisix.iam.gserviceaccount.com", + "private_key": "private_key", + "project_id": "apisix-project", + "token_uri": "https://oauth2.googleapis.com/token", + "entries_uri": "https://secretmanager.googleapis.com/v1", + "scope": ["https://www.googleapis.com/auth/cloud-platform"] + } +}' + +``` diff --git a/docs/zh/latest/terminology/secret.md b/docs/zh/latest/terminology/secret.md index 03dbf0c1be2b..810abb7ddf6f 100644 --- a/docs/zh/latest/terminology/secret.md +++ b/docs/zh/latest/terminology/secret.md @@ -40,6 +40,7 @@ APISIX 目前支持通过以下方式存储密钥: - [环境变量](#使用环境变量管理密钥) - [HashiCorp Vault](#使用-vault-管理密钥) - [AWS Secrets Manager](#使用-aws-secrets-manager-管理密钥) +- [GCP Secrets Manager](#使用-gcp-secrets-manager-管理密钥) 你可以在以下插件的 consumer 配置中通过指定格式的变量来使用 APISIX Secret 功能,比如 `key-auth` 插件。 @@ -135,7 +136,7 @@ curl http://127.0.0.1:9180/apisix/admin/consumers \ $secret://$manager/$id/$secret_name/$key ``` -- manager: 密钥管理服务,可以是 Vault、AWS 等 +- manager: 密钥管理服务,可以是 Vault、AWS、GCP 等 - APISIX Secret 资源 ID,需要与添加 APISIX Secret 资源时指定的 ID 保持一致 - secret_name: 密钥管理服务中的密钥名称 - key:密钥管理服务中密钥对应的 key @@ -295,3 +296,56 @@ curl -i http://127.0.0.1:9080/your_route -H 'apikey: value' ``` 这将验证 key-auth 插件是否正确地使用 AWS Secret Manager 中的密钥。 + +## 使用 GCP Secrets Manager 管理密钥 + +使用 GCP Secret Manager 来管理密钥意味着你可以将密钥信息保存在 GCP 服务中,在配置插件时通过特定格式的变量来引用。APISIX 目前支持对接 GCP Secret Manager, 所支持的验证方式是[OAuth 2.0](https://developers.google.com/identity/protocols/oauth2?hl=zh-cn)。 + +### 引用方式 + +``` +$secret://$manager/$id/$secret_name/$key +``` + +引用方式和之前保持一致: + +- manager: 密钥管理服务,可以是 Vault、AWS\GCP 等 +- APISIX Secret 资源 ID,需要与添加 APISIX Secret 资源时指定的 ID 保持一致 +- secret_name: 密钥管理服务中的密钥名称 +- key:当密钥的值是 JSON 字符串时,获取某个属性的值 + +### 必要参数 + +| 名称 | 必选项 | 默认值 | 描述 | +| ----------------------- | -------- | ------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------- | +| auth_config | 是 | | `auth_config` 和 `auth_file` 必须配置一个。 | +| auth_config.client_email | 是 | | 谷歌服务帐号的 email 参数。 | +| auth_config.private_key | 是 | | 谷歌服务帐号的私钥参数。 | +| auth_config.project_id | 是 | | 谷歌服务帐号的项目 ID。 | +| auth_config.token_uri | 否 | https://oauth2.googleapis.com/token | 请求谷歌服务帐户的令牌的 URI。 | +| auth_config.entries_uri | 否 | https://secretmanager.googleapis.com/v1 | 谷歌密钥服务访问端点 API。 | +| auth_config.scope | 否 | https://www.googleapis.com/auth/cloud-platform | 谷歌服务账号的访问范围,可参考 [OAuth 2.0 Scopes for Google APIs](https://developers.google.com/identity/protocols/oauth2/scopes)| +| auth_file | 是 | | `auth_config` 和 `auth_file` 必须配置一个。 | +| ssl_verify | 否 | true | 当设置为 `true` 时,启用 `SSL` 验证。 | + +你需要配置相应的认证参数,或者通过 auth_file 来指定认证文件,其中 auth_file 的内容为认证参数的 json 格式。 + +### 示例 + +以下一种正确的配置实例: + +``` +curl http://127.0.0.1:9180/apisix/admin/secrets/gcp/1 \ +-H "X-API-KEY: $admin_key" -X PUT -d ' +{ + "auth_config" : { + "client_email": "email@apisix.iam.gserviceaccount.com", + "private_key": "private_key", + "project_id": "apisix-project", + "token_uri": "https://oauth2.googleapis.com/token", + "entries_uri": "https://secretmanager.googleapis.com/v1", + "scope": ["https://www.googleapis.com/auth/cloud-platform"] + } +}' + +``` diff --git a/t/lib/server.lua b/t/lib/server.lua index 7cc8101a3af7..309873636302 100644 --- a/t/lib/server.lua +++ b/t/lib/server.lua @@ -558,6 +558,192 @@ function _M.google_logging_entries() ngx.say(data) end +function _M.google_secret_token() + local args = ngx.req.get_uri_args() + local args_token_type = args.token_type or "Bearer" + ngx.req.read_body() + local data = ngx.decode_args(ngx.req.get_body_data()) + local jwt = require("resty.jwt") + local access_scopes = "https://www.googleapis.com/auth/cloud" + local verify = jwt:verify(rsa_public_key, data["assertion"]) + if not verify.verified then + ngx.status = 401 + ngx.say(json_encode({ error = "identity authentication failed" })) + return + end + + local scopes_valid = type(verify.payload.scope) == "string" and + verify.payload.scope:find(access_scopes) + if not scopes_valid then + ngx.status = 403 + ngx.say(json_encode({ error = "no access to this scope" })) + return + end + + local expire_time = (verify.payload.exp or ngx.time()) - ngx.time() + if expire_time <= 0 then + expire_time = 0 + end + + local jwt_token = jwt:sign(rsa_private_key, { + header = { typ = "JWT", alg = "RS256" }, + payload = { exp = verify.payload.exp, scope = access_scopes } + }) + + ngx.say(json_encode({ + access_token = jwt_token, + expires_in = expire_time, + token_type = args_token_type + })) +end + +function _M.google_secret_apisix_jack() + local args = ngx.req.get_uri_args() + local args_token_type = args.token_type or "Bearer" + local jwt = require("resty.jwt") + local access_scopes = "https://www.googleapis.com/auth/cloud" + + local headers = ngx.req.get_headers() + local token = headers["Authorization"] + if not token then + ngx.status = 401 + ngx.say(json_encode({ error = "authentication header not exists" })) + return + end + + token = string.sub(token, #args_token_type + 2) + local verify = jwt:verify(rsa_public_key, token) + if not verify.verified then + ngx.status = 401 + ngx.say(json_encode({ error = "identity authentication failed" })) + return + end + + local scopes_valid = type(verify.payload.scope) == "string" and + verify.payload.scope:find(access_scopes) + if not scopes_valid then + ngx.status = 403 + ngx.say(json_encode({ error = "no access to this scope" })) + return + end + + local expire_time = (verify.payload.exp or ngx.time()) - ngx.time() + if expire_time <= 0 then + ngx.status = 403 + ngx.say(json_encode({ error = "token has expired" })) + return + end + + local response = { + name = "projects/647037004838/secrets/apisix/versions/1", + payload = { + data = "eyJrZXkiOiJ2YWx1ZSJ9", + dataCrc32c = "2296192492" + } + } + + ngx.status = 200 + ngx.say(json_encode(response)) +end + +function _M.google_secret_apisix_error_jack() + local args = ngx.req.get_uri_args() + local args_token_type = args.token_type or "Bearer" + local jwt = require("resty.jwt") + local access_scopes = "https://www.googleapis.com/auth/root/cloud" + + local headers = ngx.req.get_headers() + local token = headers["Authorization"] + if not token then + ngx.status = 401 + ngx.say(json_encode({ error = "authentication header not exists" })) + return + end + + token = string.sub(token, #args_token_type + 2) + local verify = jwt:verify(rsa_public_key, token) + if not verify.verified then + ngx.status = 401 + ngx.say(json_encode({ error = "identity authentication failed" })) + return + end + + local scopes_valid = type(verify.payload.scope) == "string" and + verify.payload.scope:find(access_scopes) + if not scopes_valid then + ngx.status = 403 + ngx.say(json_encode({ error = "no access to this scope" })) + return + end + + local expire_time = (verify.payload.exp or ngx.time()) - ngx.time() + if expire_time <= 0 then + ngx.status = 403 + ngx.say(json_encode({ error = "token has expired" })) + return + end + + local response = { + name = "projects/647037004838/secrets/apisix_error/versions/1", + payload = { + data = "eyJrZXkiOiJ2YWx1ZSJ9", + dataCrc32c = "2296192492" + } + } + + ngx.status = 200 + ngx.say(json_encode(response)) +end + +function _M.google_secret_apisix_mysql() + local args = ngx.req.get_uri_args() + local args_token_type = args.token_type or "Bearer" + local jwt = require("resty.jwt") + local access_scopes = "https://www.googleapis.com/auth/cloud" + + local headers = ngx.req.get_headers() + local token = headers["Authorization"] + if not token then + ngx.status = 401 + ngx.say(json_encode({ error = "authentication header not exists" })) + return + end + + token = string.sub(token, #args_token_type + 2) + local verify = jwt:verify(rsa_public_key, token) + if not verify.verified then + ngx.status = 401 + ngx.say(json_encode({ error = "identity authentication failed" })) + return + end + + local scopes_valid = type(verify.payload.scope) == "string" and + verify.payload.scope:find(access_scopes) + if not scopes_valid then + ngx.status = 403 + ngx.say(json_encode({ error = "no access to this scope" })) + return + end + + local expire_time = (verify.payload.exp or ngx.time()) - ngx.time() + if expire_time <= 0 then + ngx.status = 403 + ngx.say(json_encode({ error = "token has expired" })) + return + end + + local response = { + name = "projects/647037004838/secrets/apisix/versions/1", + payload = { + data = "c2VjcmV0", + dataCrc32c = "0xB03C4D4D" + } + } + + ngx.status = 200 + ngx.say(json_encode(response)) +end + function _M.plugin_proxy_rewrite_resp_header() ngx.req.read_body() local s = "plugin_proxy_rewrite_resp_header" diff --git a/t/secret/conf/error.json b/t/secret/conf/error.json new file mode 100644 index 000000000000..3d0bb6295393 --- /dev/null +++ b/t/secret/conf/error.json @@ -0,0 +1,9 @@ +{ + "private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDDzrFwnA3EvYyR\naeMgaLD3hBjvxKrz10uox1X8q7YYhf2ViRtLRUMa2bEMYksE5hbhwpNf6mKAnLOC\nUuAT6cPPdUl/agKpJXviBPIR2LuzD17WsLJHp1HxUDssSkgfCaGcOGGNfLUhhIpF\n2JUctLmxiZoAZySlSjcwupSuDJ0aPm0XO8r9H8Qu5kF2Vkz5e5bFivLTmvzrQTe4\nv5V1UI6hThElCSeUmdNF3uG3wopxlvq4zXgLTnuLbrNf/Gc4mlpV+UDgTISj32Ep\nAB2vxKEbvQw4ti8YJnGXWjxLerhfrszFw+V8lpeduiDYA44ZFoVqvzxeIsVZNtcw\nIu7PvEPNAgMBAAECggEAVpyN9m7A1F631/aLheFpLgMbeKt4puV7zQtnaJ2XrZ9P\nPR7pmNDpTu4uF3k/D8qrIm+L+uhVa+hkquf3wDct6w1JVnfQ93riImbnoKdK13ic\nDcEZCwLjByfjFMNCxZ/gAZca55fbExlqhFy6EHmMjhB8s2LsXcTHRuGxNI/Vyi49\nsxECibe0U53aqdJbVWrphIS67cpwl4TUkN6mrHsNuDYNJ9dgkpapoqp4FTFQsBqC\nafOK5qgJ68dWZ47FBUng+AZjdCncqAIuJxxItGVQP6YPsFs+OXcivIVHJr363TpC\nl85FfdvqWV5OGBbwSKhNwiTNUVvfSQVmtURGWG/HbQKBgQD4gZ1z9+Lx19kT9WTz\nlw93lxso++uhAPDTKviyWSRoEe5aN3LCd4My+/Aj+sk4ON/s2BV3ska5Im93j+vC\nrCv3uPn1n2jUhWuJ3bDqipeTW4n/CQA2m/8vd26TMk22yOkkqw2MIA8sjJ//SD7g\ntdG7up6DgGMP4hgbO89uGU7DAwKBgQDJtkKd0grh3u52Foeh9YaiAgYRwc65IE16\nUyD1OJxIuX/dYQDLlo5KyyngFa1ZhWIs7qC7r3xXH+10kfJY+Q+5YMjmZjlL8SR1\nUjqd02R9F2//6OeswyReachJZbZdtiEw3lPa4jVFYfhSe0M2ZPxMwvoXb25eyCNI\n1lYjSKq87wKBgHnLTNghjeDp4UKe6rNYPgRm0rDrhziJtX5JeUov1mALKb6dnmkh\nGfRK9g8sQqKDfXwfC6Z2gaMK9YaryujGaWYoCpoPXtmJ6oLPXH4XHuLh4mhUiP46\nxn8FEfSimuQS4/FMxH8A128GHQSI7AhGFFzlwfrBWcvXC+mNDsTvMmLxAoGARc+4\nupppfccETQZ7JsitMgD1TMwA2f2eEwoWTAitvlXFNT9PYSbYVHaAJbga6PLLCbYF\nFzAjHpxEOKYSdEyu7n/ayDL0/Z2V+qzc8KarDsg/0RgwppBbU/nUgeKb/U79qcYo\ny4ai3UKNCS70Ei1dTMvmdpnwXwlxfNIBufB6dy0CgYBMYq9Lc31GkC6PcGEEbx6W\nvjImOadWZbuOVnvEQjb5XCdcOsWsMcg96PtoeuyyHmhnEF1GsMzcIdQv/PHrvYpK\nYp8D0aqsLEgwGrJQER26FPpKmyIwvcL+nm6q5W31PnU9AOC/WEkB6Zs58hsMzD2S\nkEJQcmfVew5mFXyxuEn3zA==\n-----END PRIVATE KEY-----", + "project_id": "apisix", + "token_uri": "http://127.0.0.1:1980/google/logging/token", + "scope": [ + "https://apisix.apache.org/logs:admin" + ], + "entries_uri": "http://127.0.0.1:1980/google/logging/entries" +} diff --git a/t/secret/conf/success.json b/t/secret/conf/success.json new file mode 100644 index 000000000000..d9cfbc3c816e --- /dev/null +++ b/t/secret/conf/success.json @@ -0,0 +1,10 @@ +{ + "private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDDzrFwnA3EvYyR\naeMgaLD3hBjvxKrz10uox1X8q7YYhf2ViRtLRUMa2bEMYksE5hbhwpNf6mKAnLOC\nUuAT6cPPdUl/agKpJXviBPIR2LuzD17WsLJHp1HxUDssSkgfCaGcOGGNfLUhhIpF\n2JUctLmxiZoAZySlSjcwupSuDJ0aPm0XO8r9H8Qu5kF2Vkz5e5bFivLTmvzrQTe4\nv5V1UI6hThElCSeUmdNF3uG3wopxlvq4zXgLTnuLbrNf/Gc4mlpV+UDgTISj32Ep\nAB2vxKEbvQw4ti8YJnGXWjxLerhfrszFw+V8lpeduiDYA44ZFoVqvzxeIsVZNtcw\nIu7PvEPNAgMBAAECggEAVpyN9m7A1F631/aLheFpLgMbeKt4puV7zQtnaJ2XrZ9P\nPR7pmNDpTu4uF3k/D8qrIm+L+uhVa+hkquf3wDct6w1JVnfQ93riImbnoKdK13ic\nDcEZCwLjByfjFMNCxZ/gAZca55fbExlqhFy6EHmMjhB8s2LsXcTHRuGxNI/Vyi49\nsxECibe0U53aqdJbVWrphIS67cpwl4TUkN6mrHsNuDYNJ9dgkpapoqp4FTFQsBqC\nafOK5qgJ68dWZ47FBUng+AZjdCncqAIuJxxItGVQP6YPsFs+OXcivIVHJr363TpC\nl85FfdvqWV5OGBbwSKhNwiTNUVvfSQVmtURGWG/HbQKBgQD4gZ1z9+Lx19kT9WTz\nlw93lxso++uhAPDTKviyWSRoEe5aN3LCd4My+/Aj+sk4ON/s2BV3ska5Im93j+vC\nrCv3uPn1n2jUhWuJ3bDqipeTW4n/CQA2m/8vd26TMk22yOkkqw2MIA8sjJ//SD7g\ntdG7up6DgGMP4hgbO89uGU7DAwKBgQDJtkKd0grh3u52Foeh9YaiAgYRwc65IE16\nUyD1OJxIuX/dYQDLlo5KyyngFa1ZhWIs7qC7r3xXH+10kfJY+Q+5YMjmZjlL8SR1\nUjqd02R9F2//6OeswyReachJZbZdtiEw3lPa4jVFYfhSe0M2ZPxMwvoXb25eyCNI\n1lYjSKq87wKBgHnLTNghjeDp4UKe6rNYPgRm0rDrhziJtX5JeUov1mALKb6dnmkh\nGfRK9g8sQqKDfXwfC6Z2gaMK9YaryujGaWYoCpoPXtmJ6oLPXH4XHuLh4mhUiP46\nxn8FEfSimuQS4/FMxH8A128GHQSI7AhGFFzlwfrBWcvXC+mNDsTvMmLxAoGARc+4\nupppfccETQZ7JsitMgD1TMwA2f2eEwoWTAitvlXFNT9PYSbYVHaAJbga6PLLCbYF\nFzAjHpxEOKYSdEyu7n/ayDL0/Z2V+qzc8KarDsg/0RgwppBbU/nUgeKb/U79qcYo\ny4ai3UKNCS70Ei1dTMvmdpnwXwlxfNIBufB6dy0CgYBMYq9Lc31GkC6PcGEEbx6W\nvjImOadWZbuOVnvEQjb5XCdcOsWsMcg96PtoeuyyHmhnEF1GsMzcIdQv/PHrvYpK\nYp8D0aqsLEgwGrJQER26FPpKmyIwvcL+nm6q5W31PnU9AOC/WEkB6Zs58hsMzD2S\nkEJQcmfVew5mFXyxuEn3zA==\n-----END PRIVATE KEY-----", + "project_id": "apisix", + "token_uri": "http://127.0.0.1:1980/google/secret/token", + "scope": [ + "https://www.googleapis.com/auth/cloud" + ], + "entries_uri": "http://127.0.0.1:1984", + "client_email": "email@apisix.iam.gserviceaccount.com" +} diff --git a/t/secret/gcp.t b/t/secret/gcp.t new file mode 100644 index 000000000000..b7fc5331cf37 --- /dev/null +++ b/t/secret/gcp.t @@ -0,0 +1,737 @@ +# +# 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(); +log_level("info"); + +run_tests; + +__DATA__ + +=== TEST 1: validate different schema situation +--- config + location /t { + content_by_lua_block { + local test_case = { + {}, + {auth_file = "123"}, + {auth_file = 123}, + {auth_config = {client_email = "client", private_key = "private_key"}}, + {auth_config = {private_key = "private_key", project_id = "project_id"}}, + {auth_config = {client_email = "client", project_id = "project_id"}}, + {auth_config = {client_email = "client", private_key = "private_key", project_id = "project_id"}}, + {auth_config = {client_email = 1234, private_key = "private_key", project_id = "project_id"}}, + {auth_config = {client_email = "client", private_key = 1234, project_id = "project_id"}}, + {auth_config = {client_email = "client", private_key = "private_key", project_id = 1234}}, + {auth_config = {client_email = "client", private_key = "private_key", project_id = "project_id"}, ssl_verify = 1234}, + {auth_config = {client_email = "client", private_key = "private_key", project_id = "project_id", token_uri = 1234}}, + {auth_config = {client_email = "client", private_key = "private_key", project_id = "project_id", scope = 1234}}, + {auth_config = {client_email = "client", private_key = "private_key", project_id = "project_id", entries_uri = 1234}}, + {auth_config = {client_email = "client", private_key = "private_key", project_id = "project_id", token_uri = "token_uri", + scope = {"scope"}, entries_uri = "entries_uri"}, ssl_verify = true}, + } + local gcp = require("apisix.secret.gcp") + local core = require("apisix.core") + local metadata_schema = gcp.schema + + for _, conf in ipairs(test_case) do + local ok, err = core.schema.check(metadata_schema, conf) + ngx.say(ok and "done" or err) + end + } + } +--- request +GET /t +--- response_body +value should match only one schema, but matches none +done +property "auth_file" validation failed: wrong type: expected string, got number +property "auth_config" validation failed: property "project_id" is required +property "auth_config" validation failed: property "client_email" is required +property "auth_config" validation failed: property "private_key" is required +done +property "auth_config" validation failed: property "client_email" validation failed: wrong type: expected string, got number +property "auth_config" validation failed: property "private_key" validation failed: wrong type: expected string, got number +property "auth_config" validation failed: property "project_id" validation failed: wrong type: expected string, got number +property "ssl_verify" validation failed: wrong type: expected boolean, got number +property "auth_config" validation failed: property "token_uri" validation failed: wrong type: expected string, got number +property "auth_config" validation failed: property "scope" validation failed: wrong type: expected array, got number +property "auth_config" validation failed: property "entries_uri" validation failed: wrong type: expected string, got number +done + + + +=== TEST 2: check key: no main key +--- config + location /t { + content_by_lua_block { + local gcp = require("apisix.secret.gcp") + local conf = { + auth_config = { + client_email = "email@apisix.iam.gserviceaccount.com", + private_key = [[ +-----BEGIN PRIVATE KEY----- +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDDzrFwnA3EvYyR +aeMgaLD3hBjvxKrz10uox1X8q7YYhf2ViRtLRUMa2bEMYksE5hbhwpNf6mKAnLOC +UuAT6cPPdUl/agKpJXviBPIR2LuzD17WsLJHp1HxUDssSkgfCaGcOGGNfLUhhIpF +2JUctLmxiZoAZySlSjcwupSuDJ0aPm0XO8r9H8Qu5kF2Vkz5e5bFivLTmvzrQTe4 +v5V1UI6hThElCSeUmdNF3uG3wopxlvq4zXgLTnuLbrNf/Gc4mlpV+UDgTISj32Ep +AB2vxKEbvQw4ti8YJnGXWjxLerhfrszFw+V8lpeduiDYA44ZFoVqvzxeIsVZNtcw +Iu7PvEPNAgMBAAECggEAVpyN9m7A1F631/aLheFpLgMbeKt4puV7zQtnaJ2XrZ9P +PR7pmNDpTu4uF3k/D8qrIm+L+uhVa+hkquf3wDct6w1JVnfQ93riImbnoKdK13ic +DcEZCwLjByfjFMNCxZ/gAZca55fbExlqhFy6EHmMjhB8s2LsXcTHRuGxNI/Vyi49 +sxECibe0U53aqdJbVWrphIS67cpwl4TUkN6mrHsNuDYNJ9dgkpapoqp4FTFQsBqC +afOK5qgJ68dWZ47FBUng+AZjdCncqAIuJxxItGVQP6YPsFs+OXcivIVHJr363TpC +l85FfdvqWV5OGBbwSKhNwiTNUVvfSQVmtURGWG/HbQKBgQD4gZ1z9+Lx19kT9WTz +lw93lxso++uhAPDTKviyWSRoEe5aN3LCd4My+/Aj+sk4ON/s2BV3ska5Im93j+vC +rCv3uPn1n2jUhWuJ3bDqipeTW4n/CQA2m/8vd26TMk22yOkkqw2MIA8sjJ//SD7g +tdG7up6DgGMP4hgbO89uGU7DAwKBgQDJtkKd0grh3u52Foeh9YaiAgYRwc65IE16 +UyD1OJxIuX/dYQDLlo5KyyngFa1ZhWIs7qC7r3xXH+10kfJY+Q+5YMjmZjlL8SR1 +Ujqd02R9F2//6OeswyReachJZbZdtiEw3lPa4jVFYfhSe0M2ZPxMwvoXb25eyCNI +1lYjSKq87wKBgHnLTNghjeDp4UKe6rNYPgRm0rDrhziJtX5JeUov1mALKb6dnmkh +GfRK9g8sQqKDfXwfC6Z2gaMK9YaryujGaWYoCpoPXtmJ6oLPXH4XHuLh4mhUiP46 +xn8FEfSimuQS4/FMxH8A128GHQSI7AhGFFzlwfrBWcvXC+mNDsTvMmLxAoGARc+4 +upppfccETQZ7JsitMgD1TMwA2f2eEwoWTAitvlXFNT9PYSbYVHaAJbga6PLLCbYF +FzAjHpxEOKYSdEyu7n/ayDL0/Z2V+qzc8KarDsg/0RgwppBbU/nUgeKb/U79qcYo +y4ai3UKNCS70Ei1dTMvmdpnwXwlxfNIBufB6dy0CgYBMYq9Lc31GkC6PcGEEbx6W +vjImOadWZbuOVnvEQjb5XCdcOsWsMcg96PtoeuyyHmhnEF1GsMzcIdQv/PHrvYpK +Yp8D0aqsLEgwGrJQER26FPpKmyIwvcL+nm6q5W31PnU9AOC/WEkB6Zs58hsMzD2S +kEJQcmfVew5mFXyxuEn3zA== +-----END PRIVATE KEY-----]], + project_id = "apisix", + token_uri = "http://127.0.0.1:1980/token", + scope = { + "https://www.googleapis.com/auth/cloud-platform" + }, + }, + } + local data, err = gcp.get(conf, "/apisix") + if err then + return ngx.say(err) + end + + ngx.say("done") + } + } +--- request +GET /t +--- response_body +can't find main key, key: /apisix + + + +=== TEST 3: add secret && consumer && check +--- request +GET /t +--- config + location /t { + content_by_lua_block { + local conf = { + auth_config = { + client_email = "email@apisix.iam.gserviceaccount.com", + private_key = [[ +-----BEGIN PRIVATE KEY----- +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDDzrFwnA3EvYyR +aeMgaLD3hBjvxKrz10uox1X8q7YYhf2ViRtLRUMa2bEMYksE5hbhwpNf6mKAnLOC +UuAT6cPPdUl/agKpJXviBPIR2LuzD17WsLJHp1HxUDssSkgfCaGcOGGNfLUhhIpF +2JUctLmxiZoAZySlSjcwupSuDJ0aPm0XO8r9H8Qu5kF2Vkz5e5bFivLTmvzrQTe4 +v5V1UI6hThElCSeUmdNF3uG3wopxlvq4zXgLTnuLbrNf/Gc4mlpV+UDgTISj32Ep +AB2vxKEbvQw4ti8YJnGXWjxLerhfrszFw+V8lpeduiDYA44ZFoVqvzxeIsVZNtcw +Iu7PvEPNAgMBAAECggEAVpyN9m7A1F631/aLheFpLgMbeKt4puV7zQtnaJ2XrZ9P +PR7pmNDpTu4uF3k/D8qrIm+L+uhVa+hkquf3wDct6w1JVnfQ93riImbnoKdK13ic +DcEZCwLjByfjFMNCxZ/gAZca55fbExlqhFy6EHmMjhB8s2LsXcTHRuGxNI/Vyi49 +sxECibe0U53aqdJbVWrphIS67cpwl4TUkN6mrHsNuDYNJ9dgkpapoqp4FTFQsBqC +afOK5qgJ68dWZ47FBUng+AZjdCncqAIuJxxItGVQP6YPsFs+OXcivIVHJr363TpC +l85FfdvqWV5OGBbwSKhNwiTNUVvfSQVmtURGWG/HbQKBgQD4gZ1z9+Lx19kT9WTz +lw93lxso++uhAPDTKviyWSRoEe5aN3LCd4My+/Aj+sk4ON/s2BV3ska5Im93j+vC +rCv3uPn1n2jUhWuJ3bDqipeTW4n/CQA2m/8vd26TMk22yOkkqw2MIA8sjJ//SD7g +tdG7up6DgGMP4hgbO89uGU7DAwKBgQDJtkKd0grh3u52Foeh9YaiAgYRwc65IE16 +UyD1OJxIuX/dYQDLlo5KyyngFa1ZhWIs7qC7r3xXH+10kfJY+Q+5YMjmZjlL8SR1 +Ujqd02R9F2//6OeswyReachJZbZdtiEw3lPa4jVFYfhSe0M2ZPxMwvoXb25eyCNI +1lYjSKq87wKBgHnLTNghjeDp4UKe6rNYPgRm0rDrhziJtX5JeUov1mALKb6dnmkh +GfRK9g8sQqKDfXwfC6Z2gaMK9YaryujGaWYoCpoPXtmJ6oLPXH4XHuLh4mhUiP46 +xn8FEfSimuQS4/FMxH8A128GHQSI7AhGFFzlwfrBWcvXC+mNDsTvMmLxAoGARc+4 +upppfccETQZ7JsitMgD1TMwA2f2eEwoWTAitvlXFNT9PYSbYVHaAJbga6PLLCbYF +FzAjHpxEOKYSdEyu7n/ayDL0/Z2V+qzc8KarDsg/0RgwppBbU/nUgeKb/U79qcYo +y4ai3UKNCS70Ei1dTMvmdpnwXwlxfNIBufB6dy0CgYBMYq9Lc31GkC6PcGEEbx6W +vjImOadWZbuOVnvEQjb5XCdcOsWsMcg96PtoeuyyHmhnEF1GsMzcIdQv/PHrvYpK +Yp8D0aqsLEgwGrJQER26FPpKmyIwvcL+nm6q5W31PnU9AOC/WEkB6Zs58hsMzD2S +kEJQcmfVew5mFXyxuEn3zA== +-----END PRIVATE KEY-----]], + project_id = "apisix", + token_uri = "http://127.0.0.1:1980/google/secret/token", + scope = { + "https://www.googleapis.com/auth/cloud-platform" + }, + entries_uri = "http://127.0.0.1:1984" + }, + } + + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/secrets/gcp/mysecret', ngx.HTTP_PUT, conf) + + if code >= 300 then + ngx.status = code + return ngx.say(body) + end + + -- change consumer with secrets ref: gcp + code, body = t('/apisix/admin/consumers', + ngx.HTTP_PUT, + [[{ + "username": "jack", + "plugins": { + "key-auth": { + "key": "$secret://gcp/mysecret/jack/key" + } + } + }]] + ) + if code >= 300 then + ngx.status = code + return ngx.say(body) + end + + + local secret = require("apisix.secret") + local value = secret.fetch_by_uri("$secret://gcp/mysecret/jack/key") + + + local code, body = t('/apisix/admin/secrets/gcp/mysecret', ngx.HTTP_DELETE) + if code >= 300 then + ngx.status = code + return ngx.say(body) + end + + code, body = t('/apisix/admin/consumers', + ngx.HTTP_PUT, + [[{ + "username": "jack", + "plugins": { + "key-auth": { + "key": "$secret://gcp/mysecret/jack/key" + } + } + }]] + ) + if code >= 300 then + ngx.status = code + return ngx.say(body) + end + + local secret = require("apisix.secret") + local value = secret.fetch_by_uri("$secret://gcp/mysecret/jack/key") + if value then + ngx.say("secret value: ", value) + end + ngx.say("all done") + } + } +--- response_body +all done + + + +=== TEST 4: setup route (/projects/apisix/secrets/jack/versions/latest:access) +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "plugins": { + "serverless-pre-function": { + "phase": "rewrite", + "functions": [ + "return function(conf, ctx) + require('lib.server').google_secret_apisix_jack() + end" + ] + } + }, + "uri": "/projects/apisix/secrets/jack/versions/latest:access", + "upstream": { + "type": "roundrobin", + "nodes": { + "127.0.0.1:1980": 1 + } + } + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- request +GET /t +--- response_body +passed + + + +=== TEST 5: setup route (/projects/apisix_error/secrets/jack/versions/latest:access) +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/2', + ngx.HTTP_PUT, + [[{ + "plugins": { + "serverless-pre-function": { + "phase": "rewrite", + "functions": [ + "return function(conf, ctx) + require('lib.server').google_secret_apisix_error_jack() + end" + ] + } + }, + "uri": "/projects/apisix_error/secrets/jack/versions/latest:access", + "upstream": { + "type": "roundrobin", + "nodes": { + "127.0.0.1:1980": 1 + } + } + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- request +GET /t +--- response_body +passed + + + +=== TEST 6: setup route (/projects/apisix/secrets/mysql/versions/latest:access) +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/3', + ngx.HTTP_PUT, + [[{ + "plugins": { + "serverless-pre-function": { + "phase": "rewrite", + "functions": [ + "return function(conf, ctx) + require('lib.server').google_secret_apisix_mysql() + end" + ] + } + }, + "uri": "/projects/apisix/secrets/mysql/versions/latest:access", + "upstream": { + "type": "roundrobin", + "nodes": { + "127.0.0.1:1980": 1 + } + } + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- request +GET /t +--- response_body +passed + + + +=== TEST 7: get value from gcp by auth_file(fetch_oatuh_conf failed, read failed) +--- config + location /t { + content_by_lua_block { + local conf = { + auth_file = "t/secret/conf/nofind.json", + } + local gcp = require("apisix.secret.gcp") + local value, err = gcp.get(conf, "jack/key") + if not value then + return ngx.say(err) + end + ngx.say(value) + } + } +--- request +GET /t +--- response_body +failed to retrtive data from gcp secret manager: failed to read configuration, file: t/secret/conf/nofind.json, err: t/secret/conf/nofind.json: No such file or directory + + + +=== TEST 8: get value from gcp by auth_file(fetch_oatuh_conf success) +--- config + location /t { + content_by_lua_block { + local conf = { + auth_file = "t/secret/conf/success.json", + } + local gcp = require("apisix.secret.gcp") + local value, err = gcp.get(conf, "jack/key") + if not value then + return ngx.say(err) + end + ngx.say(value) + } + } +--- request +GET /t +--- response_body +value + + + +=== TEST 9: get value from gcp by auth_file(fetch_oatuh_conf failed, undefined) +--- config + location /t { + content_by_lua_block { + local conf = { + auth_file = "t/secret/conf/error.json", + } + local gcp = require("apisix.secret.gcp") + local value, err = gcp.get(conf, "jack/key") + if not value then + return ngx.say(err) + end + ngx.say(value) + } + } +--- request +GET /t +--- response_body +failed to retrtive data from gcp secret manager: config parse failure, file: t/secret/conf/error.json, err: property "auth_config" validation failed: property "client_email" is required + + + +=== TEST 10: get json value from gcp +--- config + location /t { + content_by_lua_block { + local conf = { + auth_config = { + client_email = "email@apisix.iam.gserviceaccount.com", + private_key = [[ +-----BEGIN PRIVATE KEY----- +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDDzrFwnA3EvYyR +aeMgaLD3hBjvxKrz10uox1X8q7YYhf2ViRtLRUMa2bEMYksE5hbhwpNf6mKAnLOC +UuAT6cPPdUl/agKpJXviBPIR2LuzD17WsLJHp1HxUDssSkgfCaGcOGGNfLUhhIpF +2JUctLmxiZoAZySlSjcwupSuDJ0aPm0XO8r9H8Qu5kF2Vkz5e5bFivLTmvzrQTe4 +v5V1UI6hThElCSeUmdNF3uG3wopxlvq4zXgLTnuLbrNf/Gc4mlpV+UDgTISj32Ep +AB2vxKEbvQw4ti8YJnGXWjxLerhfrszFw+V8lpeduiDYA44ZFoVqvzxeIsVZNtcw +Iu7PvEPNAgMBAAECggEAVpyN9m7A1F631/aLheFpLgMbeKt4puV7zQtnaJ2XrZ9P +PR7pmNDpTu4uF3k/D8qrIm+L+uhVa+hkquf3wDct6w1JVnfQ93riImbnoKdK13ic +DcEZCwLjByfjFMNCxZ/gAZca55fbExlqhFy6EHmMjhB8s2LsXcTHRuGxNI/Vyi49 +sxECibe0U53aqdJbVWrphIS67cpwl4TUkN6mrHsNuDYNJ9dgkpapoqp4FTFQsBqC +afOK5qgJ68dWZ47FBUng+AZjdCncqAIuJxxItGVQP6YPsFs+OXcivIVHJr363TpC +l85FfdvqWV5OGBbwSKhNwiTNUVvfSQVmtURGWG/HbQKBgQD4gZ1z9+Lx19kT9WTz +lw93lxso++uhAPDTKviyWSRoEe5aN3LCd4My+/Aj+sk4ON/s2BV3ska5Im93j+vC +rCv3uPn1n2jUhWuJ3bDqipeTW4n/CQA2m/8vd26TMk22yOkkqw2MIA8sjJ//SD7g +tdG7up6DgGMP4hgbO89uGU7DAwKBgQDJtkKd0grh3u52Foeh9YaiAgYRwc65IE16 +UyD1OJxIuX/dYQDLlo5KyyngFa1ZhWIs7qC7r3xXH+10kfJY+Q+5YMjmZjlL8SR1 +Ujqd02R9F2//6OeswyReachJZbZdtiEw3lPa4jVFYfhSe0M2ZPxMwvoXb25eyCNI +1lYjSKq87wKBgHnLTNghjeDp4UKe6rNYPgRm0rDrhziJtX5JeUov1mALKb6dnmkh +GfRK9g8sQqKDfXwfC6Z2gaMK9YaryujGaWYoCpoPXtmJ6oLPXH4XHuLh4mhUiP46 +xn8FEfSimuQS4/FMxH8A128GHQSI7AhGFFzlwfrBWcvXC+mNDsTvMmLxAoGARc+4 +upppfccETQZ7JsitMgD1TMwA2f2eEwoWTAitvlXFNT9PYSbYVHaAJbga6PLLCbYF +FzAjHpxEOKYSdEyu7n/ayDL0/Z2V+qzc8KarDsg/0RgwppBbU/nUgeKb/U79qcYo +y4ai3UKNCS70Ei1dTMvmdpnwXwlxfNIBufB6dy0CgYBMYq9Lc31GkC6PcGEEbx6W +vjImOadWZbuOVnvEQjb5XCdcOsWsMcg96PtoeuyyHmhnEF1GsMzcIdQv/PHrvYpK +Yp8D0aqsLEgwGrJQER26FPpKmyIwvcL+nm6q5W31PnU9AOC/WEkB6Zs58hsMzD2S +kEJQcmfVew5mFXyxuEn3zA== +-----END PRIVATE KEY-----]], + project_id = "apisix", + token_uri = "http://127.0.0.1:1980/google/secret/token", + scope = { + "https://www.googleapis.com/auth/cloud-platform" + }, + entries_uri = "http://127.0.0.1:1984" + }, + } + local gcp = require("apisix.secret.gcp") + local value, err = gcp.get(conf, "jack/key") + if not value then + return ngx.say(err) + end + ngx.say(value) + } + } +--- request +GET /t +--- response_body +value + + + +=== TEST 11: get string value from gcp +--- config + location /t { + content_by_lua_block { + local conf = { + auth_config = { + client_email = "email@apisix.iam.gserviceaccount.com", + private_key = [[ +-----BEGIN PRIVATE KEY----- +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDDzrFwnA3EvYyR +aeMgaLD3hBjvxKrz10uox1X8q7YYhf2ViRtLRUMa2bEMYksE5hbhwpNf6mKAnLOC +UuAT6cPPdUl/agKpJXviBPIR2LuzD17WsLJHp1HxUDssSkgfCaGcOGGNfLUhhIpF +2JUctLmxiZoAZySlSjcwupSuDJ0aPm0XO8r9H8Qu5kF2Vkz5e5bFivLTmvzrQTe4 +v5V1UI6hThElCSeUmdNF3uG3wopxlvq4zXgLTnuLbrNf/Gc4mlpV+UDgTISj32Ep +AB2vxKEbvQw4ti8YJnGXWjxLerhfrszFw+V8lpeduiDYA44ZFoVqvzxeIsVZNtcw +Iu7PvEPNAgMBAAECggEAVpyN9m7A1F631/aLheFpLgMbeKt4puV7zQtnaJ2XrZ9P +PR7pmNDpTu4uF3k/D8qrIm+L+uhVa+hkquf3wDct6w1JVnfQ93riImbnoKdK13ic +DcEZCwLjByfjFMNCxZ/gAZca55fbExlqhFy6EHmMjhB8s2LsXcTHRuGxNI/Vyi49 +sxECibe0U53aqdJbVWrphIS67cpwl4TUkN6mrHsNuDYNJ9dgkpapoqp4FTFQsBqC +afOK5qgJ68dWZ47FBUng+AZjdCncqAIuJxxItGVQP6YPsFs+OXcivIVHJr363TpC +l85FfdvqWV5OGBbwSKhNwiTNUVvfSQVmtURGWG/HbQKBgQD4gZ1z9+Lx19kT9WTz +lw93lxso++uhAPDTKviyWSRoEe5aN3LCd4My+/Aj+sk4ON/s2BV3ska5Im93j+vC +rCv3uPn1n2jUhWuJ3bDqipeTW4n/CQA2m/8vd26TMk22yOkkqw2MIA8sjJ//SD7g +tdG7up6DgGMP4hgbO89uGU7DAwKBgQDJtkKd0grh3u52Foeh9YaiAgYRwc65IE16 +UyD1OJxIuX/dYQDLlo5KyyngFa1ZhWIs7qC7r3xXH+10kfJY+Q+5YMjmZjlL8SR1 +Ujqd02R9F2//6OeswyReachJZbZdtiEw3lPa4jVFYfhSe0M2ZPxMwvoXb25eyCNI +1lYjSKq87wKBgHnLTNghjeDp4UKe6rNYPgRm0rDrhziJtX5JeUov1mALKb6dnmkh +GfRK9g8sQqKDfXwfC6Z2gaMK9YaryujGaWYoCpoPXtmJ6oLPXH4XHuLh4mhUiP46 +xn8FEfSimuQS4/FMxH8A128GHQSI7AhGFFzlwfrBWcvXC+mNDsTvMmLxAoGARc+4 +upppfccETQZ7JsitMgD1TMwA2f2eEwoWTAitvlXFNT9PYSbYVHaAJbga6PLLCbYF +FzAjHpxEOKYSdEyu7n/ayDL0/Z2V+qzc8KarDsg/0RgwppBbU/nUgeKb/U79qcYo +y4ai3UKNCS70Ei1dTMvmdpnwXwlxfNIBufB6dy0CgYBMYq9Lc31GkC6PcGEEbx6W +vjImOadWZbuOVnvEQjb5XCdcOsWsMcg96PtoeuyyHmhnEF1GsMzcIdQv/PHrvYpK +Yp8D0aqsLEgwGrJQER26FPpKmyIwvcL+nm6q5W31PnU9AOC/WEkB6Zs58hsMzD2S +kEJQcmfVew5mFXyxuEn3zA== +-----END PRIVATE KEY-----]], + project_id = "apisix", + token_uri = "http://127.0.0.1:1980/google/secret/token", + scope = { + "https://www.googleapis.com/auth/cloud-platform" + }, + entries_uri = "http://127.0.0.1:1984" + }, + } + local gcp = require("apisix.secret.gcp") + local value, err = gcp.get(conf, "mysql") + if not value then + return ngx.say(err) + end + ngx.say(value) + } + } +--- request +GET /t +--- response_body +secret + + + +=== TEST 12: get value from gcp(failed to get google oauth token) +--- config + location /t { + content_by_lua_block { + local conf = { + auth_config = { + client_email = "email@apisix.iam.gserviceaccount.com", + private_key = [[ +-----BEGIN PRIVATE KEY----- +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDDzrFwnA3EvYyR +aeMgaLD3hBjvxKrz10uox1X8q7YYhf2ViRtLRUMa2bEMYksE5hbhwpNf6mKAnLOC +UuAT6cPPdUl/agKpJXviBPIR2LuzD17WsLJHp1HxUDssSkgfCaGcOGGNfLUhhIpF +2JUctLmxiZoAZySlSjcwupSuDJ0aPm0XO8r9H8Qu5kF2Vkz5e5bFivLTmvzrQTe4 +v5V1UI6hThElCSeUmdNF3uG3wopxlvq4zXgLTnuLbrNf/Gc4mlpV+UDgTISj32Ep +AB2vxKEbvQw4ti8YJnGXWjxLerhfrszFw+V8lpeduiDYA44ZFoVqvzxeIsVZNtcw +Iu7PvEPNAgMBAAECggEAVpyN9m7A1F631/aLheFpLgMbeKt4puV7zQtnaJ2XrZ9P +PR7pmNDpTu4uF3k/D8qrIm+L+uhVa+hkquf3wDct6w1JVnfQ93riImbnoKdK13ic +DcEZCwLjByfjFMNCxZ/gAZca55fbExlqhFy6EHmMjhB8s2LsXcTHRuGxNI/Vyi49 +sxECibe0U53aqdJbVWrphIS67cpwl4TUkN6mrHsNuDYNJ9dgkpapoqp4FTFQsBqC +afOK5qgJ68dWZ47FBUng+AZjdCncqAIuJxxItGVQP6YPsFs+OXcivIVHJr363TpC +l85FfdvqWV5OGBbwSKhNwiTNUVvfSQVmtURGWG/HbQKBgQD4gZ1z9+Lx19kT9WTz +lw93lxso++uhAPDTKviyWSRoEe5aN3LCd4My+/Aj+sk4ON/s2BV3ska5Im93j+vC +rCv3uPn1n2jUhWuJ3bDqipeTW4n/CQA2m/8vd26TMk22yOkkqw2MIA8sjJ//SD7g +tdG7up6DgGMP4hgbO89uGU7DAwKBgQDJtkKd0grh3u52Foeh9YaiAgYRwc65IE16 +UyD1OJxIuX/dYQDLlo5KyyngFa1ZhWIs7qC7r3xXH+10kfJY+Q+5YMjmZjlL8SR1 +Ujqd02R9F2//6OeswyReachJZbZdtiEw3lPa4jVFYfhSe0M2ZPxMwvoXb25eyCNI +1lYjSKq87wKBgHnLTNghjeDp4UKe6rNYPgRm0rDrhziJtX5JeUov1mALKb6dnmkh +GfRK9g8sQqKDfXwfC6Z2gaMK9YaryujGaWYoCpoPXtmJ6oLPXH4XHuLh4mhUiP46 +xn8FEfSimuQS4/FMxH8A128GHQSI7AhGFFzlwfrBWcvXC+mNDsTvMmLxAoGARc+4 +upppfccETQZ7JsitMgD1TMwA2f2eEwoWTAitvlXFNT9PYSbYVHaAJbga6PLLCbYF +FzAjHpxEOKYSdEyu7n/ayDL0/Z2V+qzc8KarDsg/0RgwppBbU/nUgeKb/U79qcYo +y4ai3UKNCS70Ei1dTMvmdpnwXwlxfNIBufB6dy0CgYBMYq9Lc31GkC6PcGEEbx6W +vjImOadWZbuOVnvEQjb5XCdcOsWsMcg96PtoeuyyHmhnEF1GsMzcIdQv/PHrvYpK +Yp8D0aqsLEgwGrJQER26FPpKmyIwvcL+nm6q5W31PnU9AOC/WEkB6Zs58hsMzD2S +kEJQcmfVew5mFXyxuEn3zA== +-----END PRIVATE KEY-----]], + project_id = "apisix", + token_uri = "http://127.0.0.1:1980/google/secret/token", + scope = { + "https://www.googleapis.com/auth/root/cloud-platform" + }, + entries_uri = "http://127.0.0.1:1984" + }, + } + local gcp = require("apisix.secret.gcp") + local value, err = gcp.get(conf, "jack/key") + if not value then + return ngx.say(err) + end + ngx.say(value) + } + } +--- request +GET /t +--- response_body +failed to retrtive data from gcp secret manager: failed to get google oauth token +--- grep_error_log eval +qr/\{\"error\"\:\"[\w+\s+]*\"\}/ +--- grep_error_log_out +{"error":"no access to this scope"} + + + +=== TEST 13: get value from gcp (not res) +--- config + location /t { + content_by_lua_block { + local conf = { + auth_config = { + client_email = "email@apisix.iam.gserviceaccount.com", + private_key = [[ +-----BEGIN PRIVATE KEY----- +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDDzrFwnA3EvYyR +aeMgaLD3hBjvxKrz10uox1X8q7YYhf2ViRtLRUMa2bEMYksE5hbhwpNf6mKAnLOC +UuAT6cPPdUl/agKpJXviBPIR2LuzD17WsLJHp1HxUDssSkgfCaGcOGGNfLUhhIpF +2JUctLmxiZoAZySlSjcwupSuDJ0aPm0XO8r9H8Qu5kF2Vkz5e5bFivLTmvzrQTe4 +v5V1UI6hThElCSeUmdNF3uG3wopxlvq4zXgLTnuLbrNf/Gc4mlpV+UDgTISj32Ep +AB2vxKEbvQw4ti8YJnGXWjxLerhfrszFw+V8lpeduiDYA44ZFoVqvzxeIsVZNtcw +Iu7PvEPNAgMBAAECggEAVpyN9m7A1F631/aLheFpLgMbeKt4puV7zQtnaJ2XrZ9P +PR7pmNDpTu4uF3k/D8qrIm+L+uhVa+hkquf3wDct6w1JVnfQ93riImbnoKdK13ic +DcEZCwLjByfjFMNCxZ/gAZca55fbExlqhFy6EHmMjhB8s2LsXcTHRuGxNI/Vyi49 +sxECibe0U53aqdJbVWrphIS67cpwl4TUkN6mrHsNuDYNJ9dgkpapoqp4FTFQsBqC +afOK5qgJ68dWZ47FBUng+AZjdCncqAIuJxxItGVQP6YPsFs+OXcivIVHJr363TpC +l85FfdvqWV5OGBbwSKhNwiTNUVvfSQVmtURGWG/HbQKBgQD4gZ1z9+Lx19kT9WTz +lw93lxso++uhAPDTKviyWSRoEe5aN3LCd4My+/Aj+sk4ON/s2BV3ska5Im93j+vC +rCv3uPn1n2jUhWuJ3bDqipeTW4n/CQA2m/8vd26TMk22yOkkqw2MIA8sjJ//SD7g +tdG7up6DgGMP4hgbO89uGU7DAwKBgQDJtkKd0grh3u52Foeh9YaiAgYRwc65IE16 +UyD1OJxIuX/dYQDLlo5KyyngFa1ZhWIs7qC7r3xXH+10kfJY+Q+5YMjmZjlL8SR1 +Ujqd02R9F2//6OeswyReachJZbZdtiEw3lPa4jVFYfhSe0M2ZPxMwvoXb25eyCNI +1lYjSKq87wKBgHnLTNghjeDp4UKe6rNYPgRm0rDrhziJtX5JeUov1mALKb6dnmkh +GfRK9g8sQqKDfXwfC6Z2gaMK9YaryujGaWYoCpoPXtmJ6oLPXH4XHuLh4mhUiP46 +xn8FEfSimuQS4/FMxH8A128GHQSI7AhGFFzlwfrBWcvXC+mNDsTvMmLxAoGARc+4 +upppfccETQZ7JsitMgD1TMwA2f2eEwoWTAitvlXFNT9PYSbYVHaAJbga6PLLCbYF +FzAjHpxEOKYSdEyu7n/ayDL0/Z2V+qzc8KarDsg/0RgwppBbU/nUgeKb/U79qcYo +y4ai3UKNCS70Ei1dTMvmdpnwXwlxfNIBufB6dy0CgYBMYq9Lc31GkC6PcGEEbx6W +vjImOadWZbuOVnvEQjb5XCdcOsWsMcg96PtoeuyyHmhnEF1GsMzcIdQv/PHrvYpK +Yp8D0aqsLEgwGrJQER26FPpKmyIwvcL+nm6q5W31PnU9AOC/WEkB6Zs58hsMzD2S +kEJQcmfVew5mFXyxuEn3zA== +-----END PRIVATE KEY-----]], + project_id = "apisix_error", + token_uri = "http://127.0.0.1:1980/google/secret/token", + scope = { + "https://www.googleapis.com/auth/cloud-platform" + }, + entries_uri = "http://127.0.0.1:1984" + }, + } + local gcp = require("apisix.secret.gcp") + local value, err = gcp.get(conf, "jack/key") + if not value then + return ngx.say("err") + end + ngx.say(value) + } + } +--- request +GET /t +--- response_body +err + + + +=== TEST 14: get value from gcp (res status ~= 200) +--- config + location /t { + content_by_lua_block { + local conf = { + auth_config = { + client_email = "email@apisix.iam.gserviceaccount.com", + private_key = [[ +-----BEGIN PRIVATE KEY----- +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDDzrFwnA3EvYyR +aeMgaLD3hBjvxKrz10uox1X8q7YYhf2ViRtLRUMa2bEMYksE5hbhwpNf6mKAnLOC +UuAT6cPPdUl/agKpJXviBPIR2LuzD17WsLJHp1HxUDssSkgfCaGcOGGNfLUhhIpF +2JUctLmxiZoAZySlSjcwupSuDJ0aPm0XO8r9H8Qu5kF2Vkz5e5bFivLTmvzrQTe4 +v5V1UI6hThElCSeUmdNF3uG3wopxlvq4zXgLTnuLbrNf/Gc4mlpV+UDgTISj32Ep +AB2vxKEbvQw4ti8YJnGXWjxLerhfrszFw+V8lpeduiDYA44ZFoVqvzxeIsVZNtcw +Iu7PvEPNAgMBAAECggEAVpyN9m7A1F631/aLheFpLgMbeKt4puV7zQtnaJ2XrZ9P +PR7pmNDpTu4uF3k/D8qrIm+L+uhVa+hkquf3wDct6w1JVnfQ93riImbnoKdK13ic +DcEZCwLjByfjFMNCxZ/gAZca55fbExlqhFy6EHmMjhB8s2LsXcTHRuGxNI/Vyi49 +sxECibe0U53aqdJbVWrphIS67cpwl4TUkN6mrHsNuDYNJ9dgkpapoqp4FTFQsBqC +afOK5qgJ68dWZ47FBUng+AZjdCncqAIuJxxItGVQP6YPsFs+OXcivIVHJr363TpC +l85FfdvqWV5OGBbwSKhNwiTNUVvfSQVmtURGWG/HbQKBgQD4gZ1z9+Lx19kT9WTz +lw93lxso++uhAPDTKviyWSRoEe5aN3LCd4My+/Aj+sk4ON/s2BV3ska5Im93j+vC +rCv3uPn1n2jUhWuJ3bDqipeTW4n/CQA2m/8vd26TMk22yOkkqw2MIA8sjJ//SD7g +tdG7up6DgGMP4hgbO89uGU7DAwKBgQDJtkKd0grh3u52Foeh9YaiAgYRwc65IE16 +UyD1OJxIuX/dYQDLlo5KyyngFa1ZhWIs7qC7r3xXH+10kfJY+Q+5YMjmZjlL8SR1 +Ujqd02R9F2//6OeswyReachJZbZdtiEw3lPa4jVFYfhSe0M2ZPxMwvoXb25eyCNI +1lYjSKq87wKBgHnLTNghjeDp4UKe6rNYPgRm0rDrhziJtX5JeUov1mALKb6dnmkh +GfRK9g8sQqKDfXwfC6Z2gaMK9YaryujGaWYoCpoPXtmJ6oLPXH4XHuLh4mhUiP46 +xn8FEfSimuQS4/FMxH8A128GHQSI7AhGFFzlwfrBWcvXC+mNDsTvMmLxAoGARc+4 +upppfccETQZ7JsitMgD1TMwA2f2eEwoWTAitvlXFNT9PYSbYVHaAJbga6PLLCbYF +FzAjHpxEOKYSdEyu7n/ayDL0/Z2V+qzc8KarDsg/0RgwppBbU/nUgeKb/U79qcYo +y4ai3UKNCS70Ei1dTMvmdpnwXwlxfNIBufB6dy0CgYBMYq9Lc31GkC6PcGEEbx6W +vjImOadWZbuOVnvEQjb5XCdcOsWsMcg96PtoeuyyHmhnEF1GsMzcIdQv/PHrvYpK +Yp8D0aqsLEgwGrJQER26FPpKmyIwvcL+nm6q5W31PnU9AOC/WEkB6Zs58hsMzD2S +kEJQcmfVew5mFXyxuEn3zA== +-----END PRIVATE KEY-----]], + project_id = "apisix_error", + token_uri = "http://127.0.0.1:1980/google/secret/token", + scope = { + "https://www.googleapis.com/auth/cloud-platform" + }, + entries_uri = "http://127.0.0.1:1984" + }, + } + local gcp = require("apisix.secret.gcp") + local value, err = gcp.get(conf, "jack/key") + if not value then + return ngx.say("err") + end + ngx.say(value) + } + } +--- request +GET /t +--- response_body +err From a478de8a586671bc7dd0dcf89d8db18d9913ab7c Mon Sep 17 00:00:00 2001 From: HuanXin-Chen <111850224+HuanXin-Chen@users.noreply.github.com> Date: Tue, 24 Sep 2024 16:19:38 +0800 Subject: [PATCH 4/7] docs: add more secret information to the admin api (#11569) * docs(admin): add more secret information to the admin api * Update admin-api.md * Update admin-api.md * Update admin-api.md * Update admin-api.md * docs(admin): fix the style shell to json * docs(admin): Table formatting * fix(docs): ci lint problem * docs(admin): remove example --- docs/en/latest/admin-api.md | 98 ++++++++++++++++++++++++++++++++++- docs/zh/latest/admin-api.md | 100 +++++++++++++++++++++++++++++++++++- 2 files changed, 196 insertions(+), 2 deletions(-) diff --git a/docs/en/latest/admin-api.md b/docs/en/latest/admin-api.md index d928e7a6936d..b6f03cbd5855 100644 --- a/docs/en/latest/admin-api.md +++ b/docs/en/latest/admin-api.md @@ -1459,7 +1459,7 @@ Secret resource request address: /apisix/admin/secrets/{secretmanager}/{id} ### Request Body Parameters -When `{secretmanager}` is `vault`: +#### When Secret Manager is Vault | Parameter | Required | Type | Description | Example | | ----------- | -------- | ----------- | ------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------ | @@ -1497,6 +1497,102 @@ HTTP/1.1 200 OK {"key":"\/apisix\/secrets\/vault\/test2","value":{"id":"vault\/test2","token":"apisix","prefix":"apisix","update_time":1669625828,"create_time":1669625828,"uri":"http:\/\/xxx\/get"}} ``` +#### When Secret Manager is AWS + +| Parameter | Required | Type | Description | +| ----------------- | -------- | ------ | --------------------------------------- | +| access_key_id | True | string | AWS Access Key ID | +| secret_access_key | True | string | AWS Secret Access Key | +| session_token | False | string | Temporary access credential information | +| region | False | string | AWS Region | +| endpoint_url | False | URI | AWS Secret Manager URL | + +Example Configuration: + +```json +{ + "endpoint_url": "http://127.0.0.1:4566", + "region": "us-east-1", + "access_key_id": "access", + "secret_access_key": "secret", + "session_token": "token" +} +``` + +Example API usage: + +```shell +curl -i http://127.0.0.1:9180/apisix/admin/secrets/aws/test3 \ +-H "X-API-KEY: $admin_key" -X PUT -d ' +{ + "endpoint_url": "http://127.0.0.1:4566", + "region": "us-east-1", + "access_key_id": "access", + "secret_access_key": "secret", + "session_token": "token" +}' +``` + +```shell +HTTP/1.1 200 OK +... + +{"value":{"create_time":1726069970,"endpoint_url":"http://127.0.0.1:4566","region":"us-east-1","access_key_id":"access","secret_access_key":"secret","id":"aws/test3","update_time":1726069970,"session_token":"token"},"key":"/apisix/secrets/aws/test3"} +``` + +#### When Secret Manager is GCP + +| Parameter | Required | Type | Description | Example | +| ------------------------ | -------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ | +| auth_config | True | object | Either `auth_config` or `auth_file` must be provided. | | +| auth_config.client_email | True | string | Email address of the Google Cloud service account. | | +| auth_config.private_key | True | string | Private key of the Google Cloud service account. | | +| auth_config.project_id | True | string | Project ID in the Google Cloud service account. | | +| auth_config.token_uri | False | string | Token URI of the Google Cloud service account. | [https://oauth2.googleapis.com/token](https://oauth2.googleapis.com/token) | +| auth_config.entries_uri | False | string | The API access endpoint for the Google Secrets Manager. | [https://secretmanager.googleapis.com/v1](https://secretmanager.googleapis.com/v1) | +| auth_config.scope | False | string | Access scopes of the Google Cloud service account. See [OAuth 2.0 Scopes for Google APIs](https://developers.google.com/identity/protocols/oauth2/scopes) | [https://www.googleapis.com/auth/cloud-platform](https://www.googleapis.com/auth/cloud-platform) | +| auth_file | True | string | Path to the Google Cloud service account authentication JSON file. Either `auth_config` or `auth_file` must be provided. | | +| ssl_verify | False | boolean | When set to `true`, enables SSL verification as mentioned in [OpenResty docs](https://github.com/openresty/lua-nginx-module#tcpsocksslhandshake). | true | + +Example Configuration: + +```json +{ + "auth_config" : { + "client_email": "email@apisix.iam.gserviceaccount.com", + "private_key": "private_key", + "project_id": "apisix-project", + "token_uri": "https://oauth2.googleapis.com/token", + "entries_uri": "https://secretmanager.googleapis.com/v1", + "scope": ["https://www.googleapis.com/auth/cloud-platform"] + } +} +``` + +Example API usage: + +```shell +curl -i http://127.0.0.1:9180/apisix/admin/secrets/gcp/test4 \ +-H "X-API-KEY: $admin_key" -X PUT -d ' +{ + "auth_config" : { + "client_email": "email@apisix.iam.gserviceaccount.com", + "private_key": "private_key", + "project_id": "apisix-project", + "token_uri": "https://oauth2.googleapis.com/token", + "entries_uri": "https://secretmanager.googleapis.com/v1", + "scope": ["https://www.googleapis.com/auth/cloud-platform"] + } +}' +``` + +```shell +HTTP/1.1 200 OK +... + +{"value":{"id":"gcp/test4","ssl_verify":true,"auth_config":{"token_uri":"https://oauth2.googleapis.com/token","scope":["https://www.googleapis.com/auth/cloud-platform"],"entries_uri":"https://secretmanager.googleapis.com/v1","client_email":"email@apisix.iam.gserviceaccount.com","private_key":"private_key","project_id":"apisix-project"},"create_time":1726070161,"update_time":1726070161},"key":"/apisix/secrets/gcp/test4"} +``` + ### Response Parameters Currently, the response is returned from etcd. diff --git a/docs/zh/latest/admin-api.md b/docs/zh/latest/admin-api.md index 5cefb428b0a0..19d97d7808b7 100644 --- a/docs/zh/latest/admin-api.md +++ b/docs/zh/latest/admin-api.md @@ -1469,7 +1469,7 @@ Secret 资源请求地址:/apisix/admin/secrets/{secretmanager}/{id} ### body 请求参数 {#secret-config-body-requset-parameters} -当 `{secretmanager}` 是 `vault` 时: +#### 当 Secret Manager 是 Vault 时 | 名称 | 必选项 | 类型 | 描述 | 例子 | | ----------- | -------- | ----------- | ------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------ | @@ -1508,6 +1508,104 @@ HTTP/1.1 200 OK {"key":"\/apisix\/secrets\/vault\/test2","value":{"id":"vault\/test2","token":"apisix","prefix":"apisix","update_time":1669625828,"create_time":1669625828,"uri":"http:\/\/xxx\/get"}} ``` +#### 当 Secret Manager 是 AWS 时 + +| 名称 | 必选项 | 默认值 | 描述 | +| ----------------- | ------ | --------------------------------------------- | ----------------------- | +| access_key_id | 是 | | AWS 访问密钥 ID | +| secret_access_key | 是 | | AWS 访问密钥 | +| session_token | 否 | | 临时访问凭证信息 | +| region | 否 | us-east-1 | AWS 区域 | +| endpoint_url | 否 | https://secretsmanager.{region}.amazonaws.com | AWS Secret Manager 地址 | + +配置示例: + +```json +{ + "endpoint_url": "http://127.0.0.1:4566", + "region": "us-east-1", + "access_key_id": "access", + "secret_access_key": "secret", + "session_token": "token" +} + +``` + +使用示例: + +```shell +curl -i http://127.0.0.1:9180/apisix/admin/secrets/aws/test3 \ +-H "X-API-KEY: $admin_key" -X PUT -d ' +{ + "endpoint_url": "http://127.0.0.1:4566", + "region": "us-east-1", + "access_key_id": "access", + "secret_access_key": "secret", + "session_token": "token" +}' +``` + +```shell +HTTP/1.1 200 OK +... + +{"value":{"create_time":1726069970,"endpoint_url":"http://127.0.0.1:4566","region":"us-east-1","access_key_id":"access","secret_access_key":"secret","id":"aws/test3","update_time":1726069970,"session_token":"token"},"key":"/apisix/secrets/aws/test3"} +``` + +#### 当 Secret Manager 是 GCP 时 + +| 名称 | 必选项 | 默认值 | 描述 | +| ------------------------ | ------ | ---------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | +| auth_config | 是 | | `auth_config` 和 `auth_file` 必须配置一个。 | +| auth_config.client_email | 是 | | 谷歌服务帐号的 email 参数。 | +| auth_config.private_key | 是 | | 谷歌服务帐号的私钥参数。 | +| auth_config.project_id | 是 | | 谷歌服务帐号的项目 ID。 | +| auth_config.token_uri | 否 | https://oauth2.googleapis.com/token | 请求谷歌服务帐户的令牌的 URI。 | +| auth_config.entries_uri | 否 | https://secretmanager.googleapis.com/v1 | 谷歌密钥服务访问端点 API。 | +| auth_config.scope | 否 | https://www.googleapis.com/auth/cloud-platform | 谷歌服务账号的访问范围,可参考 [OAuth 2.0 Scopes for Google APIs](https://developers.google.com/identity/protocols/oauth2/scopes) | +| auth_file | 是 | | `auth_config` 和 `auth_file` 必须配置一个。 | +| ssl_verify | 否 | true | 当设置为 `true` 时,启用 `SSL` 验证。 | + +配置示例: + +```json +{ + "auth_config" : { + "client_email": "email@apisix.iam.gserviceaccount.com", + "private_key": "private_key", + "project_id": "apisix-project", + "token_uri": "https://oauth2.googleapis.com/token", + "entries_uri": "https://secretmanager.googleapis.com/v1", + "scope": ["https://www.googleapis.com/auth/cloud-platform"] + } +} + +``` + +使用示例: + +```shell +curl -i http://127.0.0.1:9180/apisix/admin/secrets/gcp/test4 \ +-H "X-API-KEY: $admin_key" -X PUT -d ' +{ + "auth_config" : { + "client_email": "email@apisix.iam.gserviceaccount.com", + "private_key": "private_key", + "project_id": "apisix-project", + "token_uri": "https://oauth2.googleapis.com/token", + "entries_uri": "https://secretmanager.googleapis.com/v1", + "scope": ["https://www.googleapis.com/auth/cloud-platform"] + } +}' +``` + +```shell +HTTP/1.1 200 OK +... + +{"value":{"id":"gcp/test4","ssl_verify":true,"auth_config":{"token_uri":"https://oauth2.googleapis.com/token","scope":["https://www.googleapis.com/auth/cloud-platform"],"entries_uri":"https://secretmanager.googleapis.com/v1","client_email":"email@apisix.iam.gserviceaccount.com","private_key":"private_key","project_id":"apisix-project"},"create_time":1726070161,"update_time":1726070161},"key":"/apisix/secrets/gcp/test4"} +``` + ### 应答参数 {#secret-config-response-parameters} 当前的响应是从 etcd 返回的。 From d38d5b698be0913ee1c28980f57f7545ad99e603 Mon Sep 17 00:00:00 2001 From: Ashing Zheng Date: Wed, 25 Sep 2024 09:31:20 +0800 Subject: [PATCH 5/7] fix: encryption/decryption for non-auth plugins in consumer (#11600) Signed-off-by: ashing --- apisix/plugin.lua | 5 +- t/node/consumer-plugin3.t | 159 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 163 insertions(+), 1 deletion(-) create mode 100644 t/node/consumer-plugin3.t diff --git a/apisix/plugin.lua b/apisix/plugin.lua index 3d3c4b39be60..dc22459aaf9d 100644 --- a/apisix/plugin.lua +++ b/apisix/plugin.lua @@ -934,7 +934,10 @@ local function get_plugin_schema_for_gde(name, schema_type) local schema if schema_type == core.schema.TYPE_CONSUMER then - schema = plugin_schema.consumer_schema + -- when we use a non-auth plugin in the consumer, + -- where the consumer_schema field does not exist, + -- we need to fallback to it's schema for encryption and decryption. + schema = plugin_schema.consumer_schema or plugin_schema.schema elseif schema_type == core.schema.TYPE_METADATA then schema = plugin_schema.metadata_schema else diff --git a/t/node/consumer-plugin3.t b/t/node/consumer-plugin3.t new file mode 100644 index 000000000000..345bccd92771 --- /dev/null +++ b/t/node/consumer-plugin3.t @@ -0,0 +1,159 @@ +# +# 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_shuffle(); +no_root_location(); + +run_tests; + +__DATA__ + +=== TEST 1: add consumer with csrf plugin (data encryption enabled) +--- yaml_config +apisix: + data_encryption: + enable_encrypt_fields: true + keyring: + - edd1c9f0985e76a2 +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local json = require("toolkit.json") + local code, body = t('/apisix/admin/consumers', + ngx.HTTP_PUT, + [[{ + "username": "jack", + "plugins": { + "key-auth": { + "key": "key-a" + }, + "csrf": { + "key": "userkey", + "expires": 1000000000 + } + } + }]] + ) + if code >= 300 then + ngx.status = code + ngx.say(body) + return + end + + ngx.sleep(0.1) + + -- verify csrf key is decrypted in admin API + local code, message, res = t('/apisix/admin/consumers/jack', + ngx.HTTP_GET + ) + if code >= 300 then + ngx.status = code + ngx.say(message) + return + end + local consumer = json.decode(res) + ngx.say(consumer.value.plugins["csrf"].key) + + -- verify csrf key is encrypted in etcd + local etcd = require("apisix.core.etcd") + local res = assert(etcd.get('/consumers/jack')) + ngx.say(res.body.node.value.plugins["csrf"].key) + } + } +--- request +GET /t +--- response_body +userkey +mt39FazQccyMqt4ctoRV7w== +--- no_error_log +[error] + + + +=== TEST 2: add route +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "uri": "/hello", + "plugins": { + "key-auth": {} + }, + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + } + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- request +GET /t +--- response_body +passed + + + +=== TEST 3: invalid request - no csrf token +--- yaml_config +apisix: + data_encryption: + enable_encrypt_fields: true + keyring: + - edd1c9f0985e76a2 +--- request +POST /hello +--- more_headers +apikey: key-a +--- error_code: 401 +--- response_body +{"error_msg":"no csrf token in headers"} + + + +=== TEST 4: valid request - with csrf token +--- yaml_config +apisix: + data_encryption: + enable_encrypt_fields: true + keyring: + - edd1c9f0985e76a2 +--- request +POST /hello +--- more_headers +apikey: key-a +apisix-csrf-token: eyJyYW5kb20iOjAuNDI5ODYzMTk3MTYxMzksInNpZ24iOiI0ODRlMDY4NTkxMWQ5NmJhMDc5YzQ1ZGI0OTE2NmZkYjQ0ODhjODVkNWQ0NmE1Y2FhM2UwMmFhZDliNjE5OTQ2IiwiZXhwaXJlcyI6MjY0MzExOTYyNH0= +Cookie: apisix-csrf-token=eyJyYW5kb20iOjAuNDI5ODYzMTk3MTYxMzksInNpZ24iOiI0ODRlMDY4NTkxMWQ5NmJhMDc5YzQ1ZGI0OTE2NmZkYjQ0ODhjODVkNWQ0NmE1Y2FhM2UwMmFhZDliNjE5OTQ2IiwiZXhwaXJlcyI6MjY0MzExOTYyNH0= +--- response_body +hello world +--- no_error_log +[error] From 263143d897774977d843ea499721fc112bde399e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=82=9F=E7=A9=BA?= Date: Thu, 26 Sep 2024 15:49:10 +0800 Subject: [PATCH 6/7] feat: implement consumer credential (#11601) --- apisix/admin/consumers.lua | 14 - apisix/admin/credentials.lua | 74 +++ apisix/admin/init.lua | 9 +- apisix/admin/resource.lua | 44 ++ apisix/consumer.lua | 120 ++++- apisix/core/config_etcd.lua | 4 +- apisix/core/etcd.lua | 67 ++- apisix/schema_def.lua | 14 + docs/en/latest/admin-api.md | 65 +++ docs/en/latest/config.json | 1 + docs/en/latest/terminology/credential.md | 151 ++++++ docs/zh/latest/admin-api.md | 64 +++ docs/zh/latest/config.json | 1 + docs/zh/latest/terminology/credential.md | 152 ++++++ t/admin/credentials.t | 494 ++++++++++++++++++ t/node/consumer-plugin.t | 44 +- t/node/consumer-plugin2.t | 2 + t/node/credential-plugin-basic-auth.t | 137 +++++ .../credential-plugin-incremental-effective.t | 125 +++++ t/node/credential-plugin-jwt-auth.t | 137 +++++ t/node/credential-plugin-key-auth.t | 137 +++++ t/node/credential-plugin-multi-credentials.t | 236 +++++++++ t/node/credential-plugin-set-request-header.t | 245 +++++++++ ...credential-plugin-work-with-other-plugin.t | 171 ++++++ 24 files changed, 2428 insertions(+), 80 deletions(-) create mode 100644 apisix/admin/credentials.lua create mode 100644 docs/en/latest/terminology/credential.md create mode 100644 docs/zh/latest/terminology/credential.md create mode 100644 t/admin/credentials.t create mode 100644 t/node/credential-plugin-basic-auth.t create mode 100644 t/node/credential-plugin-incremental-effective.t create mode 100644 t/node/credential-plugin-jwt-auth.t create mode 100644 t/node/credential-plugin-key-auth.t create mode 100644 t/node/credential-plugin-multi-credentials.t create mode 100644 t/node/credential-plugin-set-request-header.t create mode 100644 t/node/credential-plugin-work-with-other-plugin.t diff --git a/apisix/admin/consumers.lua b/apisix/admin/consumers.lua index 84485231f830..e02789069c64 100644 --- a/apisix/admin/consumers.lua +++ b/apisix/admin/consumers.lua @@ -17,8 +17,6 @@ local core = require("apisix.core") local plugins = require("apisix.admin.plugins") local resource = require("apisix.admin.resource") -local plugin = require("apisix.plugin") -local pairs = pairs local function check_conf(username, conf, need_username, schema) @@ -36,18 +34,6 @@ local function check_conf(username, conf, need_username, schema) if not ok then return nil, {error_msg = "invalid plugins configuration: " .. err} end - - local count_auth_plugin = 0 - for name, conf in pairs(conf.plugins) do - local plugin_obj = plugin.get(name) - if plugin_obj.type == 'auth' then - count_auth_plugin = count_auth_plugin + 1 - end - end - - if count_auth_plugin == 0 then - return nil, {error_msg = "require one auth plugin"} - end end if conf.group_id then diff --git a/apisix/admin/credentials.lua b/apisix/admin/credentials.lua new file mode 100644 index 000000000000..3622867528d8 --- /dev/null +++ b/apisix/admin/credentials.lua @@ -0,0 +1,74 @@ +-- +-- 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 plugins = require("apisix.admin.plugins") +local plugin = require("apisix.plugin") +local resource = require("apisix.admin.resource") +local pairs = pairs + +local function check_conf(_id, conf, _need_id, schema) + local ok, err = core.schema.check(schema, conf) + if not ok then + return nil, {error_msg = "invalid configuration: " .. err} + end + + if conf.plugins then + ok, err = plugins.check_schema(conf.plugins, core.schema.TYPE_CONSUMER) + if not ok then + return nil, {error_msg = "invalid plugins configuration: " .. err} + end + + for name, _ in pairs(conf.plugins) do + local plugin_obj = plugin.get(name) + if not plugin_obj then + return nil, {error_msg = "unknown plugin " .. name} + end + if plugin_obj.type ~= "auth" then + return nil, {error_msg = "only supports auth type plugins in consumer credential"} + end + end + end + + return true, nil +end + +-- get_credential_etcd_key is used to splice the credential's etcd key (without prefix) +-- from credential_id and sub_path. +-- Parameter credential_id is from the uri or payload; sub_path is in the form of +-- {consumer_name}/credentials or {consumer_name}/credentials/{credential_id}. +-- Only if GET credentials list, credential_id is nil, sub_path is like {consumer_name}/credentials, +-- so return value is /consumers/{consumer_name}/credentials. +-- In the other methods, credential_id is not nil, return value is +-- /consumers/{consumer_name}/credentials/{credential_id}. +local function get_credential_etcd_key(credential_id, _conf, sub_path, _args) + if credential_id then + local uri_segs = core.utils.split_uri(sub_path) + local consumer_name = uri_segs[1] + return "/consumers/" .. consumer_name .. "/credentials/" .. credential_id + end + + return "/consumers/" .. sub_path +end + +return resource.new({ + name = "credentials", + kind = "credential", + schema = core.schema.credential, + checker = check_conf, + get_resource_etcd_key = get_credential_etcd_key, + unsupported_methods = {"post", "patch"} +}) diff --git a/apisix/admin/init.lua b/apisix/admin/init.lua index 4b4c7ec3add9..d02de6667f23 100644 --- a/apisix/admin/init.lua +++ b/apisix/admin/init.lua @@ -49,6 +49,7 @@ local resources = { services = require("apisix.admin.services"), upstreams = require("apisix.admin.upstreams"), consumers = require("apisix.admin.consumers"), + credentials = require("apisix.admin.credentials"), schema = require("apisix.admin.schema"), ssls = require("apisix.admin.ssl"), plugins = require("apisix.admin.plugins"), @@ -184,6 +185,12 @@ local function run() end end + if seg_res == "consumers" and #uri_segs >= 6 and uri_segs[6] == "credentials" then + seg_sub_path = seg_id .. "/" .. seg_sub_path + seg_res = uri_segs[6] + seg_id = uri_segs[7] + end + local resource = resources[seg_res] if not resource then core.response.exit(404, {error_msg = "Unsupported resource type: ".. seg_res}) @@ -228,7 +235,7 @@ local function run() if code then if method == "get" and plugin.enable_data_encryption then - if seg_res == "consumers" then + if seg_res == "consumers" or seg_res == "credentials" then utils.decrypt_params(plugin.decrypt_conf, data, core.schema.TYPE_CONSUMER) elseif seg_res == "plugin_metadata" then utils.decrypt_params(plugin.decrypt_conf, data, core.schema.TYPE_METADATA) diff --git a/apisix/admin/resource.lua b/apisix/admin/resource.lua index 2a87716027e1..ff5c97e18d89 100644 --- a/apisix/admin/resource.lua +++ b/apisix/admin/resource.lua @@ -17,6 +17,7 @@ local core = require("apisix.core") local utils = require("apisix.admin.utils") local apisix_ssl = require("apisix.ssl") +local apisix_consumer = require("apisix.consumer") local setmetatable = setmetatable local tostring = tostring local ipairs = ipairs @@ -157,6 +158,12 @@ function _M:get(id, conf, sub_path) key = key .. "/" .. id end + -- some resources(consumers) have sub resources(credentials), + -- the key format of sub resources will differ from the main resource + if self.get_resource_etcd_key then + key = self.get_resource_etcd_key(id, conf, sub_path) + end + local res, err = core.etcd.get(key, not id) if not res then core.log.error("failed to get ", self.kind, "[", key, "] from etcd: ", err) @@ -170,6 +177,12 @@ function _M:get(id, conf, sub_path) end end + -- consumers etcd range response will include credentials, so need to filter out them + if self.name == "consumers" and res.body.list then + res.body.list = apisix_consumer.filter_consumers_list(res.body.list) + res.body.total = #res.body.list + end + utils.fix_count(res.body, id) return res.status, res.body end @@ -249,6 +262,26 @@ function _M:put(id, conf, sub_path, args) key = key .. "/" .. id + if self.get_resource_etcd_key then + key = self.get_resource_etcd_key(id, conf, sub_path, args) + end + + if self.name == "credentials" then + local consumer_key = apisix_consumer.get_consumer_key_from_credential_key(key) + local res, err = core.etcd.get(consumer_key, false) + if not res then + return 503, {error_msg = err} + end + if res.status == 404 then + return res.status, {error_msg = "consumer not found"} + end + if res.status ~= 200 then + core.log.debug("failed to get consumer for the credential, credential key: ", key, + ", consumer key: ", consumer_key, ", res.status: ", res.status) + return res.status, {error_msg = "failed to get the consumer"} + end + end + if self.name ~= "plugin_metadata" then local ok, err = utils.inject_conf_with_prev_conf(self.kind, key, conf) if not ok then @@ -296,6 +329,10 @@ function _M:delete(id, conf, sub_path, uri_args) key = key .. "/" .. id + if self.get_resource_etcd_key then + key = self.get_resource_etcd_key(id, conf, sub_path, uri_args) + end + if self.delete_checker and uri_args.force ~= "true" then local code, err = self.delete_checker(id) if err then @@ -303,6 +340,13 @@ function _M:delete(id, conf, sub_path, uri_args) end end + if self.name == "consumers" then + local res, err = core.etcd.rmdir(key .. "/credentials/") + if not res then + return 503, {error_msg = err} + end + end + local res, err = core.etcd.delete(key) if not res then core.log.error("failed to delete ", self.kind, "[", key, "] in etcd: ", err) diff --git a/apisix/consumer.lua b/apisix/consumer.lua index 6bbd6ca186c6..f69d069d6aab 100644 --- a/apisix/consumer.lua +++ b/apisix/consumer.lua @@ -15,13 +15,16 @@ -- limitations under the License. -- local core = require("apisix.core") +local config_local = require("apisix.core.config_local") local secret = require("apisix.secret") local plugin = require("apisix.plugin") local plugin_checker = require("apisix.plugin").plugin_checker +local check_schema = require("apisix.core.schema").check local error = error local ipairs = ipairs local pairs = pairs local type = type +local string_sub = string.sub local consumers @@ -33,6 +36,50 @@ local lrucache = core.lrucache.new({ ttl = 300, count = 512 }) +local function remove_etcd_prefix(key) + local prefix = "" + local local_conf = config_local.local_conf() + if local_conf.etcd and local_conf.etcd.prefix then + prefix = local_conf.etcd.prefix + end + return string_sub(key, #prefix + 1) +end + +-- /{etcd.prefix}/consumers/{consumer_name}/credentials/{credential_id} --> {consumer_name} +local function get_consumer_name_from_credential_etcd_key(key) + local uri_segs = core.utils.split_uri(remove_etcd_prefix(key)) + return uri_segs[3] +end + +local function is_credential_etcd_key(key) + if not key then + return false + end + + local uri_segs = core.utils.split_uri(remove_etcd_prefix(key)) + return uri_segs[2] == "consumers" and uri_segs[4] == "credentials" +end + +local function get_credential_id_from_etcd_key(key) + local uri_segs = core.utils.split_uri(remove_etcd_prefix(key)) + return uri_segs[5] +end + +local function filter_consumers_list(data_list) + if #data_list == 0 then + return data_list + end + + local list = {} + for _, item in ipairs(data_list) do + if not (type(item) == "table" and is_credential_etcd_key(item.key)) then + core.table.insert(list, item) + end + end + + return list +end + local function plugin_consumer() local plugins = {} @@ -40,12 +87,15 @@ local function plugin_consumer() return plugins end - for _, consumer in ipairs(consumers.values) do - if type(consumer) ~= "table" then + -- consumers.values is the list that got from etcd by prefix key {etcd_prefix}/consumers. + -- So it contains consumers and credentials. + -- The val in the for-loop may be a Consumer or a Credential. + for _, val in ipairs(consumers.values) do + if type(val) ~= "table" then goto CONTINUE end - for name, config in pairs(consumer.value.plugins or {}) do + for name, config in pairs(val.value.plugins or {}) do local plugin_obj = plugin.get(name) if plugin_obj and plugin_obj.type == "auth" then if not plugins[name] then @@ -55,14 +105,41 @@ local function plugin_consumer() } end - local new_consumer = core.table.clone(consumer.value) + -- if the val is a Consumer, clone it to the local consumer; + -- if the val is a Credential, to get the Consumer by consumer_name and then clone + -- it to the local consumer. + local consumer + if is_credential_etcd_key(val.key) then + local consumer_name = get_consumer_name_from_credential_etcd_key(val.key) + local the_consumer = consumers:get(consumer_name) + if the_consumer and the_consumer.value then + consumer = core.table.clone(the_consumer.value) + consumer.credential_id = get_credential_id_from_etcd_key(val.key) + else + -- Normally wouldn't get here: + -- it should belong to a consumer for any credential. + core.log.error("failed to get the consumer for the credential,", + " a wild credential has appeared!", + " credential key: ", val.key, ", consumer name: ", consumer_name) + goto CONTINUE + end + else + consumer = core.table.clone(val.value) + end + + -- if the consumer has labels, set the field custom_id to it. + -- the custom_id is used to set in the request headers to the upstream. + if consumer.labels then + consumer.custom_id = consumer.labels["custom_id"] + end + -- Note: the id here is the key of consumer data, which -- is 'username' field in admin - new_consumer.consumer_name = new_consumer.id - new_consumer.auth_conf = config - new_consumer.modifiedIndex = consumer.modifiedIndex - core.log.info("consumer:", core.json.delay_encode(new_consumer)) - core.table.insert(plugins[name].nodes, new_consumer) + consumer.consumer_name = consumer.id + consumer.auth_conf = config + consumer.modifiedIndex = val.modifiedIndex + core.log.info("consumer:", core.json.delay_encode(consumer)) + core.table.insert(plugins[name].nodes, consumer) end end @@ -72,6 +149,12 @@ local function plugin_consumer() return plugins end +_M.filter_consumers_list = filter_consumers_list + +function _M.get_consumer_key_from_credential_key(key) + local uri_segs = core.utils.split_uri(key) + return "/consumers/" .. uri_segs[3] +end function _M.plugin(plugin_name) local plugin_conf = core.lrucache.global("/consumers", @@ -86,6 +169,10 @@ function _M.attach_consumer(ctx, consumer, conf) ctx.consumer_name = consumer.consumer_name ctx.consumer_group_id = consumer.group_id ctx.consumer_ver = conf.conf_version + + core.request.set_header(ctx, "X-Consumer-Username", consumer.username) + core.request.set_header(ctx, "X-Credential-Identifier", consumer.credential_id) + core.request.set_header(ctx, "X-Consumer-Custom-ID", consumer.custom_id) end @@ -94,7 +181,7 @@ function _M.consumers() return nil, nil end - return consumers.values, consumers.conf_version + return filter_consumers_list(consumers.values), consumers.conf_version end @@ -120,8 +207,18 @@ function _M.consumers_kv(plugin_name, consumer_conf, key_attr) return consumers end +local function check_consumer(consumer, key) + local data_valid + local err + if is_credential_etcd_key(key) then + data_valid, err = check_schema(core.schema.credential, consumer) + else + data_valid, err = check_schema(core.schema.consumer, consumer) + end + if not data_valid then + return data_valid, err + end -local function check_consumer(consumer) return plugin_checker(consumer, core.schema.TYPE_CONSUMER) end @@ -140,7 +237,6 @@ function _M.init_worker() local err local cfg = { automatic = true, - item_schema = core.schema.consumer, checker = check_consumer, } if core.config.type ~= "etcd" then diff --git a/apisix/core/config_etcd.lua b/apisix/core/config_etcd.lua index 6e06a368f25d..5734106e7a7b 100644 --- a/apisix/core/config_etcd.lua +++ b/apisix/core/config_etcd.lua @@ -538,7 +538,7 @@ local function load_full_data(self, dir_res, headers) end if data_valid and self.checker then - data_valid, err = self.checker(item.value) + data_valid, err = self.checker(item.value, item.key) if not data_valid then log.error("failed to check item data of [", self.key, "] err:", err, " ,val: ", json.delay_encode(item.value)) @@ -674,7 +674,7 @@ local function sync_data(self) end if data_valid and res.value and self.checker then - data_valid, err = self.checker(res.value) + data_valid, err = self.checker(res.value, res.key) if not data_valid then log.error("failed to check item data of [", self.key, "] err:", err, " ,val: ", json.delay_encode(res.value)) diff --git a/apisix/core/etcd.lua b/apisix/core/etcd.lua index a537c8840a7a..e3785091bdf2 100644 --- a/apisix/core/etcd.lua +++ b/apisix/core/etcd.lua @@ -154,6 +154,22 @@ local function kvs_to_node(kvs) end _M.kvs_to_node = kvs_to_node +local function kvs_to_nodes(res, exclude_dir) + res.body.node.dir = true + res.body.node.nodes = setmetatable({}, array_mt) + if exclude_dir then + for i=2, #res.body.kvs do + res.body.node.nodes[i-1] = kvs_to_node(res.body.kvs[i]) + end + else + for i=1, #res.body.kvs do + res.body.node.nodes[i] = kvs_to_node(res.body.kvs[i]) + end + end + return res +end + + local function not_found(res) res.body.message = "Key not found" res.reason = "Not found" @@ -201,22 +217,23 @@ function _M.get_format(res, real_key, is_dir, formatter) else -- In etcd v2, the direct key asked for is `node`, others which under this dir are `nodes` -- While in v3, this structure is flatten and all keys related the key asked for are `kvs` - res.body.node = { - key = real_key, - dir = true, - nodes = setmetatable({}, array_mt) - } - local kvs = res.body.kvs - if #kvs >= 1 and not kvs[1].value then - res.body.node.createdIndex = tonumber(kvs[1].create_revision) - res.body.node.modifiedIndex = tonumber(kvs[1].mod_revision) - for i=2, #kvs do - res.body.node.nodes[i-1] = kvs_to_node(kvs[i]) + res.body.node = kvs_to_node(res.body.kvs[1]) + -- we have a init_dir (for etcd v2) value that can't be deserialized with json, + -- but we don't put init_dir for new resource type like consumer credential + if not res.body.kvs[1].value then + -- remove last "/" when necessary + if string.byte(res.body.node.key, -1) == 47 then + res.body.node.key = string.sub(res.body.node.key, 1, #res.body.node.key-1) end + res = kvs_to_nodes(res, true) else - for i=1, #kvs do - res.body.node.nodes[i] = kvs_to_node(kvs[i]) + -- get dir key by remove last part of node key, + -- for example: /apisix/consumers/jack -> /apisix/consumers + local last_slash_index = string.find(res.body.node.key, "/[^/]*$") + if last_slash_index then + res.body.node.key = string.sub(res.body.node.key, 1, last_slash_index-1) end + res = kvs_to_nodes(res, false) end end @@ -484,6 +501,30 @@ function _M.delete(key) return res, nil end +function _M.rmdir(key, opts) + local etcd_cli, prefix, err = get_etcd_cli() + if not etcd_cli then + return nil, err + end + + local res, err = etcd_cli:rmdir(prefix .. key, opts) + if not res then + return nil, err + end + + res.headers["X-Etcd-Index"] = res.body.header.revision + + if not res.body.deleted then + return not_found(res), nil + end + + v3_adapter.to_v3(res.body, "delete") + res.body.node = {} + res.body.key = prefix .. key + + return res, nil +end + --- -- Get etcd cluster and server version. -- diff --git a/apisix/schema_def.lua b/apisix/schema_def.lua index 2e289db58a0c..b4241ff2d9f5 100644 --- a/apisix/schema_def.lua +++ b/apisix/schema_def.lua @@ -710,6 +710,20 @@ _M.consumer = { additionalProperties = false, } +_M.credential = { + type = "object", + properties = { + id = id_schema, + plugins = { + type = "object", + maxProperties = 1, + }, + labels = labels_def, + create_time = timestamp_def, + update_time = timestamp_def, + desc = desc_def, + }, +} _M.upstream = upstream_schema diff --git a/docs/en/latest/admin-api.md b/docs/en/latest/admin-api.md index b6f03cbd5855..c7a236da77d0 100644 --- a/docs/en/latest/admin-api.md +++ b/docs/en/latest/admin-api.md @@ -868,6 +868,71 @@ Since `v2.2`, we can bind multiple authentication plugins to the same consumer. Currently, the response is returned from etcd. +## Credential + +Credential is used to hold the authentication credentials for the Consumer. +Credentials are used when multiple credentials need to be configured for a Consumer. + +### Credential API + +Credential resource request address:/apisix/admin/consumers/{username}/credentials/{credential_id} + +### Request Methods + +| Method | Request URI | Request Body | Description | +| ------ |----------------------------------------------------------------|--------------|------------------------------------------------| +| GET | /apisix/admin/consumers/{username}/credentials | NUll | Fetches list of all credentials of the Consumer | +| GET | /apisix/admin/consumers/{username}/credentials/{credential_id} | NUll | Fetches the Credential by `credential_id` | +| PUT | /apisix/admin/consumers/{username}/credentials/{credential_id} | {...} | Create or update a Creddential | +| DELETE | /apisix/admin/consumers/{username}/credentials/{credential_id} | NUll | Delete the Credential | + +### Request Body Parameters + +| Parameter | Required | Type | Description | Example | +| ----------- |-----| ------- |------------------------------------------------------------|-------------------------------------------------| +| plugins | False | Plugin | Auth plugins configuration. | | +| desc | False | Auxiliary | Description of usage scenarios. | credential xxxx | +| labels | False | Match Rules | Attributes of the Credential specified as key-value pairs. | {"version":"v2","build":"16","env":"production"} | + +Example Configuration: + +```shell +{ + "plugins": { + "key-auth": { + "key": "auth-one" + } + }, + "desc": "hello world" +} +``` + +### Example API usage + +Prerequisite: Consumer `jack` has been created. + +Create the `key-auth` Credential for consumer `jack`: + + ```shell + curl http://127.0.0.1:9180/apisix/admin/consumers/jack/credentials/auth-one \ + -H "X-API-KEY: $admin_key" -X PUT -i -d ' + { + "plugins": { + "key-auth": { + "key": "auth-one" + } + } + }' + ``` + + ``` + HTTP/1.1 200 OK + Date: Thu, 26 Dec 2019 08:17:49 GMT + ... + + {"key":"\/apisix\/consumers\/jack\/credentials\/auth-one","value":{"update_time":1666260780,"plugins":{"key-auth":{"key":"auth-one"}},"create_time":1666260780}} + ``` + ## Upstream Upstream is a virtual host abstraction that performs load balancing on a given set of service nodes according to the configured rules. diff --git a/docs/en/latest/config.json b/docs/en/latest/config.json index ad9c1e051523..5c43fc55af81 100644 --- a/docs/en/latest/config.json +++ b/docs/en/latest/config.json @@ -49,6 +49,7 @@ "terminology/api-gateway", "terminology/consumer", "terminology/consumer-group", + "terminology/credential", "terminology/global-rule", "terminology/plugin", "terminology/plugin-config", diff --git a/docs/en/latest/terminology/credential.md b/docs/en/latest/terminology/credential.md new file mode 100644 index 000000000000..560d4314a704 --- /dev/null +++ b/docs/en/latest/terminology/credential.md @@ -0,0 +1,151 @@ +--- +title: Credential +keywords: + - APISIX + - API Gateway + - Consumer + - Credential +description: This article describes what the Apache APISIX Credential object does and how to use it. +--- + + + +## Description + +Credential is the object that holds the [Consumer](./consumer.md) credential configuration. +A Consumer can use multiple credentials of different types. +Credentials are used when you need to configure multiple credentials for a Consumer. + +Currently, Credential can be configured with the authentication plugins `basic-auth`, `hmac-auth`, `jwt-auth`, and `key-auth`. + +### Configuration options + +The fields for defining a Credential are defined as below. + +| Field | Required | Description | +|---------|----------|---------------------------------------------------------------------------------------------------------| +| desc | False | Decriptiion of the Credential. | +| labels | False | Labels of the Credential. | +| plugins | False | The plugin configuration corresponding to Credential. For more information, see [Plugins](./plugin.md). | + +:::note + +For more information about the Credential object, you can refer to the [Admin API Credential](../admin-api.md#credential) resource guide. + +::: + +## Example + +[Consumer Example](./consumer.md#example) describes how to configure the auth plugin for Consumer and how to use it with other plugins. +In this example, the Consumer has only one credential of type key-auth. +Now suppose the user needs to configure multiple credentials for that Consumer, you can use Credential to support this. + +:::note +You can fetch the `admin_key` from `config.yaml` and save to an environment variable with the following command: + +```bash +admin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/"//g') +``` + +::: + +1. Create the Consumer without specifying the auth plug-n, but use Credential to configure the auth plugin later. + + ```shell + curl http://127.0.0.1:9180/apisix/admin/consumers \ + -H "X-API-KEY: $admin_key" -X PUT -d ' + { + "username": "jack" + }' + ``` + +2. Create 2 `key-auth` Credentials for the Consumer. + + ```shell + curl http://127.0.0.1:9180/apisix/admin/consumers/jack/key-auth-one \ + -H "X-API-KEY: $admin_key" -X PUT -d ' + { + "plugins": { + "key-auth": { + "key": "auth-one" + } + } + }' + ``` + + ```shell + curl http://127.0.0.1:9180/apisix/admin/consumers/jack/key-auth-two \ + -H "X-API-KEY: $admin_key" -X PUT -d ' + { + "plugins": { + "key-auth": { + "key": "auth-two" + } + } + }' + ``` + +3. Create a route and enable `key-auth` plugin on it. + + ```shell + curl http://127.0.0.1:9180/apisix/admin/routes/1 \ + -H "X-API-KEY: $admin_key" -X PUT -d ' + { + "plugins": { + "key-auth": {} + }, + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + }, + "uri": "/hello" + }' + ``` + +4. Test. + +Test the request with the `auth-one` and `auth-two` keys, and they both respond correctly. + + ```shell + curl http://127.0.0.1:9080/hello -H 'apikey: auth-one' -I + curl http://127.0.0.1:9080/hello -H 'apikey: auth-two' -I + ``` + +Enable the `limit-count` plugin for the Consumer. + + ```shell + curl http://127.0.0.1:9180/apisix/admin/consumers \ + -H "X-API-KEY: $admin_key" -X PUT -d ' + { + "username": "jack", + "plugins": { + "limit-count": { + "count": 2, + "time_window": 60, + "rejected_code": 503, + "key": "remote_addr" + } + } + }' + ``` + +Requesting the route more than 3 times in a row with each of the two keys, the test returns `503` and the request is restricted. diff --git a/docs/zh/latest/admin-api.md b/docs/zh/latest/admin-api.md index 19d97d7808b7..f5cd5b144b05 100644 --- a/docs/zh/latest/admin-api.md +++ b/docs/zh/latest/admin-api.md @@ -882,6 +882,70 @@ Consumer 对象 JSON 配置示例: 目前是直接返回与 etcd 交互后的结果。 +## Credential + +Credential 用以存放 Consumer 的认证凭证。当需要为 Consumer 配置多个凭证时,可以使用 Credential。 + +### 请求地址 {#credential-uri} + +Credential 资源请求地址:/apisix/admin/consumers/{username}/credentials/{credential_id} + +### 请求方法 {#consumer-request-methods} + +| 名称 | 请求 URI | 请求 body | 描述 | +| ------ |----------------------------------------------------------------| --------- | ------------- | +| GET | /apisix/admin/consumers/{username}/credentials | 无 | 获取资源列表。| +| GET | /apisix/admin/consumers/{username}/credentials/{credential_id} | 无 | 获取资源。 | +| PUT | /apisix/admin/consumers/{username}/credentials/{credential_id} | {...} | 创建资源。 | +| DELETE | /apisix/admin/consumers/{username}/credentials/{credential_id} | 无 | 删除资源。 | + +### body 请求参数 {#credential-body-request-methods} + +| 名称 | 必选项 | 类型 | 描述 | 示例值 | +| ----------- |-----| ------- |-----------------------| ------------------------------------------------ | +| plugins | 是 | Plugin | 该 Credential 对应的插件配置。 | | +| desc | 否 | 辅助 | Credential 描述。 | | +| labels | 否 | 匹配规则 | 标识附加属性的键值对。 | {"version":"v2","build":"16","env":"production"} | + +Credential 对象 JSON 配置示例: + +```shell +{ + "plugins": { + "key-auth": { + "key": "auth-one" + } + }, + "desc": "hello world" +} +``` + +### 使用示例 {#credential-example} + +前提:已创建 Consumer `jack`。 + +创建 Credential,并启用认证插件 `key-auth`: + + ```shell + curl http://127.0.0.1:9180/apisix/admin/consumers/jack/credentials/auth-one \ + -H "X-API-KEY: $admin_key" -X PUT -i -d ' + { + "plugins": { + "key-auth": { + "key": "auth-one" + } + } + }' + ``` + + ``` + HTTP/1.1 200 OK + Date: Thu, 26 Dec 2019 08:17:49 GMT + ... + + {"key":"\/apisix\/consumers\/jack\/credentials\/auth-one","value":{"update_time":1666260780,"plugins":{"key-auth":{"key":"auth-one"}},"create_time":1666260780}} + ``` + ## Upstream Upstream 是虚拟主机抽象,对给定的多个服务节点按照配置规则进行负载均衡。Upstream 的地址信息可以直接配置到 `Route`(或 `Service`) 上,当 Upstream 有重复时,需要用“引用”方式避免重复。 diff --git a/docs/zh/latest/config.json b/docs/zh/latest/config.json index 4e35f1c4d848..15547299a166 100644 --- a/docs/zh/latest/config.json +++ b/docs/zh/latest/config.json @@ -38,6 +38,7 @@ "terminology/api-gateway", "terminology/consumer", "terminology/consumer-group", + "terminology/credential", "terminology/global-rule", "terminology/plugin", "terminology/plugin-config", diff --git a/docs/zh/latest/terminology/credential.md b/docs/zh/latest/terminology/credential.md new file mode 100644 index 000000000000..4d183620240f --- /dev/null +++ b/docs/zh/latest/terminology/credential.md @@ -0,0 +1,152 @@ +--- +title: Credential +keywords: + - APISIX + - API 网关 + - 凭证 + - Credential +description: 本文介绍了 Apache APISIX Credential 对象的作用以及如何使用 Credential。 +--- + + + +## 描述 + +Credential 是存放 [Consumer](./consumer.md) 凭证配置的对象。 +一个 Consumer 可以使用不同类型的多个凭证。 +当你需要为一个 Consumer 配置不同类型的多个凭证时,就会用到 Credential。 + +目前,Credential 可以配置的身份认证插件包括 `basic-auth`、`hmac-auth`、`jwt-auth` 以及 `key-auth`。 + +## 配置选项 + + 定义 Credential 的字段如下: + +| 名称 | 必选项 | 描述 | +|---------|-----|-----------------------------------------------------| +| desc | 否 | Credential 描述。 | +| labels | 否 | Credential 标签。 | +| plugins | 否 | Credential 对应的插件配置。详细信息,请参考 [Plugins](./plugin.md)。 | + +:::note + +如需了解更多关于 Credential 对象的信息,你可以参考 [Admin API Credential](../admin-api.md#credential) 资源介绍。 + +::: + +## 使用示例 + +[Consumer 使用示例](./consumer.md#使用示例) 介绍了如何对 Consumer 配置认证插件,并介绍了如何配合其他插件使用。 +在该示例中,该 Consumer 只有一个 key-auth 类型的凭证。 +现在假设用户需要为该 Consumer 配置多个凭证,你可以使用 Credential 来支持这一点。 + +:::note + +您可以这样从 `config.yaml` 中获取 `admin_key` 并存入环境变量: + +```bash +admin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/"//g') +``` + +::: + +1. 创建 Consumer。不指定认证插件,而是稍后使用 Credential 来配置认证插件。 + + ```shell + curl http://127.0.0.1:9180/apisix/admin/consumers \ + -H "X-API-KEY: $admin_key" -X PUT -d ' + { + "username": "jack" + }' + ``` + +2. 为 Consumer 配置 2 个 启用 `key-auth` 的 Credential。 + + ```shell + curl http://127.0.0.1:9180/apisix/admin/consumers/jack/key-auth-one \ + -H "X-API-KEY: $admin_key" -X PUT -d ' + { + "plugins": { + "key-auth": { + "key": "auth-one" + } + } + }' + ``` + + ```shell + curl http://127.0.0.1:9180/apisix/admin/consumers/jack/key-auth-two \ + -H "X-API-KEY: $admin_key" -X PUT -d ' + { + "plugins": { + "key-auth": { + "key": "auth-two" + } + } + }' + ``` + +3. 创建路由,设置路由规则和启用插件配置。 + + ```shell + curl http://127.0.0.1:9180/apisix/admin/routes/1 \ + -H "X-API-KEY: $admin_key" -X PUT -d ' + { + "plugins": { + "key-auth": {} + }, + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + }, + "uri": "/hello" + }' + ``` + +4. 测试插件 + +分别使用 `auth-one` 和 `auth-two` 两个 key 来测试请求,都响应正常。 + + ```shell + curl http://127.0.0.1:9080/hello -H 'apikey: auth-one' -I + curl http://127.0.0.1:9080/hello -H 'apikey: auth-two' -I + ``` + +为该 Consumer 启用 `limit-count` 插件。 + + ```shell + curl http://127.0.0.1:9180/apisix/admin/consumers \ + -H "X-API-KEY: $admin_key" -X PUT -d ' + { + "username": "jack", + "plugins": { + "limit-count": { + "count": 2, + "time_window": 60, + "rejected_code": 503, + "key": "remote_addr" + } + } + }' + ``` + +分别使用这两个 key 连续 3 次以上请求该路由,测试返回 `503`,请求被限制。 diff --git a/t/admin/credentials.t b/t/admin/credentials.t new file mode 100644 index 000000000000..15119829c2e3 --- /dev/null +++ b/t/admin/credentials.t @@ -0,0 +1,494 @@ +# +# 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"); + +run_tests; + +__DATA__ + +=== TEST 1: create a credential for invalid consumer: consumer not found error +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/consumers/jack/credentials/credential_a', + ngx.HTTP_PUT, + [[{ + "plugins": { + "key-auth": { + "key": "the-key" + } + } + }]] + ) + + ngx.status = code + ngx.print(body) + } + } +--- request +GET /t +--- error_code: 404 +--- response_body +{"error_msg":"consumer not found"} + + + +=== TEST 2: add a consumer +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/consumers', + ngx.HTTP_PUT, + [[{ + "username":"jack", + "desc": "new consumer", + "plugins": { + "basic-auth": { + "username": "the-user", + "password": "the-password" + } + } + }]], + [[{ + "key": "/apisix/consumers/jack", + "value": + { + "username":"jack", + "desc": "new consumer", + "plugins": { + "basic-auth": { + "username": "the-user", + "password": "WvF5kpaLvIzjuk4GNIMTJg==" + } + } + } + }]] + ) + + ngx.status = code + ngx.say(body) + } + } +--- request +GET /t +--- response_body +passed + + + +=== TEST 3: add a credentials with basic-auth for the consumer jack, should success +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/consumers/jack/credentials/credential_a', + ngx.HTTP_PUT, + [[{ + "desc": "basic-auth for jack", + "plugins": { + "basic-auth": { + "username": "the-user", + "password": "the-password" + } + } + }]], + [[{ + "value":{ + "desc":"basic-auth for jack", + "id":"credential_a", + "plugins":{"basic-auth":{"username":"the-user","password":"WvF5kpaLvIzjuk4GNIMTJg=="}} + }, + "key":"/apisix/consumers/jack/credentials/credential_a" + }]] + ) + + ngx.status = code + ngx.say(body) + } + } +--- request +GET /t +--- response_body +passed + + + +=== TEST 4: add a credential with key-auth for the consumer jack, should success +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/consumers/jack/credentials/credential_b', + ngx.HTTP_PUT, + [[{ + "desc": "key-auth for jack", + "plugins": { + "key-auth": { + "key": "the-key" + } + } + }]], + [[{ + "value":{ + "desc":"key-auth for jack", + "id":"credential_b", + "plugins":{"key-auth":{"key":"JCX7x1qN5e9kHt0GuJfWpw=="}} + }, + "key":"/apisix/consumers/jack/credentials/credential_b" + }]] + ) + + ngx.status = code + ngx.say(body) + } + } +--- request +GET /t +--- response_body +passed + + + +=== TEST 5: add a credential with a plugin which is not a auth plugin, should fail +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/consumers/jack/credentials/credential_b', + ngx.HTTP_PUT, + [[{ + "desc": "limit-conn for jack", + "plugins": { + "limit-conn": { + "conn": 1, + "burst": 0, + "default_conn_delay": 0.1, + "rejected_code": 503, + "key_type": "var", + "key": "http_a" + } + } + }]] + ) + + ngx.status = code + ngx.print(body) + } + } +--- request +GET /t +--- error_code: 400 +--- response_body +{"error_msg":"only supports auth type plugins in consumer credential"} + + + +=== TEST 6: list consumers: should not contain credential +--- config + location /t { + content_by_lua_block { + local json = require("toolkit.json") + local t = require("lib.test_admin").test + local code, body, res = t('/apisix/admin/consumers', ngx.HTTP_GET) + + ngx.status = code + res = json.decode(res) + assert(res.total == 1) + assert(res.list[1].key == "/apisix/consumers/jack") + } + } +--- request +GET /t +--- response_body + + + +=== TEST 7: list credentials: should contain credential_a and credential_b +--- config + location /t { + content_by_lua_block { + local json = require("toolkit.json") + local t = require("lib.test_admin").test + local code, body, res = t('/apisix/admin/consumers/jack/credentials', ngx.HTTP_GET) + + ngx.status = code + res = json.decode(res) + assert(res.total == 2) + assert(res.list[1].key == "/apisix/consumers/jack/credentials/credential_a") + assert(res.list[2].key == "/apisix/consumers/jack/credentials/credential_b") + } + } +--- request +GET /t +--- response_body + + + +=== TEST 8: get a credential +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/consumers/jack/credentials/credential_b', + ngx.HTTP_GET, + nil, + [[{ + "key": "/apisix/consumers/jack/credentials/credential_b", + "value": { + "desc": "key-auth for jack", + "plugins": {"key-auth": {"key": "the-key"} + }} + }]] + ) + + ngx.status = code + ngx.say(body) + } + } +--- request +GET /t +--- response_body +passed + + + +=== TEST 9: update credential: should ok +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/consumers/jack/credentials/credential_b', + ngx.HTTP_PUT, + [[{ + "desc": "new description", + "plugins": { + "key-auth": { + "key": "new-key" + } + } + }]], + [[{ + "key": "/apisix/consumers/jack/credentials/credential_b", + "value": { + "desc": "new description", + "plugins": { + "key-auth": { + "key": "523EisB/dvqlIT9RzfF3ZQ==" + } + } + } + }]] + ) + + ngx.status = code + ngx.say(body) + + } + } +--- request +GET /t +--- response_body +passed + + + +=== TEST 10: delete credential +--- config + location /t { + content_by_lua_block { + local json = require("toolkit.json") + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/consumers/jack/credentials/credential_a', ngx.HTTP_DELETE) + + assert(code == 200) + ngx.status = code + + code, body, res = t('/apisix/admin/consumers/jack/credentials', ngx.HTTP_GET) + res = json.decode(res) + assert(res.total == 1) + assert(res.list[1].key == "/apisix/consumers/jack/credentials/credential_b") + } + } +--- request +GET /t +--- response_body + + + +=== TEST 11: create a credential has more than one plugin: should not ok +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/consumers/jack/credentials/xxx-yyy-zzz', + ngx.HTTP_PUT, + [[{ + "plugins": { + "key-auth": {"key": "the-key"}, + "basic-auth": {"username": "the-user", "password": "the-password"} + } + }]] + ) + + ngx.status = code + ngx.print(body) + } + } +--- request +GET /t +--- error_code: 400 +--- response_body +{"error_msg":"invalid configuration: property \"plugins\" validation failed: expect object to have at most 1 properties"} + + + +=== TEST 12: delete consumer +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/consumers/jack', + ngx.HTTP_DELETE + ) + + ngx.status = code + ngx.say(body) + } + } +--- request +GET /t +--- response_body +passed + + + +=== TEST 13: list credentials: should get 404 because the consumer is deleted +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/consumers/jack/credentials', ngx.HTTP_GET) + + ngx.status = code + ngx.print(body) + } + } +--- request +GET /t +--- error_code: 404 +--- response_body +{"message":"Key not found"} + + + +=== TEST 14: add a consumer +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/consumers', + ngx.HTTP_PUT, + [[{ + "username":"jack" + }]] + ) + + if ngx.status >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- request +GET /t +--- response_body +passed + + + +=== TEST 15: add a credential with key-auth for the consumer jack (id in the payload but not in uri), should success +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/consumers/jack/credentials', + ngx.HTTP_PUT, + [[{ + "id": "d79a5aa3", + "desc": "key-auth for jack", + "plugins": { + "key-auth": { + "key": "the-key" + } + } + }]], + [[{ + "value":{ + "desc":"key-auth for jack", + "id":"d79a5aa3", + "plugins":{"key-auth":{"key":"JCX7x1qN5e9kHt0GuJfWpw=="}} + }, + "key":"/apisix/consumers/jack/credentials/d79a5aa3" + }]] + ) + + ngx.status = code + ngx.say(body) + } + } +--- request +GET /t +--- response_body +passed + + + +=== TEST 16: add a credential with key-auth for the consumer jack but missing id in uri and payload, should fail +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/consumers/jack/credentials', + ngx.HTTP_PUT, + [[{ + "desc": "key-auth for jack", + "plugins": { + "key-auth": { + "key": "the-key" + } + } + }]] + ) + + ngx.status = code + ngx.print(body) + } + } +--- request +GET /t +--- error_code: 400 +--- response_body +{"error_msg":"missing credential id"} diff --git a/t/node/consumer-plugin.t b/t/node/consumer-plugin.t index 76e3f25bb634..b5e6d7ee5b27 100644 --- a/t/node/consumer-plugin.t +++ b/t/node/consumer-plugin.t @@ -124,39 +124,7 @@ apikey: auth-one -=== TEST 6: missing auth plugins (not allow) ---- config - location /t { - content_by_lua_block { - local t = require("lib.test_admin").test - local code, body = t('/apisix/admin/consumers', - ngx.HTTP_PUT, - [[{ - "username": "jack", - "plugins": { - "limit-count": { - "count": 2, - "time_window": 60, - "rejected_code": 503, - "key": "remote_addr" - } - } - }]] - ) - - ngx.status = code - ngx.print(body) - } - } ---- request -GET /t ---- error_code: 400 ---- response_body -{"error_msg":"require one auth plugin"} - - - -=== TEST 7: use the new configuration after the consumer's configuration is updated +=== TEST 6: use the new configuration after the consumer's configuration is updated --- config location /t { content_by_lua_block { @@ -221,7 +189,7 @@ GET /t -=== TEST 8: consumer with multiple auth plugins +=== TEST 7: consumer with multiple auth plugins --- config location /t { content_by_lua_block { @@ -258,7 +226,7 @@ passed -=== TEST 9: bind to routes +=== TEST 8: bind to routes --- config location /t { content_by_lua_block { @@ -315,7 +283,7 @@ passed -=== TEST 10: hit consumer, key-auth +=== TEST 9: hit consumer, key-auth --- request GET /hello --- more_headers @@ -327,7 +295,7 @@ find consumer John_Doe -=== TEST 11: hit consumer, hmac-auth +=== TEST 10: hit consumer, hmac-auth --- config location /t { content_by_lua_block { @@ -383,7 +351,7 @@ find consumer John_Doe -=== TEST 12: the plugins bound on the service should use the latest configuration +=== TEST 11: the plugins bound on the service should use the latest configuration --- config location /t { content_by_lua_block { diff --git a/t/node/consumer-plugin2.t b/t/node/consumer-plugin2.t index d48387c179fa..6c79ad88dd26 100644 --- a/t/node/consumer-plugin2.t +++ b/t/node/consumer-plugin2.t @@ -109,6 +109,7 @@ apikey: auth-jack host: localhost x-api-engine: APISIX x-consumer-id: 1 +x-consumer-username: jack x-real-ip: 127.0.0.1 @@ -206,6 +207,7 @@ apikey: auth-jack host: localhost x-api-engine: APISIX x-consumer-id: 1 +x-consumer-username: jack x-real-ip: 127.0.0.1 diff --git a/t/node/credential-plugin-basic-auth.t b/t/node/credential-plugin-basic-auth.t new file mode 100644 index 000000000000..c2e55acd4702 --- /dev/null +++ b/t/node/credential-plugin-basic-auth.t @@ -0,0 +1,137 @@ +# 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(); +run_tests; + +__DATA__ + +=== TEST 1: enable basic-auth on the route /hello +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "plugins": { + "basic-auth": {} + }, + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + }, + "uri": "/hello" + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- request +GET /t +--- response_body +passed + + + +=== TEST 2: create a consumer +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/consumers', + ngx.HTTP_PUT, + [[{ + "username": "jack" + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- request +GET /t +--- response_body +passed + + + +=== TEST 3: create a credential with basic-auth plugin enabled for the consumer +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/consumers/jack/credentials/34010989-ce4e-4d61-9493-b54cca8edb31', + ngx.HTTP_PUT, + [[{ + "plugins": { + "basic-auth": {"username": "foo", "password": "bar"} + } + }]], + [[{ + "value":{ + "id":"34010989-ce4e-4d61-9493-b54cca8edb31", + "plugins":{ + "basic-auth":{"username":"foo","password":"+kOEVUuRc5rC5ZwvvAMLwg=="} + } + }, + "key":"/apisix/consumers/jack/credentials/34010989-ce4e-4d61-9493-b54cca8edb31" + }]] + ) + + ngx.status = code + ngx.say(body) + } + } +--- request +GET /t +--- response_body +passed + + + +=== TEST 4: access with invalid basic-auth (invalid password) +--- request +GET /hello +--- more_headers +Authorization: Basic Zm9vOmZvbwo= +--- error_code: 401 +--- response_body +{"message":"Invalid user authorization"} + + + +=== TEST 5: access with valid basic-auth +--- request +GET /hello +--- more_headers +Authorization: Basic Zm9vOmJhcg== +--- response_body +hello world diff --git a/t/node/credential-plugin-incremental-effective.t b/t/node/credential-plugin-incremental-effective.t new file mode 100644 index 000000000000..ae619dfce3ae --- /dev/null +++ b/t/node/credential-plugin-incremental-effective.t @@ -0,0 +1,125 @@ +# 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(); +run_tests; + +__DATA__ + +=== TEST 1: test continuous watch etcd changes without APISIX reload +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + + -- enable key-auth on /hello + t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "plugins": { + "key-auth": {} + }, + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + }, + "uri": "/hello" + }]] + ) + ngx.sleep(0.2) -- On some machines, changes may not be instantly watched, so sleep makes the test more robust. + + -- request /hello without key-auth should response status 401 + local code, body = t('/hello', ngx.HTTP_GET) + assert(code == 401) + + -- add a consumer jack + t('/apisix/admin/consumers', + ngx.HTTP_PUT, + [[{ + "username":"jack" + }]], + [[{ + "key": "/apisix/consumers/jack", + "value": + { + "username":"jack" + } + }]] + ) + + -- create first credential for consumer jack + t('/apisix/admin/consumers/jack/credentials/the-first-one', + ngx.HTTP_PUT, + [[{ + "plugins":{"key-auth":{"key":"p7a3k6r4t9"}} + }]], + [[{ + "value":{ + "id":"the-first-one", + "plugins":{"key-auth":{"key":"p7a3k6r4t9"}} + }, + "key":"/apisix/consumers/jack/credentials/the-first-one" + }]] + ) + ngx.sleep(0.2) + + -- request /hello with credential a + local headers = {} + headers["apikey"] = "p7a3k6r4t9" + code, body = t('/hello', ngx.HTTP_GET, "", nil, headers) + assert(code == 200) + + -- create second credential for consumer jack + t('/apisix/admin/consumers/jack/credentials/the-second-one', + ngx.HTTP_PUT, + [[{ + "plugins":{"key-auth":{"key":"v8p3q6r7t9"}} + }]], + [[{ + "value":{ + "id":"the-second-one", + "plugins":{"key-auth":{"key":"v8p3q6r7t9"}} + }, + "key":"/apisix/consumers/jack/credentials/the-second-one" + }]] + ) + ngx.sleep(0.2) + + -- request /hello with credential b + headers["apikey"] = "v8p3q6r7t9" + code, body = t('/hello', ngx.HTTP_GET, "", nil, headers) + assert(code == 200) + + -- delete the first credential + code, body = t('/apisix/admin/consumers/jack/credentials/the-first-one', ngx.HTTP_DELETE) + assert(code == 200) + ngx.sleep(0.2) + + -- request /hello with credential a + headers["apikey"] = "p7a3k6r4t9" + code, body = t('/hello', ngx.HTTP_GET, "", nil, headers) + assert(code == 401) + } + } +--- request +GET /t diff --git a/t/node/credential-plugin-jwt-auth.t b/t/node/credential-plugin-jwt-auth.t new file mode 100644 index 000000000000..f95498d6b115 --- /dev/null +++ b/t/node/credential-plugin-jwt-auth.t @@ -0,0 +1,137 @@ +# 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(); +run_tests; + +__DATA__ + +=== TEST 1: enable jwt-auth on the route /hello +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "plugins": { + "jwt-auth": {} + }, + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + }, + "uri": "/hello" + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- request +GET /t +--- response_body +passed + + + +=== TEST 2: create a consumer +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/consumers', + ngx.HTTP_PUT, + [[{ + "username": "jack" + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- request +GET /t +--- response_body +passed + + + +=== TEST 3: create a credential with jwt-auth plugin enabled for the consumer +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/consumers/jack/credentials/34010989-ce4e-4d61-9493-b54cca8edb31', + ngx.HTTP_PUT, + [[{ + "plugins": { + "jwt-auth": {"key": "user-key", "secret": "my-secret-key"} + } + }]], + [[{ + "value":{ + "id":"34010989-ce4e-4d61-9493-b54cca8edb31", + "plugins":{ + "jwt-auth": {"key": "user-key", "secret": "kK0lkbzXrE7aiTiyK/Z0Sw=="} + } + }, + "key":"/apisix/consumers/jack/credentials/34010989-ce4e-4d61-9493-b54cca8edb31" + }]] + ) + + ngx.status = code + ngx.say(body) + } + } +--- request +GET /t +--- response_body +passed + + + +=== TEST 4: access with invalid JWT token +--- request +GET /hello +--- more_headers +Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJqd3QtdmF1bHQta2V5IiwiZXhwIjoxNjk1MTM4NjM1fQ.Au2liSZ8eQXUJR3SJESwNlIfqZdNyRyxIJK03L4dk_g +--- error_code: 401 +--- response_body +{"message":"Invalid user key in JWT token"} + + + +=== TEST 5: access with valid JWT token in header +--- request +GET /hello +--- more_headers +Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJ1c2VyLWtleSIsImV4cCI6MTg3OTMxODU0MX0.fNtFJnNmJgzbiYmGB0Yjvm-l6A6M4jRV1l4mnVFSYjs +--- response_body +hello world diff --git a/t/node/credential-plugin-key-auth.t b/t/node/credential-plugin-key-auth.t new file mode 100644 index 000000000000..558616d647e3 --- /dev/null +++ b/t/node/credential-plugin-key-auth.t @@ -0,0 +1,137 @@ +# 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(); +run_tests; + +__DATA__ + +=== TEST 1: enable key-auth on the route /hello +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "plugins": { + "key-auth": {} + }, + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + }, + "uri": "/hello" + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- request +GET /t +--- response_body +passed + + + +=== TEST 2: create consumer +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/consumers', + ngx.HTTP_PUT, + [[{ + "username": "jack" + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- request +GET /t +--- response_body +passed + + + +=== TEST 3: create a credential with key-auth plugin enabled for the consumer +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/consumers/jack/credentials/34010989-ce4e-4d61-9493-b54cca8edb31', + ngx.HTTP_PUT, + [[{ + "plugins": { + "key-auth": {"key": "p7a3k6r4t9"} + } + }]], + [[{ + "value":{ + "id":"34010989-ce4e-4d61-9493-b54cca8edb31", + "plugins":{ + "key-auth": {"key": "fsFPtg7BtXMXkvSnS9e1zw=="} + } + }, + "key":"/apisix/consumers/jack/credentials/34010989-ce4e-4d61-9493-b54cca8edb31" + }]] + ) + + ngx.status = code + ngx.say(body) + } + } +--- request +GET /t +--- response_body +passed + + + +=== TEST 4: request with an invalid key: should be not OK +--- request +GET /hello +--- more_headers +apikey: 123 +--- error_code: 401 +--- response_body +{"message":"Invalid API key in request"} + + + +=== TEST 5: request with the valid key: should be OK +--- request +GET /hello +--- more_headers +apikey: p7a3k6r4t9 +--- response_body +hello world diff --git a/t/node/credential-plugin-multi-credentials.t b/t/node/credential-plugin-multi-credentials.t new file mode 100644 index 000000000000..6b60bb37b94e --- /dev/null +++ b/t/node/credential-plugin-multi-credentials.t @@ -0,0 +1,236 @@ +# 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(); +run_tests; + +__DATA__ + +=== TEST 1: enable key-auth plugin on /hello +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + + -- basic-auth on route 1 + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "plugins": { + "key-auth": {} + }, + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + }, + "uri": "/hello" + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- request +GET /t +--- response_body +passed + + + +=== TEST 2: create a consumer +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/consumers', + ngx.HTTP_PUT, + [[{ + "username": "jack" + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- request +GET /t +--- response_body +passed + + + +=== TEST 3: create the first credential with the key-auth plugin enabled for the consumer +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/consumers/jack/credentials/the-first-one', + ngx.HTTP_PUT, + [[{ + "plugins": { + "key-auth": {"key": "p7a3k6r4t9"} + } + }]], + [[{ + "value":{ + "id":"the-first-one", + "plugins":{ + "key-auth": {"key": "fsFPtg7BtXMXkvSnS9e1zw=="} + } + }, + "key":"/apisix/consumers/jack/credentials/the-first-one" + }]] + ) + + ngx.status = code + ngx.say(body) + } + } +--- request +GET /t +--- response_body +passed + + + +=== TEST 4: create the second credential with the key-auth plugin enabled for the consumer +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/consumers/jack/credentials/the-second-one', + ngx.HTTP_PUT, + [[{ + "plugins": { + "key-auth": {"key": "v8p3q6r7t9"} + } + }]], + [[{ + "value":{ + "id":"the-second-one", + "plugins":{ + "key-auth": {"key": "QwGua2GjZjOiq+Mj3Mef2g=="} + } + }, + "key":"/apisix/consumers/jack/credentials/the-second-one" + }]] + ) + + ngx.status = code + ngx.say(body) + } + } +--- request +GET /t +--- response_body +passed + + + +=== TEST 5: request /hello with the key of the first credential: should be OK +--- request +GET /hello +--- more_headers +apikey: p7a3k6r4t9 +--- response_body +hello world + + + +=== TEST 6: request /hello with the key of second credential: should be OK +--- request +GET /hello +--- more_headers +apikey: v8p3q6r7t9 +--- response_body +hello world + + + +=== TEST 7: delete the first credential +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/consumers/jack/credentials/the-first-one', ngx.HTTP_DELETE) + + ngx.status = code + ngx.say(body) + } + } +--- request +GET /t +--- response_body +passed + + + +=== TEST 8: request /hello with the key of the first credential: should be not OK +--- request +GET /hello +--- more_headers +apikey: p7a3k6r4t9 +--- error_code: 401 + + + +=== TEST 9: request /hello with the key of the second credential: should be OK +--- request +GET /hello +--- more_headers +apikey: v8p3q6r7t9 +--- response_body +hello world + + + +=== TEST 10: delete the second credential +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/consumers/jack/credentials/the-second-one', ngx.HTTP_DELETE) + + ngx.status = code + ngx.say(body) + } + } +--- request +GET /t +--- response_body +passed + + + +=== TEST 11: request /hello with the key of the second credential: should be not OK +--- request +GET /hello +--- more_headers +apikey: v8p3q6r7t9 +--- error_code: 401 diff --git a/t/node/credential-plugin-set-request-header.t b/t/node/credential-plugin-set-request-header.t new file mode 100644 index 000000000000..51148d038512 --- /dev/null +++ b/t/node/credential-plugin-set-request-header.t @@ -0,0 +1,245 @@ +# 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(); +run_tests; + +__DATA__ + +=== TEST 1: enable key-auth on the route /echo +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "plugins": { + "key-auth": {} + }, + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + }, + "uri": "/echo" + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- request +GET /t +--- response_body +passed + + + +=== TEST 2: create consumer +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/consumers', + ngx.HTTP_PUT, + [[{ + "username": "jack" + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- request +GET /t +--- response_body +passed + + + +=== TEST 3: create a credential with key-auth plugin enabled and 'custom_id' label for the consumer +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/consumers/jack/credentials/34010989-ce4e-4d61-9493-b54cca8edb31', + ngx.HTTP_PUT, + [[{ + "plugins": { + "key-auth": {"key": "p7a3k6r4t9"} + }, + "labels": { + "custom_id": "271fc4a264bb" + } + }]], + [[{ + "value":{ + "id":"34010989-ce4e-4d61-9493-b54cca8edb31", + "plugins":{ + "key-auth": {"key": "fsFPtg7BtXMXkvSnS9e1zw=="} + }, + "labels": { + "custom_id": "271fc4a264bb" + } + }, + "key":"/apisix/consumers/jack/credentials/34010989-ce4e-4d61-9493-b54cca8edb31" + }]] + ) + + ngx.status = code + ngx.say(body) + } + } +--- request +GET /t +--- response_body +passed + + + +=== TEST 4: request the route: 'x-consumer-username' and 'x-credential-identifier' is in response headers and 'x-consumer-custom-id' is not +--- request +GET /echo HTTP/1.1 +--- more_headers +apikey: p7a3k6r4t9 +--- response_headers +x-consumer-username: jack +x-credential-identifier: 34010989-ce4e-4d61-9493-b54cca8edb31 +!x-consumer-custom-id + + + +=== TEST 5: update the consumer add label "custom_id" +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/consumers', + ngx.HTTP_PUT, + [[{ + "username": "jack", + "labels": { + "custom_id": "495aec6a" + } + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- request +GET /t +--- response_body +passed + + + +=== TEST 6: request the route: the value of 'x-consumer-custom-id' come from the consumer but not the credential or downstream +--- request +GET /echo HTTP/1.1 +--- more_headers +apikey: p7a3k6r4t9 +x-consumer-custom-id: 271fc4a264bb +--- response_headers +x-consumer-username: jack +x-credential-identifier: 34010989-ce4e-4d61-9493-b54cca8edb31 +x-consumer-custom-id: 495aec6a + + + +=== TEST 7: delete the credential +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/consumers/jack/credentials/34010989-ce4e-4d61-9493-b54cca8edb31', ngx.HTTP_DELETE) + + assert(code == 200) + ngx.status = code + } + } +--- request +GET /t +--- response_body + + + +=== TEST 8: update the consumer to enable a key-auth plugin +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/consumers', + ngx.HTTP_PUT, + [[{ + "username": "jack", + "plugins": { + "key-auth": { + "key": "p7a3k6r4t9" + } + } + }]], + [[{ + "value": { + "username": "jack", + "plugins": { + "key-auth": { + "key": "fsFPtg7BtXMXkvSnS9e1zw==" + } + } + }, + "key": "/apisix/consumers/jack" + }]] + ) + + ngx.status = code + ngx.say(body) + } + } +--- request +GET /t +--- response_body +passed + + + +=== TEST 9: request the route with headers x-credential-identifier and x-consumer-custom-id: these headers will be removed +--- request +GET /echo HTTP/1.1 +--- more_headers +apikey: p7a3k6r4t9 +x-credential-identifier: 34010989-ce4e-4d61-9493-b54cca8edb31 +x-consumer-custom-id: 271fc4a264bb +--- response_headers +x-consumer-username: jack +!x-credential-identifier +!x-consumer-custom-id diff --git a/t/node/credential-plugin-work-with-other-plugin.t b/t/node/credential-plugin-work-with-other-plugin.t new file mode 100644 index 000000000000..14bfc13c91cc --- /dev/null +++ b/t/node/credential-plugin-work-with-other-plugin.t @@ -0,0 +1,171 @@ +# 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(); +run_tests; + +__DATA__ + +=== TEST 1: enable key-auth on /hello +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + + -- basic-auth on route 1 + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "plugins": { + "key-auth": {} + }, + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + }, + "uri": "/hello" + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- request +GET /t +--- response_body +passed + + + +=== TEST 2: create a consumer +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/consumers', + ngx.HTTP_PUT, + [[{ + "username": "jack" + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- request +GET /t +--- response_body +passed + + + +=== TEST 3: create a credential with the key-auth plugin enabled for the consumer +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/consumers/jack/credentials/34010989-ce4e-4d61-9493-b54cca8edb31', + ngx.HTTP_PUT, + [[{ + "plugins": { + "key-auth": {"key": "p7a3k6r4t9"} + } + }]], + [[{ + "value":{ + "id":"34010989-ce4e-4d61-9493-b54cca8edb31", + "plugins":{ + "key-auth": {"key": "fsFPtg7BtXMXkvSnS9e1zw=="} + } + }, + "key":"/apisix/consumers/jack/credentials/34010989-ce4e-4d61-9493-b54cca8edb31" + }]] + ) + + ngx.status = code + ngx.say(body) + } + } +--- request +GET /t +--- response_body +passed + + + +=== TEST 4: request the route /hello multi times: should be OK +--- pipelined_requests eval +["GET /hello", "GET /hello", "GET /hello", "GET /hello"] +--- more_headers +apikey: p7a3k6r4t9 +--- error_code eval +[200, 200, 200, 200] + + + +=== TEST 5: enable plugin `limit-count` for the consumer +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/consumers', + ngx.HTTP_PUT, + [[{ + "username": "jack", + "plugins": { + "limit-count": { + "count": 2, + "time_window": 60, + "rejected_code": 503, + "key": "remote_addr" + } + } + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- request +GET /t +--- response_body +passed + + + +=== TEST 6: request the route /hello multi times: should be not OK, exceed the limit-count +--- pipelined_requests eval +["GET /hello", "GET /hello", "GET /hello", "GET /hello"] +--- more_headers +apikey: p7a3k6r4t9 +--- error_code eval +[200, 200, 503, 503] From 502b420340611797d695dc769d5d611635a2bcc1 Mon Sep 17 00:00:00 2001 From: Shreemaan Abhishek Date: Thu, 26 Sep 2024 13:52:30 +0545 Subject: [PATCH 7/7] chore: upgrade `skywalking-nginx-lua` version (#11603) --- apisix-master-0.rockspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apisix-master-0.rockspec b/apisix-master-0.rockspec index 913a4defe39d..829b40f0faf2 100644 --- a/apisix-master-0.rockspec +++ b/apisix-master-0.rockspec @@ -56,7 +56,7 @@ dependencies = { "lua-resty-ipmatcher = 0.6.1", "lua-resty-kafka = 0.23-0", "lua-resty-logger-socket = 2.0.1-0", - "skywalking-nginx-lua = 0.6.0", + "skywalking-nginx-lua = 1.0.1", "base64 = 1.5-2", "binaryheap = 0.4", "api7-dkjson = 0.1.1",