summaryrefslogtreecommitdiff
path: root/example/lace-example.lua
blob: 668ce33a16aa946410b4d0b5e439ab1b1f0d851c (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
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
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
-- lace/example/lace-example.lua
--
-- Lua Access Control Engine -- Example usage
--
-- Copyright 2012 Daniel Silverstone <dsilvers@digital-scurf.org>
--
-- For licence terms, see COPYING
--

-- This is an example of how to implement Lace as your access
-- control engine.  We demonstrate all the steps necessary to
-- construct a compilation context, compile a ruleset and
-- execute it with various execution contexts.
--
-- Normally this would be spread across your program with the
-- ruleset compilation done during the evaluation of config
-- files and the execution done at the point of access control.
--
-- However, for the sake of simplicity, everything is done here
-- in one file, split into sections
--

--[ Application configuration ]--------------------------------------

local base_dir = (os.getenv("EXAMPLE_DIR") or "example")

--[ Utility functions used during execution ]------------------------

local function deep_copy(t, memo)
   if not memo then memo = {} end
   if memo[t] then return memo[t] end
   local ret = {}
   local kk, vv
   for k, v in pairs(t) do
      kk, vv = k, v
      if type(k) == "table" then
	 kk = deep_copy(k)
      end
      if type(v) == "table" then
	 vv = deep_copy(v)
      end
      ret[kk] = vv
   end
   return ret
end

--[ Preparing the compilation context ]------------------------------

local lace = require 'lace'

-- Loader
--
-- This function is used by Lace to load rulesets into memory.  Note
-- that it is not required to parse anything and may return errors if
-- it so requires.  It may also throw Lua errors, although that is
-- only suggested in the most extreme of circumstances.
--
-- Its inputs are the compilation context and the name of the rule to
-- be loaded.  This is used both for initial loads (unless the ruleset
-- is given to lace.compiler.compile() and also for include statements.
--
-- If the ruleset cannot be found, the loader is required to return a
-- lace error, not throw a Lua error.  Throwing a lua error will
-- always stop compilation.  However returning a lace error can allow
-- compilation to continue if the include is an optional one.  The
-- returned lace error should contain a reference to word 1 which is
-- the name of the ruleset to be loaded.
--
-- On successful load of content, the loader should return the "real"
-- name of the loaded ruleset (used for further errors etc) and the
-- content of the ruleset as a string.
--
local function lace_loader(comp_ctx, name)
   local fname = base_dir .. "/" .. name .. ".rules"
   local fh = io.open(fname, "r")
   if not fh then
      return lace.error.error("Unable to find ruleset", {1})
   end
   local content = fh:read("*a")
   fh:close()
   return fname, content
end

-- Control types
--
-- Control types are used by Lace to allow the ruleset to define
-- behaviours and thus they provide the primary functionality of the
-- access control system.
--
-- Lace provides two control types by default, 'anyof' and 'allof'
-- which are simple combinators which require that any of their
-- arguments are true, or all of them are true, respectively.
--
-- Anything else which takes a list of defined rules behaves with the
-- allof behaviour.  So an 'allow' with multiple rules requires that
-- they all be true in order to allow the access.
--
-- Here we define a simple equality control type used in our example
-- rulesets.  Quite simply a control type must return true or false in
-- case of the match succeeding or failing.  If instead they have an
-- error for some reason, they should return a lace error with the
-- nil-error indicator set instead.
--
local function equality_control_type_run(exec_ctx, key, value)
   if exec_ctx[key] == nil then
      return lace.error.error("Key " .. key .. " not found", {}, true)
   end

   return exec_ctx[key] == value
end
--
-- The control type compiler though should return a table with the
-- information required to run the control object at execution time.
--
-- Required information is the function to call and the arguments to
-- pass it.  Everything else will be acquired from the execution
-- context at runtime.
--
-- If the compilation of the control type fails then this function
-- should return a lace error indicating what is at fault.  If the
-- function instead raises a lua error then compilation will cease,
-- but a less useful (to the user) error will be returned to the
-- caller.
--
local function equality_control_type(comp_ctx, eq, key, value, extra)
   assert(eq == "equals", "Somehow the equals control type was called for something else")
   if key == nil then
      return lace.error.error("Expected a key for equality check", {1})
   end
   if value == nil then
      return lace.error.error("Got a key, but expected a value", {1,2})
   end
   if extra ~= nil then
      return lace.error.error("Unexpected extra content", {4})
   end
   return {
      fn = equality_control_type_run,
      args = { key, value }
   }
end

-- We now have the minimum necessary to construct a compilation
-- context which can then be used to compile our ruleset.  To do this,
-- we construct a table whose _lace entry contains our loader and our
-- control types.
--
local template_compilation_context = {
   _lace = {
      loader = lace_loader,
      controltype = {
	 equals = equality_control_type,
      },
   },
}


--[ Compiling a ruleset (during config load) ]-----------------------

-- Compiling a ruleset is as simple as calling lace.compiler.compile()
-- and handing in a compilation context and the name of the ruleset to
-- load.  In order to be safe to compile multiple rulesets, you should
-- always be sure to copy a fresh compilation context for use.
--
-- Note: rulesets are loaded completely at compile time.  If there is
-- something in your ruleset which depends on access-time behaviour
-- then you should either changing how your rulesets will work, or
-- else you must compile the ruleset at access time which could be
-- wasteful.
local comp_ctx = deep_copy(template_compilation_context)
local ruleset, msg = lace.compiler.compile(comp_ctx, "NOTFOUND")

-- When the compilation fails, the 'ruleset' return will be false and
-- the msg return will be a multiline string indicating the error.
assert(ruleset == false, "Compilation somehow succeeded unexpectedly")
print ">>> Message for a not-found compilation attempt"
print(msg)
print "<<<"

-- So let's try again with a ruleset which should exist
comp_ctx = deep_copy(template_compilation_context)
-- Since we're sure it should exist, let's assert it
ruleset = assert(lace.compiler.compile(comp_ctx, "example"))

--[ Running the ruleset (during user access) ]-----------------------

-- Running the engine
--
-- Since to run the access control engine we need an execution context
-- we will put one together.  In a real application the execution
-- context will contain information pertaining to the access being
-- tested.  For this example, it will simply contain enough data to
-- either pass or fail the example ruleset.

-- Generate an execution context
--
local function gen_exec_ctx(want_to_pass)
   return {
      want_to_pass = want_to_pass and "yes" or "no"
   }
end

-- Running the ruleset
--
-- Here's an example of running a ruleset.  To run a ruleset you call
-- lace.engine.run() passing the ruleset and the execution context.
--
-- If the ruleset errors in any way, result will be false and msg will
-- be a message to give back to the user or developer.
--
local result, msg = lace.engine.run(ruleset, {})
assert(result == false, "Ruleset should have errored")
print ">>> Error from a control type failing somehow"
print(msg)
print "<<<"

-- If the ruleset succeeds, result will be one of 'allow' or 'deny'
-- and the message will be the message to give to the user if
-- necessary and log otherwise.
result, msg = lace.engine.run(ruleset, gen_exec_ctx(true))
print("should be ok", result, msg)

-- Note that even if the ruleset denies access, that's a successful
-- running of the ruleset, so you can test for denial and act
-- appropriately.
result, msg = lace.engine.run(ruleset, gen_exec_ctx(false))
print("should fail", result, msg)