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,148 @@
|
||||
import { Arbiter } from '../src/core/Arbiter.js';
|
||||
|
||||
function parseArgs(argv) {
|
||||
const args = new Map();
|
||||
for (let i = 2; i < argv.length; i++) {
|
||||
const value = argv[i];
|
||||
if (!value.startsWith('--')) continue;
|
||||
const [key, inline] = value.slice(2).split('=');
|
||||
if (inline !== undefined) {
|
||||
args.set(key, inline);
|
||||
continue;
|
||||
}
|
||||
const next = argv[i + 1];
|
||||
if (next && !next.startsWith('--')) {
|
||||
args.set(key, next);
|
||||
i++;
|
||||
} else {
|
||||
args.set(key, true);
|
||||
}
|
||||
}
|
||||
return args;
|
||||
}
|
||||
|
||||
function parseSizes(value) {
|
||||
if (!value) return null;
|
||||
return value.split(',').map(item => Number(item.trim())).filter(Number.isFinite);
|
||||
}
|
||||
|
||||
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 buildNodes(arbiter, count, prefix) {
|
||||
for (let i = 0; i < count; i++) {
|
||||
arbiter.addNode(`${prefix}:${i}`, prefix);
|
||||
}
|
||||
}
|
||||
|
||||
function buildScenario(arbiter, size, edgeFactor, rng) {
|
||||
const nodes = size;
|
||||
buildNodes(arbiter, nodes, 'user');
|
||||
buildNodes(arbiter, nodes, 'resource');
|
||||
|
||||
for (let i = 0; i < nodes; i++) {
|
||||
arbiter.addRelation(`user:${i}`, 'link', `resource:${i}`, 1.0);
|
||||
}
|
||||
|
||||
const extraEdges = Math.max(0, Math.floor(nodes * edgeFactor));
|
||||
for (let i = 0; i < extraEdges; i++) {
|
||||
const src = randInt(rng, nodes);
|
||||
const dst = randInt(rng, nodes);
|
||||
if (src === dst) continue;
|
||||
arbiter.addRelation(`user:${src}`, 'link', `resource:${dst}`, 1.0, { value: (src + dst) % 100 });
|
||||
}
|
||||
|
||||
arbiter.setRelationConfig('link', { type: 'direct' });
|
||||
arbiter.setRelationConfig('multi_hop_link', {
|
||||
type: 'multi_hop',
|
||||
relation: 'link',
|
||||
maxDepth: 3,
|
||||
pathAggregation: 'max',
|
||||
collectValues: false,
|
||||
trackPaths: false
|
||||
});
|
||||
}
|
||||
|
||||
function buildQueries(size, rng, samples) {
|
||||
const queries = [];
|
||||
for (let i = 0; i < samples; i++) {
|
||||
const userId = randInt(rng, size);
|
||||
const objectId = randInt(rng, size);
|
||||
queries.push({ userKey: `user:${userId}`, objectKey: `resource:${objectId}` });
|
||||
}
|
||||
return queries;
|
||||
}
|
||||
|
||||
function measure(label, iterations, fn) {
|
||||
const start = process.hrtime.bigint();
|
||||
for (let i = 0; i < iterations; i++) fn();
|
||||
const elapsedMs = Number(process.hrtime.bigint() - start) / 1e6;
|
||||
return { label, elapsedMs, qps: elapsedMs > 0 ? Math.round((iterations / elapsedMs) * 1000) : 0 };
|
||||
}
|
||||
|
||||
function measureMedian(label, iterations, fn, warmups, runs) {
|
||||
for (let w = 0; w < warmups; w++) {
|
||||
for (let i = 0; i < iterations; i++) fn();
|
||||
}
|
||||
|
||||
const samples = [];
|
||||
for (let r = 0; r < runs; r++) {
|
||||
samples.push(measure(label, iterations, fn).qps);
|
||||
}
|
||||
|
||||
samples.sort((a, b) => a - b);
|
||||
const mid = Math.floor(samples.length / 2);
|
||||
const median = samples.length % 2 === 0
|
||||
? Math.round(((samples[mid - 1] + samples[mid]) / 2) * 100) / 100
|
||||
: samples[mid];
|
||||
return { label, elapsedMs: null, qps: median };
|
||||
}
|
||||
|
||||
const args = parseArgs(process.argv);
|
||||
const config = {
|
||||
sizes: parseSizes(args.get('sizes')) || [2000, 5000, 10000],
|
||||
samples: Number(args.get('samples') || 2000),
|
||||
seed: Number(args.get('seed') || 42),
|
||||
edgeFactor: Number(args.get('edge-factor') || 1.5),
|
||||
includeMeta: !args.has('no-meta'),
|
||||
collectValues: !args.has('no-values'),
|
||||
warmupRuns: Number(args.get('warmup-runs') || 2),
|
||||
medianRuns: Number(args.get('median-runs') || 5),
|
||||
relationGraph: args.has('relation-graph')
|
||||
};
|
||||
|
||||
console.log('multi_hop_rule_bench');
|
||||
console.log('size,samples,meta,values,relation_graph,metric,elapsed_ms,qps');
|
||||
|
||||
for (const size of config.sizes) {
|
||||
const rng = createRng(config.seed + size);
|
||||
const arbiter = new Arbiter({ useRelationGraphTraversal: config.relationGraph });
|
||||
buildScenario(arbiter, size, config.edgeFactor, rng);
|
||||
const queries = buildQueries(size, rng, config.samples);
|
||||
const baseOptions = { fastPath: true, includeMeta: config.includeMeta, collectValues: config.collectValues };
|
||||
|
||||
const multiHop = measureMedian('multi_hop', queries.length, () => {
|
||||
const q = queries[randInt(rng, queries.length)];
|
||||
arbiter.check(q.userKey, 'multi_hop_link', q.objectKey, baseOptions);
|
||||
}, config.warmupRuns, config.medianRuns);
|
||||
|
||||
console.log([
|
||||
size,
|
||||
queries.length,
|
||||
config.includeMeta ? 'on' : 'off',
|
||||
config.collectValues ? 'on' : 'off',
|
||||
config.relationGraph ? 'on' : 'off',
|
||||
multiHop.label,
|
||||
multiHop.elapsedMs === null ? 'median' : Math.round(multiHop.elapsedMs),
|
||||
multiHop.qps
|
||||
].join(','));
|
||||
}
|
||||
Reference in New Issue
Block a user