This is an automated email from the ASF dual-hosted git repository.
AlinsRan pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/apisix.git
The following commit(s) were added to refs/heads/master by this push:
new 9f40d6c2a feat: add acl plugin (#13349)
9f40d6c2a is described below
commit 9f40d6c2a60daf6ee825ec76a90f4ee3a245a47f
Author: AlinsRan <[email protected]>
AuthorDate: Wed May 20 08:52:24 2026 +0800
feat: add acl plugin (#13349)
---
apisix/cli/config.lua | 1 +
apisix/plugins/acl.lua | 251 +++++++
conf/config.yaml.example | 1 +
docs/en/latest/config.json | 1 +
docs/en/latest/plugins/acl.md | 241 +++++++
docs/zh/latest/config.json | 1 +
docs/zh/latest/plugins/acl.md | 241 +++++++
t/admin/plugins.t | 1 +
t/plugin/acl.t | 1539 +++++++++++++++++++++++++++++++++++++++++
t/plugin/acl2.t | 115 +++
10 files changed, 2392 insertions(+)
diff --git a/apisix/cli/config.lua b/apisix/cli/config.lua
index c42ecbdee..158d2d602 100644
--- a/apisix/cli/config.lua
+++ b/apisix/cli/config.lua
@@ -221,6 +221,7 @@ local _M = {
"jwt-auth",
"jwe-decrypt",
"key-auth",
+ "acl",
"consumer-restriction",
"attach-consumer-label",
"forward-auth",
diff --git a/apisix/plugins/acl.lua b/apisix/plugins/acl.lua
new file mode 100644
index 000000000..ba0c751e9
--- /dev/null
+++ b/apisix/plugins/acl.lua
@@ -0,0 +1,251 @@
+--
+-- 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 type = type
+local ipairs = ipairs
+local pairs = pairs
+local jp = require("jsonpath")
+local re_split = require("ngx.re").split
+local core = require("apisix.core")
+local schema = {
+ type = "object",
+ properties = {
+ external_user_label_field = {type = "string", default = "groups",
minLength = 1},
+ external_user_label_field_key = {type = "string", minLength = 1},
+ external_user_label_field_parser = {
+ type = "string",
+ enum = {"segmented_text", "json", "table"},
+ },
+ external_user_label_field_separator = {
+ type = "string",
+ minLength = 1,
+ description = "The separator(regex) of the segmented_text parser",
+ },
+ allow_labels = {
+ type = "object",
+ minProperties = 1,
+ patternProperties = {
+ [".*"] = {
+ type = "array",
+ minItems = 1,
+ items = {type = "string"}
+ },
+ },
+ },
+ deny_labels = {
+ type = "object",
+ minProperties = 1,
+ patternProperties = {
+ [".*"] = {
+ type = "array",
+ minItems = 1,
+ items = {type = "string"}
+ },
+ },
+ },
+ rejected_code = {type = "integer", minimum = 200, default = 403},
+ rejected_msg = {type = "string"},
+ },
+ allOf = {
+ {
+ ["if"] = {
+ required = { "external_user_label_field_parser" },
+ properties = { external_user_label_field_parser = { const =
"segmented_text" } },
+ },
+ ["then"] = {
+ required = { "external_user_label_field_separator" },
+ },
+ },
+ },
+ anyOf = {
+ {required = {"allow_labels"}},
+ {required = {"deny_labels"}}
+ },
+}
+
+local plugin_name = "acl"
+
+local _M = {
+ version = 0.1,
+ priority = 2410,
+ name = plugin_name,
+ schema = schema,
+}
+
+local parsers = {
+ SEGMENTED_TEXT = "segmented_text",
+ JSON = "json",
+ TABLE = "table",
+}
+
+
+local function extra_values_with_parser(value, parser, sep)
+ local values = {}
+ if parser == parsers.SEGMENTED_TEXT then
+ sep = "\\s*" .. sep .. "\\s*"
+ local res, err = re_split(value, sep, "jo")
+ if res then
+ return res
+ end
+ core.log.warn("failed to split labels [", value, "], err: ", err)
+
+ return values
+ end
+
+ local typ = type(value)
+
+ if parser == parsers.TABLE then
+ if typ == "table" then
+ return value
+ end
+ core.log.warn("the parser is specified as table, but the type of value
is not table: ", typ)
+ return values
+ end
+
+ if parser == parsers.JSON then
+ if typ ~= "string" then
+ core.log.warn("the parser is specified as json array, but the
value type is not string")
+ return values
+ end
+ if not core.string.has_prefix(value, "[") then
+ core.log.warn("the parser is specified as json array, ",
+ "but the value do not has prefix '['")
+ return values
+ end
+
+ local res, err = core.json.decode(value)
+ if res then
+ return res
+ end
+ core.log.warn("failed to decode labels [", value, "] as array, err: ",
err)
+ return values
+ end
+
+ return values
+end
+
+
+local function extra_values_without_parser(value)
+ local values = {}
+ local typ = type(value)
+
+ if typ == "table" then
+ return extra_values_with_parser(value, parsers.TABLE, "")
+ end
+
+ if typ == "string" then
+ if core.string.has_prefix(value, "[") then
+ return extra_values_with_parser(value, parsers.JSON, "")
+ end
+ if core.string.find(value, ",") then
+ return extra_values_with_parser(value, parsers.SEGMENTED_TEXT, ",")
+ end
+ core.log.info("the string value can not parsed by ", parsers.JSON,
+ " or ",parsers.SEGMENTED_TEXT)
+ return { value }
+ end
+
+ core.log.error("unsupported type of label value: ", typ)
+ return values
+end
+
+
+local function contains_value(want_values, value, parser, sep)
+ local values
+ if parser then
+ values = extra_values_with_parser(value, parser, sep)
+ else
+ values = extra_values_without_parser(value)
+ end
+
+ for _, want in ipairs(want_values) do
+ for _, value in ipairs(values) do
+ if want == value then
+ return true
+ end
+ end
+ end
+ return false
+end
+
+
+local function contains_label(want_labels, labels, parser, sep)
+ if not labels then
+ return false
+ end
+ for key, values in pairs(want_labels) do
+ if labels[key] and contains_value(values, labels[key], parser, sep)
then
+ return true
+ end
+ end
+ return false
+end
+
+local function reject(conf)
+ if conf.rejected_msg then
+ return conf.rejected_code , { message = conf.rejected_msg }
+ end
+ return conf.rejected_code , { message = "The consumer is forbidden."}
+end
+
+function _M.check_schema(conf)
+ local ok, err = core.schema.check(schema, conf)
+ if not ok then
+ return false, err
+ end
+
+ local _, parse_err = jp.parse(conf.external_user_label_field)
+ if parse_err then
+ return false, "invalid external_user_label_field: " .. parse_err
+ end
+
+ return true
+end
+
+function _M.access(conf, ctx)
+ local labels
+ local parser, sep
+ if ctx.consumer then
+ labels = ctx.consumer.labels
+ elseif ctx.external_user then
+ local label_key = conf.external_user_label_field
+ if conf.external_user_label_field_key then
+ label_key = conf.external_user_label_field_key
+ end
+ local label_value = jp.value(ctx.external_user,
conf.external_user_label_field)
+ labels = { [label_key] = label_value }
+ parser = conf.external_user_label_field_parser
+ sep = conf.external_user_label_field_separator
+ else
+ return 401, { message = "Missing authentication."}
+ end
+
+ core.log.debug("consumer's or user's labels: ",
core.json.delay_encode(labels))
+
+ if conf.deny_labels then
+ if contains_label(conf.deny_labels, labels, parser, sep) then
+ return reject(conf)
+ end
+ end
+
+ if conf.allow_labels then
+ if not contains_label(conf.allow_labels, labels, parser, sep) then
+ return reject(conf)
+ end
+ end
+end
+
+return _M
diff --git a/conf/config.yaml.example b/conf/config.yaml.example
index 6023c83bc..39493fb67 100644
--- a/conf/config.yaml.example
+++ b/conf/config.yaml.example
@@ -504,6 +504,7 @@ plugins: # plugin list (sorted by
priority)
- jwt-auth # priority: 2510
- jwe-decrypt # priority: 2509
- key-auth # priority: 2500
+ - acl # priority: 2410
- consumer-restriction # priority: 2400
- attach-consumer-label # priority: 2399
- forward-auth # priority: 2002
diff --git a/docs/en/latest/config.json b/docs/en/latest/config.json
index 115448b95..f44b65ee9 100644
--- a/docs/en/latest/config.json
+++ b/docs/en/latest/config.json
@@ -148,6 +148,7 @@
"plugins/ua-restriction",
"plugins/referer-restriction",
"plugins/consumer-restriction",
+ "plugins/acl",
"plugins/csrf",
"plugins/public-api",
"plugins/gm",
diff --git a/docs/en/latest/plugins/acl.md b/docs/en/latest/plugins/acl.md
new file mode 100644
index 000000000..edd89df2e
--- /dev/null
+++ b/docs/en/latest/plugins/acl.md
@@ -0,0 +1,241 @@
+---
+title: acl
+keywords:
+ - Apache APISIX
+ - API Gateway
+ - Plugin
+ - acl
+description: The acl Plugin implements label-based access control for API
routes, allowing or denying requests based on consumer labels or external user
attributes.
+---
+
+<!--
+#
+# 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.
+#
+-->
+
+<head>
+ <link rel="canonical" href="https://docs.api7.ai/hub/acl" />
+</head>
+
+## Description
+
+The `acl` Plugin provides label-based access control for API routes. It checks
consumer labels (from APISIX [Consumers](../terminology/consumer.md)) or
external user attributes (from authentication plugins that set
`ctx.external_user`) against configured allow or deny lists.
+
+The Plugin supports three label value formats:
+
+- **table**: the label value is a Lua table (array).
+- **json**: the label value is a JSON-encoded array string, e.g.
`["admin","user"]`.
+- **segmented_text**: the label value is a delimiter-separated string, e.g.
`admin,user`.
+
+At least one of `allow_labels` or `deny_labels` must be configured. When both
are present, `deny_labels` is evaluated first.
+
+## Attributes
+
+| Name | Type | Required | Default | Valid values | Description |
+|------|------|----------|---------|--------------|-------------|
+| allow_labels | object | False* | | | Labels to allow. Keys are label names,
values are arrays of allowed label values. At least one of `allow_labels` or
`deny_labels` must be configured. |
+| deny_labels | object | False* | | | Labels to deny. Keys are label names,
values are arrays of denied label values. At least one of `allow_labels` or
`deny_labels` must be configured. |
+| rejected_code | integer | False | 403 | >= 200 | HTTP status code returned
when the request is rejected. |
+| rejected_msg | string | False | | | Custom rejection message body. If not
set, defaults to `{"message":"The consumer is forbidden."}`. |
+| external_user_label_field | string | False | `groups` | | JSONPath
expression or plain field name used to extract the label value from
`ctx.external_user`. For example, `$..groups` (JSONPath) or `groups` (plain
field name). |
+| external_user_label_field_key | string | False | | | The label key name used
for the extracted value. Defaults to the value of `external_user_label_field`. |
+| external_user_label_field_parser | string | False | | `segmented_text`,
`json`, `table` | How to parse the extracted field value. If not set, the
Plugin auto-detects the format. |
+| external_user_label_field_separator | string | False | | | Separator regex
for the `segmented_text` parser. Required when
`external_user_label_field_parser` is `segmented_text`. |
+
+## Examples
+
+The examples below demonstrate how you can configure the `acl` Plugin for
different scenarios.
+
+:::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')
+```
+
+:::
+
+### Allow Consumers by Label
+
+The example below demonstrates how to use the `acl` Plugin with
[`key-auth`](./key-auth.md) to allow only consumers that have a specific label
value.
+
+Create a Consumer `alice` with a label `team: platform`:
+
+```shell
+curl "http://127.0.0.1:9180/apisix/admin/consumers" -X PUT \
+ -H "X-API-KEY: ${admin_key}" \
+ -d '{
+ "username": "alice",
+ "plugins": {
+ "key-auth": {
+ "key": "alice-key"
+ }
+ },
+ "labels": {
+ "team": "platform"
+ }
+ }'
+```
+
+Create a second Consumer `bob` with a different label `team: sales`:
+
+```shell
+curl "http://127.0.0.1:9180/apisix/admin/consumers" -X PUT \
+ -H "X-API-KEY: ${admin_key}" \
+ -d '{
+ "username": "bob",
+ "plugins": {
+ "key-auth": {
+ "key": "bob-key"
+ }
+ },
+ "labels": {
+ "team": "sales"
+ }
+ }'
+```
+
+Create a Route with `key-auth` and `acl` configured to allow only consumers
with label `team: platform`:
+
+```shell
+curl "http://127.0.0.1:9180/apisix/admin/routes" -X PUT \
+ -H "X-API-KEY: ${admin_key}" \
+ -d '{
+ "id": "acl-allow-route",
+ "uri": "/get",
+ "plugins": {
+ "key-auth": {},
+ "acl": {
+ "allow_labels": {
+ "team": ["platform"]
+ }
+ }
+ },
+ "upstream": {
+ "type": "roundrobin",
+ "nodes": {
+ "httpbin.org:80": 1
+ }
+ }
+ }'
+```
+
+Send a request as `alice` (label `team: platform`):
+
+```shell
+curl "http://127.0.0.1:9080/get" \
+ -H "apikey: alice-key"
+```
+
+You should receive an HTTP `200` response, as `alice` has the allowed label.
+
+Send a request as `bob` (label `team: sales`):
+
+```shell
+curl "http://127.0.0.1:9080/get" \
+ -H "apikey: bob-key"
+```
+
+You should receive an HTTP `403` response, as `bob` does not have the allowed
label.
+
+### Deny Consumers by Label
+
+The example below demonstrates how to block consumers based on a label value
while allowing all others.
+
+Create a Consumer `carol` with label `role: guest`:
+
+```shell
+curl "http://127.0.0.1:9180/apisix/admin/consumers" -X PUT \
+ -H "X-API-KEY: ${admin_key}" \
+ -d '{
+ "username": "carol",
+ "plugins": {
+ "key-auth": {
+ "key": "carol-key"
+ }
+ },
+ "labels": {
+ "role": "guest"
+ }
+ }'
+```
+
+Create a Route that denies consumers with label `role: guest`:
+
+```shell
+curl "http://127.0.0.1:9180/apisix/admin/routes" -X PUT \
+ -H "X-API-KEY: ${admin_key}" \
+ -d '{
+ "id": "acl-deny-route",
+ "uri": "/get",
+ "plugins": {
+ "key-auth": {},
+ "acl": {
+ "deny_labels": {
+ "role": ["guest"]
+ }
+ }
+ },
+ "upstream": {
+ "type": "roundrobin",
+ "nodes": {
+ "httpbin.org:80": 1
+ }
+ }
+ }'
+```
+
+Send a request as `carol`:
+
+```shell
+curl "http://127.0.0.1:9080/get" \
+ -H "apikey: carol-key"
+```
+
+You should receive an HTTP `403` response.
+
+### Custom Rejection Code and Message
+
+You can customize the HTTP status code and message returned when access is
denied.
+
+```shell
+curl "http://127.0.0.1:9180/apisix/admin/routes" -X PUT \
+ -H "X-API-KEY: ${admin_key}" \
+ -d '{
+ "id": "acl-custom-reject-route",
+ "uri": "/get",
+ "plugins": {
+ "key-auth": {},
+ "acl": {
+ "allow_labels": {
+ "team": ["platform"]
+ },
+ "rejected_code": 401,
+ "rejected_msg": "Access denied: insufficient label permissions."
+ }
+ },
+ "upstream": {
+ "type": "roundrobin",
+ "nodes": {
+ "httpbin.org:80": 1
+ }
+ }
+ }'
+```
+
+When a Consumer without the required label accesses the route, they receive a
`401` response with the configured message.
diff --git a/docs/zh/latest/config.json b/docs/zh/latest/config.json
index 6499144f7..6c3713add 100644
--- a/docs/zh/latest/config.json
+++ b/docs/zh/latest/config.json
@@ -137,6 +137,7 @@
"plugins/ua-restriction",
"plugins/referer-restriction",
"plugins/consumer-restriction",
+ "plugins/acl",
"plugins/csrf",
"plugins/public-api",
"plugins/gm",
diff --git a/docs/zh/latest/plugins/acl.md b/docs/zh/latest/plugins/acl.md
new file mode 100644
index 000000000..6f5eb2540
--- /dev/null
+++ b/docs/zh/latest/plugins/acl.md
@@ -0,0 +1,241 @@
+---
+title: acl
+keywords:
+ - Apache APISIX
+ - API Gateway
+ - 插件
+ - acl
+description: acl 插件基于标签实现访问控制,通过检查消费者标签或外部用户属性来允许或拒绝请求。
+---
+
+<!--
+#
+# 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.
+#
+-->
+
+<head>
+ <link rel="canonical" href="https://docs.api7.ai/hub/acl" />
+</head>
+
+## 描述
+
+`acl` 插件为 API 路由提供基于标签的访问控制。它检查 APISIX
[消费者](../terminology/consumer.md)的标签,或来自外部认证插件(设置了
`ctx.external_user`)的用户属性,并与配置的允许列表或拒绝列表进行比对。
+
+插件支持三种标签值格式:
+
+- **table**:标签值为 Lua 表(数组)。
+- **json**:标签值为 JSON 编码的数组字符串,例如 `["admin","user"]`。
+- **segmented_text**:标签值为分隔符分隔的字符串,例如 `admin,user`。
+
+`allow_labels` 和 `deny_labels` 至少需配置其中一个。当两者同时存在时,先评估 `deny_labels`。
+
+## 属性
+
+| 名称 | 类型 | 必选项 | 默认值 | 有效值 | 描述 |
+|------|------|--------|--------|--------|------|
+| allow_labels | object | 否* | | | 允许的标签。键为标签名,值为允许的标签值数组。`allow_labels` 和
`deny_labels` 至少需配置其中一个。 |
+| deny_labels | object | 否* | | | 拒绝的标签。键为标签名,值为拒绝的标签值数组。`allow_labels` 和
`deny_labels` 至少需配置其中一个。 |
+| rejected_code | integer | 否 | 403 | >= 200 | 请求被拒绝时返回的 HTTP 状态码。 |
+| rejected_msg | string | 否 | | | 自定义拒绝消息体。若未设置,默认返回 `{"message":"The consumer
is forbidden."}`。 |
+| external_user_label_field | string | 否 | `groups` | | 用于从
`ctx.external_user` 提取标签值的 JSONPath 表达式或普通字段名称。例如,`$..groups`(JSONPath)或
`groups`(字段名称)。 |
+| external_user_label_field_key | string | 否 | | | 提取值所使用的标签键名。默认为
`external_user_label_field` 的值。 |
+| external_user_label_field_parser | string | 否 | |
`segmented_text`、`json`、`table` | 提取字段值的解析方式。若未设置,插件自动检测格式。 |
+| external_user_label_field_separator | string | 否 | | | `segmented_text`
解析器使用的分隔符(正则表达式)。当 `external_user_label_field_parser` 为 `segmented_text` 时必填。 |
+
+## 示例
+
+以下示例演示了如何为不同场景配置 `acl` 插件。
+
+:::note
+
+可以使用以下命令从 `config.yaml` 中获取 `admin_key` 并保存到环境变量:
+
+```bash
+admin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed
's/"//g')
+```
+
+:::
+
+### 按标签允许消费者
+
+以下示例演示如何将 `acl` 插件与 [`key-auth`](./key-auth.md) 结合使用,仅允许具有特定标签值的消费者访问。
+
+创建消费者 `alice`,标签为 `team: platform`:
+
+```shell
+curl "http://127.0.0.1:9180/apisix/admin/consumers" -X PUT \
+ -H "X-API-KEY: ${admin_key}" \
+ -d '{
+ "username": "alice",
+ "plugins": {
+ "key-auth": {
+ "key": "alice-key"
+ }
+ },
+ "labels": {
+ "team": "platform"
+ }
+ }'
+```
+
+创建第二个消费者 `bob`,标签为 `team: sales`:
+
+```shell
+curl "http://127.0.0.1:9180/apisix/admin/consumers" -X PUT \
+ -H "X-API-KEY: ${admin_key}" \
+ -d '{
+ "username": "bob",
+ "plugins": {
+ "key-auth": {
+ "key": "bob-key"
+ }
+ },
+ "labels": {
+ "team": "sales"
+ }
+ }'
+```
+
+创建启用了 `key-auth` 和 `acl` 的路由,仅允许标签 `team: platform` 的消费者访问:
+
+```shell
+curl "http://127.0.0.1:9180/apisix/admin/routes" -X PUT \
+ -H "X-API-KEY: ${admin_key}" \
+ -d '{
+ "id": "acl-allow-route",
+ "uri": "/get",
+ "plugins": {
+ "key-auth": {},
+ "acl": {
+ "allow_labels": {
+ "team": ["platform"]
+ }
+ }
+ },
+ "upstream": {
+ "type": "roundrobin",
+ "nodes": {
+ "httpbin.org:80": 1
+ }
+ }
+ }'
+```
+
+以 `alice`(标签 `team: platform`)的身份发送请求:
+
+```shell
+curl "http://127.0.0.1:9080/get" \
+ -H "apikey: alice-key"
+```
+
+由于 `alice` 具有允许的标签,应收到 HTTP `200` 响应。
+
+以 `bob`(标签 `team: sales`)的身份发送请求:
+
+```shell
+curl "http://127.0.0.1:9080/get" \
+ -H "apikey: bob-key"
+```
+
+由于 `bob` 不具备允许的标签,应收到 HTTP `403` 响应。
+
+### 按标签拒绝消费者
+
+以下示例演示如何基于标签值拒绝特定消费者,同时允许其他消费者访问。
+
+创建消费者 `carol`,标签为 `role: guest`:
+
+```shell
+curl "http://127.0.0.1:9180/apisix/admin/consumers" -X PUT \
+ -H "X-API-KEY: ${admin_key}" \
+ -d '{
+ "username": "carol",
+ "plugins": {
+ "key-auth": {
+ "key": "carol-key"
+ }
+ },
+ "labels": {
+ "role": "guest"
+ }
+ }'
+```
+
+创建路由,拒绝标签 `role: guest` 的消费者访问:
+
+```shell
+curl "http://127.0.0.1:9180/apisix/admin/routes" -X PUT \
+ -H "X-API-KEY: ${admin_key}" \
+ -d '{
+ "id": "acl-deny-route",
+ "uri": "/get",
+ "plugins": {
+ "key-auth": {},
+ "acl": {
+ "deny_labels": {
+ "role": ["guest"]
+ }
+ }
+ },
+ "upstream": {
+ "type": "roundrobin",
+ "nodes": {
+ "httpbin.org:80": 1
+ }
+ }
+ }'
+```
+
+以 `carol` 的身份发送请求:
+
+```shell
+curl "http://127.0.0.1:9080/get" \
+ -H "apikey: carol-key"
+```
+
+应收到 HTTP `403` 响应。
+
+### 自定义拒绝状态码和消息
+
+可以自定义访问被拒绝时返回的 HTTP 状态码和消息。
+
+```shell
+curl "http://127.0.0.1:9180/apisix/admin/routes" -X PUT \
+ -H "X-API-KEY: ${admin_key}" \
+ -d '{
+ "id": "acl-custom-reject-route",
+ "uri": "/get",
+ "plugins": {
+ "key-auth": {},
+ "acl": {
+ "allow_labels": {
+ "team": ["platform"]
+ },
+ "rejected_code": 401,
+ "rejected_msg": "Access denied: insufficient label permissions."
+ }
+ },
+ "upstream": {
+ "type": "roundrobin",
+ "nodes": {
+ "httpbin.org:80": 1
+ }
+ }
+ }'
+```
+
+当不具备所需标签的消费者访问该路由时,将收到 `401` 响应和配置的消息。
diff --git a/t/admin/plugins.t b/t/admin/plugins.t
index eea7505ca..e05926db0 100644
--- a/t/admin/plugins.t
+++ b/t/admin/plugins.t
@@ -87,6 +87,7 @@ basic-auth
jwt-auth
jwe-decrypt
key-auth
+acl
consumer-restriction
attach-consumer-label
forward-auth
diff --git a/t/plugin/acl.t b/t/plugin/acl.t
new file mode 100644
index 000000000..09de05d25
--- /dev/null
+++ b/t/plugin/acl.t
@@ -0,0 +1,1539 @@
+# 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 jack
+--- 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": {
+ "basic-auth": {
+ "username": "jack",
+ "password": "123456"
+ }
+ },
+ "labels": {
+ "org": "apache",
+ "project": "gateway,apisix,web-server"
+ }
+ }]]
+ )
+ if code >= 300 then
+ ngx.status = code
+ end
+ ngx.say(body)
+ }
+ }
+--- request
+GET /t
+--- response_body
+passed
+
+
+
+=== TEST 2: add consumer rose
+--- 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": "rose",
+ "plugins": {
+ "basic-auth": {
+ "username": "rose",
+ "password": "123456"
+ }
+ },
+ "labels": {
+ "org": "[\"opensource\",\"apache\"]",
+ "project":
"[\"tomcat\",\"web-server\",\"http,server\"]"
+ }
+ }]]
+ )
+ if code >= 300 then
+ ngx.status = code
+ end
+ ngx.say(body)
+ }
+ }
+--- request
+GET /t
+--- response_body
+passed
+
+
+
+=== TEST 3: set allow_labels
+--- 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",
+ "upstream": {
+ "type": "roundrobin",
+ "nodes": {
+ "127.0.0.1:1980": 1
+ }
+ },
+ "plugins": {
+ "basic-auth": {},
+ "acl": {
+ "allow_labels": {
+ "org": ["apache"]
+ }
+ }
+ }
+ }]]
+ )
+
+ if code >= 300 then
+ ngx.status = code
+ end
+ ngx.say(body)
+ }
+ }
+--- request
+GET /t
+--- response_body
+passed
+
+
+
+=== TEST 4: verify unauthorized
+--- request
+GET /hello
+--- error_code: 401
+--- response_body
+{"message":"Missing authorization in request"}
+
+
+
+=== TEST 5: verify jack
+--- request
+GET /hello
+--- more_headers
+Authorization: Basic amFjazoxMjM0NTY=
+--- response_body
+hello world
+
+
+
+=== TEST 6: verify rose
+--- request
+GET /hello
+--- more_headers
+Authorization: Basic cm9zZToxMjM0NTY=
+--- response_body
+hello world
+
+
+
+=== TEST 7: set allow_labels
+--- 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",
+ "upstream": {
+ "type": "roundrobin",
+ "nodes": {
+ "127.0.0.1:1980": 1
+ }
+ },
+ "plugins": {
+ "basic-auth": {},
+ "acl": {
+ "allow_labels": {
+ "project": ["apisix"]
+ }
+ }
+ }
+ }]]
+ )
+
+ if code >= 300 then
+ ngx.status = code
+ end
+ ngx.say(body)
+ }
+ }
+--- request
+GET /t
+--- response_body
+passed
+
+
+
+=== TEST 8: verify jack
+--- request
+GET /hello
+--- more_headers
+Authorization: Basic amFjazoxMjM0NTY=
+--- response_body
+hello world
+
+
+
+=== TEST 9: verify rose
+--- request
+GET /hello
+--- more_headers
+Authorization: Basic cm9zZToxMjM0NTY=
+--- error_code: 403
+--- response_body
+{"message":"The consumer is forbidden."}
+
+
+
+=== TEST 10: set deny_labels
+--- 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",
+ "upstream": {
+ "type": "roundrobin",
+ "nodes": {
+ "127.0.0.1:1980": 1
+ }
+ },
+ "plugins": {
+ "basic-auth": {},
+ "acl": {
+ "deny_labels": {
+ "project": ["apisix"]
+ },
+ "rejected_msg": "request is forbidden"
+ }
+ }
+ }]]
+ )
+
+ if code >= 300 then
+ ngx.status = code
+ end
+ ngx.say(body)
+ }
+ }
+--- request
+GET /t
+--- response_body
+passed
+
+
+
+=== TEST 11: verify jack
+--- request
+GET /hello
+--- more_headers
+Authorization: Basic amFjazoxMjM0NTY=
+--- error_code: 403
+--- response_body
+{"message":"request is forbidden"}
+
+
+
+=== TEST 12: verify rose
+--- request
+GET /hello
+--- more_headers
+Authorization: Basic cm9zZToxMjM0NTY=
+--- response_body
+hello world
+
+
+
+=== TEST 13: set deny_labels with multiple values
+--- 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",
+ "upstream": {
+ "type": "roundrobin",
+ "nodes": {
+ "127.0.0.1:1980": 1
+ }
+ },
+ "plugins": {
+ "basic-auth": {},
+ "acl": {
+ "deny_labels": {
+ "project": ["apisix", "tomcat"]
+ }
+ }
+ }
+ }]]
+ )
+
+ if code >= 300 then
+ ngx.status = code
+ end
+ ngx.say(body)
+ }
+ }
+--- request
+GET /t
+--- response_body
+passed
+
+
+
+=== TEST 14: verify jack
+--- request
+GET /hello
+--- more_headers
+Authorization: Basic amFjazoxMjM0NTY=
+--- error_code: 403
+
+
+
+=== TEST 15: verify rose
+--- request
+GET /hello
+--- more_headers
+Authorization: Basic cm9zZToxMjM0NTY=
+--- error_code: 403
+
+
+
+=== TEST 16: set allow_labels with comma
+--- 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",
+ "upstream": {
+ "type": "roundrobin",
+ "nodes": {
+ "127.0.0.1:1980": 1
+ }
+ },
+ "plugins": {
+ "basic-auth": {},
+ "acl": {
+ "allow_labels": {
+ "project": ["http,server"]
+ }
+ }
+ }
+ }]]
+ )
+
+ if code >= 300 then
+ ngx.status = code
+ end
+ ngx.say(body)
+ }
+ }
+--- request
+GET /t
+--- response_body
+passed
+
+
+
+=== TEST 17: verify jack
+--- request
+GET /hello
+--- more_headers
+Authorization: Basic amFjazoxMjM0NTY=
+--- error_code: 403
+
+
+
+=== TEST 18: verify rose
+--- request
+GET /hello
+--- more_headers
+Authorization: Basic cm9zZToxMjM0NTY=
+--- response_body
+hello world
+
+
+
+=== TEST 19: test acl with external user
+--- 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",
+ "upstream": {
+ "type": "roundrobin",
+ "nodes": {
+ "127.0.0.1:1980": 1
+ }
+ },
+ "plugins": {
+ "serverless-pre-function": {
+ "phase": "access",
+ "functions" : ["return function(conf, ctx)
+ local core =
require(\"apisix.core\");
+ local uri_args =
core.request.get_uri_args(ctx) or {};
+ if type(uri_args.team) ==
\"table\" then ctx.external_user = { team = uri_args.team } else
ctx.external_user = { team = { uri_args.team } } end;
+ end"]
+ },
+ "acl": {
+ "external_user_label_field": "team",
+ "allow_labels": {
+ "team": ["cloud","infra","devops","qa"]
+ }
+ }
+ }
+ }]]
+ )
+
+ if code >= 300 then
+ ngx.status = code
+ end
+ ngx.say(body)
+ }
+ }
+--- request
+GET /t
+--- response_body
+passed
+
+
+
+=== TEST 20: verify infra team
+--- request
+GET /hello?team=infra
+--- response_body
+hello world
+
+
+
+=== TEST 21: verify infra & fake team
+--- request
+GET /hello?team=infra&team=fake
+--- response_body
+hello world
+
+
+
+=== TEST 22: verify fake team
+--- request
+GET /hello?team=fake
+--- error_code: 403
+
+
+
+=== TEST 23: set acl with external user parsed by JSONPath (parser is table)
+--- 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",
+ "upstream": {
+ "type": "roundrobin",
+ "nodes": {
+ "127.0.0.1:1980": 1
+ }
+ },
+ "plugins": {
+ "serverless-pre-function": {
+ "functions": [
+ "return function(conf, ctx)
ctx.external_user = { orgs = { api7 = { team = {\"cloud\", \"infra\"} } } };
end"
+ ],
+ "phase": "access"
+ },
+ "acl": {
+ "allow_labels": {
+ "org": ["api7", "apache"],
+ "team": ["cloud", "infra"]
+ },
+ "external_user_label_field": "$.orgs..team",
+ "external_user_label_field_key": "team",
+ "external_user_label_field_parser": "table",
+ "rejected_code": 403
+ }
+ }
+ }]]
+ )
+
+ if code >= 300 then
+ ngx.status = code
+ end
+ ngx.say(body)
+ }
+ }
+--- request
+GET /t
+--- response_body
+passed
+
+
+
+=== TEST 24: test acl with external user parsed by JSONPath (parser is table)
+--- request
+GET /hello
+--- response_body
+hello world
+
+
+
+=== TEST 25: set acl with external user parsed by JSONPath (parser is
segmented_text)
+--- 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",
+ "upstream": {
+ "type": "roundrobin",
+ "nodes": {
+ "127.0.0.1:1980": 1
+ }
+ },
+ "plugins": {
+ "serverless-pre-function": {
+ "functions": [
+ "return function(conf, ctx)
ctx.external_user = { orgs = { api7 = { team = \"cloud|infra\" } } }; end"
+ ],
+ "phase": "access"
+ },
+ "acl": {
+ "allow_labels": {
+ "org": ["api7", "apache"],
+ "team": ["cloud", "infra"]
+ },
+ "external_user_label_field": "$.orgs..team",
+ "external_user_label_field_key": "team",
+ "external_user_label_field_parser":
"segmented_text",
+ "external_user_label_field_separator": "\\|",
+ "rejected_code": 403
+ }
+ }
+ }]]
+ )
+
+ if code >= 300 then
+ ngx.status = code
+ end
+ ngx.say(body)
+ }
+ }
+--- request
+GET /t
+--- response_body
+passed
+
+
+
+=== TEST 26: test acl with external user parsed by JSONPath (parser is
segmented_text)
+--- request
+GET /hello
+--- response_body
+hello world
+
+
+
+=== TEST 27: set acl with external user parsed by JSONPath (parser is json)
+--- 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",
+ "upstream": {
+ "type": "roundrobin",
+ "nodes": {
+ "127.0.0.1:1980": 1
+ }
+ },
+ "plugins": {
+ "serverless-pre-function": {
+ "functions": [
+ "return function(conf, ctx)
ctx.external_user = { orgses = { api7 = { team = \"[\\\"cloud\\\",
\\\"infra\\\"]\" } } }; end"
+ ],
+ "phase": "access"
+ },
+ "acl": {
+ "allow_labels": {
+ "org": ["api7", "apache"],
+ "team": ["cloud", "infra"]
+ },
+ "external_user_label_field": "$..team",
+ "external_user_label_field_key": "team",
+ "external_user_label_field_parser": "json",
+ "rejected_code": 403
+ }
+ }
+ }]]
+ )
+
+ if code >= 300 then
+ ngx.status = code
+ end
+ ngx.say(body)
+ }
+ }
+--- request
+GET /t
+--- response_body
+passed
+
+
+
+=== TEST 28: test acl with external user parsed by JSONPath (parser is json)
+--- request
+GET /hello
+--- response_body
+hello world
+
+
+
+=== TEST 29: set acl parser "segmented_text", but can not extract expect value
by the invalid separator
+--- 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",
+ "upstream": {
+ "type": "roundrobin",
+ "nodes": {
+ "127.0.0.1:1980": 1
+ }
+ },
+ "plugins": {
+ "serverless-pre-function": {
+ "functions": [
+ "return function(conf, ctx)
ctx.external_user = { orgs = { api7 = { team = \"cloud|infra\" } } }; end"
+ ],
+ "phase": "access"
+ },
+ "acl": {
+ "allow_labels": {
+ "org": ["api7", "apache"],
+ "team": ["cloud", "infra"]
+ },
+ "external_user_label_field": "$.orgs..team",
+ "external_user_label_field_key": "team",
+ "external_user_label_field_parser":
"segmented_text",
+ "external_user_label_field_separator": "|",
+ "rejected_code": 403
+ }
+ }
+ }]]
+ )
+
+ if code >= 300 then
+ ngx.status = code
+ end
+ ngx.say(body)
+ }
+ }
+--- request
+GET /t
+--- response_body
+passed
+
+
+
+=== TEST 30: test ACL with the invalid separator
+# User may want to split the text "cloud|infra" to be ["cloud", "infra"] by
char "|", but it does not.
+# Because the char "|" is a regex expression, the text "cloud|infra" will be
split to ['c','l','o','u','d','|','i','n','f','r','a'].
+# If you want to split text by "|" you should use "\\|".
+# This is a normal case, no error_log here.
+--- request
+GET /hello
+--- error_code: 403
+
+
+
+=== TEST 31: set external_user info that ACL can extract multiple values from
it.
+--- 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",
+ "upstream": {
+ "type": "roundrobin",
+ "nodes": {
+ "127.0.0.1:1980": 1
+ }
+ },
+ "plugins": {
+ "serverless-pre-function": {
+ "functions": [
+ "return function(conf, ctx)
ctx.external_user = { orgs = { api7 = { team = \"cloud|infra\" }, apache = {
team = { \"devops\", \"qa\" } } } }; end"
+ ],
+ "phase": "access"
+ },
+ "acl": {
+ "allow_labels": {
+ "org": ["api7", "apache"],
+ "team": ["cloud", "infra"]
+ },
+ "external_user_label_field": "$.orgs..team",
+ "external_user_label_field_key": "team",
+ "external_user_label_field_parser":
"segmented_text",
+ "external_user_label_field_separator": "\\|",
+ "rejected_code": 403
+ }
+ }
+ }]]
+ )
+
+ if code >= 300 then
+ ngx.status = code
+ end
+ ngx.say(body)
+ }
+ }
+--- request
+GET /t
+--- response_body
+passed
+
+
+
+=== TEST 32: test the ACL extract multiple values from external_user info and
the first value can not be expected.
+# User may expect the value extracted is "cloud|infra", but it is not.
+# Because the values extracted are multiple, we can not expect the value
"cloud|infra" is the first.
+# This is a normal case, no error_log here.
+--- request
+GET /hello
+--- error_code: 403
+
+
+
+=== TEST 33: use JSONPath to extract value but a correct
external_user_label_field and external_user_label_field_parser is missing.
+--- 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",
+ "upstream": {
+ "type": "roundrobin",
+ "nodes": {
+ "127.0.0.1:1980": 1
+ }
+ },
+ "plugins": {
+ "serverless-pre-function": {
+ "functions": [
+ "return function(conf, ctx)
ctx.external_user = { orgs = { api7 = { team = \"cloud,infra\" } } }; end"
+ ],
+ "phase": "access"
+ },
+ "acl": {
+ "allow_labels": {
+ "org": ["api7", "apache"],
+ "team": ["cloud", "infra"]
+ },
+ "external_user_label_field": "$..team",
+ "rejected_code": 403
+ }
+ }
+ }]]
+ )
+
+ if code >= 300 then
+ ngx.status = code
+ end
+ ngx.say(body)
+ }
+ }
+--- request
+GET /t
+--- response_body
+passed
+
+
+
+=== TEST 34: test using JSONPath but a label key is missing.
+# Using the JSONPath "$..team" to extract value and a label key is missing,
the ACL will use the JSONPath as the key to match labels.
+# It's obvious that our use of "$. .team" does not match any value in ACL
allow_labels/deny_labels.
+# This is a normal case, no error_log here.
+--- request
+GET /hello
+--- error_code: 403
+
+
+
+=== TEST 35: set invalid separator
+--- 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",
+ "upstream": {
+ "type": "roundrobin",
+ "nodes": {
+ "127.0.0.1:1980": 1
+ }
+ },
+ "plugins": {
+ "serverless-pre-function": {
+ "functions": [
+ "return function(conf, ctx)
ctx.external_user = { orgs = { api7 = { team = \"cloud,infra\" } } }; end"
+ ],
+ "phase": "access"
+ },
+ "acl": {
+ "allow_labels": {
+ "org": ["api7", "apache"],
+ "team": ["cloud", "infra"]
+ },
+ "external_user_label_field": "$..team",
+ "external_user_label_field_key": "team",
+ "external_user_label_field_parser":
"segmented_text",
+ "external_user_label_field_separator":
"(invalid(pattern",
+ "rejected_code": 403
+ }
+ }
+ }]]
+ )
+
+ if code >= 300 then
+ ngx.status = code
+ end
+ ngx.say(body)
+ }
+ }
+--- request
+GET /t
+--- response_body
+passed
+
+
+
+=== TEST 36: test invalid separator, ngx.re.split will be fail.
+# The value extracted is "cloud,infra",
+# ACL parser try to parser it as Lua table.
+# It will fail and forbidden all.
+--- request
+GET /hello
+--- error_code: 403
+--- error_log eval
+qr/failed to split labels \[cloud,infra\]/
+
+
+
+=== TEST 37: set the parser "table" but the type of the value extracted is not
a table
+--- 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",
+ "upstream": {
+ "type": "roundrobin",
+ "nodes": {
+ "127.0.0.1:1980": 1
+ }
+ },
+ "plugins": {
+ "serverless-pre-function": {
+ "functions": [
+ "return function(conf, ctx)
ctx.external_user = { orgs = { api7 = { team = \"cloud,infra\" } } }; end"
+ ],
+ "phase": "access"
+ },
+ "acl": {
+ "allow_labels": {
+ "org": ["api7", "apache"],
+ "team": ["cloud", "infra"]
+ },
+ "external_user_label_field": "$..team",
+ "external_user_label_field_key": "team",
+ "external_user_label_field_parser": "table",
+ "rejected_code": 403
+ }
+ }
+ }]]
+ )
+
+ if code >= 300 then
+ ngx.status = code
+ end
+ ngx.say(body)
+ }
+ }
+--- request
+GET /t
+--- response_body
+passed
+
+
+
+=== TEST 38: test the parser is "table" but the type of the value extracted is
not a table
+# The value extracted is "cloud,infra",
+# ACL parser try to parser it as Lua table.
+# It will fail and forbidden all.
+--- request
+GET /hello
+--- error_code: 403
+--- error_log
+extra_values_with_parser(): the parser is specified as table, but the type of
value is not table: string
+
+
+
+=== TEST 39: set the parser "json" but the type of the value extracted is not
string
+--- 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",
+ "upstream": {
+ "type": "roundrobin",
+ "nodes": {
+ "127.0.0.1:1980": 1
+ }
+ },
+ "plugins": {
+ "serverless-pre-function": {
+ "functions": [
+ "return function(conf, ctx)
ctx.external_user = { orgs = { api7 = { team = {\"cloud\", \"infra\"} } } };
end"
+ ],
+ "phase": "access"
+ },
+ "acl": {
+ "allow_labels": {
+ "org": ["api7", "apache"],
+ "team": ["cloud", "infra"]
+ },
+ "external_user_label_field": "$..team",
+ "external_user_label_field_key": "team",
+ "external_user_label_field_parser": "json",
+ "rejected_code": 403
+ }
+ }
+ }]]
+ )
+
+ if code >= 300 then
+ ngx.status = code
+ end
+ ngx.say(body)
+ }
+ }
+--- request
+GET /t
+--- response_body
+passed
+
+
+
+=== TEST 40: test the parser is "json" but the type of the value extracted is
not string
+# The value extracted is {"cloud", "infra"}, a Lua table.
+# The ACL try to parser it as a serialized JSON.
+# It will fail and forbidden all.
+--- request
+GET /hello
+--- error_code: 403
+--- error_log
+extra_values_with_parser(): the parser is specified as json array, but the
value type is not string
+
+
+
+=== TEST 41: set the parser "json" but the value extracted has no prefix "["
+--- 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",
+ "upstream": {
+ "type": "roundrobin",
+ "nodes": {
+ "127.0.0.1:1980": 1
+ }
+ },
+ "plugins": {
+ "serverless-pre-function": {
+ "functions": [
+ "return function(conf, ctx)
ctx.external_user = { orgs = { api7 = { team = \"cloud\" } } }; end"
+ ],
+ "phase": "access"
+ },
+ "acl": {
+ "allow_labels": {
+ "org": ["api7", "apache"],
+ "team": ["cloud", "infra"]
+ },
+ "external_user_label_field": "$..team",
+ "external_user_label_field_key": "team",
+ "external_user_label_field_parser": "json",
+ "rejected_code": 403
+ }
+ }
+ }]]
+ )
+
+ if code >= 300 then
+ ngx.status = code
+ end
+ ngx.say(body)
+ }
+ }
+--- request
+GET /t
+--- response_body
+passed
+
+
+
+=== TEST 42: test the parser is "json" but the value extracted has no prefix
"["
+# The value extracted is "cloud".
+# The ACL try to parse it as a serialized JSON string.
+# It will fail and forbidden all.
+--- request
+GET /hello
+--- error_code: 403
+--- error_log
+extra_values_with_parser(): the parser is specified as json array, but the
value do not has prefix '['
+
+
+
+=== TEST 43: set the parser "json" and the value extracted has prefix "[" but
it is a invalid JSON
+--- 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",
+ "upstream": {
+ "type": "roundrobin",
+ "nodes": {
+ "127.0.0.1:1980": 1
+ }
+ },
+ "plugins": {
+ "serverless-pre-function": {
+ "functions": [
+ "return function(conf, ctx)
ctx.external_user = { orgs = { api7 = { team = \"[cloud\" } } }; end"
+ ],
+ "phase": "access"
+ },
+ "acl": {
+ "allow_labels": {
+ "org": ["api7", "apache"],
+ "team": ["cloud", "infra"]
+ },
+ "external_user_label_field": "$..team",
+ "external_user_label_field_key": "team",
+ "external_user_label_field_parser": "json",
+ "rejected_code": 403
+ }
+ }
+ }]]
+ )
+
+ if code >= 300 then
+ ngx.status = code
+ end
+ ngx.say(body)
+ }
+ }
+--- request
+GET /t
+--- response_body
+passed
+
+
+
+=== TEST 44: test the parser is "json" and the value extracted has prefix "["
but it is a invalid JSON
+# The value extracted is "cloud".
+# The ACL try to parse it as a serialized JSON string.
+# It will fail and forbidden all.
+--- request
+GET /hello
+--- error_code: 403
+--- error_log
+extra_values_with_parser(): failed to decode labels [[cloud] as array, err:
Expected value but found invalid token at character 2
+
+
+
+=== TEST 45: set no parser, value has no prefix "[" and no separator ",",
external_user_label_field as labels key
+--- 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",
+ "upstream": {
+ "type": "roundrobin",
+ "nodes": {
+ "127.0.0.1:1980": 1
+ }
+ },
+ "plugins": {
+ "serverless-pre-function": {
+ "functions": [
+ "return function(conf, ctx)
ctx.external_user = { team = \"cloud\" }; end"
+ ],
+ "phase": "access"
+ },
+ "acl": {
+ "allow_labels": {
+ "org": ["api7", "apache"],
+ "team": ["cloud", "infra"]
+ },
+ "external_user_label_field": "team",
+ "rejected_code": 403
+ }
+ }
+ }]]
+ )
+
+ if code >= 300 then
+ ngx.status = code
+ end
+ ngx.say(body)
+ }
+ }
+--- request
+GET /t
+--- response_body
+passed
+
+
+
+=== TEST 46: test no parser, value has no prefix "[" and no separator ",",
external_user_label_field as labels key
+# The value extracted is "cloud".
+# There is no parser and the value type is "string", so ACL treat it as a Lua
table {"cloud"}.
+# It can match the ACL allow_labels, so response 200 OK.
+--- request
+GET /hello
+--- response_body
+hello world
+--- log_level: info
+--- error_log
+extra_values_without_parser(): the string value can not parsed by json or
segmented_text
+
+
+
+=== TEST 47: TEST SCHEMA: invalid external_user_label_field_parser
+--- 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",
+ "upstream": {
+ "type": "roundrobin",
+ "nodes": {
+ "127.0.0.1:1980": 1
+ }
+ },
+ "plugins": {
+ "acl": {
+ "allow_labels": {
+ "org": ["api7", "apache"],
+ "team": ["cloud", "infra"]
+ },
+ "external_user_label_field": "team",
+ "external_user_label_field_parser":
"an-invalid-parser",
+ "rejected_code": 403
+ }
+ }
+ }]]
+ )
+
+ if code >= 300 then
+ ngx.status = code
+ end
+ ngx.print(body)
+ }
+ }
+--- request
+GET /t
+--- error_code: 400
+--- response_body
+{"error_msg":"failed to check the configuration of plugin acl err: property
\"external_user_label_field_parser\" validation failed: matches none of the
enum values"}
+
+
+
+=== TEST 48: TEST SCHEMA: external_user_label_field_parser="segmented_text"
but external_user_label_field_separator is missing
+--- 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",
+ "upstream": {
+ "type": "roundrobin",
+ "nodes": {
+ "127.0.0.1:1980": 1
+ }
+ },
+ "plugins": {
+ "acl": {
+ "allow_labels": {
+ "org": ["api7", "apache"],
+ "team": ["cloud", "infra"]
+ },
+ "external_user_label_field": "team",
+ "external_user_label_field_parser":
"segmented_text",
+ "rejected_code": 403
+ }
+ }
+ }]]
+ )
+
+ if code >= 300 then
+ ngx.status = code
+ end
+ ngx.print(body)
+ }
+ }
+--- request
+GET /t
+--- error_code: 400
+--- response_body
+{"error_msg":"failed to check the configuration of plugin acl err: allOf 1
failed: then clause did not match"}
+
+
+
+=== TEST 49: TEST SCHEMA: invalid external_user_label_field_key (specified but
empty)
+--- 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",
+ "upstream": {
+ "type": "roundrobin",
+ "nodes": {
+ "127.0.0.1:1980": 1
+ }
+ },
+ "plugins": {
+ "acl": {
+ "allow_labels": {
+ "org": ["api7", "apache"],
+ "team": ["cloud", "infra"]
+ },
+ "external_user_label_field": "team",
+ "external_user_label_field_parser":
"segmented_text",
+ "external_user_label_field_key": "",
+ "rejected_code": 403
+ }
+ }
+ }]]
+ )
+
+ if code >= 300 then
+ ngx.status = code
+ end
+ ngx.print(body)
+ }
+ }
+--- request
+GET /t
+--- error_code: 400
+--- response_body
+{"error_msg":"failed to check the configuration of plugin acl err: property
\"external_user_label_field_key\" validation failed: string too short, expected
at least 1, got 0"}
+
+
+
+=== TEST 50: TEST SCHEMA: invalid external_user_label_field_key (specified but
not string)
+--- 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",
+ "upstream": {
+ "type": "roundrobin",
+ "nodes": {
+ "127.0.0.1:1980": 1
+ }
+ },
+ "plugins": {
+ "acl": {
+ "allow_labels": {
+ "org": ["api7", "apache"],
+ "team": ["cloud", "infra"]
+ },
+ "external_user_label_field": "team",
+ "external_user_label_field_parser":
"segmented_text",
+ "external_user_label_field_separator": {},
+ "rejected_code": 403
+ }
+ }
+ }]]
+ )
+
+ if code >= 300 then
+ ngx.status = code
+ end
+ ngx.print(body)
+ }
+ }
+--- request
+GET /t
+--- error_code: 400
+--- response_body
+{"error_msg":"failed to check the configuration of plugin acl err: property
\"external_user_label_field_separator\" validation failed: wrong type: expected
string, got table"}
+
+
+
+=== TEST 51: TEST SCHEMA: invalid external_user_label_field_separator
(specified but empty)
+--- 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",
+ "upstream": {
+ "type": "roundrobin",
+ "nodes": {
+ "127.0.0.1:1980": 1
+ }
+ },
+ "plugins": {
+ "acl": {
+ "allow_labels": {
+ "org": ["api7", "apache"],
+ "team": ["cloud", "infra"]
+ },
+ "external_user_label_field": "team",
+ "external_user_label_field_parser":
"segmented_text",
+ "external_user_label_field_separator": "",
+ "rejected_code": 403
+ }
+ }
+ }]]
+ )
+
+ if code >= 300 then
+ ngx.status = code
+ end
+ ngx.print(body)
+ }
+ }
+--- request
+GET /t
+--- error_code: 400
+--- response_body
+{"error_msg":"failed to check the configuration of plugin acl err: property
\"external_user_label_field_separator\" validation failed: string too short,
expected at least 1, got 0"}
+
+
+
+=== TEST 52: TEST SCHEMA: invalid external_user_label_field_separator
(specified but not string)
+--- 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",
+ "upstream": {
+ "type": "roundrobin",
+ "nodes": {
+ "127.0.0.1:1980": 1
+ }
+ },
+ "plugins": {
+ "acl": {
+ "allow_labels": {
+ "org": ["api7", "apache"],
+ "team": ["cloud", "infra"]
+ },
+ "external_user_label_field": "team",
+ "external_user_label_field_parser":
"segmented_text",
+ "external_user_label_field_separator": {},
+ "rejected_code": 403
+ }
+ }
+ }]]
+ )
+
+ if code >= 300 then
+ ngx.status = code
+ end
+ ngx.print(body)
+ }
+ }
+--- request
+GET /t
+--- error_code: 400
+--- response_body
+{"error_msg":"failed to check the configuration of plugin acl err: property
\"external_user_label_field_separator\" validation failed: wrong type: expected
string, got table"}
+
+
+
+=== TEST 53: TEST SCHEMA: invalid external_user_label_field (invalid JSONPath
syntax)
+--- 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",
+ "upstream": {
+ "type": "roundrobin",
+ "nodes": {
+ "127.0.0.1:1980": 1
+ }
+ },
+ "plugins": {
+ "acl": {
+ "allow_labels": {
+ "team": ["cloud"]
+ },
+ "external_user_label_field": "$..([invalid",
+ "rejected_code": 403
+ }
+ }
+ }]]
+ )
+
+ if code >= 300 then
+ ngx.status = code
+ end
+ ngx.print(body)
+ }
+ }
+--- request
+GET /t
+--- error_code: 400
+--- response_body_like
+failed to check the configuration of plugin acl err: invalid
external_user_label_field:.*
+
+
+
+=== TEST 54: delete 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_DELETE )
+
+ ngx.status = code
+ ngx.say(body)
+ }
+ }
+--- request
+GET /t
+--- response_body
+passed
+
+
+
+=== TEST 55: delete jack
+--- 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 56: delete rose
+--- config
+ location /t {
+ content_by_lua_block {
+ local t = require("lib.test_admin").test
+ local code, body = t( '/apisix/admin/consumers/rose',
ngx.HTTP_DELETE )
+
+ ngx.status = code
+ ngx.say(body)
+ }
+ }
+--- request
+GET /t
+--- response_body
+passed
diff --git a/t/plugin/acl2.t b/t/plugin/acl2.t
new file mode 100644
index 000000000..9d8c73773
--- /dev/null
+++ b/t/plugin/acl2.t
@@ -0,0 +1,115 @@
+# 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 jack with comma-delimited labels
+--- 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": {
+ "basic-auth": {
+ "username": "jack",
+ "password": "123456"
+ }
+ },
+ "labels": {
+ "org": "apache",
+ "project": "gateway,apisix,web-server"
+ }
+ }]]
+ )
+ if code >= 300 then
+ ngx.status = code
+ end
+ ngx.say(body)
+ }
+ }
+--- request
+GET /t
+--- response_body
+passed
+
+
+
+=== TEST 2: set allow_labels
+--- 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",
+ "upstream": {
+ "type": "roundrobin",
+ "nodes": {
+ "127.0.0.1:1980": 1
+ }
+ },
+ "plugins": {
+ "basic-auth": {},
+ "acl": {
+ "allow_labels": {
+ "project": ["apisix"]
+ }
+ }
+ }
+ }]]
+ )
+
+ if code >= 300 then
+ ngx.status = code
+ end
+ ngx.say(body)
+ }
+ }
+--- request
+GET /t
+--- response_body
+passed
+
+
+
+=== TEST 3: verify unauthorized
+--- request
+GET /hello
+--- error_code: 401
+--- response_body
+{"message":"Missing authorization in request"}
+
+
+
+=== TEST 4: verify jack
+--- request
+GET /hello
+--- more_headers
+Authorization: Basic amFjazoxMjM0NTY=
+--- response_body
+hello world