blob: f7af1dc5c3835e8a24efd0d798ac7711b47f5971 [file] [log] [blame]
Sapan Bhatia3e3c1cd2017-07-15 01:35:44 -04001import astunparse
2import ast
3import random
4import string
Sapan Bhatia5ea307d2017-07-19 00:13:21 -04005import jinja2
Sapan Bhatia3e3c1cd2017-07-15 01:35:44 -04006from plyxproto.parser import *
7import pdb
8
Sapan Bhatia5ea307d2017-07-19 00:13:21 -04009BINOPS = ['|', '&', '->']
Sapan Bhatia3e3c1cd2017-07-15 01:35:44 -040010QUANTS = ['exists', 'forall']
11
12
13class PolicyException(Exception):
14 pass
15
16
17class AutoVariable:
18 def __init__(self, base):
19 self.base = base
20
21 def __iter__(self):
22 self.idx = 0
23 return self
24
25 def next(self):
26 var = 'i%d' % self.idx
27 self.idx += 1
28 return var
29
30
31def gen_random_string():
32 return ''.join(random.choice(string.ascii_lowercase + string.digits) for _ in range(5))
33
34
35class FOL2Python:
36 def __init__(self, context_map=None):
37 # This will produce i0, i1, i2 etc.
38 self.loopvar = iter(AutoVariable('i'))
39 self.verdictvar = iter(AutoVariable('result'))
40
41 self.loop_variable = self.loopvar.next()
42 self.verdict_variable = self.verdictvar.next()
43 self.context_map = context_map
44
45 if not self.context_map:
46 self.context_map = {'user': 'self', 'obj': 'obj'}
47
48 def loop_next(self):
49 self.loop_variable = self.loopvar.next()
50
51 def verdict_next(self):
52 self.verdict_variable = self.verdictvar.next()
53
54 def gen_enumerate(self, fol):
55 pass
56
57 def format_term_for_query(self, model, term, django=False):
58 if term.startswith(model + '.'):
59 term = term[len(model) + 1:]
60 if django:
61 term = term.replace('.', '__')
62 else:
63 term = '__elt' + '.' + term
64 return term
65
66 def fol_to_python_filter(self, model, e, django=False, negate=False):
67 try:
68 (k, v), = e.items()
69 except AttributeError:
70 return [self.format_term_for_query(model, e)]
71
72 if django:
73 if negate:
74 # De Morgan's negation
75 q_bracket = '~Q(%s)'
76 or_expr = ','
77 and_expr = '|'
78 else:
79 q_bracket = 'Q(%s)'
80 or_expr = '|'
81 and_expr = ','
82 else:
83 if negate:
84 # De Morgan's negation
85 q_bracket = 'not %s'
86 or_expr = ' and '
87 and_expr = ' or '
88 else:
89 q_bracket = '%s'
90 or_expr = ' or '
91 and_expr = ' and '
92
Sapan Bhatia5ea307d2017-07-19 00:13:21 -040093 if k in ['=','in']:
Sapan Bhatia3e3c1cd2017-07-15 01:35:44 -040094 v = [self.format_term_for_query(
95 model, term, django=django) for term in v]
96 if django:
Sapan Bhatia5ea307d2017-07-19 00:13:21 -040097 operator_map = {'=':' = ','in':'__in'}
Sapan Bhatia3e3c1cd2017-07-15 01:35:44 -040098 else:
Sapan Bhatia5ea307d2017-07-19 00:13:21 -040099 operator_map = {'=':' == ','in':'in'}
100 operator = operator_map[k]
Sapan Bhatia3e3c1cd2017-07-15 01:35:44 -0400101 return [q_bracket % operator.join(v)]
102 elif k == '|':
103 components = [self.fol_to_python_filter(
104 model, x, django=django).pop() for x in v]
105 return [or_expr.join(components)]
106 elif k == '&':
107 components = [self.fol_to_python_filter(
108 model, x, django=django).pop() for x in v]
109 return [and_expr.join(components)]
Sapan Bhatia5ea307d2017-07-19 00:13:21 -0400110 elif k == '->':
Sapan Bhatia3e3c1cd2017-07-15 01:35:44 -0400111 components = [self.fol_to_python_filter(
112 model, x, django=django).pop() for x in v]
113 return ['~%s | %s' % (components[0], components[1])]
114
115 """ Convert a single leaf node from a string
116 to an AST"""
117
118 def str_to_ast(self, s):
119 ast_module = ast.parse(s)
120 return ast_module.body[0]
121
122 def hoist_constants(self, fol, var=None):
123 try:
124 (k, v), = fol.items()
125 except AttributeError:
126 k = 'term'
127 v = fol
128
Sapan Bhatiad3fcb662017-07-25 21:13:48 -0400129 if k in ['python', 'policy'] :
Sapan Bhatia3e3c1cd2017-07-15 01:35:44 -0400130 # Tainted, don't optimize
131 if var:
132 return {'hoist': []}
133 else:
134 return fol
135
136 if var:
137 if k == 'term':
138 if not v.startswith(var):
139 return {'hoist': ['const', fol], 'result': 'True'}
140 else:
141 return {'hoist': [], 'result': fol}
Sapan Bhatia5ea307d2017-07-19 00:13:21 -0400142 elif k in ['=', 'in']:
Sapan Bhatia3e3c1cd2017-07-15 01:35:44 -0400143 lhs, rhs = v
144 if not lhs.startswith(var) and not rhs.startswith(var):
Sapan Bhatia5ea307d2017-07-19 00:13:21 -0400145 return {'hoist': [k, fol], 'result': 'True'} # XXX
Sapan Bhatia3e3c1cd2017-07-15 01:35:44 -0400146 else:
147 return {'hoist': [], 'result': fol}
148 elif k in BINOPS:
149 lhs, rhs = v
150 rlhs = self.hoist_constants(lhs, var)
151 rrhs = self.hoist_constants(rhs, var)
152
Sapan Bhatia5ea307d2017-07-19 00:13:21 -0400153 if rlhs['hoist'] and rrhs['hoist'] and rlhs['result']=='True' and llhs['result']=='True':
Sapan Bhatia3e3c1cd2017-07-15 01:35:44 -0400154 return {'hoist': ['=', fol], 'result': 'True'}
155 elif rlhs['hoist']:
156 return {'hoist': [k, lhs], 'result': rhs}
157 elif rrhs['hoist']:
158 return {'hoist': [k, rhs], 'result': lhs}
159 else:
160 return {'hoist': [], 'result': fol}
161
162 elif k in QUANTS:
163 var2, expr = v
164 result = self.hoist_constants(expr, var2)
165 if result['hoist']:
166 if result['result'] == 'True':
167 return {'hoist': ['const'], 'result': result['hoist'][1]}
168 elif result['hoist'][0] in BINOPS:
169 return {'hoist': ['const'], 'result': {result['hoist'][0]: [result['hoist'][1], {k: [var2, result['result']]}]}}
170 else:
171 return {'hoist': ['const'], 'result': {k: [var2, result['result']]}}
172 else:
173 result = self.hoist_constants(expr, var)
174 if result['result'] == 'True':
175 return {'hoist': ['&', fol], 'result': 'True'}
176 else:
177 return {'hoist': [], 'result': fol}
178 else:
179 return {'hoist': [], 'result': fol}
180 else:
181 if k in BINOPS:
182 lhs, rhs = v
183 rlhs = self.hoist_constants(lhs)
184 rrhs = self.hoist_constants(rhs)
185 return {k: [rlhs, rrhs]}
186 elif k in QUANTS:
187 var, expr = v
188 result = self.hoist_constants(expr, var)
189 if result['hoist']:
190 if result['result'] == 'True':
191 return result['hoist'][1]
192 elif result['hoist'][0] in BINOPS:
193 return {result['hoist'][0]: [result['hoist'][1], {k: [var, result['result']]}]}
194 else:
195 return {k: [var, result['result']]}
196 else:
197 return fol
198 else:
199 return fol
200
Sapan Bhatia5ea307d2017-07-19 00:13:21 -0400201 def gen_validation_function(self, fol, policy_name, message, tag):
202 if not tag:
203 tag = gen_random_string()
204
Sapan Bhatiad3fcb662017-07-25 21:13:48 -0400205 policy_function_name_template = 'policy_%s_' + '%(random_string)s' % {'random_string': tag}
206 policy_function_name = policy_function_name_template % policy_name
Sapan Bhatia5ea307d2017-07-19 00:13:21 -0400207 self.verdict_next()
208 function_str = """
209def %(fn_name)s(obj, ctx):
Sapan Bhatia9227b4d2017-07-25 23:14:48 -0400210 if not %(vvar)s: raise ValidationError("%(message)s".format(obj=obj, ctx=ctx))
Sapan Bhatia5ea307d2017-07-19 00:13:21 -0400211 """ % {'fn_name': policy_function_name, 'vvar': self.verdict_variable, 'message': message}
212
213 function_ast = self.str_to_ast(function_str)
Sapan Bhatiad3fcb662017-07-25 21:13:48 -0400214 policy_code = self.gen_test(policy_function_name_template, fol, self.verdict_variable)
Sapan Bhatia5ea307d2017-07-19 00:13:21 -0400215
216
217 function_ast.body = [policy_code] + function_ast.body
218
219 return function_ast
220
Sapan Bhatia3e3c1cd2017-07-15 01:35:44 -0400221 def gen_test_function(self, fol, policy_name, tag):
222 if not tag:
223 tag = gen_random_string()
224
Sapan Bhatiad3fcb662017-07-25 21:13:48 -0400225 policy_function_name_template = 'policy_%s_' + '%(random_string)s' % {'random_string': tag}
226 policy_function_name = policy_function_name_template % policy_name
227
Sapan Bhatia3e3c1cd2017-07-15 01:35:44 -0400228 self.verdict_next()
229 function_str = """
230def %(fn_name)s(obj, ctx):
231 return %(vvar)s
232 """ % {'fn_name': policy_function_name, 'vvar': self.verdict_variable}
233
234 function_ast = self.str_to_ast(function_str)
Sapan Bhatiad3fcb662017-07-25 21:13:48 -0400235 policy_code = self.gen_test(policy_function_name_template, fol, self.verdict_variable)
Sapan Bhatia3e3c1cd2017-07-15 01:35:44 -0400236
237 function_ast.body = [policy_code] + function_ast.body
238
Sapan Bhatia5ea307d2017-07-19 00:13:21 -0400239 return function_ast
Sapan Bhatia3e3c1cd2017-07-15 01:35:44 -0400240
Sapan Bhatiad3fcb662017-07-25 21:13:48 -0400241 def gen_test(self, fn_template, fol, verdict_var, bindings=None):
Sapan Bhatia3e3c1cd2017-07-15 01:35:44 -0400242 if isinstance(fol, str):
243 return self.str_to_ast('%(verdict_var)s = %(constant)s' % {'verdict_var': verdict_var, 'constant': fol})
244
245 (k, v), = fol.items()
246
Sapan Bhatiad3fcb662017-07-25 21:13:48 -0400247 if k == 'policy':
248 policy_name, object_name = v
249
250 policy_fn = fn_template % policy_name
251 call_str = """
252%(verdict_var)s = %(policy_fn)s(obj.%(object_name)s, ctx)
253 """ % {'verdict_var': self.verdict_variable, 'policy_fn': policy_fn, 'object_name': object_name}
254
255 call_ast = self.str_to_ast(call_str)
256 return call_ast
Sapan Bhatia5ea307d2017-07-19 00:13:21 -0400257 if k == 'python':
258 try:
259 expr_ast = self.str_to_ast(v)
260 except SyntaxError:
261 raise PolicyException('Syntax error in %s' % v)
262
263 if not isinstance(expr_ast, ast.Expr):
264 raise PolicyException(
265 '%s is not an expression' % expr_ast)
266
267 assignment_str = """
268%(verdict_var)s = (%(escape_expr)s)
269 """ % {'verdict_var': self.verdict_variable, 'escape_expr': v}
270
271 assignment_ast = self.str_to_ast(assignment_str)
272 return assignment_ast
273 elif k == 'not':
274 top_vvar = self.verdict_variable
275 self.verdict_next()
276 sub_vvar = self.verdict_variable
Sapan Bhatiad3fcb662017-07-25 21:13:48 -0400277 block = self.gen_test(fn_template, v, sub_vvar)
Sapan Bhatia5ea307d2017-07-19 00:13:21 -0400278 assignment_str = """
279%(verdict_var)s = not (%(subvar)s)
280 """ % {'verdict_var': top_vvar, 'subvar': sub_vvar}
281
282 assignment_ast = self.str_to_ast(assignment_str)
283
284 return ast.Module(body=[block, assignment_ast])
285 elif k in ['=','in']:
Sapan Bhatia3e3c1cd2017-07-15 01:35:44 -0400286 # This is the simplest case, we don't recurse further
287 # To use terms that are not simple variables, use
288 # the Python escape, e.g. {{ slice.creator is not None }}
289 lhs, rhs = v
290
291 assignments = []
292
293 try:
294 for t in lhs, rhs:
295 py_expr = t['python']
296
297 self.verdict_next()
298 vv = self.verdict_variable
299
300 try:
301 expr_ast = self.str_to_ast(py_expr)
302 except SyntaxError:
303 raise PolicyException('Syntax error in %s' % v)
304
305 if not isinstance(expr_ast, ast.Expr):
306 raise PolicyException(
307 '%s is not an expression' % expr_ast)
308
309 assignment_str = """
310%(verdict_var)s = (%(escape_expr)s)
311 """ % {'verdict_var': vv, 'escape_expr': py_expr}
312
313 if t == lhs:
314 lhs = vv
315 else:
316 rhs = vv
317
318 assignment_ast = self.str_to_ast(assignment_str)
319 assignments.append(assignment_ast)
320 except TypeError:
321 pass
322
Sapan Bhatia5ea307d2017-07-19 00:13:21 -0400323 if k=='=':
324 operator='=='
325 elif k=='in':
326 operator='in'
327
Sapan Bhatia3e3c1cd2017-07-15 01:35:44 -0400328 comparison_str = """
Sapan Bhatia5ea307d2017-07-19 00:13:21 -0400329%(verdict_var)s = (%(lhs)s %(operator)s %(rhs)s)
330 """ % {'verdict_var': verdict_var, 'lhs': lhs, 'rhs': rhs, 'operator':operator}
Sapan Bhatia3e3c1cd2017-07-15 01:35:44 -0400331
332 comparison_ast = self.str_to_ast(comparison_str)
Sapan Bhatia3e3c1cd2017-07-15 01:35:44 -0400333 combined_ast = ast.Module(body=assignments + [comparison_ast])
Sapan Bhatia5ea307d2017-07-19 00:13:21 -0400334
Sapan Bhatia3e3c1cd2017-07-15 01:35:44 -0400335 return combined_ast
336 elif k in BINOPS:
337 lhs, rhs = v
338
Sapan Bhatia5ea307d2017-07-19 00:13:21 -0400339 top_vvar = verdict_var
Sapan Bhatia3e3c1cd2017-07-15 01:35:44 -0400340
341 self.verdict_next()
342 lvar = self.verdict_variable
343
344 self.verdict_next()
345 rvar = self.verdict_variable
346
Sapan Bhatiad3fcb662017-07-25 21:13:48 -0400347 lblock = self.gen_test(fn_template, lhs, lvar)
348 rblock = self.gen_test(fn_template, rhs, rvar)
Sapan Bhatia3e3c1cd2017-07-15 01:35:44 -0400349
350 invert = ''
351 if k == '&':
352 binop = 'and'
353 elif k == '|':
354 binop = 'or'
Sapan Bhatia5ea307d2017-07-19 00:13:21 -0400355 elif k == '->':
Sapan Bhatia3e3c1cd2017-07-15 01:35:44 -0400356 binop = 'or'
357 invert = 'not'
358
359 binop_str = """
360%(verdict_var)s = %(invert)s %(lvar)s %(binop)s %(rvar)s
361 """ % {'verdict_var': top_vvar, 'invert': invert, 'lvar': lvar, 'binop': binop, 'rvar': rvar}
362
363 binop_ast = self.str_to_ast(binop_str)
364
365 combined_ast = ast.Module(body=[lblock, rblock, binop_ast])
366 return combined_ast
367 elif k == 'exists':
368 # If the variable starts with a capital letter,
369 # we assume that it is a model. If it starts with
370 # a small letter, we assume it is an enumerable
371 #
372 # We do not support nested exists yet. FIXME.
373
374 var, expr = v
375
376 if var.istitle():
377 f = self.fol_to_python_filter(var, expr, django=True)
378 entry = f.pop()
379
380 python_str = """
Sapan Bhatia9227b4d2017-07-25 23:14:48 -0400381%(verdict_var)s = not not %(model)s.objects.filter(%(query)s)
Sapan Bhatia3e3c1cd2017-07-15 01:35:44 -0400382 """ % {'verdict_var': verdict_var, 'model': var, 'query': entry}
383
384 python_ast = ast.parse(python_str)
385 else:
386 f = self.fol_to_python_filter(var, expr, django=False)
387 entry = f.pop()
388
389 python_str = """
390%(verdict_var)s = filter(lambda __elt:%(query)s, %(model)s)
391 """ % {'verdict_var': verdict_var, 'model': var, 'query': entry}
392
393 python_ast = ast.parse(python_str)
394
395 return python_ast
396 elif k=='forall':
397 var, expr = v
398
399 if var.istitle():
400 f = self.fol_to_python_filter(var, expr, django=True, negate = True)
401 entry = f.pop()
402
403 self.verdict_next()
404 vvar = self.verdict_variable
405
406 python_str = """
Sapan Bhatia9227b4d2017-07-25 23:14:48 -0400407%(verdict_var)s = not not %(model)s.objects.filter(%(query)s)
Sapan Bhatia3e3c1cd2017-07-15 01:35:44 -0400408 """ % {'verdict_var': vvar, 'model': var, 'query': entry}
409
410 python_ast = ast.parse(python_str)
411 else:
412 f = self.fol_to_python_filter(var, expr, django=False, negate = True)
413 entry = f.pop()
414
415 python_str = """
416%(verdict_var)s = next(elt for elt in %(model)s if %(query)s)
417 """ % {'verdict_var': vvar, 'model': var, 'query': entry}
418
419 python_ast = ast.parse(python_str)
420
421 negate_str = """
422%(verdict_var)s = not %(vvar)s
423 """ % {'verdict_var': verdict_var, 'vvar': vvar}
424
425 negate_ast = ast.parse(negate_str)
426
427 return ast.Module(body=[python_ast, negate_ast])
428
Sapan Bhatia5ea307d2017-07-19 00:13:21 -0400429def xproto_fol_to_python_test(policy, fol, model, tag=None):
430 if isinstance(fol, jinja2.Undefined):
431 raise Exception('Could not find policy:', policy)
432
Sapan Bhatia3e3c1cd2017-07-15 01:35:44 -0400433 f2p = FOL2Python()
434 fol = f2p.hoist_constants(fol)
Sapan Bhatia5ea307d2017-07-19 00:13:21 -0400435 a = f2p.gen_test_function(fol, policy, tag='enforcer')
436 return astunparse.unparse(a)
Sapan Bhatia3e3c1cd2017-07-15 01:35:44 -0400437
Sapan Bhatia5ea307d2017-07-19 00:13:21 -0400438def xproto_fol_to_python_validator(policy, fol, model, message, tag=None):
439 if isinstance(fol, jinja2.Undefined):
440 raise Exception('Could not find policy:', policy)
441
442 f2p = FOL2Python()
443 fol = f2p.hoist_constants(fol)
444 a = f2p.gen_validation_function(fol, policy, message, tag='validator')
445
446 return astunparse.unparse(a)
Sapan Bhatia3e3c1cd2017-07-15 01:35:44 -0400447
448def main():
449 while True:
450 inp = raw_input()
451 fol_lexer = lex.lex(module=FOLLexer())
452 fol_parser = yacc.yacc(module=FOLParser(), start='goal')
453
454 val = fol_parser.parse(inp, lexer=fol_lexer)
Sapan Bhatia5ea307d2017-07-19 00:13:21 -0400455 a = xproto_fol_to_python_test('pol', val, 'output', 'Test')
Sapan Bhatia3e3c1cd2017-07-15 01:35:44 -0400456 print a
457
458
459if __name__ == "__main__":
460 main()