From 458c18f62be20e065c8aae82c330b941c2d24c41 Mon Sep 17 00:00:00 2001 From: Michal Cichra Date: Thu, 15 Feb 2018 08:13:20 +0100 Subject: [PATCH 1/3] [sandbox] expose `arg` So policies can detect if they are being executed on the CLI and decide to do something different like not loading themselves. Because some might require shared dict that is not available. --- gateway/src/apicast/cli/environment.lua | 2 +- gateway/src/apicast/policy_loader.lua | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/gateway/src/apicast/cli/environment.lua b/gateway/src/apicast/cli/environment.lua index a5aa4646d..48880a648 100644 --- a/gateway/src/apicast/cli/environment.lua +++ b/gateway/src/apicast/cli/environment.lua @@ -147,7 +147,7 @@ function _M:add(env) end local config = loadfile(path, 't', { - print = print, inspect = require('inspect'), context = self._context, + print = print, inspect = require('inspect'), context = self._context, arg = arg, cli = arg, tonumber = tonumber, tostring = tostring, os = { getenv = resty_env.value }, pcall = pcall, require = require, assert = assert, error = error, }) diff --git a/gateway/src/apicast/policy_loader.lua b/gateway/src/apicast/policy_loader.lua index d4c4503b7..62829bb8a 100644 --- a/gateway/src/apicast/policy_loader.lua +++ b/gateway/src/apicast/policy_loader.lua @@ -195,7 +195,7 @@ _M.env = export([[ table.insert table.maxn table.move table.pack table.remove table.sort table.unpack - ngx + ngx arg ]], {}) _M.env._G = _M.env From a708870d706ed55c8004dd3f9625fefd222b7bd3 Mon Sep 17 00:00:00 2001 From: Michal Cichra Date: Thu, 15 Feb 2018 08:26:58 +0100 Subject: [PATCH 2/3] [sandbox] extract sandbox from policy loader * to split concerns of policy loading and a sandbox --- gateway/src/apicast/policy_loader.lua | 259 +------------------------- gateway/src/resty/sandbox.lua | 255 +++++++++++++++++++++++++ 2 files changed, 264 insertions(+), 250 deletions(-) create mode 100644 gateway/src/resty/sandbox.lua diff --git a/gateway/src/apicast/policy_loader.lua b/gateway/src/apicast/policy_loader.lua index 62829bb8a..246920953 100644 --- a/gateway/src/apicast/policy_loader.lua +++ b/gateway/src/apicast/policy_loader.lua @@ -5,215 +5,19 @@ -- And even loading several independent copies of the same policy with no shared state. -- Each object returned by the loader is new table and shares only shared APIcast code. +local sandbox = require('resty.sandbox') + local format = string.format -local error = error -local type = type local ipairs = ipairs -local loadfile = loadfile local insert = table.insert local setmetatable = setmetatable -local concat = table.concat local pcall = pcall -local _G = _G local _M = {} -local searchpath = package.searchpath -local root_loaded = package.loaded - -local root_require = require - -local preload = package.preload - local resty_env = require('resty.env') local re = require('ngx.re') ---- create a require function not using the global namespace --- loading code from policy namespace should have no effect on the global namespace --- but poliocy can load shared libraries that would be cached globally -local function gen_require(package) - - local function not_found(modname, err) - return error(format("module '%s' not found:%s", modname, err), 0) - end - - --- helper function to safely use the native require function - local function fallback(modname) - local mod - - mod = package.loaded[modname] - - if not mod then - ngx.log(ngx.DEBUG, 'native require for: ', modname) - mod = root_require(modname) - end - - return mod - end - - --- helper function to find and return correct loader for a module - local function find_loader(modname) - local loader, file, err, ret - - -- http://www.lua.org/manual/5.2/manual.html#pdf-package.searchers - - -- When looking for a module, require calls each of these searchers in ascending order, - for i=1, #package.searchers do - -- with the module name (the argument given to require) as its sole parameter. - ret, err = package.searchers[i](modname) - - -- The function can return another function (the module loader) - -- plus an extra value that will be passed to that loader, - if type(ret) == 'function' then - loader = ret - file = err - break - -- or a string explaining why it did not find that module - elseif type(ret) == 'string' then - err = ret - end - -- (or nil if it has nothing to say). - end - - return loader, file, err - end - - --- reimplemented require function - -- - return a module if it was already loaded (globally or locally) - -- - try to find loader function - -- - fallback to global require - -- @tparam string modname module name - -- @tparam boolean exclusive load only policy code, turns off the fallback loader - return function(modname, exclusive) - -- http://www.lua.org/manual/5.2/manual.html#pdf-require - ngx.log(ngx.DEBUG, 'sandbox require: ', modname) - - -- The function starts by looking into the package.loaded table - -- to determine whether modname is already loaded. - -- NOTE: this is different from the spec: use the top level package.loaded, - -- otherwise it would try to sandbox load already loaded shared code - local mod = root_loaded[modname] - - -- If it is, then require returns the value stored at package.loaded[modname]. - if mod then return mod end - - -- Otherwise, it tries to find a loader for the module. - local loader, file, err = find_loader(modname) - - -- Once a loader is found, - if loader then - ngx.log(ngx.DEBUG, 'sandboxed require for: ', modname, ' file: ', file) - -- require calls the loader with two arguments: - -- modname and an extra value dependent on how it got the loader. - -- (If the loader came from a file, this extra value is the file name.) - mod = loader(modname, file) - elseif not exclusive then - ngx.log(ngx.DEBUG, 'fallback loader for: ', modname, ' error: ', err) - mod = fallback(modname) - else - -- If there is any error loading or running the module, - -- or if it cannot find any loader for the module, then require raises an error. - return not_found(modname, err) - end - - -- If the loader returns any non-nil value, - if mod ~= nil then - -- require assigns the returned value to package.loaded[modname]. - package.loaded[modname] = mod - - -- If the loader does not return a non-nil value - -- and has not assigned any value to package.loaded[modname], - elseif not package.loaded[modname] then - -- then require assigns true to this entry. - package.loaded[modname] = true - end - - -- In any case, require returns the final value of package.loaded[modname]. - return package.loaded[modname] - end -end - -local function export(list, env) - assert(env, 'missing env') - list:gsub('%S+', function(id) - local module, method = id:match('([^%.]+)%.([^%.]+)') - if module then - env[module] = env[module] or {} - env[module][method] = _G[module][method] - else - env[id] = _G[id] - end - end) - - return env -end - ---- this is environment exposed to the policies --- that means this is very light sandbox so policies don't mutate global env --- and most importantly we replace the require function with our own --- The env intentionally does not expose getfenv so sandboxed code can't get top level globals. --- And also does not expose functions for loading code from filesystem (loadfile, dofile). --- Neither exposes debug functions unless openresty was compiled --with-debug. --- But it exposes ngx as the same object, so it can be changed from within the policy. -_M.env = export([[ - _VERSION assert print xpcall pcall error - unpack next ipairs pairs select - collectgarbage gcinfo newproxy loadstring load - setmetatable getmetatable - tonumber tostring type - rawget rawequal rawlen rawset - - bit.arshift bit.band bit.bnot bit.bor bit.bswap bit.bxor - bit.lshift bit.rol bit.ror bit.rshift bit.tobit bit.tohex - - coroutine.create coroutine.resume coroutine.running coroutine.status - coroutine.wrap coroutine.yield coroutine.isyieldable - - debug.traceback - - io.open io.close io.flush io.tmpfile io.type - io.input io.output io.stderr io.stdin io.stdout - io.popen io.read io.lines io.write - - math.abs math.acos math.asin math.atan math.atan2 - math.ceil math.cos math.cosh math.deg math.exp math.floor - math.fmod math.frexp math.ldexp math.log math.pi - math.log10 math.max math.min math.modf math.pow - math.rad math.random math.randomseed math.huge - math.sin math.sinh math.sqrt math.tan math.tanh - - os.clock os.date os.time os.difftime - os.execute os.getenv - os.rename os.tmpname os.remove - - string.byte string.char string.dump string.find - string.format string.lower string.upper string.len - string.gmatch string.match string.gsub string.sub - string.rep string.reverse - - table.concat table.foreach table.foreachi table.getn - table.insert table.maxn table.move table.pack - table.remove table.sort table.unpack - - ngx arg -]], {}) - -_M.env._G = _M.env - --- add debug functions only when nginx was compiled --with-debug -if ngx.config.debug then - _M.env = export([[ debug.debug debug.getfenv debug.gethook debug.getinfo - debug.getlocal debug.getmetatable debug.getregistry - debug.getupvalue debug.getuservalue debug.setfenv - debug.sethook debug.setlocal debug.setmetatable - debug.setupvalue debug.setuservalue debug.upvalueid debug.upvaluejoin - ]], _M.env) -end - -local mt = { - __call = function(loader, ...) return loader.env.require(...) end -} - do local function apicast_dir() return resty_env.value('APICAST_DIR') or '.' @@ -233,64 +37,19 @@ do end end -function _M.new(name, version, paths) +function _M:call(name, version, dir) + local v = version or 'builtin' local load_paths = {} - for _, path in ipairs(paths or _M.policy_load_paths()) do - insert(load_paths, format('%s/%s/%s/?.lua', path, name, version)) + for _, path in ipairs(dir or self.policy_load_paths()) do + insert(load_paths, format('%s/%s/%s/?.lua', path, name, v)) end - if version == 'builtin' then - insert(load_paths, format('%s/%s/?.lua', _M.builtin_policy_load_path(), name)) + if v == 'builtin' then + insert(load_paths, format('%s/%s/?.lua', self.builtin_policy_load_path(), name)) end - -- need to create global variable package that mimics the native one - local package = { - loaded = {}, - preload = preload, - searchers = {}, -- http://www.lua.org/manual/5.2/manual.html#pdf-package.searchers - searchpath = searchpath, - path = concat(load_paths, ';'), - cpath = '', -- no C libraries allowed in policies - } - - -- creating new env for each policy means they can't accidentaly share global variables - local env = setmetatable({ - require = gen_require(package), - package = package, - }, { __index = _M.env }) - - -- The first searcher simply looks for a loader in the package.preload table. - insert(package.searchers, function(modname) return package.preload[modname] end) - -- The second searcher looks for a loader as a Lua library, using the path stored at package.path. - -- The search is done as described in function package.searchpath. - insert(package.searchers, function(modname) - local file, err = searchpath(modname, package.path) - local loader - - if file then - loader, err = loadfile(file, 'bt', env) - - ngx.log(ngx.DEBUG, 'loading file: ', file) - - if loader then return loader, file end - end - - return err - end) - - local self = { - env = env, - name = name, - version = version, - } - - return setmetatable(self, mt) -end - -function _M:call(name, version, dir) - local v = version or 'builtin' - local loader = self.new(name, v, dir) + local loader = sandbox.new(load_paths) ngx.log(ngx.DEBUG, 'loading policy: ', name, ' version: ', v) diff --git a/gateway/src/resty/sandbox.lua b/gateway/src/resty/sandbox.lua new file mode 100644 index 000000000..9f633d87b --- /dev/null +++ b/gateway/src/resty/sandbox.lua @@ -0,0 +1,255 @@ +--- Sandbox +-- @module resty.sandbox +-- It uses sandboxed require to isolate dependencies and not mutate global state. +-- That allows for loading several versions of the same code with different dependencies. +-- And even loading several independent copies of the same code with no shared state. +-- Each object returned by the loader is new table and shares only shared code outside the defined load paths. + +local format = string.format +local error = error +local type = type +local loadfile = loadfile +local insert = table.insert +local setmetatable = setmetatable +local concat = table.concat + +local _G = _G +local _M = {} + +local searchpath = package.searchpath +local root_loaded = package.loaded + +local root_require = require + +local preload = package.preload + +--- create a require function not using the global namespace +-- loading code from a namespace should have no effect on the global namespace +-- but that code can load shared libraries that would be cached globally +local function gen_require(package) + + local function not_found(modname, err) + return error(format("module '%s' not found:%s", modname, err), 0) + end + + --- helper function to safely use the native require function + local function fallback(modname) + local mod + + mod = package.loaded[modname] + + if not mod then + ngx.log(ngx.DEBUG, 'native require for: ', modname) + mod = root_require(modname) + end + + return mod + end + + --- helper function to find and return correct loader for a module + local function find_loader(modname) + local loader, file, err, ret + + -- http://www.lua.org/manual/5.2/manual.html#pdf-package.searchers + + -- When looking for a module, require calls each of these searchers in ascending order, + for i=1, #package.searchers do + -- with the module name (the argument given to require) as its sole parameter. + ret, err = package.searchers[i](modname) + + -- The function can return another function (the module loader) + -- plus an extra value that will be passed to that loader, + if type(ret) == 'function' then + loader = ret + file = err + break + -- or a string explaining why it did not find that module + elseif type(ret) == 'string' then + err = ret + end + -- (or nil if it has nothing to say). + end + + return loader, file, err + end + + --- reimplemented require function + -- - return a module if it was already loaded (globally or locally) + -- - try to find loader function + -- - fallback to global require + -- @tparam string modname module name + -- @tparam boolean exclusive load only sandboxed code, turns off the fallback loader + return function(modname, exclusive) + -- http://www.lua.org/manual/5.2/manual.html#pdf-require + ngx.log(ngx.DEBUG, 'sandbox require: ', modname) + + -- The function starts by looking into the package.loaded table + -- to determine whether modname is already loaded. + -- NOTE: this is different from the spec: use the top level package.loaded, + -- otherwise it would try to sandbox load already loaded shared code + local mod = root_loaded[modname] + + -- If it is, then require returns the value stored at package.loaded[modname]. + if mod then return mod end + + -- Otherwise, it tries to find a loader for the module. + local loader, file, err = find_loader(modname) + + -- Once a loader is found, + if loader then + ngx.log(ngx.DEBUG, 'sandboxed require for: ', modname, ' file: ', file) + -- require calls the loader with two arguments: + -- modname and an extra value dependent on how it got the loader. + -- (If the loader came from a file, this extra value is the file name.) + mod = loader(modname, file) + elseif not exclusive then + ngx.log(ngx.DEBUG, 'fallback loader for: ', modname, ' error: ', err) + mod = fallback(modname) + else + -- If there is any error loading or running the module, + -- or if it cannot find any loader for the module, then require raises an error. + return not_found(modname, err) + end + + -- If the loader returns any non-nil value, + if mod ~= nil then + -- require assigns the returned value to package.loaded[modname]. + package.loaded[modname] = mod + + -- If the loader does not return a non-nil value + -- and has not assigned any value to package.loaded[modname], + elseif not package.loaded[modname] then + -- then require assigns true to this entry. + package.loaded[modname] = true + end + + -- In any case, require returns the final value of package.loaded[modname]. + return package.loaded[modname] + end +end + +local function export(list, env) + assert(env, 'missing env') + list:gsub('%S+', function(id) + local module, method = id:match('([^%.]+)%.([^%.]+)') + if module then + env[module] = env[module] or {} + env[module][method] = _G[module][method] + else + env[id] = _G[id] + end + end) + + return env +end + +--- this is environment exposed to the sandbox +-- that means this is very light sandbox so sandboxed code does not mutate global env +-- and most importantly we replace the require function with our own +-- The env intentionally does not expose getfenv so sandboxed code can't get top level globals. +-- And also does not expose functions for loading code from filesystem (loadfile, dofile). +-- Neither exposes debug functions unless openresty was compiled --with-debug. +-- But it exposes ngx as the same object, so it can be changed from within the sandbox. +_M.env = export([[ + _VERSION assert print xpcall pcall error + unpack next ipairs pairs select + collectgarbage gcinfo newproxy loadstring load + setmetatable getmetatable + tonumber tostring type + rawget rawequal rawlen rawset + + bit.arshift bit.band bit.bnot bit.bor bit.bswap bit.bxor + bit.lshift bit.rol bit.ror bit.rshift bit.tobit bit.tohex + + coroutine.create coroutine.resume coroutine.running coroutine.status + coroutine.wrap coroutine.yield coroutine.isyieldable + + debug.traceback + + io.open io.close io.flush io.tmpfile io.type + io.input io.output io.stderr io.stdin io.stdout + io.popen io.read io.lines io.write + + math.abs math.acos math.asin math.atan math.atan2 + math.ceil math.cos math.cosh math.deg math.exp math.floor + math.fmod math.frexp math.ldexp math.log math.pi + math.log10 math.max math.min math.modf math.pow + math.rad math.random math.randomseed math.huge + math.sin math.sinh math.sqrt math.tan math.tanh + + os.clock os.date os.time os.difftime + os.execute os.getenv + os.rename os.tmpname os.remove + + string.byte string.char string.dump string.find + string.format string.lower string.upper string.len + string.gmatch string.match string.gsub string.sub + string.rep string.reverse + + table.concat table.foreach table.foreachi table.getn + table.insert table.maxn table.move table.pack + table.remove table.sort table.unpack + + ngx arg +]], {}) + +_M.env._G = _M.env + +-- add debug functions only when nginx was compiled --with-debug +if ngx.config.debug then + _M.env = export([[ debug.debug debug.getfenv debug.gethook debug.getinfo + debug.getlocal debug.getmetatable debug.getregistry + debug.getupvalue debug.getuservalue debug.setfenv + debug.sethook debug.setlocal debug.setmetatable + debug.setupvalue debug.setuservalue debug.upvalueid debug.upvaluejoin + ]], _M.env) +end + +local mt = { + __call = function(loader, ...) return loader.env.require(...) end +} + +function _M.new(load_paths) + -- need to create global variable package that mimics the native one + local package = { + loaded = {}, + preload = preload, + searchers = {}, -- http://www.lua.org/manual/5.2/manual.html#pdf-package.searchers + searchpath = searchpath, + path = concat(load_paths, ';'), + cpath = '', -- no C libraries allowed in sandbox + } + + -- creating new env for each sadnbox means they can't accidentaly share global variables + local env = setmetatable({ + require = gen_require(package), + package = package, + }, { __index = _M.env }) + + -- The first searcher simply looks for a loader in the package.preload table. + insert(package.searchers, function(modname) return package.preload[modname] end) + -- The second searcher looks for a loader as a Lua library, using the path stored at package.path. + -- The search is done as described in function package.searchpath. + insert(package.searchers, function(modname) + local file, err = searchpath(modname, package.path) + local loader + + if file then + loader, err = loadfile(file, 'bt', env) + + ngx.log(ngx.DEBUG, 'loading file: ', file) + + if loader then return loader, file end + end + + return err + end) + + local self = { + env = env + } + + return setmetatable(self, mt) +end + +return _M From df7011f59183a2fe37d83c3f9951e11be963da21 Mon Sep 17 00:00:00 2001 From: Michal Cichra Date: Thu, 15 Feb 2018 08:31:31 +0100 Subject: [PATCH 3/3] [cli] use resty.sandbox to load environment configurations --- CHANGELOG.md | 1 + gateway/src/apicast/cli/environment.lua | 17 ++++++++++------- gateway/src/resty/sandbox.lua | 4 +++- 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ee47c595..d40468229 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - 3scale configuration (staging/production) can be passed as `-3` or `--channel` on the CLI [PR #590](https://github.com/3scale/apicast/pull/590) - APIcast CLI loads environments defined by `APICAST_ENVIRONMENT` variable [PR #590](https://github.com/3scale/apicast/pull/590) - Endpoint in management API to retrieve all the JSON manifests of the policies [PR #592](https://github.com/3scale/apicast/pull/592) +- More complete global environment when loading environment policies [PR #596](https://github.com/3scale/apicast/pull/596) ## Fixed diff --git a/gateway/src/apicast/cli/environment.lua b/gateway/src/apicast/cli/environment.lua index 48880a648..4c185d3ba 100644 --- a/gateway/src/apicast/cli/environment.lua +++ b/gateway/src/apicast/cli/environment.lua @@ -6,13 +6,12 @@ local pl_path = require('pl.path') local resty_env = require('resty.env') local linked_list = require('apicast.linked_list') +local sandbox = require('resty.sandbox') local util = require('apicast.util') local setmetatable = setmetatable local loadfile = loadfile -local pcall = pcall local require = require local assert = assert -local error = error local print = print local pairs = pairs local ipairs = ipairs @@ -146,11 +145,15 @@ function _M:add(env) return nil, 'no configuration found' end - local config = loadfile(path, 't', { - print = print, inspect = require('inspect'), context = self._context, arg = arg, cli = arg, - tonumber = tonumber, tostring = tostring, os = { getenv = resty_env.value }, - pcall = pcall, require = require, assert = assert, error = error, - }) + -- using sandbox is not strictly needed, + -- but it is a nice way to add some extra env to the loaded code + -- and not using global variables + local box = sandbox.new() + local config = loadfile(path, 't', setmetatable({ + inspect = require('inspect'), context = self._context, + arg = arg, cli = arg, + os = { getenv = resty_env.value }, + }, { __index = box.env })) if not config then return nil, 'invalid config' diff --git a/gateway/src/resty/sandbox.lua b/gateway/src/resty/sandbox.lua index 9f633d87b..848494f26 100644 --- a/gateway/src/resty/sandbox.lua +++ b/gateway/src/resty/sandbox.lua @@ -209,6 +209,8 @@ local mt = { __call = function(loader, ...) return loader.env.require(...) end } +local empty_t = {} + function _M.new(load_paths) -- need to create global variable package that mimics the native one local package = { @@ -216,7 +218,7 @@ function _M.new(load_paths) preload = preload, searchers = {}, -- http://www.lua.org/manual/5.2/manual.html#pdf-package.searchers searchpath = searchpath, - path = concat(load_paths, ';'), + path = concat(load_paths or empty_t, ';'), cpath = '', -- no C libraries allowed in sandbox }