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,249 @@
|
||||
import { Arbiter } from '../src/core/Arbiter.js';
|
||||
import { OWAFusion } from '../src/utils/OWAFusion.js';
|
||||
|
||||
function parseArgNumber(args, name, fallback) {
|
||||
const idx = args.indexOf(name);
|
||||
if (idx === -1 || idx + 1 >= args.length) return fallback;
|
||||
const value = Number(args[idx + 1]);
|
||||
return Number.isFinite(value) ? value : fallback;
|
||||
}
|
||||
|
||||
function hasArg(args, name) {
|
||||
return args.includes(name);
|
||||
}
|
||||
|
||||
class Profiler {
|
||||
constructor() {
|
||||
this.stats = new Map();
|
||||
}
|
||||
|
||||
_record(label, deltaMs) {
|
||||
let entry = this.stats.get(label);
|
||||
if (!entry) {
|
||||
entry = { calls: 0, totalMs: 0 };
|
||||
this.stats.set(label, entry);
|
||||
}
|
||||
entry.calls += 1;
|
||||
entry.totalMs += deltaMs;
|
||||
}
|
||||
|
||||
wrap(obj, methodName, label) {
|
||||
if (!obj || typeof obj[methodName] !== 'function') return;
|
||||
const original = obj[methodName];
|
||||
if (original.__profiled) return;
|
||||
const profiler = this;
|
||||
const wrapped = function(...args) {
|
||||
const start = process.hrtime.bigint();
|
||||
try {
|
||||
return original.apply(this, args);
|
||||
} finally {
|
||||
const deltaMs = Number(process.hrtime.bigint() - start) / 1e6;
|
||||
profiler._record(label, deltaMs);
|
||||
}
|
||||
};
|
||||
wrapped.__profiled = true;
|
||||
obj[methodName] = wrapped;
|
||||
}
|
||||
|
||||
report() {
|
||||
const entries = Array.from(this.stats.entries()).map(([label, data]) => ({
|
||||
label,
|
||||
calls: data.calls,
|
||||
totalMs: data.totalMs,
|
||||
avgMs: data.calls ? data.totalMs / data.calls : 0
|
||||
}));
|
||||
entries.sort((a, b) => b.totalMs - a.totalMs);
|
||||
console.log('profile:function_ms');
|
||||
for (const entry of entries) {
|
||||
console.log(` ${entry.label} calls=${entry.calls} total_ms=${entry.totalMs.toFixed(3)} avg_ms=${entry.avgMs.toFixed(6)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function buildOwaComparatorScenario(arbiter, size) {
|
||||
for (let i = 0; i < size; i++) {
|
||||
arbiter.addNode(`user:${i}`, 'user');
|
||||
arbiter.addNode(`resource:${i}`, 'resource');
|
||||
arbiter.addRelation(`user:${i}`, 'risk_score', `resource:${i}`, 1.0, { value: i % 2 === 0 ? 20 : 80 });
|
||||
arbiter.addRelation(`user:${i}`, 'risk_bonus', `resource:${i}`, 1.0, { value: i % 2 === 0 ? 5 : 15 });
|
||||
arbiter.addRelation(`user:${i}`, 'risk_noise', `resource:${i}`, 1.0, { value: i % 2 === 0 ? 0 : 10 });
|
||||
arbiter.addRelation(`resource:${i}`, 'risk_limit', `resource:${i}`, 1.0, { value: 40 });
|
||||
arbiter.addRelation(`resource:${i}`, 'risk_cap', `resource:${i}`, 1.0, { value: 45 });
|
||||
}
|
||||
|
||||
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_union_left', {
|
||||
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]
|
||||
}
|
||||
});
|
||||
|
||||
arbiter.setRelationConfig('risk_union_right', {
|
||||
union: {
|
||||
rules: [
|
||||
{ type: 'direct', relation: 'risk_limit' },
|
||||
{ type: 'direct', relation: 'risk_cap' }
|
||||
],
|
||||
aggregator: 'owa',
|
||||
owaWeights: [0.6, 0.4]
|
||||
}
|
||||
});
|
||||
|
||||
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]
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function resetSortCounter() {
|
||||
OWAFusion.sortCounter = { total: 0, byMode: {}, byMethod: {} };
|
||||
return OWAFusion.sortCounter;
|
||||
}
|
||||
|
||||
function snapshotSortCounter() {
|
||||
const counter = OWAFusion.sortCounter || { total: 0, byMode: {}, byMethod: {} };
|
||||
return {
|
||||
total: counter.total || 0,
|
||||
byMode: { ...counter.byMode },
|
||||
byMethod: { ...counter.byMethod }
|
||||
};
|
||||
}
|
||||
|
||||
function runTrace(arbiter, relation, userKey, objectKey, label) {
|
||||
resetSortCounter();
|
||||
const start = process.hrtime.bigint();
|
||||
const result = arbiter.check(userKey, relation, objectKey, {
|
||||
fastPath: true,
|
||||
includeMeta: true,
|
||||
cacheRuleResult: false
|
||||
});
|
||||
const durationMs = Number(process.hrtime.bigint() - start) / 1e6;
|
||||
const counts = snapshotSortCounter();
|
||||
|
||||
console.log(`trace:${label}`);
|
||||
console.log(` result=${result.possibility.toFixed(3)} duration_ms=${durationMs.toFixed(3)}`);
|
||||
console.log(` sorts_total=${counts.total}`);
|
||||
console.log(` sorts_by_method=${JSON.stringify(counts.byMethod)}`);
|
||||
console.log(` sorts_by_mode=${JSON.stringify(counts.byMode)}`);
|
||||
}
|
||||
|
||||
function runBench(arbiter, relation, size, runs, label, useResourceKey = false) {
|
||||
const userKey = `user:${Math.floor(size / 2)}`;
|
||||
const objectKey = `resource:${Math.floor(size / 2)}`;
|
||||
const subjectKey = useResourceKey ? objectKey : userKey;
|
||||
const targetKey = useResourceKey ? objectKey : objectKey;
|
||||
resetSortCounter();
|
||||
|
||||
for (let i = 0; i < 200; i++) {
|
||||
arbiter.check(subjectKey, relation, targetKey, { fastPath: true, cacheRuleResult: false });
|
||||
}
|
||||
|
||||
const start = process.hrtime.bigint();
|
||||
for (let i = 0; i < runs; i++) {
|
||||
arbiter.check(subjectKey, relation, targetKey, { fastPath: true, cacheRuleResult: false });
|
||||
}
|
||||
const durationMs = Number(process.hrtime.bigint() - start) / 1e6;
|
||||
const counts = snapshotSortCounter();
|
||||
const perQuery = durationMs / runs;
|
||||
const sortsPerQuery = runs > 0 ? counts.total / runs : 0;
|
||||
|
||||
console.log(`bench:${label}`);
|
||||
console.log(` runs=${runs} total_ms=${durationMs.toFixed(2)} per_query_ms=${perQuery.toFixed(4)}`);
|
||||
console.log(` sorts_total=${counts.total} sorts_per_query=${sortsPerQuery.toFixed(3)}`);
|
||||
console.log(` sorts_by_method=${JSON.stringify(counts.byMethod)}`);
|
||||
console.log(` sorts_by_mode=${JSON.stringify(counts.byMode)}`);
|
||||
}
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
const size = parseArgNumber(args, '--size', 10000);
|
||||
const runs = parseArgNumber(args, '--runs', 20000);
|
||||
const traceOne = hasArg(args, '--trace-one');
|
||||
const profile = hasArg(args, '--profile');
|
||||
|
||||
const arbiter = new Arbiter({
|
||||
enableRuleResultCache: false,
|
||||
disableCaching: true
|
||||
});
|
||||
buildOwaComparatorScenario(arbiter, size);
|
||||
|
||||
let profiler = null;
|
||||
if (profile) {
|
||||
profiler = new Profiler();
|
||||
profiler.wrap(arbiter, 'check', 'Arbiter.check');
|
||||
profiler.wrap(arbiter.authChecker, 'check', 'AuthorizationChecker.check');
|
||||
profiler.wrap(arbiter.authChecker.ruleEvaluator, 'evaluateRule', 'RuleEvaluator.evaluateRule');
|
||||
profiler.wrap(arbiter.authChecker.ruleEvaluator.logicalOperators, 'evaluateUnion', 'LogicalOperators.evaluateUnion');
|
||||
profiler.wrap(arbiter.authChecker.ruleEvaluator.ruleHandlers.direct, 'evaluate', 'DirectRule.evaluate');
|
||||
const comparator = arbiter.authChecker.ruleEvaluator.ruleHandlers.relational_comparator?.rule;
|
||||
if (comparator) {
|
||||
profiler.wrap(comparator, '_evaluateOperand', 'RelationalComparatorRule._evaluateOperand');
|
||||
profiler.wrap(comparator, '_extractValues', 'RelationalComparatorRule._extractValues');
|
||||
profiler.wrap(comparator, '_aggregateCrispValues', 'RelationalComparatorRule._aggregateCrispValues');
|
||||
profiler.wrap(comparator, '_compareBlurredValues', 'RelationalComparatorRule._compareBlurredValues');
|
||||
}
|
||||
profiler.wrap(OWAFusion, 'fuseWithMeta', 'OWAFusion.fuseWithMeta');
|
||||
profiler.wrap(OWAFusion, 'fuseTriplesWithMeta', 'OWAFusion.fuseTriplesWithMeta');
|
||||
}
|
||||
|
||||
if (traceOne) {
|
||||
const userKey = `user:${Math.floor(size / 2)}`;
|
||||
const objectKey = `resource:${Math.floor(size / 2)}`;
|
||||
const resourceKey = objectKey;
|
||||
runTrace(arbiter, 'risk_union_left', userKey, objectKey, 'union_left');
|
||||
runTrace(arbiter, 'risk_union_right', resourceKey, resourceKey, 'union_right');
|
||||
runTrace(arbiter, 'risk_ok_owa', userKey, objectKey, 'comparator_nested');
|
||||
} else {
|
||||
runBench(arbiter, 'risk_union_left', size, Math.max(1000, Math.floor(runs / 2)), 'union_left');
|
||||
runBench(arbiter, 'risk_union_right', size, Math.max(1000, Math.floor(runs / 2)), 'union_right', true);
|
||||
runBench(arbiter, 'risk_ok_owa', size, runs, 'comparator_nested');
|
||||
}
|
||||
|
||||
if (profiler) {
|
||||
profiler.report();
|
||||
}
|
||||
Reference in New Issue
Block a user