gristlabs_grist-core/sandbox/grist/acl_formula.py
Dmitry S bc3a472324 (core) Implement new representation of ACL rules.
Summary:
- Added fields to _grist_ACLRules for the new Granular ACL representation
- Include a corresponding migration.

- Added ACLPermissions module with merging PermissionSets and converting to/from string.
- Implemented parsing of ACL formulas and compiling them into JS functions.
- Add automatic parsing of ACL formulas when ACLRules are added or updated.
- Convert GranularAccess to load and interpret new-style rules.
- Convert ACL UI to load and save new-style rules.

For now, no attempt to do anything better on the server or UI side, only to
reproduce previous behavior.

Test Plan: Added unittests for new files; fixed those for existing files.

Reviewers: paulfitz

Reviewed By: paulfitz

Differential Revision: https://phab.getgrist.com/D2664
2020-11-18 08:58:03 -05:00

92 lines
3.1 KiB
Python

import ast
import json
def parse_acl_formula(acl_formula):
"""
Parse an ACL formula expression into a parse tree that we can interpret in JS, e.g.
"rec.office == 'Seattle' and user.email in ['sally@', 'xie@']".
The idea is to support enough to express ACL rules flexibly, but we don't need to support too
much, since rules should be reasonably simple.
The returned tree has the form [NODE_TYPE, arguments...], with these NODE_TYPEs supported:
And|Or ...values
Add|Sub|Mult|Div|Mod left, right
Not operand
Eq|NotEq|Lt|LtE|Gt|GtE left, right
Is|IsNot|In|NotIn left, right
List ...elements
Const value (number, string, bool)
Name name (string)
Attr node, attr_name
"""
try:
tree = ast.parse(acl_formula, mode='eval')
return _TreeConverter().visit(tree)
except SyntaxError as err:
# In case of an error, include line and offset.
raise SyntaxError("%s on line %s col %s" % (err.args[0], err.lineno, err.offset))
def parse_acl_formula_json(acl_formula):
"""
As parse_acl_formula(), but stringifies the result, and converts empty string to empty string.
"""
return json.dumps(parse_acl_formula(acl_formula)) if acl_formula else ""
named_constants = {
'True': True,
'False': False,
'None': None,
}
class _TreeConverter(ast.NodeVisitor):
# AST nodes are documented here: https://docs.python.org/2/library/ast.html#abstract-grammar
# pylint:disable=no-self-use
def visit_Expression(self, node):
return self.visit(node.body)
def visit_BoolOp(self, node):
return [node.op.__class__.__name__] + [self.visit(v) for v in node.values]
def visit_BinOp(self, node):
if not isinstance(node.op, (ast.Add, ast.Sub, ast.Mult, ast.Div, ast.Mod)):
return self.generic_visit(node)
return [node.op.__class__.__name__, self.visit(node.left), self.visit(node.right)]
def visit_UnaryOp(self, node):
if not isinstance(node.op, (ast.Not)):
return self.generic_visit(node)
return [node.op.__class__.__name__, self.visit(node.operand)]
def visit_Compare(self, node):
# We don't try to support chained comparisons like "1 < 2 < 3" (though it wouldn't be hard).
if len(node.ops) != 1 or len(node.comparators) != 1:
raise ValueError("Can't use chained comparisons")
return [node.ops[0].__class__.__name__, self.visit(node.left), self.visit(node.comparators[0])]
def visit_Name(self, node):
if node.id in named_constants:
return ["Const", named_constants[node.id]]
return ["Name", node.id]
def visit_Attribute(self, node):
return ["Attr", self.visit(node.value), node.attr]
def visit_Num(self, node):
return ["Const", node.n]
def visit_Str(self, node):
return ["Const", node.s]
def visit_List(self, node):
return ["List"] + [self.visit(e) for e in node.elts]
def visit_Tuple(self, node):
return self.visit_List(node) # We don't distinguish tuples and lists
def generic_visit(self, node):
raise ValueError("Unsupported syntax at %s:%s" % (node.lineno, node.col_offset + 1))