Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: alternate formula eval implementation (backport #902) #905

Merged
merged 3 commits into from
Sep 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 51 additions & 2 deletions hrms/payroll/doctype/salary_slip/salary_slip.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
# License: GNU General Public License v3. See license.txt


import unicodedata
from datetime import date

import frappe
Expand Down Expand Up @@ -1047,14 +1048,14 @@ def eval_condition_and_formula(self, struct_row, data):
try:
condition = sanitize_expression(struct_row.condition)
if condition:
if not frappe.safe_eval(condition, self.whitelisted_globals, data):
if not _safe_eval(condition, self.whitelisted_globals, data):
return None
amount = struct_row.amount
if struct_row.amount_based_on_formula:
formula = sanitize_expression(struct_row.formula)
if formula:
amount = flt(
frappe.safe_eval(formula, self.whitelisted_globals, data), struct_row.precision("amount")
_safe_eval(formula, self.whitelisted_globals, data), struct_row.precision("amount")
)
if amount:
data[struct_row.abbr] = amount
Expand Down Expand Up @@ -2235,3 +2236,51 @@ def throw_error_message(row, error, title, description=None):
).format(**data)

frappe.throw(message, title=title)


def _safe_eval(code: str, eval_globals: dict | None = None, eval_locals: dict | None = None):
"""Old version of safe_eval from framework.

Note: current frappe.safe_eval transforms code so if you have nested
iterations with too much depth then it can hit recursion limit of python.
There's no workaround for this and people need large formulas in some
countries so this is alternate implementation for that.

WARNING: DO NOT use this function anywhere else outside of this file.
"""
code = unicodedata.normalize("NFKC", code)

_check_attributes(code)

whitelisted_globals = {"int": int, "float": float, "long": int, "round": round}
if not eval_globals:
eval_globals = {}

eval_globals["__builtins__"] = {}
eval_globals.update(whitelisted_globals)
return eval(code, eval_globals, eval_locals) # nosemgrep


def _check_attributes(code: str) -> None:
import ast

from frappe.utils.safe_exec import UNSAFE_ATTRIBUTES

unsafe_attrs = set(UNSAFE_ATTRIBUTES).union(["__"]) - {"format"}

for attribute in unsafe_attrs:
if attribute in code:
raise SyntaxError(f'Illegal rule {frappe.bold(code)}. Cannot use "{attribute}"')

BLOCKED_NODES = (ast.NamedExpr,)

tree = ast.parse(code, mode="eval")
for node in ast.walk(tree):
if isinstance(node, BLOCKED_NODES):
raise SyntaxError(f"Operation not allowed: line {node.lineno} column {node.col_offset}")
if (
isinstance(node, ast.Attribute)
and isinstance(node.attr, str)
and node.attr in UNSAFE_ATTRIBUTES
):
raise SyntaxError(f'Illegal rule {frappe.bold(code)}. Cannot use "{node.attr}"')
40 changes: 39 additions & 1 deletion hrms/payroll/doctype/salary_slip/test_salary_slip.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,10 @@
create_payroll_period,
)
from hrms.payroll.doctype.payroll_entry.payroll_entry import get_month_details
from hrms.payroll.doctype.salary_slip.salary_slip import make_salary_slip_from_timesheet
from hrms.payroll.doctype.salary_slip.salary_slip import (
_safe_eval,
make_salary_slip_from_timesheet,
)
from hrms.payroll.doctype.salary_structure.salary_structure import make_salary_slip
from hrms.tests.test_utils import get_first_sunday

Expand Down Expand Up @@ -1387,6 +1390,41 @@ def test_variable_tax_component(self):
self.assertListEqual(tax_component, ["_Test TDS"])


class TestSalarySlipSafeEval(FrappeTestCase):
def test_safe_eval_for_salary_slip(self):
TEST_CASES = {
"1+1": 2,
'"abc" in "abl"': False,
'"a" in "abl"': True,
'"a" in ("a", "b")': True,
'"a" in {"a", "b"}': True,
'"a" in {"a": 1, "b": 2}': True,
'"a" in ["a" ,"b"]': True,
}

for code, result in TEST_CASES.items():
self.assertEqual(_safe_eval(code), result)

self.assertRaises(NameError, _safe_eval, "frappe.utils.os.path", {})

# Doc/dict objects
user = frappe.new_doc("User")
user.user_type = "System User"
user.enabled = 1
self.assertTrue(_safe_eval("user_type == 'System User'", eval_locals=user.as_dict()))
self.assertEqual(
"System User Test", _safe_eval("user_type + ' Test'", eval_locals=user.as_dict())
)
self.assertEqual(1, _safe_eval("int(enabled)", eval_locals=user.as_dict()))

# Walrus not allowed
self.assertRaises(SyntaxError, _safe_eval, "(x := (40+2))")

# Format check but saner
self.assertTrue(_safe_eval("'x' != 'Information Techonology'"))
self.assertRaises(SyntaxError, _safe_eval, "'blah'.format(1)")


def make_income_tax_components():
tax_components = [
{
Expand Down
Loading