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.
166 lines
4.9 KiB
JavaScript
166 lines
4.9 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import { describe, test } from 'node:test';
|
|
import { Arbiter } from '../../src/core/Arbiter.js';
|
|
import { OWAFusion } from '../../src/utils/OWAFusion.js';
|
|
|
|
function createRng(seed) {
|
|
let state = seed >>> 0;
|
|
return () => {
|
|
state = (1664525 * state + 1013904223) >>> 0;
|
|
return state / 0x100000000;
|
|
};
|
|
}
|
|
|
|
function randInt(rng, max) {
|
|
return Math.floor(rng() * max);
|
|
}
|
|
|
|
function randFloat(rng, min = 0, max = 1) {
|
|
return min + (max - min) * rng();
|
|
}
|
|
|
|
function buildComparatorArbiter() {
|
|
const arbiter = new Arbiter();
|
|
arbiter.addNode('user:1', 'user');
|
|
arbiter.addNode('resource:1', 'resource');
|
|
|
|
arbiter.setRelationConfig('risk_score', { type: 'direct' });
|
|
arbiter.setRelationConfig('risk_bonus', { type: 'direct' });
|
|
arbiter.setRelationConfig('risk_noise', { type: 'direct' });
|
|
arbiter.setRelationConfig('risk_limit', { type: 'direct' });
|
|
|
|
arbiter.setRelationConfig('risk_ok_owa', {
|
|
type: 'relational_comparator',
|
|
comparator: '<=',
|
|
fallbackBehavior: 'deny',
|
|
left: {
|
|
rule: {
|
|
union: {
|
|
rules: [
|
|
{ type: 'direct', relation: 'risk_score' },
|
|
{ type: 'direct', relation: 'risk_bonus' },
|
|
{ type: 'direct', relation: 'risk_noise' }
|
|
],
|
|
aggregator: 'owa',
|
|
owaWeights: [0.5, 0.3, 0.2]
|
|
}
|
|
},
|
|
extractValue: true,
|
|
valueRelation: 'risk_score',
|
|
aggregator: 'owa',
|
|
owaWeights: [0.5, 0.3, 0.2]
|
|
},
|
|
right: {
|
|
rule: { type: 'direct', relation: 'risk_limit' },
|
|
extractValue: true,
|
|
valueRelation: 'risk_limit',
|
|
evaluateFrom: 'object'
|
|
}
|
|
});
|
|
|
|
arbiter.registerDependencyIndex(new Map([
|
|
['risk_score', {
|
|
all: new Set(['risk_ok_owa']),
|
|
byLevel: {
|
|
never: new Set(),
|
|
always: new Set(),
|
|
requires: new Set(),
|
|
when: new Set(),
|
|
unless: new Set(),
|
|
ordinary: new Set(['risk_ok_owa'])
|
|
}
|
|
}],
|
|
['risk_bonus', {
|
|
all: new Set(['risk_ok_owa']),
|
|
byLevel: {
|
|
never: new Set(),
|
|
always: new Set(),
|
|
requires: new Set(),
|
|
when: new Set(),
|
|
unless: new Set(),
|
|
ordinary: new Set(['risk_ok_owa'])
|
|
}
|
|
}],
|
|
['risk_noise', {
|
|
all: new Set(['risk_ok_owa']),
|
|
byLevel: {
|
|
never: new Set(),
|
|
always: new Set(),
|
|
requires: new Set(),
|
|
when: new Set(),
|
|
unless: new Set(),
|
|
ordinary: new Set(['risk_ok_owa'])
|
|
}
|
|
}],
|
|
['risk_limit', {
|
|
all: new Set(['risk_ok_owa']),
|
|
byLevel: {
|
|
never: new Set(),
|
|
always: new Set(),
|
|
requires: new Set(),
|
|
when: new Set(),
|
|
unless: new Set(),
|
|
ordinary: new Set(['risk_ok_owa'])
|
|
}
|
|
}]
|
|
]));
|
|
|
|
return arbiter;
|
|
}
|
|
|
|
describe('Comparator aggregation properties', () => {
|
|
test('OWA aggregated value drives comparator decision', () => {
|
|
const rng = createRng(24);
|
|
const arbiter = buildComparatorArbiter();
|
|
const metas = [{}, {}, {}];
|
|
|
|
for (let i = 0; i < 120; i++) {
|
|
const values = [
|
|
randFloat(rng, 0, 100),
|
|
randFloat(rng, 0, 100),
|
|
randFloat(rng, 0, 100)
|
|
];
|
|
const limit = randFloat(rng, 0, 100);
|
|
|
|
arbiter.removeRelation('user:1', 'risk_score', 'resource:1');
|
|
arbiter.removeRelation('user:1', 'risk_bonus', 'resource:1');
|
|
arbiter.removeRelation('user:1', 'risk_noise', 'resource:1');
|
|
arbiter.removeRelation('resource:1', 'risk_limit', 'resource:1');
|
|
|
|
arbiter.addRelation('user:1', 'risk_score', 'resource:1', 1.0, { value: values[0] });
|
|
arbiter.addRelation('user:1', 'risk_bonus', 'resource:1', 1.0, { value: values[1] });
|
|
arbiter.addRelation('user:1', 'risk_noise', 'resource:1', 1.0, { value: values[2] });
|
|
arbiter.addRelation('resource:1', 'risk_limit', 'resource:1', 1.0, { value: limit });
|
|
|
|
const fused = OWAFusion.fuseWithMeta(values, metas, [0.5, 0.3, 0.2], 'owa', true).value;
|
|
const expectedAllow = fused <= limit;
|
|
|
|
const result = arbiter.check('user:1', 'risk_ok_owa', 'resource:1', { fastPath: false });
|
|
const allow = result.possibility > 0;
|
|
assert.strictEqual(allow, expectedAllow, 'comparator respects aggregation');
|
|
}
|
|
});
|
|
|
|
test('increasing a component value does not decrease fused value', () => {
|
|
const rng = createRng(88);
|
|
const arbiter = buildComparatorArbiter();
|
|
const metas = [{}, {}, {}];
|
|
|
|
for (let i = 0; i < 120; i++) {
|
|
const values = [
|
|
randFloat(rng, 0, 100),
|
|
randFloat(rng, 0, 100),
|
|
randFloat(rng, 0, 100)
|
|
];
|
|
const index = randInt(rng, values.length);
|
|
const delta = randFloat(rng, 0, 20);
|
|
const bumped = values.slice();
|
|
bumped[index] += delta;
|
|
|
|
const base = OWAFusion.fuseWithMeta(values, metas, [0.5, 0.3, 0.2], 'owa', true).value;
|
|
const higher = OWAFusion.fuseWithMeta(bumped, metas, [0.5, 0.3, 0.2], 'owa', true).value;
|
|
assert.ok(higher >= base - 1e-6, 'fused value is monotonic');
|
|
}
|
|
});
|
|
});
|