gristlabs_grist-core/app/server/lib/ACLFormula.ts
Paul Fitzpatrick 6af811f7ab (core) give more detailed reasons for access denied when memos are present
Summary:
With this change, if a comment is added to an ACL formula, then that comment will be offered to the user if access is denied and that rule could potentially have granted access.

The code is factored so that when access is permitted, or when partially visible tables are being filtered, there is little overhead. Comments are gathered only when an explicit denial of access.

Test Plan: added tests, updated tests

Reviewers: dsagal

Reviewed By: dsagal

Differential Revision: https://phab.getgrist.com/D2730
2021-02-15 17:02:24 -05:00

102 lines
4.5 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]));
}
case 'Comment': return _compileNode(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)}'`);
}
return (typeof value.get === 'function' ? value.get(attrName) : // InfoView
value[attrName]);
}
/**
* 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)));
}