mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
105 lines
4.6 KiB
TypeScript
105 lines
4.6 KiB
TypeScript
|
/**
|
||
|
* Representation and compilation of ACL formulas.
|
||
|
*
|
||
|
* An example of an ACL formula is: "rec.office == 'Seattle' and user.email in ['sally@', 'xie@']".
|
||
|
* These formulas are parsed in Python into a tree with nodes of the form [NODE_TYPE, ...args].
|
||
|
* See sandbox/grist/acl_formula.py for details.
|
||
|
*
|
||
|
* This modules includes typings for the nodes, and compileAclFormula() function that turns such a
|
||
|
* tree into an actual boolean function.
|
||
|
*/
|
||
|
import {CellValue} from 'app/common/DocActions';
|
||
|
import {ErrorWithCode} from 'app/common/ErrorWithCode';
|
||
|
import {AclMatchFunc, AclMatchInput, ParsedAclFormula} from 'app/common/GranularAccessClause';
|
||
|
import constant = require('lodash/constant');
|
||
|
|
||
|
/**
|
||
|
* Compile a parsed ACL formula into an actual function that can evaluate a request.
|
||
|
*/
|
||
|
export function compileAclFormula(parsedAclFormula: ParsedAclFormula): AclMatchFunc {
|
||
|
const compiled = _compileNode(parsedAclFormula);
|
||
|
return (input) => Boolean(compiled(input));
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Type for intermediate functions, which may return values other than booleans.
|
||
|
*/
|
||
|
type AclEvalFunc = (input: AclMatchInput) => any;
|
||
|
|
||
|
/**
|
||
|
* Compile a single node of the parsed formula tree.
|
||
|
*/
|
||
|
function _compileNode(parsedAclFormula: ParsedAclFormula): AclEvalFunc {
|
||
|
const rawArgs = parsedAclFormula.slice(1);
|
||
|
const args = rawArgs as ParsedAclFormula[];
|
||
|
switch (parsedAclFormula[0]) {
|
||
|
case 'And': { const parts = args.map(_compileNode); return (input) => parts.every(p => p(input)); }
|
||
|
case 'Or': { const parts = args.map(_compileNode); return (input) => parts.some(p => p(input)); }
|
||
|
case 'Add': return _compileAndCombine(args, ([a, b]) => a + b);
|
||
|
case 'Sub': return _compileAndCombine(args, ([a, b]) => a - b);
|
||
|
case 'Mult': return _compileAndCombine(args, ([a, b]) => a * b);
|
||
|
case 'Div': return _compileAndCombine(args, ([a, b]) => a / b);
|
||
|
case 'Mod': return _compileAndCombine(args, ([a, b]) => a % b);
|
||
|
case 'Not': return _compileAndCombine(args, ([a]) => !a);
|
||
|
case 'Eq': return _compileAndCombine(args, ([a, b]) => a === b);
|
||
|
case 'NotEq': return _compileAndCombine(args, ([a, b]) => a !== b);
|
||
|
case 'Lt': return _compileAndCombine(args, ([a, b]) => a < b);
|
||
|
case 'LtE': return _compileAndCombine(args, ([a, b]) => a <= b);
|
||
|
case 'Gt': return _compileAndCombine(args, ([a, b]) => a > b);
|
||
|
case 'GtE': return _compileAndCombine(args, ([a, b]) => a >= b);
|
||
|
case 'Is': return _compileAndCombine(args, ([a, b]) => a === b);
|
||
|
case 'IsNot': return _compileAndCombine(args, ([a, b]) => a !== b);
|
||
|
case 'In': return _compileAndCombine(args, ([a, b]) => b.includes(a));
|
||
|
case 'NotIn': return _compileAndCombine(args, ([a, b]) => !b.includes(a));
|
||
|
case 'List': return _compileAndCombine(args, (values) => values);
|
||
|
case 'Const': return constant(parsedAclFormula[1] as CellValue);
|
||
|
case 'Name': {
|
||
|
const name = rawArgs[0] as keyof AclMatchInput;
|
||
|
if (!['user', 'rec', 'newRec'].includes(name)) {
|
||
|
throw new Error(`Unknown variable '${name}'`);
|
||
|
}
|
||
|
return (input) => input[name];
|
||
|
}
|
||
|
case 'Attr': {
|
||
|
const attrName = rawArgs[1] as string;
|
||
|
return _compileAndCombine([args[0]], ([value]) => getAttr(value, attrName, args[0]));
|
||
|
}
|
||
|
}
|
||
|
throw new Error(`Unknown node type '${parsedAclFormula[0]}'`);
|
||
|
}
|
||
|
|
||
|
function describeNode(node: ParsedAclFormula): string {
|
||
|
if (node[0] === 'Name') {
|
||
|
return node[1] as string;
|
||
|
} else if (node[0] === 'Attr') {
|
||
|
return describeNode(node[1] as ParsedAclFormula) + '.' + (node[2] as string);
|
||
|
} else {
|
||
|
return 'value';
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function getAttr(value: any, attrName: string, valueNode: ParsedAclFormula): any {
|
||
|
if (value == null) {
|
||
|
if (valueNode[0] === 'Name' && (valueNode[1] === 'rec' || valueNode[1] === 'newRec')) {
|
||
|
// This code is recognized by GranularAccess to know when a rule is row-specific.
|
||
|
throw new ErrorWithCode('NEED_ROW_DATA', `Missing row data '${valueNode[1]}'`);
|
||
|
}
|
||
|
throw new Error(`No value for '${describeNode(valueNode)}'`);
|
||
|
}
|
||
|
const result = (typeof value.get === 'function' ? value.get(attrName) : // InfoView
|
||
|
value[attrName]);
|
||
|
if (result === undefined) {
|
||
|
throw new Error(`No attribute '${describeNode(valueNode)}.${attrName}'`);
|
||
|
}
|
||
|
return result;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Helper for operators: compile a list of nodes, then when evaluating, evaluate them all and
|
||
|
* combine the array of results using the given combine() function.
|
||
|
*/
|
||
|
function _compileAndCombine(args: ParsedAclFormula[], combine: (values: any[]) => any): AclEvalFunc {
|
||
|
const compiled = args.map(_compileNode);
|
||
|
return (input: AclMatchInput) => combine(compiled.map(c => c(input)));
|
||
|
}
|