Files
core/tests/helpers/big-graph-generator.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

598 lines
19 KiB
JavaScript

import { Arbiter } from '../../src/core/Arbiter.js';
/**
* Big Graph Generator for Enterprise Authorization Testing
* Generates realistic authorization graphs with complex multi-hop relationships
*/
export class BigGraphGenerator {
constructor(config = {}) {
this.config = {
scale: 'small',
seed: 12345,
...config
};
// Scale configurations - reduced for testing
this.scaleConfigs = {
small: {
users: 100,
documents: 500,
relations: 1000,
complexChains: 200, // Multi-hop authorization chains
targetQPS: 100,
maxMemoryMB: 128,
maxLatencyMs: 50
},
medium: {
users: 500,
documents: 2000,
relations: 5000,
complexChains: 1000,
targetQPS: 200,
maxMemoryMB: 256,
maxLatencyMs: 100
},
large: {
users: 1000,
documents: 5000,
relations: 10000,
complexChains: 2000,
targetQPS: 500,
maxMemoryMB: 512,
maxLatencyMs: 200
},
enterprise: {
users: 2000,
documents: 10000,
relations: 20000,
complexChains: 5000,
targetQPS: 1000,
maxMemoryMB: 1024,
maxLatencyMs: 500
},
million: {
users: 100000,
documents: 900000,
relations: 2000000,
complexChains: 10000, // Reduced complexity to avoid stack overflow
targetQPS: 100,
maxMemoryMB: 8192,
maxLatencyMs: 1000
}
};
// Initialize random number generator with seed
this.rng = this._createSeededRNG(this.config.seed);
}
/**
* Create seeded random number generator for reproducible results
*/
_createSeededRNG(seed) {
let state = seed;
return {
next: () => {
state = (state * 1664525 + 1013904223) % 4294967296;
return state / 4294967296;
}
};
}
/**
* Generate authorization graph for specified model
*/
generateGraph(model = 'enterprise') {
const scale = this.scaleConfigs[this.config.scale];
const graphData = {
users: [],
documents: [],
relations: [],
enterprises: [],
saasTenants: [],
metadata: {
model,
scale: this.config.scale,
generatedAt: new Date().toISOString(),
seed: this.config.seed
}
};
// Generate users
graphData.users = this._generateUsers(scale);
// Generate documents
graphData.documents = this._generateDocuments(scale);
// Generate basic user-document relationships
const basicRelations = this._generateBasicRelations(scale, graphData.users, graphData.documents);
graphData.relations.push(...basicRelations);
// Generate complex authorization chains (multi-hop relationships)
const { complexChains, roleNodes, departmentNodes } = this._generateComplexAuthorizationChains(scale, graphData.users, graphData.documents);
graphData.relations.push(...complexChains);
// Add the generated nodes to the graph data
graphData.roles = roleNodes;
graphData.departments = departmentNodes;
// Generate organizational hierarchies
const orgRelations = this._generateOrganizationalHierarchies(scale, graphData.users);
graphData.relations.push(...orgRelations);
// Generate long organizational chains for multi-hop testing
const longChains = this._generateLongOrganizationalChains(scale, graphData.users, graphData.documents);
graphData.relations.push(...longChains);
// Generate role-based access chains
const roleChains = this._generateRoleBasedChains(scale, graphData.users, graphData.documents);
graphData.relations.push(...roleChains);
return graphData;
}
/**
* Load graph data into Arbiter instance
*/
loadIntoArbiter(graphData, options = {}) {
const arbiter = new Arbiter({
fastConstructionMode: true,
...options
});
// Add all nodes
graphData.users.forEach(user => {
arbiter.addNode(user.id, 'user');
});
graphData.documents.forEach(doc => {
arbiter.addNode(doc.id, 'document');
});
// Add role nodes if they exist
if (graphData.roles) {
graphData.roles.forEach(role => {
arbiter.addNode(role.id, 'role');
});
}
// Add department nodes if they exist
if (graphData.departments) {
graphData.departments.forEach(dept => {
arbiter.addNode(dept.id, 'department');
});
}
// Add intermediate nodes for long chains
const longChainRelations = graphData.relations.filter(r => r.metadata?.type === 'long_chain');
const intermediateNodes = new Set();
longChainRelations.forEach(relation => {
// Add all intermediate nodes (teams, departments, divisions, companies)
if (relation.src.startsWith('team:') || relation.src.startsWith('dept:') ||
relation.src.startsWith('division:') || relation.src.startsWith('company:')) {
intermediateNodes.add(relation.src);
}
if (relation.dst.startsWith('team:') || relation.dst.startsWith('division:') || relation.dst.startsWith('company:')) {
intermediateNodes.add(relation.dst);
}
});
intermediateNodes.forEach(nodeId => {
const nodeType = nodeId.startsWith('team:') ? 'team' :
nodeId.startsWith('dept:') ? 'department' :
nodeId.startsWith('division:') ? 'division' : 'company';
arbiter.addNode(nodeId, nodeType);
});
// Add all relations
graphData.relations.forEach(relation => {
arbiter.addRelation(relation.src, relation.relation, relation.dst, relation.possibility);
});
// Disable fast construction mode and build indices now that loading is complete
arbiter.setFastConstructionMode(false);
// Configure relation types with proper ReBAC patterns
const relationTypes = ['can_read', 'can_write', 'can_delete', 'can_share', 'can_admin'];
relationTypes.forEach(relType => {
// Configure direct relations
arbiter.setRelationConfig(relType, { type: 'direct' });
// Configure role-based access using chain rule for proper role-based access
const roleBasedRel = `${relType}_via_role`;
arbiter.setRelationConfig(roleBasedRel, {
type: 'chain',
steps: [
{ relation: 'member_of', direction: 'out' },
{ relation: relType, direction: 'out' }
]
});
});
// Configure membership relations as direct
const membershipTypes = ['member_of', 'manager_of', 'reports_to'];
membershipTypes.forEach(relType => {
arbiter.setRelationConfig(relType, { type: 'direct' });
});
// Configure department-based access using chain rule
const departmentBasedRel = 'can_write_via_department';
arbiter.setRelationConfig(departmentBasedRel, {
type: 'chain',
steps: [
{ relation: 'member_of', direction: 'out' },
{ relation: 'can_write', direction: 'out' }
]
});
// Configure multi-hop access for long chain traversal (computationally intensive)
// Use chain rule for complex multi-step authorization
arbiter.setRelationConfig('can_access_multi_hop', {
type: 'chain',
steps: [
{ relation: 'member_of', direction: 'out' },
{ relation: 'member_of', direction: 'out' },
{ relation: 'member_of', direction: 'out' },
{ relation: 'member_of', direction: 'out' },
{ relation: 'can_read', direction: 'out' }
],
collectValues: true,
valueAggregation: 'sum'
});
return arbiter;
}
/**
* Generate users
*/
_generateUsers(scale) {
const users = [];
const names = ['Alice', 'Bob', 'Charlie', 'Diana', 'Eve', 'Frank', 'Grace', 'Henry', 'Ivy', 'Jack'];
const surnames = ['Smith', 'Johnson', 'Williams', 'Brown', 'Jones', 'Garcia', 'Miller', 'Davis', 'Rodriguez', 'Martinez'];
for (let i = 0; i < scale.users; i++) {
const firstName = names[Math.floor(this.rng.next() * names.length)];
const lastName = surnames[Math.floor(this.rng.next() * surnames.length)];
users.push({
id: `user:${firstName} ${lastName}-${i}`,
type: 'employee',
name: `${firstName} ${lastName}`,
email: `${firstName.toLowerCase()} ${lastName.toLowerCase()}@company.com`,
role: ['manager', 'employee', 'contractor', 'intern'][Math.floor(this.rng.next() * 4)],
clearance: ['public', 'internal', 'confidential', 'secret', 'top_secret'][Math.floor(this.rng.next() * 5)],
department: `dept:${this._generateDepartmentName()}-${i}`,
organization: `org:company-${Math.floor(i / 50)}`,
manager: null,
startDate: new Date(Date.now() - Math.floor(this.rng.next() * 365 * 24 * 60 * 60 * 1000)).toISOString(),
isActive: true
});
}
return users;
}
/**
* Generate documents
*/
_generateDocuments(scale) {
const documents = [];
const docTypes = ['report', 'analysis', 'proposal', 'contract', 'policy', 'spec', 'budget'];
for (let i = 0; i < scale.documents; i++) {
const docType = docTypes[Math.floor(this.rng.next() * docTypes.length)];
documents.push({
id: `doc:${docType}-${i}`,
type: 'document',
name: `${docType}_${i}`,
classification: ['public', 'internal', 'confidential', 'secret', 'top_secret'][Math.floor(this.rng.next() * 5)],
department: `dept:${this._generateDepartmentName()}-${i}`,
organization: `org:company-${Math.floor(i / 100)}`,
requiredClearance: ['public', 'internal', 'confidential', 'secret', 'top_secret'][Math.floor(this.rng.next() * 5)],
cost: Math.floor(this.rng.next() * 10000),
createdDate: new Date(Date.now() - Math.floor(this.rng.next() * 365 * 24 * 60 * 60 * 1000)).toISOString(),
lastModified: new Date(Date.now() - Math.floor(this.rng.next() * 30 * 24 * 60 * 60 * 1000)).toISOString(),
size: Math.floor(this.rng.next() * 1000000),
tags: this._generateTags()
});
}
return documents;
}
/**
* Generate basic user-document relationships
* FIXED: Reduce direct relations to avoid conflicts with chain relations
*/
_generateBasicRelations(scale, users, documents) {
const relations = [];
const operations = ['can_read', 'can_write', 'can_delete', 'can_share'];
// Reduce the number of direct relations to avoid conflicts with chain relations
const reducedRelations = Math.floor(scale.relations * 0.3); // Only 30% of original
for (let i = 0; i < reducedRelations; i++) {
const user = users[Math.floor(this.rng.next() * users.length)];
const document = documents[Math.floor(this.rng.next() * documents.length)];
const operation = operations[Math.floor(this.rng.next() * operations.length)];
const possibility = 0.5 + (this.rng.next() * 0.5); // 0.5 to 1.0
relations.push({
src: user.id,
relation: operation,
dst: document.id,
possibility,
metadata: {
role: user.role,
clearance: user.clearance,
department: user.department,
grantedDate: new Date().toISOString(),
expiresDate: null
}
});
}
return relations;
}
/**
* Generate complex authorization chains (multi-hop relationships)
* These create realistic enterprise authorization scenarios
*/
_generateComplexAuthorizationChains(scale, users, documents) {
const relations = [];
const roleNodes = [];
const departmentNodes = [];
const chainCount = scale.complexChains || Math.floor(scale.relations * 0.2);
// Create role nodes first
const roleTypes = ['admin', 'manager', 'employee', 'contractor', 'intern'];
const createdRoles = new Set();
for (let i = 0; i < chainCount; i++) {
const user = users[Math.floor(this.rng.next() * users.length)];
const document = documents[Math.floor(this.rng.next() * documents.length)];
// Chain 1: User -> Role -> Document (Role-based access)
const roleType = roleTypes[Math.floor(this.rng.next() * roleTypes.length)];
const roleId = `role:${roleType}-${Math.floor(this.rng.next() * 10)}`;
// Create role node if it doesn't exist
if (!createdRoles.has(roleId)) {
roleNodes.push({
id: roleId,
type: 'role',
name: `${roleType} role`,
roleType: roleType,
permissions: ['can_read', 'can_write', 'can_share']
});
createdRoles.add(roleId);
}
relations.push({
src: user.id,
relation: 'member_of',
dst: roleId,
possibility: 0.9,
metadata: { type: 'role_membership' }
});
relations.push({
src: roleId,
relation: 'can_read',
dst: document.id,
possibility: 0.8,
metadata: { type: 'role_permission' }
});
// Chain 2: User -> Department -> Document (Department-based access)
const deptName = this._generateDepartmentName();
const deptId = `dept:${deptName}-${Math.floor(this.rng.next() * 20)}`;
// Create department node if it doesn't exist
if (!createdRoles.has(deptId)) {
departmentNodes.push({
id: deptId,
type: 'department',
name: `${deptName} department`,
departmentName: deptName,
permissions: ['can_read', 'can_write']
});
createdRoles.add(deptId);
}
relations.push({
src: user.id,
relation: 'member_of',
dst: deptId,
possibility: 0.95,
metadata: { type: 'department_membership' }
});
relations.push({
src: deptId,
relation: 'can_write',
dst: document.id,
possibility: 0.7,
metadata: { type: 'department_permission' }
});
// Chain 3: User -> Manager -> Document (Manager approval chain)
const manager = users[Math.floor(this.rng.next() * users.length)];
relations.push({
src: user.id,
relation: 'reports_to',
dst: manager.id,
possibility: 0.9,
metadata: { type: 'reporting_chain' }
});
relations.push({
src: manager.id,
relation: 'can_share',
dst: document.id,
possibility: 0.6,
metadata: { type: 'manager_permission' }
});
}
return { complexChains: relations, roleNodes, departmentNodes };
}
/**
* Generate long organizational chains for multi-hop testing
* These create deep hierarchies that will be computationally intensive to traverse
*/
_generateLongOrganizationalChains(scale, users, documents) {
const relations = [];
const chainCount = Math.floor(scale.users * 0.1); // 10% of users get long chains
for (let i = 0; i < chainCount; i++) {
const user = users[Math.floor(this.rng.next() * users.length)];
const document = documents[Math.floor(this.rng.next() * documents.length)];
// Create a long chain: User -> Team -> Department -> Division -> Company -> Document
const teamId = `team:${this._generateTeamName()}-${Math.floor(this.rng.next() * 50)}`;
const deptId = `dept:${this._generateDepartmentName()}-${Math.floor(this.rng.next() * 100)}`;
const divisionId = `division:${this._generateDivisionName()}-${Math.floor(this.rng.next() * 20)}`;
const companyId = `company:${this._generateCompanyName()}-${Math.floor(this.rng.next() * 10)}`;
// Create the long chain with decreasing possibility (realistic for deep hierarchies)
relations.push({
src: user.id,
relation: 'member_of',
dst: teamId,
possibility: 0.95,
metadata: { type: 'long_chain', step: 1, chainId: `chain-${i}` }
});
relations.push({
src: teamId,
relation: 'member_of',
dst: deptId,
possibility: 0.9,
metadata: { type: 'long_chain', step: 2, chainId: `chain-${i}` }
});
relations.push({
src: deptId,
relation: 'member_of',
dst: divisionId,
possibility: 0.85,
metadata: { type: 'long_chain', step: 3, chainId: `chain-${i}` }
});
relations.push({
src: divisionId,
relation: 'member_of',
dst: companyId,
possibility: 0.8,
metadata: { type: 'long_chain', step: 4, chainId: `chain-${i}` }
});
relations.push({
src: companyId,
relation: 'can_read',
dst: document.id,
possibility: 0.75,
metadata: { type: 'long_chain', step: 5, chainId: `chain-${i}` }
});
}
return relations;
}
/**
* Generate organizational hierarchies
*/
_generateOrganizationalHierarchies(scale, users) {
const relations = [];
// Create department hierarchies
for (let i = 0; i < Math.floor(scale.users / 10); i++) {
const deptId = `dept:${this._generateDepartmentName()}-${i}`;
const parentDeptId = `dept:${this._generateDepartmentName()}-${Math.floor(i / 3)}`;
if (i > 0) {
relations.push({
src: deptId,
relation: 'reports_to',
dst: parentDeptId,
possibility: 0.9,
metadata: { type: 'department_hierarchy' }
});
}
}
return relations;
}
/**
* Generate role-based access chains
*/
_generateRoleBasedChains(scale, users, documents) {
const relations = [];
// Create role hierarchies
const roles = ['admin', 'manager', 'senior', 'employee', 'contractor', 'intern'];
for (let i = 0; i < roles.length - 1; i++) {
const currentRole = `role:${roles[i]}`;
const parentRole = `role:${roles[i + 1]}`;
relations.push({
src: currentRole,
relation: 'inherits_from',
dst: parentRole,
possibility: 0.8,
metadata: { type: 'role_inheritance' }
});
}
return relations;
}
/**
* Generate department name
*/
_generateDepartmentName() {
const departments = ['engineering', 'finance', 'hr', 'legal', 'operations', 'marketing', 'sales'];
return departments[Math.floor(this.rng.next() * departments.length)];
}
_generateTeamName() {
const teams = ['Frontend', 'Backend', 'DevOps', 'QA', 'Design', 'Analytics', 'Security', 'Mobile'];
return teams[Math.floor(this.rng.next() * teams.length)];
}
_generateDivisionName() {
const divisions = ['Product', 'Engineering', 'Sales', 'Marketing', 'Operations', 'Finance', 'Legal', 'HR'];
return divisions[Math.floor(this.rng.next() * divisions.length)];
}
_generateCompanyName() {
const companies = ['AcmeCorp', 'TechGiant', 'InnovateLabs', 'DataFlow', 'CloudSystems', 'NextGen', 'FutureTech', 'SmartSolutions'];
return companies[Math.floor(this.rng.next() * companies.length)];
}
/**
* Generate document tags
*/
_generateTags() {
const allTags = ['important', 'urgent', 'confidential', 'draft', 'final', 'reviewed', 'approved'];
const tagCount = Math.floor(this.rng.next() * 3) + 1;
const tags = [];
for (let i = 0; i < tagCount; i++) {
const tag = allTags[Math.floor(this.rng.next() * allTags.length)];
if (!tags.includes(tag)) {
tags.push(tag);
}
}
return tags;
}
}