diff --git a/apisix/cli/config.lua b/apisix/cli/config.lua index 956eef30c267..a08ebd96a283 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 000000000000..ba0c751e95df --- /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 ae7155a86b06..420285633324 100644 --- a/conf/config.yaml.example +++ b/conf/config.yaml.example @@ -503,6 +503,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 d24eacc3f8e9..921e30cc9782 100644 --- a/docs/en/latest/config.json +++ b/docs/en/latest/config.json @@ -147,6 +147,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 000000000000..edd89df2e961 --- /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. +--- + + + +
+ + + +## 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 1ab36fa2d43a..a337be349df4 100644 --- a/docs/zh/latest/config.json +++ b/docs/zh/latest/config.json @@ -135,6 +135,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 000000000000..6f5eb2540d63 --- /dev/null +++ b/docs/zh/latest/plugins/acl.md @@ -0,0 +1,241 @@ +--- +title: acl +keywords: + - Apache APISIX + - API Gateway + - 插件 + - acl +description: acl 插件基于标签实现访问控制,通过检查消费者标签或外部用户属性来允许或拒绝请求。 +--- + + + + + + + +## 描述 + +`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 adb98b28bc17..419766b8a84e 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 000000000000..09de05d2504f --- /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 000000000000..9d8c73773ec3 --- /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