717ae1031e
Zanzibar-style authorization graph engine (direct/chain/TTU/defeasible/ binary modes, condensed snapshots, value relations) with 39 rigor test campaigns. Includes fixes for snapshot binary writer/reader format mismatch (snapshot-of-snapshot corruption), possibility write-boundary validation, empty-graph snapshot serialization, relation lookup cache direction collision, config-redefinition cache invalidation, binary threshold semantics, defeasible compiled routing, and comparator reason whitelisting.
309 lines
16 KiB
JavaScript
309 lines
16 KiB
JavaScript
import { LogicalOperators } from '../../src/authorization/rules/LogicalOperators.js';
|
|
import { describe, it, beforeEach } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
|
|
// Minimal mock ruleEvaluator for child rule evaluation
|
|
function makeMockRuleEvaluator(resultsByRule) {
|
|
return {
|
|
evaluateRule: (userId, userKey, objectId, objectKey, rule, visited, currentRelation, options) => {
|
|
if (typeof rule === 'string') return resultsByRule[rule];
|
|
if (rule && rule.mockKey) return resultsByRule[rule.mockKey];
|
|
if (rule && rule.type && resultsByRule[rule.type]) return resultsByRule[rule.type];
|
|
return resultsByRule.default || { possibility: 0, reliability: 1.0, meta: { reason: 'no_rule' }, collectedValues: [] };
|
|
}
|
|
};
|
|
}
|
|
|
|
describe('LogicalOperators', () => {
|
|
let logicalOps;
|
|
let arbiter;
|
|
let ruleEvaluator;
|
|
|
|
beforeEach(() => {
|
|
arbiter = {};
|
|
});
|
|
|
|
it('evaluates union (max) correctly', () => {
|
|
ruleEvaluator = makeMockRuleEvaluator({
|
|
a: { possibility: 0.7, reliability: 0.9, meta: { ruleType: 'direct', rule: { mockKey: 'a' } }, collectedValues: [1] },
|
|
b: { possibility: 0.4, reliability: 0.8, meta: { ruleType: 'direct', rule: { mockKey: 'b' } }, collectedValues: [2] },
|
|
c: { possibility: 0.9, reliability: 0.7, meta: { ruleType: 'direct', rule: { mockKey: 'c' } }, collectedValues: [3] }
|
|
});
|
|
logicalOps = new LogicalOperators(arbiter, ruleEvaluator);
|
|
const rule = { type: 'logical', union: { rules: [{ mockKey: 'a' }, { mockKey: 'b' }, { mockKey: 'c' }], aggregator: 'max' } };
|
|
const res = logicalOps._evaluateRule('u', 'u', 'o', 'o', rule, {}, null, { collectValues: true });
|
|
assert.strictEqual(res.possibility, 0.9);
|
|
assert.ok(res.meta.operation === 'union');
|
|
assert.deepStrictEqual(res.collectedValues, [1, 2, 3]);
|
|
});
|
|
|
|
it('evaluates intersection (min) correctly', () => {
|
|
ruleEvaluator = makeMockRuleEvaluator({
|
|
a: { possibility: 0.7, reliability: 0.9, meta: { ruleType: 'direct', rule: { mockKey: 'a' } }, collectedValues: [1] },
|
|
b: { possibility: 0.4, reliability: 0.8, meta: { ruleType: 'direct', rule: { mockKey: 'b' } }, collectedValues: [2] },
|
|
c: { possibility: 0.9, reliability: 0.7, meta: { ruleType: 'direct', rule: { mockKey: 'c' } }, collectedValues: [3] }
|
|
});
|
|
logicalOps = new LogicalOperators(arbiter, ruleEvaluator);
|
|
const rule = { type: 'logical', intersection: { rules: [{ mockKey: 'a' }, { mockKey: 'b' }, { mockKey: 'c' }], aggregator: 'min' } };
|
|
const res = logicalOps._evaluateRule('u', 'u', 'o', 'o', rule, {}, null, { collectValues: true });
|
|
assert.strictEqual(res.possibility, 0.4);
|
|
assert.ok(res.meta.operation === 'intersection');
|
|
assert.deepStrictEqual(res.collectedValues, [1, 2, 3]);
|
|
});
|
|
|
|
it('evaluates union (mean/average) correctly', () => {
|
|
ruleEvaluator = makeMockRuleEvaluator({
|
|
a: { possibility: 0.6, reliability: 0.9, meta: { ruleType: 'direct', rule: { mockKey: 'a' } }, collectedValues: [1] },
|
|
b: { possibility: 0.8, reliability: 0.8, meta: { ruleType: 'direct', rule: { mockKey: 'b' } }, collectedValues: [2] }
|
|
});
|
|
logicalOps = new LogicalOperators(arbiter, ruleEvaluator);
|
|
const rule = { type: 'logical', union: { rules: [{ mockKey: 'a' }, { mockKey: 'b' }], aggregator: 'mean' } };
|
|
const res = logicalOps._evaluateRule('u', 'u', 'o', 'o', rule, {}, null, { collectValues: true });
|
|
assert.ok(Math.abs(res.possibility - 0.7) < 1e-8);
|
|
assert.ok(res.meta.operation === 'union');
|
|
});
|
|
|
|
it('evaluates intersection (pessimistic) correctly', () => {
|
|
ruleEvaluator = makeMockRuleEvaluator({
|
|
a: { possibility: 0.9, reliability: 0.9, meta: { ruleType: 'direct', rule: { mockKey: 'a' } }, collectedValues: [1] },
|
|
b: { possibility: 0.6, reliability: 0.8, meta: { ruleType: 'direct', rule: { mockKey: 'b' } }, collectedValues: [2] }
|
|
});
|
|
logicalOps = new LogicalOperators(arbiter, ruleEvaluator);
|
|
const rule = { type: 'logical', intersection: { rules: [{ mockKey: 'a' }, { mockKey: 'b' }], aggregator: 'pessimistic' } };
|
|
const res = logicalOps._evaluateRule('u', 'u', 'o', 'o', rule, {}, null, { collectValues: true });
|
|
assert.ok(res.possibility < 0.9 && res.possibility > 0.6);
|
|
assert.ok(res.meta.operation === 'intersection');
|
|
});
|
|
|
|
it('evaluates exclusion (A AND NOT B) correctly', () => {
|
|
ruleEvaluator = makeMockRuleEvaluator({
|
|
a: { possibility: 0.8, reliability: 0.9, meta: { ruleType: 'direct', rule: { mockKey: 'a' } }, collectedValues: [1] },
|
|
b: { possibility: 0.5, reliability: 0.7, meta: { ruleType: 'direct', rule: { mockKey: 'b' } }, collectedValues: [2] }
|
|
});
|
|
logicalOps = new LogicalOperators(arbiter, ruleEvaluator);
|
|
const rule = { type: 'logical', exclusion: { rules: [{ mockKey: 'a' }, { mockKey: 'b' }] } };
|
|
const res = logicalOps._evaluateRule('u', 'u', 'o', 'o', rule, {}, null, { collectValues: true });
|
|
assert.ok(Math.abs(res.possibility - (0.8 * (1 - 0.5))) < 1e-8);
|
|
assert.ok(res.meta.operation === 'exclusion');
|
|
});
|
|
|
|
it('evaluates custom OWA weights for union', () => {
|
|
ruleEvaluator = makeMockRuleEvaluator({
|
|
a: { possibility: 0.2, reliability: 0.9, meta: { ruleType: 'direct', rule: { mockKey: 'a' } }, collectedValues: [1] },
|
|
b: { possibility: 0.8, reliability: 0.8, meta: { ruleType: 'direct', rule: { mockKey: 'b' } }, collectedValues: [2] }
|
|
});
|
|
logicalOps = new LogicalOperators(arbiter, ruleEvaluator);
|
|
const rule = { type: 'logical', union: { rules: [{ mockKey: 'a' }, { mockKey: 'b' }], owaWeights: [0.7, 0.3] } };
|
|
const res = logicalOps._evaluateRule('u', 'u', 'o', 'o', rule, {}, null, {});
|
|
assert.ok(Math.abs(res.possibility - (0.8 * 0.7 + 0.2 * 0.3)) < 1e-8);
|
|
assert.ok(res.meta.operation === 'union');
|
|
});
|
|
|
|
it('evaluates binary defeasible logic (never overrides everything)', () => {
|
|
// never wins over everything
|
|
ruleEvaluator = makeMockRuleEvaluator({
|
|
never: { possibility: 1, reliability: 0.95, meta: { ruleType: 'never' }, collectedValues: [1] },
|
|
strict: { possibility: 1, reliability: 0.95, meta: { ruleType: 'strict' }, collectedValues: [2] },
|
|
defeater: { possibility: 0.6, reliability: 0.8, meta: { ruleType: 'defeater' }, collectedValues: [3] },
|
|
defeasible: { possibility: 0.7, reliability: 0.7, meta: { ruleType: 'defeasible' }, collectedValues: [4] }
|
|
});
|
|
logicalOps = new LogicalOperators(arbiter, ruleEvaluator);
|
|
const rule = {
|
|
type: 'logical',
|
|
never: { union: [{ mockKey: 'never' }] },
|
|
always: { direct: { mockKey: 'strict' } },
|
|
unless: { union: [{ mockKey: 'defeater' }] },
|
|
when: { intersection: [{ mockKey: 'defeasible' }] },
|
|
mode: 'binary'
|
|
};
|
|
const res = logicalOps._evaluateRule('u', 'u', 'o', 'o', rule, {}, null, {});
|
|
assert.strictEqual(res.possibility, 0);
|
|
assert.ok(res.meta.never);
|
|
assert.ok(res.meta.reason === 'never_rule_triggered');
|
|
assert.ok(res.meta.mode === 'binary');
|
|
});
|
|
|
|
it('evaluates binary defeasible logic (strict, defeater, defeasible)', () => {
|
|
// strict wins
|
|
ruleEvaluator = makeMockRuleEvaluator({
|
|
strict: { possibility: 1, reliability: 0.95, meta: { ruleType: 'strict' }, collectedValues: [1] },
|
|
defeater: { possibility: 0.6, reliability: 0.8, meta: { ruleType: 'defeater' }, collectedValues: [2] },
|
|
defeasible: { possibility: 0.7, reliability: 0.7, meta: { ruleType: 'defeasible' }, collectedValues: [3] }
|
|
});
|
|
logicalOps = new LogicalOperators(arbiter, ruleEvaluator);
|
|
const rule = {
|
|
type: 'logical',
|
|
always: { direct: { mockKey: 'strict' } },
|
|
unless: { union: [{ mockKey: 'defeater' }] },
|
|
when: { intersection: [{ mockKey: 'defeasible' }] },
|
|
mode: 'binary'
|
|
};
|
|
const res = logicalOps._evaluateRule('u', 'u', 'o', 'o', rule, {}, null, {});
|
|
assert.strictEqual(res.possibility, 1);
|
|
assert.ok(res.meta.strict);
|
|
assert.ok(res.meta.mode === 'binary');
|
|
});
|
|
|
|
it('evaluates binary defeasible logic (requirements take precedence over defeaters)', () => {
|
|
// requirements not met should win over defeaters
|
|
ruleEvaluator = makeMockRuleEvaluator({
|
|
strict: { possibility: 0, reliability: 0.95, meta: { ruleType: 'strict' }, collectedValues: [1] },
|
|
requires: { possibility: 0.1, reliability: 0.9, meta: { ruleType: 'requires' }, collectedValues: [2] },
|
|
defeater: { possibility: 0.8, reliability: 0.8, meta: { ruleType: 'defeater' }, collectedValues: [3] },
|
|
defeasible: { possibility: 0.7, reliability: 0.7, meta: { ruleType: 'defeasible' }, collectedValues: [4] }
|
|
});
|
|
logicalOps = new LogicalOperators(arbiter, ruleEvaluator);
|
|
const rule = {
|
|
type: 'logical',
|
|
always: { direct: { mockKey: 'strict' } },
|
|
requires: { union: [{ mockKey: 'requires' }] },
|
|
unless: { union: [{ mockKey: 'defeater' }] },
|
|
when: { intersection: [{ mockKey: 'defeasible' }] },
|
|
mode: 'binary'
|
|
};
|
|
const res = logicalOps._evaluateRule('u', 'u', 'o', 'o', rule, {}, null, {});
|
|
assert.strictEqual(res.possibility, 0);
|
|
assert.ok(res.meta.reason === 'requirements_not_met');
|
|
assert.ok(res.meta.mode === 'binary');
|
|
});
|
|
|
|
it('evaluates binary defeasible logic (defeater wins)', () => {
|
|
// defeater wins when requirements are met
|
|
ruleEvaluator = makeMockRuleEvaluator({
|
|
strict: { possibility: 0, reliability: 0.95, meta: { ruleType: 'strict' }, collectedValues: [1] },
|
|
requires: { possibility: 0.9, reliability: 0.9, meta: { ruleType: 'requires' }, collectedValues: [2] },
|
|
defeater: { possibility: 0.8, reliability: 0.8, meta: { ruleType: 'defeater' }, collectedValues: [3] },
|
|
defeasible: { possibility: 0.7, reliability: 0.7, meta: { ruleType: 'defeasible' }, collectedValues: [4] }
|
|
});
|
|
logicalOps = new LogicalOperators(arbiter, ruleEvaluator);
|
|
const rule = {
|
|
type: 'logical',
|
|
always: { direct: { mockKey: 'strict' } },
|
|
requires: { union: [{ mockKey: 'requires' }] },
|
|
unless: { union: [{ mockKey: 'defeater' }] },
|
|
when: { intersection: [{ mockKey: 'defeasible' }] },
|
|
mode: 'binary'
|
|
};
|
|
const res = logicalOps._evaluateRule('u', 'u', 'o', 'o', rule, {}, null, {});
|
|
assert.strictEqual(res.possibility, 0);
|
|
assert.ok(res.meta.reason === 'defeated_by_unless');
|
|
assert.ok(res.meta.mode === 'binary');
|
|
});
|
|
|
|
it('evaluates threshold mode with never early exit', () => {
|
|
ruleEvaluator = makeMockRuleEvaluator({
|
|
never: { possibility: 0.9, reliability: 0.95, meta: { ruleType: 'never' }, collectedValues: [1] },
|
|
strict: { possibility: 0.85, reliability: 0.95, meta: { ruleType: 'strict' }, collectedValues: [2] },
|
|
defeater: { possibility: 0.2, reliability: 0.8, meta: { ruleType: 'defeater' }, collectedValues: [3] },
|
|
defeasible: { possibility: 0.7, reliability: 0.7, meta: { ruleType: 'defeasible' }, collectedValues: [4] }
|
|
});
|
|
logicalOps = new LogicalOperators(arbiter, ruleEvaluator);
|
|
const rule = {
|
|
type: 'logical',
|
|
never: { union: [{ mockKey: 'never' }] },
|
|
always: { direct: { mockKey: 'strict' } },
|
|
unless: { union: [{ mockKey: 'defeater' }] },
|
|
when: { intersection: [{ mockKey: 'defeasible' }] },
|
|
mode: 'threshold'
|
|
};
|
|
const res = logicalOps._evaluateRule('u', 'u', 'o', 'o', rule, {}, null, {});
|
|
assert.strictEqual(res.possibility, 0);
|
|
assert.ok(res.meta.earlyExit === 'never_rule');
|
|
assert.ok(res.meta.never);
|
|
});
|
|
|
|
it('evaluates threshold mode with early exit', () => {
|
|
ruleEvaluator = makeMockRuleEvaluator({
|
|
strict: { possibility: 0.85, reliability: 0.95, meta: { ruleType: 'strict' }, collectedValues: [1] },
|
|
defeater: { possibility: 0.2, reliability: 0.8, meta: { ruleType: 'defeater' }, collectedValues: [2] },
|
|
defeasible: { possibility: 0.7, reliability: 0.7, meta: { ruleType: 'defeasible' }, collectedValues: [3] }
|
|
});
|
|
logicalOps = new LogicalOperators(arbiter, ruleEvaluator);
|
|
const rule = {
|
|
type: 'logical',
|
|
always: { direct: { mockKey: 'strict' } },
|
|
unless: { union: [{ mockKey: 'defeater' }] },
|
|
when: { intersection: [{ mockKey: 'defeasible' }] },
|
|
mode: 'threshold'
|
|
};
|
|
const res = logicalOps._evaluateRule('u', 'u', 'o', 'o', rule, {}, null, {});
|
|
assert.strictEqual(res.possibility, 0.85);
|
|
assert.ok(res.meta.earlyExit === 'strict_rule');
|
|
});
|
|
|
|
it('handles empty union/intersection/exclusion', () => {
|
|
ruleEvaluator = makeMockRuleEvaluator({});
|
|
logicalOps = new LogicalOperators(arbiter, ruleEvaluator);
|
|
const unionRule = { type: 'logical', union: { rules: [] } };
|
|
const intersectionRule = { type: 'logical', intersection: { rules: [] } };
|
|
const exclusionRule = { type: 'logical', exclusion: { rules: [] } };
|
|
assert.strictEqual(logicalOps._evaluateRule('u', 'u', 'o', 'o', unionRule, {}, null, {}).possibility, 0);
|
|
assert.strictEqual(logicalOps._evaluateRule('u', 'u', 'o', 'o', intersectionRule, {}, null, {}).possibility, 0);
|
|
assert.strictEqual(logicalOps._evaluateRule('u', 'u', 'o', 'o', exclusionRule, {}, null, {}).possibility, 0);
|
|
});
|
|
|
|
it('handles all-zero and all-one cases', () => {
|
|
ruleEvaluator = makeMockRuleEvaluator({
|
|
a: { possibility: 0, reliability: 1.0, meta: {}, collectedValues: [] },
|
|
b: { possibility: 0, reliability: 1.0, meta: {}, collectedValues: [] }
|
|
});
|
|
logicalOps = new LogicalOperators(arbiter, ruleEvaluator);
|
|
const rule = { type: 'logical', union: { rules: [{ mockKey: 'a' }, { mockKey: 'b' }] } };
|
|
assert.strictEqual(logicalOps._evaluateRule('u', 'u', 'o', 'o', rule, {}, null, {}).possibility, 0);
|
|
ruleEvaluator = makeMockRuleEvaluator({
|
|
a: { possibility: 1, reliability: 1.0, meta: {}, collectedValues: [] },
|
|
b: { possibility: 1, reliability: 1.0, meta: {}, collectedValues: [] }
|
|
});
|
|
logicalOps = new LogicalOperators(arbiter, ruleEvaluator);
|
|
assert.strictEqual(logicalOps._evaluateRule('u', 'u', 'o', 'o', rule, {}, null, {}).possibility, 1);
|
|
});
|
|
|
|
it('propagates meta and collectedValues', () => {
|
|
ruleEvaluator = makeMockRuleEvaluator({
|
|
a: { possibility: 0.5, reliability: 0.9, meta: { foo: 'bar' }, collectedValues: [1, 2] },
|
|
b: { possibility: 0.8, reliability: 0.8, meta: { baz: 'qux' }, collectedValues: [3] }
|
|
});
|
|
logicalOps = new LogicalOperators(arbiter, ruleEvaluator);
|
|
const rule = { type: 'logical', union: { rules: [{ mockKey: 'a' }, { mockKey: 'b' }] } };
|
|
const res = logicalOps._evaluateRule('u', 'u', 'o', 'o', rule, {}, null, { collectValues: true });
|
|
assert.ok(res.meta);
|
|
assert.deepStrictEqual(res.collectedValues, [1, 2, 3]);
|
|
});
|
|
|
|
it('handles missing rule types gracefully', () => {
|
|
ruleEvaluator = makeMockRuleEvaluator({});
|
|
logicalOps = new LogicalOperators(arbiter, ruleEvaluator);
|
|
const rule = { type: 'logical', union: { rules: [{ mockKey: 'missing' }] } };
|
|
const res = logicalOps._evaluateRule('u', 'u', 'o', 'o', rule, {}, null, {});
|
|
assert.strictEqual(res.possibility, 0);
|
|
assert.ok(res.meta && res.meta.reason === 'missing_rule');
|
|
});
|
|
|
|
it('supports fastPath/early exit logic in union', () => {
|
|
let callCount = 0;
|
|
ruleEvaluator = {
|
|
evaluateRule: (userId, userKey, objectId, objectKey, rule, visited, currentRelation, options) => {
|
|
callCount++;
|
|
if (callCount === 1) return { possibility: 0.95, reliability: 0.9, meta: {}, collectedValues: [1] };
|
|
return { possibility: 0.2, reliability: 0.8, meta: {}, collectedValues: [2] };
|
|
}
|
|
};
|
|
logicalOps = new LogicalOperators(arbiter, ruleEvaluator);
|
|
const rule = { type: 'logical', union: { rules: [{ mockKey: 'a' }, { mockKey: 'b' }] }, fastPath: true, minPossibility: 0.9 };
|
|
const res = logicalOps._evaluateRule('u', 'u', 'o', 'o', rule, {}, null, { fastPath: true, minPossibility: 0.9, collectValues: true });
|
|
assert.strictEqual(res.possibility, 0.95);
|
|
assert.deepStrictEqual(res.collectedValues, [1]);
|
|
});
|
|
|
|
it('supports priority/aggregator effects', () => {
|
|
ruleEvaluator = makeMockRuleEvaluator({
|
|
a: { possibility: 0.7, reliability: 0.9, meta: { rule: { priority: 10 } }, collectedValues: [1] },
|
|
b: { possibility: 0.6, reliability: 0.8, meta: { rule: { priority: 5 } }, collectedValues: [2] }
|
|
});
|
|
logicalOps = new LogicalOperators(arbiter, ruleEvaluator);
|
|
const rule = { type: 'logical', union: { rules: [{ mockKey: 'a', priority: 10 }, { mockKey: 'b', priority: 5 }], aggregator: 'priority' } };
|
|
const res = logicalOps._evaluateRule('u', 'u', 'o', 'o', rule, {}, null, {});
|
|
assert.ok(res.possibility > 0.6 && res.possibility < 0.8);
|
|
});
|
|
});
|