Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[sandbox] environment policies have the same env as policy loader #596

Merged
merged 3 commits into from
Feb 15, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
17 changes: 10 additions & 7 deletions gateway/src/apicast/cli/environment.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
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'
Expand Down
259 changes: 9 additions & 250 deletions gateway/src/apicast/policy_loader.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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
]], {})

_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 '.'
Expand All @@ -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)

Expand Down
Loading