Skip to content

Commit

Permalink
feat(request-aware-table) add request aware table
Browse files Browse the repository at this point in the history
add a request-aware table able to detect accesses from different
requests.
  • Loading branch information
samugi committed Jun 8, 2023
1 parent 750ba97 commit 7530d1d
Show file tree
Hide file tree
Showing 5 changed files with 248 additions and 0 deletions.
1 change: 1 addition & 0 deletions kong-3.4.0-0.rockspec
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ build = {
["kong.tools.kong-lua-sandbox"] = "kong/tools/kong-lua-sandbox.lua",
["kong.tools.protobuf"] = "kong/tools/protobuf.lua",
["kong.tools.mime_type"] = "kong/tools/mime_type.lua",
["kong.tools.request_aware_table"] = "kong/tools/request_aware_table.lua",

["kong.runloop.handler"] = "kong/runloop/handler.lua",
["kong.runloop.events"] = "kong/runloop/events.lua",
Expand Down
1 change: 1 addition & 0 deletions kong/templates/nginx_kong.lua
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ exit_worker_by_lua_block {
> if (role == "traditional" or role == "data_plane") and #proxy_listeners > 0 then
# Load variable indexes
lua_kong_load_var_index default;
lua_kong_load_var_index $request_id;
upstream kong_upstream {
server 0.0.0.1;
Expand Down
89 changes: 89 additions & 0 deletions kong/tools/request_aware_table.lua
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,
}
156 changes: 156 additions & 0 deletions spec/01-unit/28-request-aware-table_spec.lua
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)
1 change: 1 addition & 0 deletions spec/fixtures/custom_nginx.template
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ http {
> if (role == "traditional" or role == "data_plane") and #proxy_listeners > 0 then
# Load variable indexes
lua_kong_load_var_index default;
lua_kong_load_var_index $request_id;

upstream kong_upstream {
server 0.0.0.1;
Expand Down

0 comments on commit 7530d1d

Please sign in to comment.