--[[ Policy file handling for Alpine Wall Copyright (C) 2012-2013 Kaarle Ritvanen See LICENSE file for license details ]]-- module(..., package.seeall) require 'json' require 'lfs' require 'awall.dependency' local class = require('awall.object').class local raise = require('awall.uerror').raise local util = require('awall.util') local PolicyConfig = class() function PolicyConfig:init(data, source, policies) self.data = data self.source = source self.policies = policies end function PolicyConfig:expand() local function expand(value) if type(value) == 'table' then return util.map(value, expand) end local visited = {} local pattern = '%$(%a[%w_]*)' while type(value) == 'string' and string.find(value, pattern) do local si, ei, name = string.find(value, pattern) if util.contains(visited, name) then raise('Circular variable definition: '..name) end table.insert(visited, name) local var = self.data.variable[name] if not var then raise('Invalid variable reference: '..name) end if si == 1 and ei == string.len(value) then value = var elseif util.contains({'number', 'string'}, type(var)) then value = string.sub(value, 1, si - 1)..var..string.sub(value, ei + 1, -1) else raise('Attempted to concatenate complex variable: '..name) end end if value == '' then return end return value end return expand(self.data) end local Policy = class() function Policy:init() self.enabled = self.type == 'mandatory' end function Policy:load() local file = io.open(self.path) if not file then raise('Unable to read policy file '..self.path) end local data = '' for line in file:lines() do data = data..line end file:close() local success, res = pcall(json.decode, data) if success then return res end raise(res..' while parsing '..self.path) end function Policy:checkoptional() if self.type ~= 'optional' then raise('Not an optional policy: '..name) end end function Policy:enable() self:checkoptional() if self.enabled then raise('Policy already enabled: '..self.name) end assert(lfs.link(self.path, self.confdir..'/'..self.fname, true)) end function Policy:disable() self:checkoptional() if not self.enabled then raise('Policy already disabled: '..self.name) end assert(os.remove(self.confdir..'/'..self.fname)) end local defdirs = { mandatory={'/etc/awall', '/usr/share/awall/mandatory'}, optional={'/etc/awall/optional', '/usr/share/awall/optional'}, private={'/etc/awall/private', '/usr/share/awall/private'} } PolicySet = class() function PolicySet:init(dirs) local confdir = (dirs.mandatory or defdirs.mandatory)[1] self.policies = {} for i, cls in ipairs{'private', 'optional', 'mandatory'} do for i, dir in ipairs(dirs[cls] or defdirs[cls]) do for fname in lfs.dir(dir) do local si, ei, name = string.find(fname, '^([%w-]+)%.json$') if name then local pol = self.policies[name] local path = dir..'/'..fname if string.sub(path, 1, 1) ~= '/' then path = lfs.currentdir()..'/'..path end local attrs = lfs.attributes(path) local loc = attrs.dev..':'..attrs.ino if pol then if pol.loc ~= loc then raise('Duplicate policy name: '..name) end if dir == confdir and pol.type == 'optional' then pol.enabled = true else pol.type = cls end else self.policies[name] = Policy.morph{ name=name, type=cls, path=path, fname=fname, loc=loc, confdir=confdir } end end end end end end function PolicySet:load() local imported = {} local function require(policy) if imported[policy.name] then return end local data = policy:load() imported[policy.name] = data if not data.after then data.after = data.import end for i, name in util.listpairs(data.import) do require(self.policies[name]) end end for name, policy in pairs(self.policies) do if policy.enabled then require(policy) end end local order = awall.dependency.order(imported) if type(order) ~= 'table' then raise('Circular ordering directives: '..order) end local input = {} local source = {} for i, name in ipairs(order) do for cls, objs in pairs(imported[name]) do if not util.contains({'description', 'import', 'after', 'before'}, cls) then if not source[cls] then source[cls] = {} end if not input[cls] then input[cls] = objs for k, v in pairs(objs) do source[cls][k] = name end elseif objs[1] then local last = #input[cls] util.extend(input[cls], objs) for i = 1,#objs do source[cls][last + i] = name end else for k, v in pairs(objs) do input[cls][k] = v source[cls][k] = name end end end end end return PolicyConfig.new(input, source, util.keys(imported)) end