Commit c609c91f authored by Kaarle Ritvanen's avatar Kaarle Ritvanen

overhaul of policy file handling

private policies which can be imported but not directly enabled
show more information about policies with awall list -a, fixes #1467
override policy file paths using AWALL_PATH_* environment variables
parent 8a4a82b0
# Installer Makefile for Alpine Wall
# Copyright (C) 2012 Kaarle Ritvanen
# Copyright (C) 2012-2013 Kaarle Ritvanen
# See LICENSE file for license details
ROOT_DIR := /
......@@ -42,6 +42,7 @@ $(eval $(call rename,sample-policy.json,$(poldir)/sample/sample-policy.json,644)
$(eval $(call mkdir,$(confdir)))
$(eval $(call mkdir,$(confdir)/optional))
$(eval $(call mkdir,$(poldir)/optional))
$(eval $(call mkdir,$(poldir)/private))
$(eval $(call mkdir,var/run/awall))
install: $(foreach f,$(files),$(ROOT_DIR)/$(f))
......
......@@ -11,9 +11,6 @@ require 'lfs'
require 'signal'
require 'stringy'
short_opts = 'fo:V'
long_opts = {force='f', ['output-dir']='o', verify='V'}
function help()
io.stderr:write([[
Alpine Wall
......@@ -54,7 +51,7 @@ Enable/disable optional policies:
awall {enable|disable} <policy>...
List optional policies:
awall list
awall list [-a|--all]
The 'enabled' status means that the policy has been enabled by the
user. The 'disabled' status means that the policy is not in
......@@ -62,6 +59,9 @@ List optional policies:
enabled by the user but is in use because it is required by
another policy which is in use.
Normally, the command lists only optional policies. Specifying
--all makes it list all policies and more information about them.
Dump variable and zone definitions:
awall dump [level]
......@@ -71,18 +71,6 @@ Dump variable and zone definitions:
os.exit(1)
end
params = {}
if stringy.endswith(arg[0], '/awall-cli') then
basedir = string.sub(arg[0], 1, -11)
params.i = {basedir..'/json'}
params.I = {}
short_opts = short_opts..'i:I:'
long_opts['input-dir'] = 'i'
long_opts['import-path'] = 'I'
end
if not arg[1] then help() end
if not stringy.startswith(arg[1], '-') then
......@@ -90,12 +78,18 @@ if not stringy.startswith(arg[1], '-') then
table.remove(arg, 1)
end
opts, opind = alt_getopt.get_opts(arg, short_opts, long_opts)
opts, opind = alt_getopt.get_opts(
arg,
'afo:V',
{all='a', force='f', ['output-dir']='o', verify='V'}
)
for switch, value in pairs(opts) do
if switch == 'f' then force = true
if switch == 'a' then all = true
elseif switch == 'f' then force = true
elseif switch == 'c' then verbose = true
elseif switch == 'V' then verify = true
elseif switch == 'o' then outputdir = value
else table.insert(params[switch], value) end
else assert(false) end
end
if not mode then
......@@ -111,25 +105,63 @@ if not util.contains({'translate', 'activate', 'fallback', 'flush',
'enable', 'disable', 'list', 'dump'},
mode) then help() end
pol_paths = {}
for i, cls in ipairs{'mandatory', 'optional', 'private'} do
path = os.getenv('AWALL_PATH_'..string.upper(cls))
if path then pol_paths[cls] = util.split(path, ':') end
end
require 'awall.uerror'
if stringy.endswith(arg[0], '/awall-cli') then
basedir = string.sub(arg[0], 1, -11)
if not pol_paths.mandatory then
pol_paths.mandatory = {'/etc/awall'}
end
table.insert(pol_paths.mandatory, basedir..'/json')
end
local uerror = require('awall.uerror')
if not awall.uerror.call(
if not uerror.call(
function()
require 'awall'
policyset = awall.PolicySet.new(params.i, params.I)
policyset = awall.PolicySet.new(pol_paths)
if mode == 'list' then
util.printtabular(policyset:list())
imported = policyset:load().policies
data = {}
for i, name in util.sortedkeys(policyset.policies) do
policy = policyset.policies[name]
if all or policy.type == 'optional' then
if policy.enabled then status = 'enabled'
elseif util.contains(imported, name) then status = 'required'
else status = 'disabled' end
polinfo = {name, status, policy:load().description}
if all then
table.insert(polinfo, 2, policy.type)
table.insert(polinfo, 4, policy.path)
end
table.insert(data, polinfo)
end
end
util.printtabular(data)
os.exit()
end
if util.contains({'disable', 'enable'}, mode) then
if opind > #arg then help() end
repeat
policyset[mode](policyset, arg[opind])
name = arg[opind]
policy = policyset.policies[name]
if not policy then uerror.raise('No such policy: '..name) end
policy[mode](policy)
opind = opind + 1
until opind > #arg
os.exit()
......@@ -246,7 +278,7 @@ if not awall.uerror.call(
os.exit(1)
end
if awall.uerror.call(config.activate, config) then
if uerror.call(config.activate, config) then
if not force then
io.stderr:write('New firewall configuration activated\n')
......
......@@ -8,15 +8,14 @@ module(..., package.seeall)
require 'json'
require 'lfs'
require 'lpc'
require 'awall.dependency'
require 'awall.object'
local class = require('awall.object').class
local raise = require('awall.uerror').raise
local util = require('awall.util')
local PolicyConfig = awall.object.class()
local PolicyConfig = class()
function PolicyConfig:init(data, source, policies)
self.data = data
......@@ -59,102 +58,115 @@ function PolicyConfig:expand()
end
local Policy = class()
local function open(name, dirs)
if not string.match(name, '^[%w-]+$') then
raise('Invalid characters in policy name: '..name)
end
for i, dir in ipairs(dirs) do
local path = dir..'/'..name..'.json'
file = io.open(path)
if file then return file, path end
end
end
function Policy:init() self.enabled = self.type == 'mandatory' end
local function find(name, dirs)
local file, path = open(name, dirs)
if file then file:close() end
return path
end
function Policy:load()
local file = io.open(self.path)
if not file then raise('Unable to read policy file '..self.path) end
local function list(dirs)
local allnames = {}
local res = {}
for i, dir in ipairs(dirs) do
local names = {}
local paths = {}
local data = ''
for line in file:lines() do data = data..line end
file:close()
for fname in lfs.dir(dir) do
local si, ei, name = string.find(fname, '^([%w-]+)%.json$')
if name then
if util.contains(allnames, name) then
raise('Duplicate policy name: '..name)
end
table.insert(allnames, name)
local success, res = pcall(json.decode, data)
if success then return res end
raise(res..' while parsing '..self.path)
end
table.insert(names, name)
paths[name] = dir..'/'..fname
end
end
function Policy:checkoptional()
if self.type ~= 'optional' then raise('Not an optional policy: '..name) end
end
table.sort(names)
for i, name in ipairs(names) do
table.insert(res, {name, paths[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
return res
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
PolicySet = awall.object.class()
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'}
}
function PolicySet:init(confdirs, importdirs)
self.autodirs = confdirs or {'/usr/share/awall/mandatory', '/etc/awall'}
self.confdir = self.autodirs[#self.autodirs]
self.importdirs = importdirs or {'/usr/share/awall/optional',
'/etc/awall/optional'}
end
PolicySet = class()
function PolicySet:init(dirs)
local confdir = (dirs.mandatory or defdirs.mandatory)[1]
self.policies = {}
function PolicySet:loadJSON(name, fname)
local file
if fname then
file = io.open(fname)
else
file, fname = open(name, self.importdirs)
end
if not file then raise('Unable to read policy file '..fname) end
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 data = ''
for line in file:lines() do data = data..line end
file:close()
local path = dir..'/'..fname
if string.sub(path, 1, 1) ~= '/' then
path = lfs.currentdir()..'/'..path
end
local success, res = pcall(json.decode, data)
if success then return res end
raise(res..' while parsing '..fname)
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 policies = {}
local function require(name, fname)
if policies[name] then return end
local imported = {}
local function require(policy)
if imported[policy.name] then return end
local policy = self:loadJSON(name, fname)
policies[name] = policy
local data = policy:load()
imported[policy.name] = data
if not policy.after then policy.after = policy.import end
for i, iname in util.listpairs(policy.import) do require(iname) end
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 i, pol in ipairs(list(self.autodirs)) do require(unpack(pol)) end
for name, policy in pairs(self.policies) do
if policy.enabled then require(policy) end
end
local order = awall.dependency.order(policies)
local order = awall.dependency.order(imported)
if type(order) ~= 'table' then
raise('Circular ordering directives: '..order)
end
......@@ -164,7 +176,7 @@ function PolicySet:load()
local source = {}
for i, name in ipairs(order) do
for cls, objs in pairs(policies[name]) 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
......@@ -188,55 +200,5 @@ function PolicySet:load()
end
end
return PolicyConfig.new(input, source, util.keys(policies))
end
function PolicySet:findsymlink(name)
local symlink = find(name, {self.confdir})
if symlink and lfs.symlinkattributes(symlink).mode ~= 'link' then
raise('Not an optional policy: '..name)
end
return symlink
end
function PolicySet:enable(name)
if self:findsymlink(name) then raise('Policy already enabled: '..name)
else
local target = find(name, self.importdirs)
if not target then raise('Policy not found: '..name) end
if string.sub(target, 1, 1) ~= '/' then
target = lfs.currentdir()..'/'..target
end
local pid, stdin, stdout = lpc.run('ln', '-s', target, self.confdir)
stdin:close()
stdout:close()
assert(lpc.wait(pid) == 0)
end
end
function PolicySet:disable(name)
local symlink = self:findsymlink(name)
if not symlink then raise('Policy not enabled: '..name) end
assert(os.remove(symlink))
end
function PolicySet:list()
local imported = self:load().policies
local res = {}
for i, pol in ipairs(list(self.importdirs)) do
local name = pol[1]
local status
if self:findsymlink(name) then status = 'enabled'
elseif util.contains(imported, name) then status = 'required'
else status = 'disabled' end
table.insert(res,
{name, status, self:loadJSON(name, pol[2]).description})
end
return res
return PolicyConfig.new(input, source, util.keys(imported))
end
......@@ -7,6 +7,20 @@ See LICENSE file for license details
module(..., package.seeall)
function split(s, sep)
if s == '' then return {} end
local res = {}
while true do
local si, ei = string.find(s, sep, 1, true)
if not si then
table.insert(res, s)
return res
end
table.insert(res, string.sub(s, 1, si - 1))
s = string.sub(s, ei + 1, -1)
end
end
function list(var)
if not var then return {} end
if type(var) ~= 'table' then return {var} end
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment