policy.lua 5.35 KB
Newer Older
1 2
--[[
Policy file handling for Alpine Wall
3
Copyright (C) 2012-2014 Kaarle Ritvanen
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
4
See LICENSE file for license details
5 6 7 8 9 10 11
]]--

module(..., package.seeall)

require 'json'
require 'lfs'

12
require 'awall.dependency'
13
local class = require('awall.object').class
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
14
local raise = require('awall.uerror').raise
15

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
16
local util = require('awall.util')
17
local contains = util.contains
18
local listpairs = util.listpairs
19 20


21
local PolicyConfig = class()
22

23
function PolicyConfig:init(data, source, policies)
24
   self.data = data
25
   self.source = source
26
   self.policies = policies
27 28
end

29
function PolicyConfig:expand()
30

31 32
   local function expand(value)
      if type(value) == 'table' then return util.map(value, expand) end
33

34 35 36
      local visited = {}
      local pattern = '%$(%a[%w_]*)'
      
37 38 39
      while type(value) == 'string' do
	 local si, ei, name = value:find(pattern)
	 if not si then break end
40
	 
41
	 if contains(visited, name) then
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
42
	    raise('Circular variable definition: '..name)
43 44 45 46
	 end
	 table.insert(visited, name)
	 
	 local var = self.data.variable[name]
47
	 if var == nil then raise('Invalid variable reference: '..name) end
48
	 
49
	 if si == 1 and ei == value:len() then value = var
50
	 elseif contains({'number', 'string'}, type(var)) then
51
	    value = value:sub(1, si - 1)..var..value:sub(ei + 1, -1)
52
	 else
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
53
	    raise('Attempted to concatenate complex variable: '..name)
54 55
	 end
      end
56

57 58
      if value == '' then return end
      return value
59 60
   end

61
   return expand(self.data)
62 63 64
end


65
local Policy = class()
66

67
function Policy:init() self.enabled = self.type == 'mandatory' end
68

69 70 71
function Policy:load()
   local file = io.open(self.path)
   if not file then raise('Unable to read policy file '..self.path) end
72

73 74 75
   local data = ''
   for line in file:lines() do data = data..line end
   file:close()
76

77 78 79 80
   local success, res = pcall(json.decode, data)
   if success then return res end
   raise(res..' while parsing '..self.path)
end
81

82 83 84
function Policy:checkoptional()
   if self.type ~= 'optional' then raise('Not an optional policy: '..name) end
end
85

86 87 88 89 90
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
91

92 93 94 95
function Policy:disable()
   self:checkoptional()
   if not self.enabled then raise('Policy already disabled: '..self.name) end
   assert(os.remove(self.confdir..'/'..self.fname))
96 97 98
end


99 100 101 102 103
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'}
}
104

105
PolicySet = class()
106

107 108 109
function PolicySet:init(dirs)
   local confdir = (dirs.mandatory or defdirs.mandatory)[1]
   self.policies = {}
110

111 112 113
   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
114
	    local si, ei, name = fname:find('^([%w-]+)%.json$')
115 116
	    if name then
	       local pol = self.policies[name]
117

118
	       local path = dir..'/'..fname
119
	       if path:sub(1, 1) ~= '/' then
120 121
		  path = lfs.currentdir()..'/'..path
	       end
122

123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148
	       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
149 150 151
end


152
function PolicySet:load()
153

154
   local imported = {['%defaults']={}}
155 156 157
   
   local function require(policy)
      if imported[policy.name] then return end
158

159 160
      local data = policy:load()
      imported[policy.name] = data
161

162 163 164 165 166 167 168 169 170
      if not data.after then
	 data.after = {}
	 for _, name in listpairs(data.import) do
	    if not contains(data.before, name) then
	       table.insert(data.after, name)
	    end
	 end
      end

171
      if not contains(data.before, '%defaults') then
172
	 data.after = util.list(data.after)
173 174 175
	 table.insert(data.after, '%defaults')
      end

176
      for i, name in listpairs(data.import) do
177
	 if name:sub(1, 1) ~= '%' then
178 179 180 181 182
	    local pol = self.policies[name]
	    if not pol then
	       raise('Invalid policy reference from '..policy.name..': '..name)
	    end
	    require(pol)
183
	 end
184
      end
185
   end
186

187 188 189
   for name, policy in pairs(self.policies) do
      if policy.enabled then require(policy) end
   end
190 191


192
   local order = awall.dependency.order(imported)
193
   if type(order) ~= 'table' then
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
194
      raise('Circular ordering directives: '..order)
195
   end
196

197

198 199
   local input = {}
   local source = {}
200

201
   for i, name in ipairs(order) do
202
      for cls, objs in pairs(imported[name]) do
203 204 205 206
	 if not contains(
	    {'description', 'import', 'after', 'before'},
	    cls
	 ) then
207 208 209 210 211 212 213 214 215 216 217
	    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

218
	    else
219 220 221 222
	       for k, v in pairs(objs) do
		  input[cls][k] = v
		  source[cls][k] = name
	       end
223 224 225 226 227
	    end
	 end
      end
   end

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
228
   return PolicyConfig(input, source, util.keys(imported))
229
end