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.
475 lines
14 KiB
JavaScript
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`);
|