initial commit: @arbiter/core authorization engine with js-rigor hardening
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.
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
import { ParentRule } from '../../src/authorization/rules/ParentRule.js';
|
||||
import { Arbiter } from '../../src/core/Arbiter.js';
|
||||
import { describe, it, beforeEach } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
|
||||
describe('ParentRule', () => {
|
||||
let arbiter;
|
||||
let parentRule;
|
||||
|
||||
beforeEach(() => {
|
||||
// Minimal mock Arbiter with relationManager and inference
|
||||
arbiter = {
|
||||
relationManager: {
|
||||
getRelationsFromSrc: (id, rel) => {
|
||||
if (rel === 'parent' && id === 'child1') {
|
||||
return [{ src: 'child1', dst: 'parentA', rel: 'parent', possibility: 0.8 }];
|
||||
}
|
||||
if (rel === 'parent' && id === 'child2') {
|
||||
return [];
|
||||
}
|
||||
return [];
|
||||
},
|
||||
getRelationsToDst: (id, rel) => {
|
||||
if (rel === 'parent' && id === 'child1') {
|
||||
return [{ src: 'parentA', dst: 'child1', rel: 'parent', possibility: 0.8 }];
|
||||
}
|
||||
if (rel === 'parent' && id === 'child2') {
|
||||
return [];
|
||||
}
|
||||
return [];
|
||||
},
|
||||
getRelationsByName: (rel) => {
|
||||
if (rel === 'parent') {
|
||||
return [
|
||||
{ src: 'parentA', dst: 'child1', rel: 'parent', possibility: 0.8 },
|
||||
{ src: 'parentB', dst: 'child2', rel: 'parent', possibility: 0.4 }
|
||||
];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
},
|
||||
keyByNodeId: new Map([
|
||||
['parentA', 'parentA'],
|
||||
['parentB', 'parentB'],
|
||||
['child1', 'child1'],
|
||||
['child2', 'child2']
|
||||
]),
|
||||
resolveKey: (id) => arbiter.keyByNodeId.get(id),
|
||||
indices: {
|
||||
getDirectRelation: (srcId, rel, dstId) => {
|
||||
if (srcId === 'user' && dstId === 'parentA') return { possibility: 0.8 };
|
||||
if (srcId === 'user' && dstId === 'parentB') return { possibility: 0.6 };
|
||||
return null;
|
||||
}
|
||||
},
|
||||
_getInferenceEngine: () => ({
|
||||
params: { minCaseThreshold: 1 },
|
||||
estimatePolicyElement: (src, rel, dst) => {
|
||||
if (src === 'parentB' && rel === 'parent' && dst === 'child2') {
|
||||
return { outcome: 'positive', totalCases: 2, possibility: 0.6 };
|
||||
}
|
||||
return { outcome: 'negative', totalCases: 1, possibility: 0.1 };
|
||||
}
|
||||
})
|
||||
};
|
||||
parentRule = new ParentRule(arbiter);
|
||||
});
|
||||
|
||||
it('returns correct possibility and meta for direct parent', () => {
|
||||
const rule = { type: 'parent', parentRelation: 'parent' };
|
||||
const res = parentRule._evaluateRule('user', 'user', 'child1', 'child1', rule, {}, null, {});
|
||||
assert.strictEqual(res.possibility, 0.8);
|
||||
assert.ok(res.meta.parentRule);
|
||||
assert.strictEqual(res.meta.parentRule.type, 'parent_access_checked');
|
||||
assert.strictEqual(res.meta.parentRule.parentKey, 'parentA');
|
||||
});
|
||||
|
||||
// it('returns correct possibility and meta for inferred parent', () => {
|
||||
// // Mock inference result
|
||||
// arbiter.inferencer = {
|
||||
// inferRelation: () => ({
|
||||
// possibility: 0.6, // Inferred possibility
|
||||
// meta: { type: 'inferred', reliability: 0.8 }
|
||||
// })
|
||||
// };
|
||||
// const res = parentRule._evaluateRule('child1', 'child1', 'grandparent', 'grandparent', ruleInferred, {}, null, {});
|
||||
// assert.ok(res.possibility > 0.5 && res.possibility < 0.7); // Check within inferred range
|
||||
// assert.deepStrictEqual(res.meta.type, 'inferred');
|
||||
// });
|
||||
|
||||
it('returns 0 possibility if no parent and no inference', () => {
|
||||
const rule = { type: 'parent', parentRelation: 'parent', allowInference: false };
|
||||
const res = parentRule._evaluateRule('user', 'user', 'child2', 'child2', rule, {}, null, {});
|
||||
assert.strictEqual(res.possibility, 0);
|
||||
assert.ok(res.meta.parentRule);
|
||||
assert.strictEqual(res.meta.parentRule.type, 'no_parent_relationship_found');
|
||||
});
|
||||
|
||||
it('applies minPossibility threshold', () => {
|
||||
const rule = { type: 'parent', parentRelation: 'parent' };
|
||||
const res = parentRule._evaluateRule('user', 'user', 'child1', 'child1', rule, {}, null, { minPossibility: 0.9, fastPath: true });
|
||||
assert.strictEqual(res.possibility, 0);
|
||||
assert.ok(res.meta.parentRule.cutoffAppliedPostFusion);
|
||||
});
|
||||
|
||||
it('applies OWA aggregation for multiple parents', () => {
|
||||
// Add a second direct parent for child1
|
||||
arbiter.relationManager.getRelationsToDst = (id, rel) => {
|
||||
if (rel === 'parent' && id === 'child1') {
|
||||
return [
|
||||
{ src: 'parentA', dst: 'child1', rel: 'parent', possibility: 0.8 },
|
||||
{ src: 'parentB', dst: 'child1', rel: 'parent', possibility: 0.6 }
|
||||
];
|
||||
}
|
||||
return [];
|
||||
};
|
||||
const rule = { type: 'parent', parentRelation: 'parent', aggregator: 'mean' };
|
||||
const res = parentRule._evaluateRule('user', 'user', 'child1', 'child1', rule, {}, null, {});
|
||||
assert.ok(res.possibility > 0.6 && res.possibility < 0.8);
|
||||
assert.ok(res.meta.parentRule.fusionMethod === 'mean');
|
||||
assert.strictEqual(res.meta.parentRule.pathsConsidered, 2);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user