From 1c820c3e7325a053ab519ab429e44baf4417be4b Mon Sep 17 00:00:00 2001 From: Abhishek Choudhary Date: Mon, 22 Jun 2026 16:39:36 +0800 Subject: [PATCH 1/2] fix(attach-consumer-label): always drop client-supplied configured headers --- apisix/plugins/attach-consumer-label.lua | 5 ++++ .../latest/plugins/attach-consumer-label.md | 2 +- .../latest/plugins/attach-consumer-label.md | 2 +- t/plugin/attach-consumer-label.t | 26 +++++++++++++++++++ 4 files changed, 33 insertions(+), 2 deletions(-) diff --git a/apisix/plugins/attach-consumer-label.lua b/apisix/plugins/attach-consumer-label.lua index 6d3396a02810..8f8dae6033b2 100644 --- a/apisix/plugins/attach-consumer-label.lua +++ b/apisix/plugins/attach-consumer-label.lua @@ -46,6 +46,11 @@ function _M.check_schema(conf) end function _M.before_proxy(conf, ctx) + -- always drop client-supplied copies of the configured headers + for header in pairs(conf.headers) do + core.request.set_header(ctx, header, nil) + end + -- check if the consumer is exists in the context if not ctx.consumer then return diff --git a/docs/en/latest/plugins/attach-consumer-label.md b/docs/en/latest/plugins/attach-consumer-label.md index 2f5557d8edc0..9bfecda01ec4 100644 --- a/docs/en/latest/plugins/attach-consumer-label.md +++ b/docs/en/latest/plugins/attach-consumer-label.md @@ -40,7 +40,7 @@ The `attach-consumer-label` Plugin attaches custom consumer-related labels, in a | Name | Type | Required | Default | Valid values | Description | |----------|--------|----------|---------|--------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| headers | object | True | | | Key-value pairs of Consumer labels to be attached to request headers, where key is the request header name, such as `X-Consumer-Role`, and the value is a reference to the custom label key, such as `$role`. Note that the value should always start with a dollar sign (`$`). If a referenced Consumer label value is not configured on the Consumer, the corresponding header will not be attached to the request. | +| headers | object | True | | | Key-value pairs of Consumer labels to be attached to request headers, where key is the request header name, such as `X-Consumer-Role`, and the value is a reference to the custom label key, such as `$role`. Note that the value should always start with a dollar sign (`$`). Any client-supplied value of a configured header is always removed first, so the Upstream only ever sees the gateway-set value. If a referenced Consumer label value is not configured on the Consumer, the corresponding header will not be attached to the request. | ## Examples diff --git a/docs/zh/latest/plugins/attach-consumer-label.md b/docs/zh/latest/plugins/attach-consumer-label.md index 5981cc737e65..10fc6d0c1681 100644 --- a/docs/zh/latest/plugins/attach-consumer-label.md +++ b/docs/zh/latest/plugins/attach-consumer-label.md @@ -40,7 +40,7 @@ description: attach-consumer-label 插件将自定义消费者标签附加到经 | 名称 | 类型 | 必选项 | 默认值 | 有效值 | 描述 | |----------|--------|--------|--------|--------|----------------------------------------------------------------------------------------------------------------------------------------------------| -| headers | object | 是 | | | 要附加到请求标头的消费者标签键值对,其中键是请求标头名称(例如 `X-Consumer-Role`),值是对消费者标签键的引用(例如 `$role`)。注意,值必须以美元符号(`$`)开头。如果消费者上未配置被引用的标签,则对应的请求标头将不会被附加。 | +| headers | object | 是 | | | 要附加到请求标头的消费者标签键值对,其中键是请求标头名称(例如 `X-Consumer-Role`),值是对消费者标签键的引用(例如 `$role`)。注意,值必须以美元符号(`$`)开头。客户端传入的同名请求标头会被先行移除,因此上游只会看到网关设置的值。如果消费者上未配置被引用的标签,则对应的请求标头将不会被附加。 | ## 示例 diff --git a/t/plugin/attach-consumer-label.t b/t/plugin/attach-consumer-label.t index 615b1cf09517..c6074f1e078d 100644 --- a/t/plugin/attach-consumer-label.t +++ b/t/plugin/attach-consumer-label.t @@ -463,3 +463,29 @@ X-Global-Consumer-Company: api7 X-Global-Consumer-Department: devops --- no_error_log [error] + + + +=== TEST 16: client-supplied configured header is dropped when the consumer has no labels +--- config + location /t { + content_by_lua_block { + local core = require("apisix.core") + local plugin = require("apisix.plugins.attach-consumer-label") + local ctx = ngx.ctx + ctx.var = {} + -- authenticated consumer without any labels + ctx.consumer = { username = "bob" } + local conf = { headers = { ["X-Consumer-Role"] = "$role" } } + plugin.before_proxy(conf, ctx) + ngx.say("X-Consumer-Role: ", tostring(core.request.header(ctx, "X-Consumer-Role"))) + } + } +--- request +GET /t +--- more_headers +X-Consumer-Role: admin +--- response_body +X-Consumer-Role: nil +--- no_error_log +[error] From f93ad81018f9b1ec2fd4c14ffe73704a7e84f7b8 Mon Sep 17 00:00:00 2001 From: Abhishek Choudhary Date: Tue, 23 Jun 2026 11:56:00 +0800 Subject: [PATCH 2/2] refactor(attach-consumer-label): set headers in a single pass and add e2e test Clear the configured headers only on the no-label early-return path and keep a single set loop when labels exist (the set already overwrites any client-supplied value). Replace the direct before_proxy unit test with an end-to-end request that asserts the client-supplied header is absent from what the upstream actually receives. --- apisix/plugins/attach-consumer-label.lua | 18 ++--- t/plugin/attach-consumer-label.t | 88 ++++++++++++++++++++---- 2 files changed, 83 insertions(+), 23 deletions(-) diff --git a/apisix/plugins/attach-consumer-label.lua b/apisix/plugins/attach-consumer-label.lua index 8f8dae6033b2..d76cbf99b4a5 100644 --- a/apisix/plugins/attach-consumer-label.lua +++ b/apisix/plugins/attach-consumer-label.lua @@ -46,25 +46,21 @@ function _M.check_schema(conf) end function _M.before_proxy(conf, ctx) - -- always drop client-supplied copies of the configured headers - for header in pairs(conf.headers) do - core.request.set_header(ctx, header, nil) - end + local labels = ctx.consumer and ctx.consumer.labels - -- check if the consumer is exists in the context - if not ctx.consumer then + -- no labels to map: just drop client-supplied copies of the configured headers + if not labels then + for header in pairs(conf.headers) do + core.request.set_header(ctx, header, nil) + end return end - local labels = ctx.consumer.labels core.log.info("consumer username: ", ctx.consumer.username, " labels: ", core.json.delay_encode(labels)) - if not labels then - return - end for header, label_key in pairs(conf.headers) do - -- remove leading $ character + -- remove leading $ character, set value (nil clears any client-supplied copy) local label_value = labels[label_key:sub(2)] core.request.set_header(ctx, header, label_value) end diff --git a/t/plugin/attach-consumer-label.t b/t/plugin/attach-consumer-label.t index c6074f1e078d..db0ef634a205 100644 --- a/t/plugin/attach-consumer-label.t +++ b/t/plugin/attach-consumer-label.t @@ -466,26 +466,90 @@ X-Global-Consumer-Department: devops -=== TEST 16: client-supplied configured header is dropped when the consumer has no labels +=== TEST 16: add a no-label consumer and a route that echoes upstream-received headers --- config location /t { content_by_lua_block { - local core = require("apisix.core") - local plugin = require("apisix.plugins.attach-consumer-label") - local ctx = ngx.ctx - ctx.var = {} - -- authenticated consumer without any labels - ctx.consumer = { username = "bob" } - local conf = { headers = { ["X-Consumer-Role"] = "$role" } } - plugin.before_proxy(conf, ctx) - ngx.say("X-Consumer-Role: ", tostring(core.request.header(ctx, "X-Consumer-Role"))) + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/consumers', + ngx.HTTP_PUT, + [[{ + "username": "bob" + }]] + ) + + if code >= 300 then + ngx.status = code + ngx.say(body) + return + end + + local code, body = t('/apisix/admin/consumers/bob/credentials/a', + ngx.HTTP_PUT, + [[{ + "plugins": { + "key-auth": { + "key": "key-bob" + } + } + }]] + ) + + if code >= 300 then + ngx.status = code + ngx.say(body) + return + end + + local code, body = t('/apisix/admin/routes/2', + ngx.HTTP_PUT, + [[{ + "uri": "/print_request_received", + "plugins": { + "key-auth": {}, + "attach-consumer-label": { + "headers": { + "X-Consumer-Role": "$role" + } + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + } + }]] + ) + + if code >= 300 then + ngx.status = code + ngx.say(body) + return + end + + ngx.say(body) } } --- request GET /t +--- response_body +passed +--- no_error_log +[error] + + + +=== TEST 17: client-supplied configured header is dropped at the upstream when the consumer has no labels +# the upstream body must contain the authenticated consumer marker +# (x-consumer-username: bob) AND must not contain the client-supplied +# x-consumer-role, proving it was stripped before proxying upstream +--- request +GET /print_request_received --- more_headers +apikey: key-bob X-Consumer-Role: admin ---- response_body -X-Consumer-Role: nil +--- response_body_like eval +qr/(?s)^(?!.*x-consumer-role).*x-consumer-username: bob/ --- no_error_log [error]