This is an automated email from the ASF dual-hosted git repository.
shreemaan-abhishek 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 72c4eaf58 fix(authz-keycloak): copy permissions before appending
http_method_as_scope (#13410)
72c4eaf58 is described below
commit 72c4eaf583cc587650f7cd63c516788034d24bdd
Author: Shreemaan Abhishek <[email protected]>
AuthorDate: Tue May 26 09:25:47 2026 +0800
fix(authz-keycloak): copy permissions before appending http_method_as_scope
(#13410)
---
apisix/plugins/authz-keycloak.lua | 3 +
t/plugin/authz-keycloak5.t | 138 ++++++++++++++++++++++++++++++++++++++
2 files changed, 141 insertions(+)
diff --git a/apisix/plugins/authz-keycloak.lua
b/apisix/plugins/authz-keycloak.lua
index 7c8eae5c1..b75f334f7 100644
--- a/apisix/plugins/authz-keycloak.lua
+++ b/apisix/plugins/authz-keycloak.lua
@@ -597,6 +597,9 @@ local function evaluate_permissions(conf, ctx, token)
end
if scope then
+ -- Copy the permissions before appending the method scope, so the
+ -- derived scope is not written back into the reused plugin config.
+ permission = core.table.clone(permission)
-- Loop over permissions and add scope.
for k, v in pairs(permission) do
if v:find("#", 1, true) then
diff --git a/t/plugin/authz-keycloak5.t b/t/plugin/authz-keycloak5.t
new file mode 100644
index 000000000..c3907053d
--- /dev/null
+++ b/t/plugin/authz-keycloak5.t
@@ -0,0 +1,138 @@
+#
+# 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';
+
+log_level('warn');
+repeat_each(1);
+no_long_string();
+no_root_location();
+run_tests;
+
+__DATA__
+
+=== TEST 1: http_method_as_scope must not accumulate the request method across
requests
+--- http_config
+ lua_shared_dict mock_perms 1m;
+ server {
+ listen 10931;
+ server_name localhost;
+
+ # mock token endpoint: records the permission(s) it receives, in order.
+ location = /token {
+ content_by_lua_block {
+ ngx.req.read_body()
+ local args = ngx.decode_args(ngx.req.get_body_data() or "")
+ local perm = args.permission
+ if type(perm) == "table" then
+ perm = table.concat(perm, ",")
+ end
+ perm = perm or ""
+
+ local d = ngx.shared.mock_perms
+ local n = d:incr("count", 1, 0)
+ d:set("perm_" .. n, perm)
+
+ ngx.status = 200
+ ngx.say('{"result":true}')
+ }
+ }
+
+ # returns the ordered list of permission strings the gateway sent.
+ location = /dump {
+ content_by_lua_block {
+ local d = ngx.shared.mock_perms
+ local n = tonumber(d:get("count")) or 0
+ local out = {}
+ for i = 1, n do
+ out[i] = d:get("perm_" .. i)
+ end
+ ngx.print(table.concat(out, "\n"))
+ }
+ }
+
+ # plain upstream for the protected route.
+ location / {
+ content_by_lua_block {
+ ngx.print("UPSTREAM-REACHED")
+ }
+ }
+ }
+--- config
+ location /t {
+ content_by_lua_block {
+ local t = require("lib.test_admin").test
+ local http = require("resty.http")
+
+ local code = t('/apisix/admin/routes/1', ngx.HTTP_PUT, [[{
+ "uri": "/course",
+ "plugins": {
+ "authz-keycloak": {
+ "token_endpoint": "http://127.0.0.1:10931/token",
+ "client_id": "course_management",
+ "permissions": ["course_resource"],
+ "lazy_load_paths": false,
+ "http_method_as_scope": true,
+ "grant_type":
"urn:ietf:params:oauth:grant-type:uma-ticket",
+ "timeout": 3000
+ }
+ },
+ "upstream": {
+ "type": "roundrobin",
+ "nodes": { "127.0.0.1:10931": 1 }
+ }
+ }]])
+ if code >= 300 then ngx.say("setup route failed: ", code); return
end
+ ngx.sleep(0.5)
+
+ local function call()
+ local httpc = http.new()
+ local res, err =
httpc:request_uri("http://127.0.0.1:1984/course", {
+ method = "GET",
+ headers = { ["Authorization"] = "Bearer dummy-user-token"
},
+ })
+ if not res then
+ ngx.say("request error: ", tostring(err))
+ return
+ end
+ if res.status ~= 200 then
+ ngx.say("unexpected status: ", res.status)
+ return
+ end
+ if res.body ~= "UPSTREAM-REACHED" then
+ ngx.say("unexpected body: ", tostring(res.body))
+ return
+ end
+ end
+
+ -- two identical GET requests share the cached route conf;
+ -- the configured permissions list must not accumulate the
+ -- request method across requests.
+ call()
+ call()
+
+ local httpc = http.new()
+ local dump = httpc:request_uri("http://127.0.0.1:10931/dump")
+ ngx.say(dump and dump.body or "no dump")
+ }
+ }
+--- request
+GET /t
+--- response_body
+course_resource#GET
+course_resource#GET
+--- no_error_log
+[alert]