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,177 @@
|
||||
import { Arbiter } from '../src/core/Arbiter.js';
|
||||
|
||||
// Create scenario matching owa_comparator_nested
|
||||
const arbiter = new Arbiter();
|
||||
const SIZE = 5000;
|
||||
|
||||
// Build nodes
|
||||
for (let i = 0; i < SIZE; i++) {
|
||||
arbiter.addNode(`user:${i}`, 'user');
|
||||
arbiter.addNode(`resource:${i}`, 'resource');
|
||||
}
|
||||
|
||||
// Build relations (same as owa-comparator-micro-bench)
|
||||
for (let i = 0; i < SIZE; i++) {
|
||||
const isSafe = i % 2 === 0;
|
||||
const score = isSafe ? 20 : 80;
|
||||
const bonus = isSafe ? 5 : 15;
|
||||
const noise = isSafe ? 0 : 10;
|
||||
arbiter.addRelation(`user:${i}`, 'risk_score', `resource:${i}`, 1.0, { value: score });
|
||||
arbiter.addRelation(`user:${i}`, 'risk_bonus', `resource:${i}`, 1.0, { value: bonus });
|
||||
arbiter.addRelation(`user:${i}`, 'risk_noise', `resource:${i}`, 1.0, { value: noise });
|
||||
arbiter.addRelation(`resource:${i}`, 'risk_limit', `resource:${i}`, 1.0, { value: 40 });
|
||||
arbiter.addRelation(`resource:${i}`, 'risk_cap', `resource:${i}`, 1.0, { value: 45 });
|
||||
}
|
||||
|
||||
// Set relation configs
|
||||
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_cap', { 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: {
|
||||
union: {
|
||||
rules: [
|
||||
{ type: 'direct', relation: 'risk_limit' },
|
||||
{ type: 'direct', relation: 'risk_cap' }
|
||||
],
|
||||
aggregator: 'owa',
|
||||
owaWeights: [0.6, 0.4]
|
||||
}
|
||||
},
|
||||
extractValue: true,
|
||||
valueRelation: 'risk_limit',
|
||||
evaluateFrom: 'object',
|
||||
aggregator: 'owa',
|
||||
owaWeights: [0.6, 0.4]
|
||||
}
|
||||
});
|
||||
|
||||
// Warm up
|
||||
for (let i = 0; i < 100; i++) {
|
||||
arbiter.check(`user:${i}`, 'risk_ok_owa', `resource:${i}`, { fastPath: true });
|
||||
}
|
||||
|
||||
// Profile with timing at each step
|
||||
const ITERATIONS = 1000;
|
||||
let totalMs = 0;
|
||||
|
||||
// Detailed timing breakdown
|
||||
const timings = {
|
||||
total: 0,
|
||||
nodeIdLookup: 0,
|
||||
configGet: 0,
|
||||
visitedCheck: 0,
|
||||
ruleEval: 0
|
||||
};
|
||||
|
||||
const start = process.hrtime.bigint();
|
||||
for (let i = 0; i < ITERATIONS; i++) {
|
||||
const idx = i % SIZE;
|
||||
arbiter.check(`user:${idx}`, 'risk_ok_owa', `resource:${idx}`, { fastPath: true });
|
||||
}
|
||||
totalMs = Number(process.hrtime.bigint() - start) / 1e6;
|
||||
const qps = Math.round((ITERATIONS / totalMs) * 1000);
|
||||
|
||||
console.log(`Total: ${totalMs.toFixed(2)}ms for ${ITERATIONS} checks = ${qps} QPS`);
|
||||
console.log(`Per check: ${(totalMs / ITERATIONS).toFixed(4)}ms`);
|
||||
|
||||
// Now let's profile object allocations
|
||||
console.log('\n--- Checking allocation patterns ---');
|
||||
|
||||
// Check visited set behavior
|
||||
const visited = new Set();
|
||||
const visitKey1 = { userKey: 'user:0', relation: 'test', objectKey: 'resource:0' };
|
||||
const visitKey2 = { userKey: 'user:0', relation: 'test', objectKey: 'resource:0' };
|
||||
visited.add(visitKey1);
|
||||
console.log(`Same keys, different objects in Set: ${visited.has(visitKey2)}`); // false - reference equality
|
||||
|
||||
// Count how many allocations happen per check
|
||||
console.log('\n--- Checking comparison: direct vs comparator ---');
|
||||
|
||||
// Profile direct check
|
||||
const directStart = process.hrtime.bigint();
|
||||
for (let i = 0; i < ITERATIONS; i++) {
|
||||
const idx = i % SIZE;
|
||||
arbiter.check(`user:${idx}`, 'risk_score', `resource:${idx}`, { fastPath: true });
|
||||
}
|
||||
const directMs = Number(process.hrtime.bigint() - directStart) / 1e6;
|
||||
console.log(`Direct: ${directMs.toFixed(2)}ms = ${Math.round((ITERATIONS / directMs) * 1000)} QPS`);
|
||||
|
||||
// Profile union eval only
|
||||
const unionRule = {
|
||||
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]
|
||||
}
|
||||
};
|
||||
const unionStart = process.hrtime.bigint();
|
||||
for (let i = 0; i < ITERATIONS; i++) {
|
||||
const idx = i % SIZE;
|
||||
const userId = arbiter.nodeIdByKey.get(`user:${idx}`);
|
||||
const objectId = arbiter.nodeIdByKey.get(`resource:${idx}`);
|
||||
arbiter.authChecker.ruleEvaluator.evaluateRule(
|
||||
userId, `user:${idx}`, objectId, `resource:${idx}`,
|
||||
unionRule, new Set(), 'risk_ok_owa', { fastPath: true }
|
||||
);
|
||||
}
|
||||
const unionMs = Number(process.hrtime.bigint() - unionStart) / 1e6;
|
||||
console.log(`Union eval: ${unionMs.toFixed(2)}ms = ${Math.round((ITERATIONS / unionMs) * 1000)} QPS`);
|
||||
|
||||
// What's the overhead of authChecker.check vs direct ruleEvaluator call?
|
||||
const checkStart = process.hrtime.bigint();
|
||||
for (let i = 0; i < ITERATIONS; i++) {
|
||||
const idx = i % SIZE;
|
||||
arbiter.authChecker.check(`user:${idx}`, 'risk_ok_owa', `resource:${idx}`, { fastPath: true });
|
||||
}
|
||||
const checkMs = Number(process.hrtime.bigint() - checkStart) / 1e6;
|
||||
console.log(`authChecker.check: ${checkMs.toFixed(2)}ms = ${Math.round((ITERATIONS / checkMs) * 1000)} QPS`);
|
||||
|
||||
// Direct to evaluateRule for relational_comparator
|
||||
const comparatorRule = arbiter.relationConfigs.get('risk_ok_owa');
|
||||
const comparatorStart = process.hrtime.bigint();
|
||||
for (let i = 0; i < ITERATIONS; i++) {
|
||||
const idx = i % SIZE;
|
||||
const userId = arbiter.nodeIdByKey.get(`user:${idx}`);
|
||||
const objectId = arbiter.nodeIdByKey.get(`resource:${idx}`);
|
||||
arbiter.authChecker.ruleEvaluator.evaluateRule(
|
||||
userId, `user:${idx}`, objectId, `resource:${idx}`,
|
||||
comparatorRule, new Set(), 'risk_ok_owa', { fastPath: true }
|
||||
);
|
||||
}
|
||||
const comparatorMs = Number(process.hrtime.bigint() - comparatorStart) / 1e6;
|
||||
console.log(`ruleEvaluator.evaluateRule (comparator): ${comparatorMs.toFixed(2)}ms = ${Math.round((ITERATIONS / comparatorMs) * 1000)} QPS`);
|
||||
|
||||
console.log('\n--- Summary ---');
|
||||
console.log(`Direct check baseline: ${Math.round((ITERATIONS / directMs) * 1000)} QPS`);
|
||||
console.log(`Union eval: ${Math.round((ITERATIONS / unionMs) * 1000)} QPS (${(unionMs/directMs).toFixed(1)}x slower than direct)`);
|
||||
console.log(`Full comparator via ruleEvaluator: ${Math.round((ITERATIONS / comparatorMs) * 1000)} QPS (${(comparatorMs/directMs).toFixed(1)}x slower than direct)`);
|
||||
console.log(`Full comparator via authChecker: ${Math.round((ITERATIONS / checkMs) * 1000)} QPS (${(checkMs/directMs).toFixed(1)}x slower than direct)`);
|
||||
Reference in New Issue
Block a user