[MERGE] trunk-coverage-event-ron (improve tests in module event)
[odoo/odoo.git] / openerp / tools / safe_eval.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #    Copyright (C) 2004-2010 OpenERP s.a. (<http://www.openerp.com>).
4 #
5 #    This program is free software: you can redistribute it and/or modify
6 #    it under the terms of the GNU Affero General Public License as
7 #    published by the Free Software Foundation, either version 3 of the
8 #    License, or (at your option) any later version.
9 #
10 #    This program is distributed in the hope that it will be useful,
11 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
12 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 #    GNU Affero General Public License for more details.
14 #
15 #    You should have received a copy of the GNU Affero General Public License
16 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
17 #
18 ##############################################################################
19
20 """
21 safe_eval module - methods intended to provide more restricted alternatives to
22                    evaluate simple and/or untrusted code.
23
24 Methods in this module are typically used as alternatives to eval() to parse
25 OpenERP domain strings, conditions and expressions, mostly based on locals
26 condition/math builtins.
27 """
28
29 # Module partially ripped from/inspired by several different sources:
30 #  - http://code.activestate.com/recipes/286134/
31 #  - safe_eval in lp:~xrg/openobject-server/optimize-5.0
32 #  - safe_eval in tryton http://hg.tryton.org/hgwebdir.cgi/trytond/rev/bbb5f73319ad
33 #  - python 2.6's ast.literal_eval
34
35 from opcode import HAVE_ARGUMENT, opmap, opname
36 from types import CodeType
37 import logging
38 import os
39
40 __all__ = ['test_expr', 'literal_eval', 'safe_eval', 'const_eval', 'ext_eval' ]
41
42 # The time module is usually already provided in the safe_eval environment
43 # but some code, e.g. datetime.datetime.now() (Windows/Python 2.5.2, bug
44 # lp:703841), does import time.
45 _ALLOWED_MODULES = ['_strptime', 'time']
46
47 _CONST_OPCODES = set(opmap[x] for x in [
48     'POP_TOP', 'ROT_TWO', 'ROT_THREE', 'ROT_FOUR', 'DUP_TOP','POP_BLOCK','SETUP_LOOP',
49     'BUILD_LIST', 'BUILD_MAP', 'BUILD_TUPLE',
50     'LOAD_CONST', 'RETURN_VALUE', 'STORE_SUBSCR'] if x in opmap)
51
52 _EXPR_OPCODES = _CONST_OPCODES.union(set(opmap[x] for x in [
53     'UNARY_POSITIVE', 'UNARY_NEGATIVE', 'UNARY_NOT',
54     'UNARY_INVERT', 'BINARY_POWER', 'BINARY_MULTIPLY',
55     'BINARY_DIVIDE', 'BINARY_FLOOR_DIVIDE', 'BINARY_TRUE_DIVIDE',
56     'BINARY_MODULO', 'BINARY_ADD', 'BINARY_SUBTRACT', 'BINARY_SUBSCR',
57     'BINARY_LSHIFT', 'BINARY_RSHIFT', 'BINARY_AND', 'BINARY_XOR',
58     'BINARY_OR'] if x in opmap))
59
60 _SAFE_OPCODES = _EXPR_OPCODES.union(set(opmap[x] for x in [
61     'STORE_MAP', 'LOAD_NAME', 'CALL_FUNCTION', 'COMPARE_OP', 'LOAD_ATTR',
62     'STORE_NAME', 'GET_ITER', 'FOR_ITER', 'LIST_APPEND', 'DELETE_NAME',
63     'JUMP_FORWARD', 'JUMP_IF_TRUE', 'JUMP_IF_FALSE', 'JUMP_ABSOLUTE',
64     'MAKE_FUNCTION', 'SLICE+0', 'SLICE+1', 'SLICE+2', 'SLICE+3',
65     # New in Python 2.7 - http://bugs.python.org/issue4715 :
66     'JUMP_IF_FALSE_OR_POP', 'JUMP_IF_TRUE_OR_POP', 'POP_JUMP_IF_FALSE',
67     'POP_JUMP_IF_TRUE', 'SETUP_EXCEPT', 'END_FINALLY'
68     ] if x in opmap))
69
70 _logger = logging.getLogger('safe_eval')
71
72 def _get_opcodes(codeobj):
73     """_get_opcodes(codeobj) -> [opcodes]
74
75     Extract the actual opcodes as a list from a code object
76
77     >>> c = compile("[1 + 2, (1,2)]", "", "eval")
78     >>> _get_opcodes(c)
79     [100, 100, 23, 100, 100, 102, 103, 83]
80     """
81     i = 0
82     opcodes = []
83     byte_codes = codeobj.co_code
84     while i < len(byte_codes):
85         code = ord(byte_codes[i])
86         opcodes.append(code)
87         if code >= HAVE_ARGUMENT:
88             i += 3
89         else:
90             i += 1
91     return opcodes
92
93 def test_expr(expr, allowed_codes, mode="eval"):
94     """test_expr(expression, allowed_codes[, mode]) -> code_object
95
96     Test that the expression contains only the allowed opcodes.
97     If the expression is valid and contains only allowed codes,
98     return the compiled code object.
99     Otherwise raise a ValueError, a Syntax Error or TypeError accordingly.
100     """
101     try:
102         if mode == 'eval':
103             # eval() does not like leading/trailing whitespace
104             expr = expr.strip()
105         code_obj = compile(expr, "", mode)
106     except (SyntaxError, TypeError):
107         _logger.debug('Invalid eval expression', exc_info=True)
108         raise
109     except Exception:
110         _logger.debug('Disallowed or invalid eval expression', exc_info=True)
111         raise ValueError("%s is not a valid expression" % expr)
112     for code in _get_opcodes(code_obj):
113         if code not in allowed_codes:
114             raise ValueError("opcode %s not allowed (%r)" % (opname[code], expr))
115     return code_obj
116
117
118 def const_eval(expr):
119     """const_eval(expression) -> value
120
121     Safe Python constant evaluation
122
123     Evaluates a string that contains an expression describing
124     a Python constant. Strings that are not valid Python expressions
125     or that contain other code besides the constant raise ValueError.
126
127     >>> const_eval("10")
128     10
129     >>> const_eval("[1,2, (3,4), {'foo':'bar'}]")
130     [1, 2, (3, 4), {'foo': 'bar'}]
131     >>> const_eval("1+2")
132     Traceback (most recent call last):
133     ...
134     ValueError: opcode BINARY_ADD not allowed
135     """
136     c = test_expr(expr, _CONST_OPCODES)
137     return eval(c)
138
139 def expr_eval(expr):
140     """expr_eval(expression) -> value
141
142     Restricted Python expression evaluation
143
144     Evaluates a string that contains an expression that only
145     uses Python constants. This can be used to e.g. evaluate
146     a numerical expression from an untrusted source.
147
148     >>> expr_eval("1+2")
149     3
150     >>> expr_eval("[1,2]*2")
151     [1, 2, 1, 2]
152     >>> expr_eval("__import__('sys').modules")
153     Traceback (most recent call last):
154     ...
155     ValueError: opcode LOAD_NAME not allowed
156     """
157     c = test_expr(expr, _EXPR_OPCODES)
158     return eval(c)
159
160
161 # Port of Python 2.6's ast.literal_eval for use under Python 2.5
162 SAFE_CONSTANTS = {'None': None, 'True': True, 'False': False}
163
164 try:
165     # first, try importing directly
166     from ast import literal_eval
167 except ImportError:
168     import _ast as ast
169
170     def _convert(node):
171         if isinstance(node, ast.Str):
172             return node.s
173         elif isinstance(node, ast.Num):
174             return node.n
175         elif isinstance(node, ast.Tuple):
176             return tuple(map(_convert, node.elts))
177         elif isinstance(node, ast.List):
178             return list(map(_convert, node.elts))
179         elif isinstance(node, ast.Dict):
180             return dict((_convert(k), _convert(v)) for k, v
181                         in zip(node.keys, node.values))
182         elif isinstance(node, ast.Name):
183             if node.id in SAFE_CONSTANTS:
184                 return SAFE_CONSTANTS[node.id]
185         raise ValueError('malformed or disallowed expression')
186
187     def parse(expr, filename='<unknown>', mode='eval'):
188         """parse(source[, filename], mode]] -> code object
189         Parse an expression into an AST node.
190         Equivalent to compile(expr, filename, mode, PyCF_ONLY_AST).
191         """
192         return compile(expr, filename, mode, ast.PyCF_ONLY_AST)
193
194     def literal_eval(node_or_string):
195         """literal_eval(expression) -> value
196         Safely evaluate an expression node or a string containing a Python
197         expression.  The string or node provided may only consist of the
198         following Python literal structures: strings, numbers, tuples,
199         lists, dicts, booleans, and None.
200
201         >>> literal_eval('[1,True,"spam"]')
202         [1, True, 'spam']
203
204         >>> literal_eval('1+3')
205         Traceback (most recent call last):
206         ...
207         ValueError: malformed or disallowed expression
208         """
209         if isinstance(node_or_string, basestring):
210             node_or_string = parse(node_or_string)
211         if isinstance(node_or_string, ast.Expression):
212             node_or_string = node_or_string.body
213         return _convert(node_or_string)
214
215 def _import(name, globals=None, locals=None, fromlist=None, level=-1):
216     if globals is None:
217         globals = {}
218     if locals is None:
219         locals = {}
220     if fromlist is None:
221         fromlist = []
222     if name in _ALLOWED_MODULES:
223         return __import__(name, globals, locals, level)
224     raise ImportError(name)
225
226 def safe_eval(expr, globals_dict=None, locals_dict=None, mode="eval", nocopy=False):
227     """safe_eval(expression[, globals[, locals[, mode[, nocopy]]]]) -> result
228
229     System-restricted Python expression evaluation
230
231     Evaluates a string that contains an expression that mostly
232     uses Python constants, arithmetic expressions and the
233     objects directly provided in context.
234
235     This can be used to e.g. evaluate
236     an OpenERP domain expression from an untrusted source.
237
238     Throws TypeError, SyntaxError or ValueError (not allowed) accordingly.
239
240     >>> safe_eval("__import__('sys').modules")
241     Traceback (most recent call last):
242     ...
243     ValueError: opcode LOAD_NAME not allowed
244
245     """
246     if isinstance(expr, CodeType):
247         raise ValueError("safe_eval does not allow direct evaluation of code objects.")
248
249     if '__subclasses__' in expr:
250        raise ValueError('expression not allowed (__subclasses__)')
251
252     if globals_dict is None:
253         globals_dict = {}
254
255     # prevent altering the globals/locals from within the sandbox
256     # by taking a copy.
257     if not nocopy:
258         # isinstance() does not work below, we want *exactly* the dict class
259         if (globals_dict is not None and type(globals_dict) is not dict) \
260             or (locals_dict is not None and type(locals_dict) is not dict):
261             logging.getLogger('safe_eval').warning('Looks like you are trying to pass a dynamic environment,"\
262                               "you should probably pass nocopy=True to safe_eval()')
263
264         globals_dict = dict(globals_dict)
265         if locals_dict is not None:
266             locals_dict = dict(locals_dict)
267
268     globals_dict.update(
269             __builtins__ = {
270                 '__import__': _import,
271                 'True': True,
272                 'False': False,
273                 'None': None,
274                 'str': str,
275                 'globals': locals,
276                 'locals': locals,
277                 'bool': bool,
278                 'dict': dict,
279                 'list': list,
280                 'tuple': tuple,
281                 'map': map,
282                 'abs': abs,
283                 'min': min,
284                 'max': max,
285                 'reduce': reduce,
286                 'filter': filter,
287                 'round': round,
288                 'len': len,
289                 'set' : set
290             }
291     )
292     return eval(test_expr(expr,_SAFE_OPCODES, mode=mode), globals_dict, locals_dict)
293
294 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: