2020-11-17 21:49:32 +00:00
|
|
|
/**
|
|
|
|
* 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');
|
|
|
|
|
2021-03-19 20:01:24 +00:00
|
|
|
const GRIST_CONSTANTS: Record<string, string> = {
|
|
|
|
EDITOR: 'editors',
|
|
|
|
OWNER: 'owners',
|
|
|
|
VIEWER: 'viewers',
|
|
|
|
};
|
|
|
|
|
2020-11-17 21:49:32 +00:00
|
|
|
/**
|
|
|
|
* 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;
|
2021-03-19 20:01:24 +00:00
|
|
|
if (GRIST_CONSTANTS[name]) { return constant(GRIST_CONSTANTS[name]); }
|
2020-11-17 21:49:32 +00:00
|
|
|
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]));
|
|
|
|
}
|
2021-02-15 21:36:33 +00:00
|
|
|
case 'Comment': return _compileNode(args[0]);
|
2020-11-17 21:49:32 +00:00
|
|
|
}
|
|
|
|
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)}'`);
|
|
|
|
}
|
2020-12-28 05:40:10 +00:00
|
|
|
return (typeof value.get === 'function' ? value.get(attrName) : // InfoView
|
|
|
|
value[attrName]);
|
2020-11-17 21:49:32 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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)));
|
|
|
|
}
|