-
Notifications
You must be signed in to change notification settings - Fork 4.8k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(tools): add request aware table to help detect concurrent reques…
…ts accessing the same shared table (#11017) KAG-1570 --------- Co-authored-by: Datong Sun <[email protected]> Co-authored-by: Datong Sun <[email protected]>
- Loading branch information
1 parent
c54eddd
commit 39a545d
Showing
8 changed files
with
327 additions
and
19 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
message: Add a request-aware table able to detect accesses from different requests. | ||
type: feature | ||
scope: Core |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,123 @@ | ||
--- NOTE: tool is designed to assist with **detecting** request contamination | ||
-- issues on CI, during test runs. It does not offer security safeguards. | ||
|
||
local table_new = require "table.new" | ||
local table_clear = require "table.clear" | ||
|
||
local debug_mode = (kong.configuration.log_level == "debug") | ||
local error = error | ||
local rawset = rawset | ||
local setmetatable = setmetatable | ||
local get_phase = ngx.get_phase | ||
local var = ngx.var | ||
|
||
|
||
local NGX_VAR_PHASES = { | ||
set = true, | ||
rewrite = true, | ||
access = true, | ||
content = true, | ||
header_filter = true, | ||
body_filter = true, | ||
log = true, | ||
balancer = true, | ||
} | ||
local ALLOWED_REQUEST_ID_K = "__allowed_request_id" | ||
|
||
|
||
-- Check if access is allowed for table, based on the request ID | ||
local function enforce_sequential_access(table) | ||
if not NGX_VAR_PHASES[get_phase()] then | ||
-- allow access and reset allowed request ID | ||
rawset(table, ALLOWED_REQUEST_ID_K, nil) | ||
return | ||
end | ||
|
||
local curr_request_id = var.request_id | ||
local allowed_request_id = rawget(table, ALLOWED_REQUEST_ID_K) | ||
if not allowed_request_id then | ||
-- first access. Set allowed request ID and allow access | ||
rawset(table, ALLOWED_REQUEST_ID_K, curr_request_id) | ||
return | ||
end | ||
|
||
if curr_request_id ~= table[ALLOWED_REQUEST_ID_K] then | ||
error("concurrent access from different request to shared table detected", 2) | ||
end | ||
end | ||
|
||
|
||
local function clear_table(self) | ||
if not debug_mode then | ||
table_clear(self) | ||
return | ||
end | ||
|
||
table_clear(self.__data) | ||
rawset(self, ALLOWED_REQUEST_ID_K, nil) | ||
end | ||
|
||
|
||
local __proxy_mt = { | ||
__index = function(t, k) | ||
if k == "clear" then | ||
return clear_table | ||
end | ||
|
||
enforce_sequential_access(t) | ||
return t.__data[k] | ||
end, | ||
|
||
__newindex = function(t, k, v) | ||
if k == "clear" then | ||
error("cannot set the 'clear' method of request aware table", 2) | ||
end | ||
|
||
enforce_sequential_access(t) | ||
t.__data[k] = v | ||
end, | ||
} | ||
|
||
|
||
local __direct_mt = { | ||
__index = { clear = clear_table }, | ||
|
||
__newindex = function(t, k, v) | ||
if k == "clear" then | ||
error("cannot set the 'clear' method of request aware table", 2) | ||
end | ||
|
||
rawset(t, k, v) | ||
end, | ||
} | ||
|
||
|
||
-- Request aware table constructor | ||
-- | ||
-- Creates a new table with request-aware access logic to protect the | ||
-- underlying data from race conditions. | ||
-- The table includes a :clear() method to delete all elements. | ||
-- | ||
-- The request-aware access logic is turned off when `debug_mode` is disabled. | ||
-- | ||
-- @param narr (optional) pre allocated array elements | ||
-- @param nrec (optional) pre allocated hash elements | ||
-- @return The newly created table with request-aware access | ||
local function new(narr, nrec) | ||
if not narr then narr = 0 end | ||
if not nrec then nrec = 0 end | ||
|
||
local data = table_new(narr, nrec) | ||
|
||
-- return table without proxy when debug_mode is disabled | ||
if not debug_mode then | ||
return setmetatable(data, __direct_mt) | ||
end | ||
|
||
-- wrap table in proxy (for access checks) when debug_mode is enabled | ||
return setmetatable({ __data = data }, __proxy_mt) | ||
end | ||
|
||
return { | ||
new = new, | ||
} |
125 changes: 125 additions & 0 deletions
125
spec/02-integration/05-proxy/33-request-aware-table_spec.lua
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,125 @@ | ||
local helpers = require "spec.helpers" | ||
|
||
local LOG_LEVELS = { | ||
"debug", | ||
"info", | ||
-- any other log level behaves the same as "info" | ||
} | ||
|
||
|
||
local function trigger_plugin_new_table() | ||
local client = helpers.proxy_client() | ||
local res = client:get("/", { | ||
query = { | ||
new_tab = true, | ||
} | ||
}) | ||
assert.response(res).has.status(200) | ||
assert.logfile().has.no.line("[error]", true) | ||
client:close() | ||
end | ||
|
||
local function trigger_plugin_clear_table() | ||
local client = helpers.proxy_client() | ||
local res = client:get("/", { | ||
query = { | ||
clear = true, | ||
} | ||
}) | ||
assert.response(res).has.status(200) | ||
assert.logfile().has.no.line("[error]", true) | ||
client:close() | ||
end | ||
|
||
for _, log_level in ipairs(LOG_LEVELS) do | ||
local concurrency_checks = log_level == "debug" | ||
|
||
for _, strategy in helpers.each_strategy() do | ||
describe("request aware table tests [#" .. strategy .. "] concurrency checks: " .. tostring(concurrency_checks), function() | ||
local client | ||
lazy_setup(function() | ||
local bp = helpers.get_db_utils(strategy, { | ||
"plugins", | ||
"routes", | ||
"services", | ||
}, { | ||
"request-aware-table" | ||
}) | ||
|
||
local service = assert(bp.services:insert({ | ||
url = helpers.mock_upstream_url | ||
})) | ||
|
||
local route = bp.routes:insert({ | ||
service = service, | ||
paths = { "/" } | ||
}) | ||
|
||
bp.plugins:insert({ | ||
name = "request-aware-table", | ||
route = { id = route.id }, | ||
}) | ||
|
||
helpers.start_kong({ | ||
database = strategy, | ||
plugins = "bundled, request-aware-table", | ||
nginx_conf = "spec/fixtures/custom_nginx.template", | ||
log_level = log_level, | ||
}) | ||
end) | ||
|
||
lazy_teardown(function() | ||
helpers.stop_kong() | ||
end) | ||
|
||
before_each(function() | ||
helpers.clean_logfile() | ||
trigger_plugin_new_table() | ||
client = helpers.proxy_client() | ||
end) | ||
|
||
after_each(function() | ||
if client then | ||
client:close() | ||
end | ||
end) | ||
|
||
it("allows access when there are no race conditions", function() | ||
local res = client:get("/") | ||
assert.response(res).has.status(200) | ||
assert.logfile().has.no.line("[error]", true) | ||
end) | ||
|
||
it("denies access when there are race conditions and checks are enabled (else allows)", function() | ||
-- access from request 1 (don't clear) | ||
local r = client:get("/") | ||
assert.response(r).has.status(200) | ||
|
||
-- access from request 2 | ||
r = client:get("/") | ||
if concurrency_checks then | ||
assert.logfile().has.line("concurrent access from different request to shared table detected", true) | ||
else | ||
assert.response(r).has.status(200) | ||
end | ||
end) | ||
|
||
it("allows access when table is cleared between requests", function() | ||
-- access from request 1 (clear) | ||
local r = client:get("/") | ||
assert.response(r).has.status(200) | ||
trigger_plugin_clear_table() | ||
|
||
-- access from request 2 (clear) | ||
r = client:get("/") | ||
assert.response(r).has.status(200) | ||
trigger_plugin_clear_table() | ||
|
||
-- access from request 3 | ||
r = client:get("/") | ||
assert.response(r).has.status(200) | ||
assert.logfile().has.no.line("[error]", true) | ||
end) | ||
end) | ||
end | ||
end |
41 changes: 41 additions & 0 deletions
41
spec/fixtures/custom_plugins/kong/plugins/request-aware-table/handler.lua
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
local RAT = require "kong.tools.request_aware_table" | ||
|
||
local kong = kong | ||
local tab | ||
|
||
local _M = { | ||
PRIORITY = 1001, | ||
VERSION = "1.0", | ||
} | ||
|
||
local function access_table() | ||
-- write access | ||
tab.foo = "bar" | ||
tab.bar = "baz" | ||
-- read/write access | ||
tab.baz = tab.foo .. tab.bar | ||
end | ||
|
||
|
||
function _M:access(conf) | ||
local query = kong.request.get_query() | ||
if query.new_tab == "true" then | ||
-- new table | ||
tab = RAT.new() | ||
ngx.exit(200) | ||
end | ||
|
||
if query.clear == "true" then | ||
-- clear table | ||
tab:clear() | ||
ngx.exit(200) | ||
end | ||
|
||
-- access multiple times during same request | ||
for _ = 1, 3 do | ||
access_table() | ||
end | ||
end | ||
|
||
|
||
return _M |
11 changes: 11 additions & 0 deletions
11
spec/fixtures/custom_plugins/kong/plugins/request-aware-table/schema.lua
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
return { | ||
name = "request-aware-table", | ||
fields = { | ||
{ | ||
config = { | ||
type = "record", | ||
fields = { } | ||
} | ||
} | ||
} | ||
} |
39a545d
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bazel Build
Docker image available
kong/kong:39a545d17e670daf479e5d9b84434ef45d258eac
Artifacts available https://github.com/Kong/kong/actions/runs/6491290786