-- gitano.lace -- -- Interface to Lace for rules management -- -- Copyright 2012-2017 Daniel Silverstone -- All rights reserved. -- -- Redistribution and use in source and binary forms, with or without -- modification, are permitted provided that the following conditions -- are met: -- 1. Redistributions of source code must retain the above copyright -- notice, this list of conditions and the following disclaimer. -- 2. Redistributions in binary form must reproduce the above copyright -- notice, this list of conditions and the following disclaimer in the -- documentation and/or other materials provided with the distribution. -- 3. Neither the name of the author nor the names of their contributors -- may be used to endorse or promote products derived from this software -- without specific prior written permission. -- -- THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND -- ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -- IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -- ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE -- FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -- DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS -- OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) -- HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -- LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY -- OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF -- SUCH DAMAGE. -- local lace = require 'lace' local util = require 'gitano.util' local gall = require 'gall' local log = require 'gitano.log' local i18n = require 'gitano.i18n' local pat = require 'gitano.patterns' local pcre = require "rex_pcre" local global_lookaside = { ["_basis"] = [[ include global:core ]] } local function _loader(ctx, _name) local global_name = _name:match(pat.LACE_GLOBAL_DEFINITION) local name, tree, sha = global_name or _name if not global_name then -- Project load if ctx.project and not ctx.project_tree then ctx.project_tree = gall.tree.flatten(ctx.project.content.tree.content) end if ctx.project_tree then tree = ctx.project_tree sha = ctx.project.sha end else -- Global load if global_lookaside[global_name] then local resolved = global_lookaside[global_name] if type(resolved) == "function" then resolved = resolved(ctx) end return "global_lookaside::" .. global_name, resolved end if not ctx.global_tree then ctx.global_tree = gall.tree.flatten(ctx.global.content.tree.content) end tree = ctx.global_tree sha = ctx.global.sha end if not tree then log.ddebug(i18n.expand("DEBUG_EMPTY_OBJECT_NASCENT")) return "empty", "# Nothing to see here\n" end local blob_name = "rules/" .. name .. ".lace" local real_name = sha .. "::" .. blob_name if not tree[blob_name] or tree[blob_name].type ~= "blob" then local m = i18n.expand("ERROR_UNABLE_TO_FIND_RULE", { name=blob_name, sha=sha, inname=_name }) log.ddebug(m) return lace.error.error(m) end return real_name, tree[blob_name].obj.content end local match_types = { exact = function(want, have) return want == have end, prefix = function(want, have) return have:sub(1, #want) == want end, suffix = function(want, have) return have:sub(-#want) == want end, pattern = function(want, have) return (have:match(want) ~= nil) end, pcre = function(want, have) return (pcre.match(have, want) ~= nil) end } -- Match aliases (auto-inverted) match_types.is = match_types.exact match_types.starts = match_types.prefix match_types.ends = match_types.suffix match_types.startswith = match_types.prefix match_types.endswith = match_types.suffix -- Inverted matches do local inverted_matches = {} for k, v in pairs(match_types) do inverted_matches["!" .. k] = function(...) return not v(...) end end for k, v in pairs(inverted_matches) do match_types[k] = v end end local function _do_simple_match(ctx, key, matchtype, value) value = util.process_expansion(ctx, value) local kk = ctx[key] or "" local check = match_types[matchtype] if type(kk) == "function" then -- Realise the value first ctx[key] = kk(ctx) kk = ctx[key] end if type(kk) == "string" then return check(value, kk) else local ret = false for k in pairs(kk) do ret = ret or check(value, k) end if matchtype:sub(1,1) == "!" then ret = not ret end return ret end end local function _simple_match(ctx, key, matchtype, value, guard) if guard ~= nil then return lace.error.error(i18n.expand("ERROR_UNEXPECTED_ADDITIONAL_ARGUMENT"), {4}) end if value == nil then return lace.error.error(i18n.expand("ERROR_MISSING_MATCHTYPE_OR_VALUE"), {1}) end if match_types[matchtype] == nil then return lace.error.error(i18n.expand("ERROR_UNKNOWN_MATCHTYPE"), {2}) end return { fn = _do_simple_match, args = { key, matchtype, util.prep_expansion(value) } } end local simples = { -- Trivial strings/lists "operation", "ref", "group", "user", "repository", "source", "oldsha", "newsha", "target", -- Trees for update operations (flat lists) "start_tree", "target_tree", -- Tree diffs for update operations (flat lists) "treediff/targets", "treediff/added", "treediff/deleted", "treediff/modified", "treediff/renamed", "treediff/renamedto", -- Stuff for 'as' users "as_user", "as_group", -- Stuff for site admin commands "targetuser", "targetgroup", "member", -- Stuff from update hooks for object types "oldtype", "oldtaggedtype", "oldtaggedsha", "oldsigned", "newtype", "newtaggedtype", "newtaggedsha", "newsigned", -- Stuff for keyring command "keyringname", -- Stuff for config command "key", "value", } local matchers = {} for _, s in ipairs(simples) do matchers[s] = _simple_match end local base_compcontext = { _lace = { loader = _loader, controltype = matchers, } } local function cloddly_bless(ctx) local function indexer(tab, name) if name:sub(1,7) == "config/" then tab[name] = _simple_match return _simple_match end end setmetatable(ctx._lace.controltype, {__index = indexer}) return ctx end local function compile_ruleset(repo, adminsha, globaladminsha) -- repo is a gitano repository object. -- We trust that we can compile the repo's ruleset which involves -- finding the admin sha for the repo (unless given) and then the global -- admin sha (unless given) and using that to compile a full ruleset. local compcontext = cloddly_bless(util.deep_copy(base_compcontext)) compcontext.repo = repo if not repo.is_nascent then if not adminsha then compcontext.project = repo.git:get("refs/gitano/admin") else compcontext.project = repo.git:get(adminsha) end end if not globaladminsha then compcontext.global = repo.config.commit else compcontext.global = repo.config.repo.git:get(globaladminsha) end return lace.compiler.compile(compcontext, "global:_basis") end local function run_ruleset(ruleset, ctx) -- First check if we're running as bypass if ctx.as_user == "gitano-bypass" then log.state(i18n.expand("LACE_BYPASS_BANNER_HEADER")) log.state(i18n.expand("LACE_BYPASS_ALERT_MESSAGE")) log.state(i18n.expand("LACE_BYPASS_BANNER_FOOTER")) return "allow", i18n.expand("LACE_BYPASSED") end if ctx.as_user == nil and ctx.user == "gitano-bypass" then log.state(i18n.expand("LACE_BYPASS_BANNER_HEADER")) log.state(i18n.expand("LACE_BYPASS_ALERT_MESSAGE")) log.state(i18n.expand("LACE_BYPASS_BANNER_FOOTER")) return "allow", i18n.expand("LACE_BYPASSED") end return lace.engine.run(ruleset, ctx) end return { compile = compile_ruleset, run = run_ruleset, }