Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 38 additions & 3 deletions apisix/plugins/acl.lua
Original file line number Diff line number Diff line change
Expand Up @@ -225,10 +225,45 @@ function _M.access(conf, ctx)
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)
-- jp.query() returns nil (not {}) when ctx.external_user is not a table,
-- so guard before taking its length.
local label_values = jp.query(ctx.external_user, conf.external_user_label_field) or {}
local label_value
if #label_values <= 1 then
-- 0 or 1 match: preserve the original jp.value() semantics and let the
-- configured parser run on the single value (a single matched value may
-- itself be a table, which the json/segmented_text parser is expected to
-- reject). label_values[1] is nil when there are no matches.
label_value = label_values[1]
parser = conf.external_user_label_field_parser
sep = conf.external_user_label_field_separator
else
-- Multi-match: apply the configured parser to EACH matched value and
-- merge the results, so allow/deny matching still sees the fully parsed
-- sub-values (e.g. a match like "infra|qa" with segmented_text). Passing
-- the raw list straight to the string parsers would otherwise crash
-- (re_split over a table) or silently skip parsing and miss denied
-- labels. The merged list is already parsed, so parser/sep stay unset.
local cfg_parser = conf.external_user_label_field_parser
local cfg_sep = conf.external_user_label_field_separator
local merged = {}
for _, v in ipairs(label_values) do
local parsed
if cfg_parser and type(v) == "string" then
parsed = extra_values_with_parser(v, cfg_parser, cfg_sep)
else
-- A matched value that is itself a table is already a list of
-- labels; route it through the type-aware path instead of the
-- string parser (segmented_text's re_split cannot handle a table).
parsed = extra_values_without_parser(v)
end
for _, pv in ipairs(parsed) do
merged[#merged + 1] = pv
end
end
label_value = merged
end
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
Expand Down
80 changes: 72 additions & 8 deletions t/plugin/acl.t
Original file line number Diff line number Diff line change
Expand Up @@ -749,13 +749,15 @@ 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.
=== TEST 32: ACL parses every multi-match value, so a packed allowed label is honored
# $.orgs..team matches "cloud|infra" (a string) and {"devops","qa"} (a table).
# Each match is parsed individually: "cloud|infra" -> ["cloud","infra"], the table
# -> ["devops","qa"]. The merged set contains "cloud"/"infra", which match
# allow_labels.team, so the request is allowed.
--- request
GET /hello
--- error_code: 403
--- response_body
hello world



Expand Down Expand Up @@ -1486,7 +1488,69 @@ failed to check the configuration of plugin acl err: invalid external_user_label



=== TEST 54: delete route
=== TEST 54: multi-match JSONPath + parser; denied label hidden inside a parsed value
--- 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 = { teams = { { name = \"cloud\" }, { name = \"infra,qa\" } } }; end"
],
"phase": "access"
},
"acl": {
"deny_labels": {
"name": ["infra"]
},
"external_user_label_field": "$..name",
"external_user_label_field_key": "name",
"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 55: each match is parsed individually before allow/deny matching
# $..name matches ["cloud", "infra,qa"]. The configured segmented_text parser
# must be applied to EACH match, so "infra,qa" expands to ["infra", "qa"] and the
# denied label "infra" is caught -> 403. Skipping the parser on multi-match (or
# only inspecting the first match) would let this request through.
--- request
GET /hello
--- error_code: 403
--- response_body
{"message":"The consumer is forbidden."}



=== TEST 56: delete route
--- config
location /t {
content_by_lua_block {
Expand All @@ -1504,7 +1568,7 @@ passed



=== TEST 55: delete jack
=== TEST 57: delete jack
--- config
location /t {
content_by_lua_block {
Expand All @@ -1522,7 +1586,7 @@ passed



=== TEST 56: delete rose
=== TEST 58: delete rose
--- config
location /t {
content_by_lua_block {
Expand Down
Loading