Files
core/benchmarks/setup-canonical-benchmark-graph.js
T
John Dvorak 717ae1031e 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.
2026-07-31 13:44:06 -07:00

475 lines
14 KiB
JavaScript

import fs from 'node:fs';
// Zipfian distribution generator
function zipfian(n, s = 1.0) {
const harmonic = Array.from({ length: n }, (_, i) => 1 / Math.pow(i + 1, s))
.reduce((sum, val) => sum + val, 0);
return function() {
const r = Math.random() * harmonic;
let sum = 0;
for (let i = 0; i < n; i++) {
sum += 1 / Math.pow(i + 1, s);
if (sum >= r) return i;
}
return n - 1;
};
}
// Preferential attachment with aging
class PreferentialAttachment {
constructor() {
this.degrees = new Map();
this.totalDegree = 0;
this.ageDecay = 0.95; // Older connections become less likely
}
addNode(nodeKey) {
this.degrees.set(nodeKey, 1); // Start with degree 1
this.totalDegree += 1;
}
selectNode() {
if (this.totalDegree === 0) return null;
let r = Math.random() * this.totalDegree;
for (const [nodeKey, degree] of this.degrees) {
r -= degree;
if (r <= 0) return nodeKey;
}
return Array.from(this.degrees.keys())[0]; // Fallback
}
addEdge(srcKey, dstKey) {
this.degrees.set(srcKey, (this.degrees.get(srcKey) || 0) + 1);
this.degrees.set(dstKey, (this.degrees.get(dstKey) || 0) + 1);
this.totalDegree += 2;
}
decay() {
// Age all connections slightly
for (const [nodeKey, degree] of this.degrees) {
const newDegree = Math.max(1, degree * this.ageDecay);
this.totalDegree += newDegree - degree;
this.degrees.set(nodeKey, newDegree);
}
}
}
function generateCanonicalBenchmarkGraph(config = {}) {
const {
numUsers = 10000,
numGroups = 100,
numRoles = 20,
numProjects = 200,
numDocs = 5000,
numDevices = 1000,
numLocations = 50,
zipfianExponent = 1.2,
preferentialStrength = 0.8
} = config;
console.log('🏗️ Generating Canonical Benchmark Graph');
console.log(`Users: ${numUsers}, Groups: ${numGroups}, Docs: ${numDocs}`);
console.log(`Zipfian exponent: ${zipfianExponent}, Preferential strength: ${preferentialStrength}`);
const nodes = [];
const relations = [];
const metadata = {
generation: {
timestamp: new Date().toISOString(),
config,
stats: {}
},
testCases: {
highConfidenceAllow: [],
mediumConfidenceAllow: [],
lowConfidenceAllow: [],
shouldDeny: [],
uncertain: [],
admins: [],
newEmployees: []
},
relationConfigs: {}
};
// Zipfian generators for realistic distributions
const groupZipf = zipfian(numGroups, zipfianExponent);
const roleZipf = zipfian(numRoles, zipfianExponent);
const projectZipf = zipfian(numProjects, zipfianExponent);
const docZipf = zipfian(numDocs, zipfianExponent * 0.8); // Slightly flatter for docs
// Preferential attachment trackers
const groupPA = new PreferentialAttachment();
const projectPA = new PreferentialAttachment();
const userPA = new PreferentialAttachment();
console.log('Creating nodes...');
// Create organizational structure
for (let i = 0; i < numGroups; i++) {
const groupKey = `group:${i}`;
nodes.push({ key: groupKey, type: 'group' });
groupPA.addNode(groupKey);
}
for (let i = 0; i < numRoles; i++) {
nodes.push({ key: `role:${i}`, type: 'role' });
}
for (let i = 0; i < numProjects; i++) {
const projectKey = `project:${i}`;
nodes.push({ key: projectKey, type: 'project' });
projectPA.addNode(projectKey);
}
for (let i = 0; i < numLocations; i++) {
nodes.push({ key: `location:${i}`, type: 'location' });
}
for (let i = 0; i < numDevices; i++) {
nodes.push({ key: `device:${i}`, type: 'device' });
}
// Create users with realistic distributions
console.log('Creating users and basic relations...');
for (let i = 0; i < numUsers; i++) {
const userKey = `user:${i}`;
nodes.push({ key: userKey, type: 'user' });
userPA.addNode(userKey);
// Zipfian group membership (some groups are much more popular)
const numGroupMemberships = Math.min(5, Math.floor(Math.random() * 3) + 1);
for (let j = 0; j < numGroupMemberships; j++) {
const groupIdx = groupZipf();
const groupKey = `group:${groupIdx}`;
relations.push({
src: userKey,
rel: 'member_of',
dst: groupKey,
possibility: 0.95 + Math.random() * 0.05 // High confidence
});
groupPA.addEdge(userKey, groupKey);
}
// Role assignment (Zipfian - some roles much more common)
if (Math.random() < 0.8) { // 80% of users have roles
const roleIdx = roleZipf();
relations.push({
src: userKey,
rel: 'has_role',
dst: `role:${roleIdx}`,
possibility: 0.9 + Math.random() * 0.1
});
}
// Project assignment (preferential attachment)
const numProjects = Math.floor(Math.random() * 3) + 1;
for (let j = 0; j < numProjects; j++) {
let projectKey;
if (Math.random() < preferentialStrength) {
projectKey = projectPA.selectNode();
} else {
projectKey = `project:${Math.floor(Math.random() * numProjects)}`;
}
if (projectKey) {
relations.push({
src: userKey,
rel: 'works_on',
dst: projectKey,
possibility: 0.85 + Math.random() * 0.15
});
projectPA.addEdge(userKey, projectKey);
}
}
// Location assignment
if (Math.random() < 0.9) { // 90% have locations
const locationIdx = Math.floor(Math.random() * numLocations);
relations.push({
src: userKey,
rel: 'located_at',
dst: `location:${locationIdx}`,
possibility: 0.98
});
}
// Device assignment
if (Math.random() < 0.7) { // 70% have assigned devices
const deviceIdx = Math.floor(Math.random() * numDevices);
relations.push({
src: userKey,
rel: 'uses_device',
dst: `device:${deviceIdx}`,
possibility: 0.9 + Math.random() * 0.1
});
}
if (i % 1000 === 0) console.log(` Created ${i} users...`);
}
// Create documents with realistic access patterns
console.log('Creating documents and access patterns...');
for (let i = 0; i < numDocs; i++) {
const docKey = `doc:${i}`;
nodes.push({ key: docKey, type: 'doc' });
// Document ownership (Zipfian - some groups own many docs)
const ownerGroupIdx = groupZipf();
const ownerGroupKey = `group:${ownerGroupIdx}`;
relations.push({
src: ownerGroupKey,
rel: 'owns',
dst: docKey,
possibility: 1.0
});
// Direct user access (some users have direct access)
if (Math.random() < 0.3) { // 30% of docs have direct user access
const numDirectUsers = Math.floor(Math.random() * 5) + 1;
for (let j = 0; j < numDirectUsers; j++) {
const userIdx = Math.floor(Math.random() * numUsers);
const confidence = Math.random();
let possibility;
if (confidence < 0.3) possibility = 0.95 + Math.random() * 0.05; // High confidence
else if (confidence < 0.7) possibility = 0.7 + Math.random() * 0.2; // Medium confidence
else possibility = 0.4 + Math.random() * 0.3; // Lower confidence
relations.push({
src: `user:${userIdx}`,
rel: 'can_read',
dst: docKey,
possibility
});
}
}
// Project-based access
if (Math.random() < 0.4) { // 40% of docs are project-related
const projectIdx = Math.floor(Math.random() * numProjects);
relations.push({
src: `project:${projectIdx}`,
rel: 'can_read',
dst: docKey,
possibility: 0.8 + Math.random() * 0.2
});
}
if (i % 500 === 0) console.log(` Created ${i} documents...`);
}
// Create hierarchical relationships
console.log('Creating hierarchical relationships...');
// Group hierarchies (some groups are parents of others)
for (let i = 0; i < numGroups; i++) {
if (Math.random() < 0.3) { // 30% chance of having a parent
const parentIdx = Math.floor(Math.random() * numGroups);
if (parentIdx !== i) {
relations.push({
src: `group:${parentIdx}`,
rel: 'parent_of',
dst: `group:${i}`,
possibility: 1.0
});
}
}
}
// Role hierarchies
for (let i = 1; i < numRoles; i++) {
if (Math.random() < 0.4) { // 40% chance of role inheritance
const parentRoleIdx = Math.floor(Math.random() * i);
relations.push({
src: `role:${parentRoleIdx}`,
rel: 'inherits_from',
dst: `role:${i}`,
possibility: 1.0
});
}
}
// Create special test users
console.log('Creating special test users...');
// Super admins (high access to everything)
for (let i = 0; i < 3; i++) {
const adminKey = `admin:${i}`;
nodes.push({ key: adminKey, type: 'admin' });
metadata.testCases.admins.push(adminKey);
// Admin access to all groups and many docs
for (let g = 0; g < numGroups; g++) {
relations.push({
src: adminKey,
rel: 'admin_of',
dst: `group:${g}`,
possibility: 1.0
});
}
// Admin access to many documents
for (let d = 0; d < Math.min(1000, numDocs); d++) {
relations.push({
src: adminKey,
rel: 'can_read',
dst: `doc:${d}`,
possibility: 0.98
});
}
}
// New employees (minimal access)
for (let i = 0; i < 10; i++) {
const newEmpKey = `newbie:${i}`;
nodes.push({ key: newEmpKey, type: 'user' });
metadata.testCases.newEmployees.push(newEmpKey);
// Only basic group membership
relations.push({
src: newEmpKey,
rel: 'member_of',
dst: 'group:0', // Everyone group
possibility: 1.0
});
}
// Generate test cases for different confidence levels
console.log('Generating test cases...');
// High confidence cases (direct access)
for (let i = 0; i < 100; i++) {
const userIdx = Math.floor(Math.random() * numUsers);
const docIdx = Math.floor(Math.random() * numDocs);
metadata.testCases.highConfidenceAllow.push([`user:${userIdx}`, 'can_read', `doc:${docIdx}`]);
}
// Medium confidence cases (group-based access)
for (let i = 0; i < 100; i++) {
const userIdx = Math.floor(Math.random() * numUsers);
const docIdx = Math.floor(Math.random() * numDocs);
metadata.testCases.mediumConfidenceAllow.push([`user:${userIdx}`, 'can_read', `doc:${docIdx}`]);
}
// Low confidence cases (similarity-based)
for (let i = 0; i < 100; i++) {
const userIdx = Math.floor(Math.random() * numUsers);
const docIdx = Math.floor(Math.random() * numDocs);
metadata.testCases.lowConfidenceAllow.push([`user:${userIdx}`, 'can_read', `doc:${docIdx}`]);
}
// Should deny cases (cross-tenant, etc.)
for (let i = 0; i < 50; i++) {
const newEmpIdx = Math.floor(Math.random() * 10);
const docIdx = Math.floor(Math.random() * 1000) + 1000; // Higher doc numbers
metadata.testCases.shouldDeny.push([`newbie:${newEmpIdx}`, 'can_read', `doc:${docIdx}`]);
}
// Uncertain cases (edge cases)
for (let i = 0; i < 50; i++) {
const userIdx = Math.floor(Math.random() * numUsers);
const docIdx = Math.floor(Math.random() * numDocs);
metadata.testCases.uncertain.push([`user:${userIdx}`, 'can_read', `doc:${docIdx}`]);
}
// Define complex relation configurations
metadata.relationConfigs = {
can_read: {
union: [
{ type: 'direct', priority: 10, weight: 1.0 },
{
type: 'tuple_to_userset',
tuplesetRelation: 'member_of',
computedRelation: 'can_read',
priority: 8,
weight: 0.9
},
{
type: 'tuple_to_userset',
tuplesetRelation: 'works_on',
computedRelation: 'can_read',
priority: 7,
weight: 0.8
},
{
type: 'similar_to',
relation: 'can_read',
k: 5,
similarityThreshold: 0.4,
priority: 5,
weight: 0.6
},
{
type: 'multi_hop',
path: ['has_role', 'can_read'],
priority: 4,
weight: 0.7
}
]
},
admin_of: {
union: [
{ type: 'direct', priority: 10 },
{
type: 'tuple_to_userset',
tuplesetRelation: 'inherits_from',
computedRelation: 'admin_of',
priority: 8
}
]
}
};
// Calculate statistics
metadata.generation.stats = {
totalNodes: nodes.length,
totalRelations: relations.length,
nodeTypes: {
users: nodes.filter(n => n.type === 'user').length,
groups: nodes.filter(n => n.type === 'group').length,
docs: nodes.filter(n => n.type === 'doc').length,
roles: nodes.filter(n => n.type === 'role').length,
projects: nodes.filter(n => n.type === 'project').length,
devices: nodes.filter(n => n.type === 'device').length,
locations: nodes.filter(n => n.type === 'location').length,
admins: nodes.filter(n => n.type === 'admin').length
},
relationTypes: relations.reduce((acc, rel) => {
acc[rel.rel] = (acc[rel.rel] || 0) + 1;
return acc;
}, {}),
avgPossibility: relations.filter(r => r.possibility).reduce((sum, r) => sum + r.possibility, 0) / relations.filter(r => r.possibility).length
};
console.log('\\n📊 Graph Statistics:');
console.log(`Total nodes: ${metadata.generation.stats.totalNodes}`);
console.log(`Total relations: ${metadata.generation.stats.totalRelations}`);
console.log(`Node types:`, metadata.generation.stats.nodeTypes);
console.log(`Relation types:`, metadata.generation.stats.relationTypes);
console.log(`Average possibility: ${metadata.generation.stats.avgPossibility.toFixed(3)}`);
return { nodes, relations, metadata };
}
// Generate the canonical benchmark graph
console.log('🚀 Generating Canonical Benchmark Graph...');
const canonicalGraph = generateCanonicalBenchmarkGraph({
numUsers: 10000,
numGroups: 100,
numRoles: 20,
numProjects: 200,
numDocs: 5000,
numDevices: 1000,
numLocations: 50,
zipfianExponent: 1.2,
preferentialStrength: 0.8
});
// Save to file
const filename = 'canonical-benchmark-graph.json';
console.log(`\\n💾 Saving to ${filename}...`);
fs.writeFileSync(filename, JSON.stringify(canonicalGraph, null, 2));
console.log('\\n✅ Canonical benchmark graph generated successfully!');
console.log(`📁 Saved to: ${filename}`);
console.log(`📏 Size: ${(fs.statSync(filename).size / 1024 / 1024).toFixed(2)} MB`);