|
|
|
# -*- coding: utf-8 -*-
|
|
|
|
# pylint:disable=line-too-long
|
|
|
|
|
|
|
|
import unittest
|
|
|
|
from acl_formula import parse_acl_formula
|
|
|
|
import test_engine
|
|
|
|
|
|
|
|
|
|
|
|
class TestACLFormula(unittest.TestCase):
|
|
|
|
def test_basic(self):
|
|
|
|
# Test a few basic formulas and structures, hitting everything we expect to support
|
|
|
|
self.assertEqual(parse_acl_formula(
|
|
|
|
"user.Email == 'X@'"),
|
|
|
|
["Eq", ["Attr", ["Name", "user"], "Email"],
|
|
|
|
["Const", "X@"]])
|
|
|
|
|
|
|
|
self.assertEqual(parse_acl_formula(
|
|
|
|
"user.Role in ('editors', 'owners')"),
|
|
|
|
["In", ["Attr", ["Name", "user"], "Role"],
|
|
|
|
["List", ["Const", "editors"], ["Const", "owners"]]])
|
|
|
|
|
|
|
|
self.assertEqual(parse_acl_formula(
|
|
|
|
"user.Role not in ('editors', 'owners')"),
|
|
|
|
["NotIn", ["Attr", ["Name", "user"], "Role"],
|
|
|
|
["List", ["Const", "editors"], ["Const", "owners"]]])
|
|
|
|
|
|
|
|
self.assertEqual(parse_acl_formula(
|
|
|
|
"rec.office == 'Seattle' and user.email in ['sally@', 'xie@']"),
|
|
|
|
['And',
|
|
|
|
['Eq', ['Attr', ['Name', 'rec'], 'office'], ['Const', 'Seattle']],
|
|
|
|
['In',
|
|
|
|
['Attr', ['Name', 'user'], 'email'],
|
|
|
|
['List', ['Const', 'sally@'], ['Const', 'xie@']]
|
|
|
|
]])
|
|
|
|
|
|
|
|
self.assertEqual(parse_acl_formula(
|
|
|
|
"user.IsAdmin or rec.assigned is None or (not newRec.HasDuplicates and rec.StatusIndex <= newRec.StatusIndex)"),
|
|
|
|
['Or',
|
|
|
|
['Attr', ['Name', 'user'], 'IsAdmin'],
|
|
|
|
['Is', ['Attr', ['Name', 'rec'], 'assigned'], ['Const', None]],
|
|
|
|
['And',
|
|
|
|
['Not', ['Attr', ['Name', 'newRec'], 'HasDuplicates']],
|
|
|
|
['LtE', ['Attr', ['Name', 'rec'], 'StatusIndex'], ['Attr', ['Name', 'newRec'], 'StatusIndex']]
|
|
|
|
]
|
|
|
|
])
|
|
|
|
|
|
|
|
self.assertEqual(parse_acl_formula(
|
|
|
|
"r.A <= n.A + 1 or r.A >= n.A - 1 or r.B < n.B * 2.5 or r.B > n.B / 2.5 or r.C % 2 != 0"),
|
|
|
|
['Or',
|
|
|
|
['LtE',
|
|
|
|
['Attr', ['Name', 'r'], 'A'],
|
|
|
|
['Add', ['Attr', ['Name', 'n'], 'A'], ['Const', 1]]],
|
|
|
|
['GtE',
|
|
|
|
['Attr', ['Name', 'r'], 'A'],
|
|
|
|
['Sub', ['Attr', ['Name', 'n'], 'A'], ['Const', 1]]],
|
|
|
|
['Lt',
|
|
|
|
['Attr', ['Name', 'r'], 'B'],
|
|
|
|
['Mult', ['Attr', ['Name', 'n'], 'B'], ['Const', 2.5]]],
|
|
|
|
['Gt',
|
|
|
|
['Attr', ['Name', 'r'], 'B'],
|
|
|
|
['Div', ['Attr', ['Name', 'n'], 'B'], ['Const', 2.5]]],
|
|
|
|
['NotEq',
|
|
|
|
['Mod', ['Attr', ['Name', 'r'], 'C'], ['Const', 2]],
|
|
|
|
['Const', 0]]
|
|
|
|
])
|
|
|
|
|
|
|
|
self.assertEqual(parse_acl_formula(
|
|
|
|
"rec.A is True or rec.A is not False"),
|
|
|
|
['Or',
|
|
|
|
['Is', ['Attr', ['Name', 'rec'], 'A'], ['Const', True]],
|
|
|
|
['IsNot', ['Attr', ['Name', 'rec'], 'A'], ['Const', False]]
|
|
|
|
])
|
|
|
|
|
|
|
|
self.assertEqual(parse_acl_formula(
|
|
|
|
"user.Office.City == 'Seattle' and user.Status.IsActive"),
|
|
|
|
['And',
|
|
|
|
['Eq',
|
|
|
|
['Attr', ['Attr', ['Name', 'user'], 'Office'], 'City'],
|
|
|
|
['Const', 'Seattle']],
|
|
|
|
['Attr', ['Attr', ['Name', 'user'], 'Status'], 'IsActive']
|
|
|
|
])
|
|
|
|
|
|
|
|
self.assertEqual(parse_acl_formula(
|
|
|
|
"True # Comment! "),
|
|
|
|
['Comment', ['Const', True], 'Comment!'])
|
|
|
|
|
|
|
|
self.assertEqual(parse_acl_formula(
|
|
|
|
"\"#x\" == \" # Not a comment \"#Comment!"),
|
|
|
|
['Comment',
|
|
|
|
['Eq', ['Const', '#x'], ['Const', ' # Not a comment ']],
|
|
|
|
'Comment!'
|
|
|
|
])
|
|
|
|
|
|
|
|
self.assertEqual(parse_acl_formula(
|
|
|
|
"# Allow owners\nuser.Access == 'owners' # ignored\n# comment ignored"),
|
|
|
|
['Comment',
|
|
|
|
['Eq', ['Attr', ['Name', 'user'], 'Access'], ['Const', 'owners']],
|
|
|
|
'Allow owners'
|
|
|
|
])
|
|
|
|
|
|
|
|
def test_unsupported(self):
|
|
|
|
# Test a few constructs we expect to fail
|
|
|
|
# Not an expression
|
|
|
|
self.assertRaises(SyntaxError, parse_acl_formula, "return 1")
|
|
|
|
self.assertRaises(SyntaxError, parse_acl_formula, "def foo(): pass")
|
|
|
|
|
|
|
|
# Unsupported node type
|
|
|
|
self.assertRaisesRegex(ValueError, r'Unsupported syntax', parse_acl_formula, "max(rec)")
|
|
|
|
self.assertRaisesRegex(ValueError, r'Unsupported syntax', parse_acl_formula, "user.id in {1, 2, 3}")
|
|
|
|
self.assertRaisesRegex(ValueError, r'Unsupported syntax', parse_acl_formula, "1 if user.IsAnon else 2")
|
|
|
|
|
|
|
|
# Unsupported operation
|
|
|
|
self.assertRaisesRegex(ValueError, r'Unsupported syntax', parse_acl_formula, "1 | 2")
|
|
|
|
self.assertRaisesRegex(ValueError, r'Unsupported syntax', parse_acl_formula, "1 << 2")
|
|
|
|
self.assertRaisesRegex(ValueError, r'Unsupported syntax', parse_acl_formula, "~test")
|
|
|
|
|
|
|
|
# Syntax error
|
|
|
|
self.assertRaises(SyntaxError, parse_acl_formula, "[(]")
|
|
|
|
self.assertRaises(SyntaxError, parse_acl_formula, "user.id in (1,2))")
|
|
|
|
self.assertRaisesRegex(SyntaxError, r'invalid syntax on line 1 col 9', parse_acl_formula, "foo and !bar")
|
|
|
|
|
|
|
|
class TestACLFormulaUserActions(test_engine.EngineTestCase):
|
|
|
|
def test_acl_actions(self):
|
|
|
|
# Adding or updating ACLRules automatically includes aclFormula compilation.
|
|
|
|
|
|
|
|
# Single Add
|
|
|
|
out_actions = self.apply_user_action(
|
|
|
|
['AddRecord', '_grist_ACLRules', None, {"resource": 1, "aclFormula": "user.UserID == 7"}],
|
|
|
|
)
|
|
|
|
self.assertPartialOutActions(out_actions, { "stored": [
|
|
|
|
["AddRecord", "_grist_ACLRules", 1, {"resource": 1, "aclFormula": "user.UserID == 7",
|
|
|
|
"aclFormulaParsed": '["Eq", ["Attr", ["Name", "user"], "UserID"], ["Const", 7]]',
|
|
|
|
"rulePos": 1.0
|
|
|
|
}],
|
|
|
|
]})
|
|
|
|
|
|
|
|
# Single Update
|
|
|
|
out_actions = self.apply_user_action(
|
|
|
|
['UpdateRecord', '_grist_ACLRules', 1, {
|
|
|
|
"aclFormula": "user.UserID == 8",
|
|
|
|
"aclFormulaParsed": "hello"
|
|
|
|
}],
|
|
|
|
)
|
|
|
|
self.assertPartialOutActions(out_actions, { "stored": [
|
|
|
|
["UpdateRecord", "_grist_ACLRules", 1, {
|
|
|
|
"aclFormula": "user.UserID == 8",
|
|
|
|
"aclFormulaParsed": '["Eq", ["Attr", ["Name", "user"], "UserID"], ["Const", 8]]',
|
|
|
|
}],
|
|
|
|
]})
|
|
|
|
|
|
|
|
# BulkAddRecord
|
|
|
|
out_actions = self.apply_user_action(['BulkAddRecord', '_grist_ACLRules', [None, None], {
|
|
|
|
"resource": [1, 1],
|
|
|
|
"aclFormula": ["user.IsGood", "user.IsBad"],
|
|
|
|
"aclFormulaParsed": ["[1]", '["ignored"]'], # Should get overwritten
|
|
|
|
}])
|
|
|
|
self.assertPartialOutActions(out_actions, { "stored": [
|
|
|
|
[ 'BulkAddRecord', '_grist_ACLRules', [2, 3], {
|
|
|
|
"resource": [1, 1],
|
|
|
|
"aclFormula": ["user.IsGood", "user.IsBad"],
|
|
|
|
"aclFormulaParsed": [ # Gets overwritten
|
|
|
|
'["Attr", ["Name", "user"], "IsGood"]',
|
|
|
|
'["Attr", ["Name", "user"], "IsBad"]',
|
|
|
|
],
|
|
|
|
"rulePos": [2.0, 3.0], # Gets filled in.
|
|
|
|
}],
|
|
|
|
]})
|
|
|
|
|
|
|
|
# BulkUpdateRecord
|
|
|
|
out_actions = self.apply_user_action(['BulkUpdateRecord', '_grist_ACLRules', [2, 3], {
|
|
|
|
"aclFormula": ["not user.IsGood", ""],
|
|
|
|
}])
|
|
|
|
self.assertPartialOutActions(out_actions, { "stored": [
|
|
|
|
[ 'BulkUpdateRecord', '_grist_ACLRules', [2, 3], {
|
|
|
|
"aclFormula": ["not user.IsGood", ""],
|
|
|
|
"aclFormulaParsed": ['["Not", ["Attr", ["Name", "user"], "IsGood"]]', ''],
|
|
|
|
}],
|
|
|
|
]})
|