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.
250 lines
8.7 KiB
JavaScript
250 lines
8.7 KiB
JavaScript
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();
|
|
}
|