@clickup/ent-framework
Version:
A PostgreSQL graph-database-alike library with microsharding and row-level security
124 lines • 5.05 kB
JavaScript
Object.defineProperty(exports, "__esModule", { value: true });
exports.evaluate = evaluate;
const misc_1 = require("../../internal/misc");
const EntAccessError_1 = require("../errors/EntAccessError");
/**
* This is a hearth of permissions checking, a machine which evaluates the rules
* chain from top to bottom (one after another) and makes the decision based on
* the following logic:
* - ALLOW immediately allows the chain, the rest of the rules are not checked.
* It's an eager allowance.
* - DENY immediately denies the chain, the rest of the rules are not checked.
* It's an eager denial.
* - TOLERATE delegates the decision to the next rules; if it's the last
* decision in the chain, then allows the chain. I.e. it's like an allowance,
* but only if everyone else is tolerant.
* - SKIP also delegates the decision to the next rules, but if it's the last
* rule in the chain (i.e. nothing to skip to anymore), denies the chain. I.e.
* it's "I don't vote here, please ask others".
* - An empty chain is always denied.
*
* Having TOLERATE decision may sound superfluous, but unfortunately it's not.
* The TOLERATE enables usage of the same machinery for both read-like checks
* (where we typically want ANY of the rules to be okay with the row) and for
* write-like checks (where we typically want ALL rules to be okay with the
* row). Having the same logic for everything simplifies the code.
*
* If parallel argument is true, all the rules are run at once in concurrent
* promises before the machine starts. This doesn't affect the final result,
* just speeds up processing if we know that there is a high chance that most of
* the rules will likely return TOLERATE and we'll anyway need to evaluate all
* of them (e.g. most of the rules are Require, like in write operations). As
* opposed, for read operation, there is a high chance for the first rule (which
* is often AllowIf) to succeed, so we evaluate the rules sequentially, not in
* parallel (to minimize the number of DB queries).
*
* Example of a chain (the order of rules always matters!):
* - new Require(new OutgoingEdgePointsToVC("user_id"))
* - new Require(new CanReadOutgoingEdge("post_id", EntPost))
*
* Example of a chain:
* - new AllowIf(new OutgoingEdgePointsToVC("user_id"))
* - new AllowIf(new CanReadOutgoingEdge("post_id", EntPost))
*
* Example of a chain:
* - new DenyIf(new UserIsPendingApproval())
* - new AllowIf(new OutgoingEdgePointsToVC("user_id"))
*/
async function evaluate(vc, input, rules, fashion) {
const results = fashion === "parallel"
? await (0, misc_1.mapJoin)(rules, async (rule) => ruleEvaluate(rule, vc, input))
: [];
let lastResult = null;
for (let i = 0; i < rules.length; i++) {
if (!results[i]) {
results[i] = await ruleEvaluate(rules[i], vc, input);
}
lastResult = results[i];
switch (lastResult.decision) {
case "ALLOW":
return {
allow: true,
results,
cause: resultsToCause(results),
};
case "DENY":
return {
allow: false,
results,
cause: resultsToCause(results),
};
case "TOLERATE":
case "SKIP":
continue;
default:
throw Error("BUG: weird RuleResult " + (0, misc_1.inspectCompact)(lastResult));
}
}
const cause = resultsToCause(results);
if (!lastResult) {
return { allow: false, results, cause };
}
if (lastResult.decision === "SKIP") {
return { allow: false, results, cause };
}
if (lastResult.decision === "TOLERATE") {
return { allow: true, results, cause };
}
throw Error("BUG: weird last rule result: " + (0, misc_1.inspectCompact)(lastResult));
}
/**
* Evaluates one rule turning all EntAccessError exceptions (if they happen)
* into DENY decision (and annotating them with the rule name which caused the
* exception). All "wild" (non-EntAccessError) are thrown through.
*/
async function ruleEvaluate(rule, vc, input) {
try {
return await rule.evaluate(vc, input);
}
catch (error) {
if (error instanceof EntAccessError_1.EntAccessError) {
// This includes e.g. derived EntValidationError class.
return {
decision: "DENY",
rule,
cause: error,
};
}
throw error;
}
}
/**
* A helper function which returns a debugging text for a list of rule
* evaluation results.
*/
function resultsToCause(results) {
return results.length === 0
? "No rules defined"
: results
.map(({ rule, decision, cause }) => `Rule ${rule.name} returned ${decision}` +
(cause ? ", because:\n" + (0, misc_1.indent)(cause.message) : ""))
.join("\n");
}
//# sourceMappingURL=evaluate.js.map
;