-
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(request-aware-table) add request aware table
add a request-aware table able to detect accesses from different requests.
- Loading branch information
Showing
5 changed files
with
248 additions
and
0 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
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,89 @@ | ||
local table_clear = require "table.clear" | ||
|
||
local get_phase = ngx.get_phase | ||
local fmt = string.format | ||
local ngx_var = ngx.var | ||
|
||
local NGX_VAR_PHASES = { | ||
set = true, | ||
rewrite = true, | ||
balancer = true, | ||
access = true, | ||
content = true, | ||
header_filter = true, | ||
body_filter = true, | ||
log = true, | ||
} | ||
|
||
|
||
--- Request aware table constructor | ||
-- Wraps an existing table (or creates a new one) with request-aware access | ||
-- logic to protect the underlying data from race conditions. | ||
-- @param data_table (optional) The target table to use as underlying data | ||
-- @param request_awareness_mode (optional) The mode for request awareness | ||
-- - "off": No request awareness mode | ||
-- - any other value, or nil: Control access for every r/w operation | ||
-- @return The newly created table with request-aware access | ||
local function new(data_table, request_awareness_mode) | ||
if data_table and type(data_table) ~= "table" then | ||
error("data_table must be a table", 2) | ||
end | ||
|
||
local allowed_request_id | ||
local proxy = {} | ||
local data = data_table or {} | ||
|
||
-- Check if access is allowed based on the request ID | ||
local function enforce_sequential_access() | ||
local curr_phase = get_phase() | ||
if not NGX_VAR_PHASES[curr_phase] then | ||
error(fmt("cannot enforce sequential access in %s phase", curr_phase), 2) | ||
end | ||
|
||
local curr_request_id = ngx_var.request_id | ||
|
||
allowed_request_id = allowed_request_id or curr_request_id | ||
|
||
if curr_request_id ~= allowed_request_id then | ||
error("race condition detected; access to table forbidden", 2) | ||
end | ||
end | ||
|
||
--- Clear data table | ||
-- @tparam function fn (optional) An optional function to use instead | ||
-- of `table.clear` to clear the data table | ||
function proxy.clear(fn) | ||
if fn then | ||
fn(data) | ||
|
||
else | ||
table_clear(data) | ||
end | ||
|
||
allowed_request_id = nil | ||
end | ||
|
||
local _proxy_mt = { | ||
__index = function(_, k) | ||
if request_awareness_mode ~= "off" then | ||
enforce_sequential_access() | ||
end | ||
|
||
return data[k] | ||
end, | ||
|
||
__newindex = function(_, k, v) | ||
if request_awareness_mode ~= "off" then | ||
enforce_sequential_access() | ||
end | ||
|
||
data[k] = v | ||
end | ||
} | ||
|
||
return setmetatable(proxy, _proxy_mt) | ||
end | ||
|
||
return { | ||
new = new, | ||
} |
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,156 @@ | ||
local utils = require "kong.tools.utils" | ||
local tablex = require "pl.tablex" | ||
|
||
local rat | ||
|
||
local function assert_rw_allowed(tab, orig_t) | ||
for k, v in pairs(orig_t or {}) do | ||
-- reads from orig_t succeed | ||
local val = assert.has_no.errors(function() return tab[k] end) | ||
assert.equal(v, val) | ||
end | ||
|
||
local k = utils.random_string() | ||
local v = utils.random_string() | ||
-- writing new values succeeds | ||
assert.has_no.errors(function() tab[k] = v end) | ||
-- reading new values succeeds | ||
local val = assert.has_no.errors(function() return tab[k] end) | ||
assert.equal(v, val) | ||
end | ||
|
||
local function assert_rw_denied(tab, orig_t) | ||
local err_str = "race condition detected" | ||
for k, v in pairs(orig_t or {}) do | ||
-- reads from orig_t error out | ||
assert.error_matches(function() return nil, tab[k] == v end, err_str) | ||
end | ||
|
||
local k = utils.random_string() | ||
local v = utils.random_string() | ||
-- writing new values errors out | ||
assert.error_matches(function() tab[k] = v end, err_str) | ||
-- reading new values errors out | ||
assert.error_matches(function() return tab[k] == v end, err_str) | ||
end | ||
|
||
describe("Request aware table", function() | ||
local old_ngx | ||
local tab | ||
|
||
lazy_setup(function() | ||
old_ngx = ngx | ||
_G.ngx = { | ||
get_phase = function() return "access" end, | ||
var = {}, | ||
} | ||
rat = require "kong.tools.request_aware_table" | ||
end) | ||
|
||
lazy_teardown(function() | ||
_G.ngx = old_ngx | ||
end) | ||
|
||
describe("with concurrency check enabled", function() | ||
local orig_t | ||
|
||
before_each(function() | ||
orig_t = { | ||
k1 = utils.random_string(), | ||
k2 = utils.random_string(), | ||
} | ||
tab = rat.new(orig_t, "on") | ||
end) | ||
|
||
it("allows access when there are no race conditions", function() | ||
-- create a new RAT with request_id = 1 (clear after use) | ||
_G.ngx.var.request_id = "1" | ||
assert_rw_allowed(tab, orig_t) | ||
tab.clear() | ||
|
||
-- reuse RAT with different request_id (allowed) | ||
_G.ngx.var.request_id = "2" | ||
assert_rw_allowed(tab) | ||
end) | ||
|
||
it("denies access when there are race conditions", function() | ||
-- create a new RAT with request_id = 1 (no clear) | ||
_G.ngx.var.request_id = "1" | ||
assert_rw_allowed(tab, orig_t) | ||
|
||
-- reuse RAT with different request_id (not allowed) | ||
_G.ngx.var.request_id = "2" | ||
assert_rw_denied(tab) | ||
end) | ||
|
||
it("clears the table successfully", function() | ||
-- create a new RAT with request_id = 1 (clear after use) | ||
_G.ngx.var.request_id = "1" | ||
assert_rw_allowed(tab, orig_t) | ||
tab.clear() | ||
|
||
assert.same(0, tablex.size(orig_t)) | ||
end) | ||
|
||
it("allows defining a custom clear function", function() | ||
-- create a new RAT with request_id = 1 (clear after use) | ||
_G.ngx.var.request_id = "1" | ||
orig_t.persist = "persistent_value" | ||
assert_rw_allowed(tab, orig_t) | ||
|
||
-- custom clear function that keeps persistent_value | ||
tab.clear(function(t) | ||
for k in pairs(t) do | ||
if k ~= "persist" then | ||
t[k] = nil | ||
end | ||
end | ||
end) | ||
|
||
-- confirm persistent_value is the only key left | ||
assert.same(1, tablex.size(orig_t)) | ||
assert.equal("persistent_value", tab.persist) | ||
|
||
-- clear the whole table and confirm it's empty | ||
tab.clear() | ||
assert.same(0, tablex.size(orig_t)) | ||
end) | ||
end) | ||
|
||
describe("with concurrency check disabled", function() | ||
local orig_t | ||
|
||
before_each(function() | ||
orig_t = { | ||
k1 = utils.random_string(), | ||
k2 = utils.random_string(), | ||
} | ||
tab = rat.new(orig_t, "off") | ||
end) | ||
|
||
before_each(function() | ||
tab.clear() | ||
end) | ||
|
||
it("allows access when there are no race conditions", function() | ||
-- create a new RAT with request_id = 1 (clear after use) | ||
_G.ngx.var.request_id = "1" | ||
assert_rw_allowed(tab, orig_t) | ||
tab.clear() | ||
|
||
-- reuse RAT with different request_id (allowed) | ||
_G.ngx.var.request_id = "2" | ||
assert_rw_allowed(tab, orig_t) | ||
end) | ||
|
||
it("allows access when there are race conditions", function() | ||
-- create a new RAT with request_id = 1, (no clear) | ||
_G.ngx.var.request_id = "1" | ||
assert_rw_allowed(tab, orig_t) | ||
|
||
-- reuse RAT with different request_id (allowed with check disabled) | ||
_G.ngx.var.request_id = "2" | ||
assert_rw_allowed(tab, orig_t) | ||
end) | ||
end) | ||
end) |
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