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", 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 ebc0cbea9539..0db360eacbb9 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 c6a8370ad288..6694dc80d027 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]