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.
432 lines
15 KiB
JavaScript
432 lines
15 KiB
JavaScript
/**
|
|
* Test realistic chain performance with actual graph relations
|
|
*/
|
|
|
|
import { test as _test } from 'node:test';
|
|
const test = process.env.RUN_PERF_TESTS === '1' ? _test : _test.skip;
|
|
import assert from 'node:assert/strict';
|
|
import { BigGraphGenerator } from '../helpers/big-graph-generator.js';
|
|
import { ChainRule } from '../../src/authorization/rules/ChainRule.js';
|
|
|
|
test('measures realistic chain QPS with actual relations', async () => {
|
|
if (process.env.TEST_DEBUG === '1') console.log('🎯 Testing realistic chain QPS with actual relations...');
|
|
|
|
const generator = new BigGraphGenerator();
|
|
const graphData = generator.generateGraph('enterprise');
|
|
const arbiter = generator.loadIntoArbiter(graphData);
|
|
|
|
const rule = new ChainRule(arbiter);
|
|
|
|
// Create a realistic chain rule using actual relations
|
|
const chainRule = {
|
|
type: 'chain',
|
|
steps: [
|
|
{ relation: 'member_of', direction: 'out' },
|
|
{ relation: 'can_read', direction: 'out' }
|
|
],
|
|
collectValues: false,
|
|
valueAggregation: 'sum'
|
|
};
|
|
|
|
// Find realistic chain paths using actual relations
|
|
if (process.env.TEST_DEBUG === '1') console.log(' Finding realistic chain paths...');
|
|
|
|
// Get users with member_of relations
|
|
const usersWithMemberships = graphData.relations
|
|
.filter(r => r.relation === 'member_of' && r.src.startsWith('user:'))
|
|
.map(r => r.src);
|
|
|
|
// Get objects with can_read relations
|
|
const objectsWithReadPermissions = graphData.relations
|
|
.filter(r => r.relation === 'can_read' && r.dst.startsWith('doc:'))
|
|
.map(r => r.dst);
|
|
|
|
if (process.env.TEST_DEBUG === '1') console.log(` Found ${usersWithMemberships.length} users with memberships`);
|
|
if (process.env.TEST_DEBUG === '1') console.log(` Found ${objectsWithReadPermissions.length} objects with read permissions`);
|
|
|
|
// Create realistic test pairs
|
|
const realisticPairs = [];
|
|
for (let i = 0; i < Math.min(10, usersWithMemberships.length, objectsWithReadPermissions.length); i++) {
|
|
realisticPairs.push({
|
|
user: usersWithMemberships[i],
|
|
object: objectsWithReadPermissions[i]
|
|
});
|
|
}
|
|
|
|
if (process.env.TEST_DEBUG === '1') console.log(` Testing with ${realisticPairs.length} realistic pairs...`);
|
|
|
|
// Test 1: Without caching (clear cache between queries)
|
|
if (process.env.TEST_DEBUG === '1') console.log(' Test 1: Without caching...');
|
|
const start1 = Date.now();
|
|
let queryCount1 = 0;
|
|
let positiveResults1 = 0;
|
|
const end1 = start1 + 3000; // 3 seconds
|
|
|
|
while (Date.now() < end1) {
|
|
for (const pair of realisticPairs) {
|
|
// Clear cache to simulate no caching
|
|
rule.chainResultCache.clear();
|
|
rule.chainPathCache.clear();
|
|
|
|
const result = rule.evaluate(
|
|
arbiter.nodeIdByKey.get(pair.user),
|
|
pair.user,
|
|
arbiter.nodeIdByKey.get(pair.object),
|
|
pair.object,
|
|
chainRule,
|
|
new Set(),
|
|
'can_read_via_membership',
|
|
{}
|
|
);
|
|
|
|
if (result.possibility > 0) {
|
|
positiveResults1++;
|
|
}
|
|
queryCount1++;
|
|
}
|
|
}
|
|
|
|
const duration1 = Date.now() - start1;
|
|
const qps1 = (queryCount1 / duration1) * 1000;
|
|
const positiveRate1 = (positiveResults1 / queryCount1) * 100;
|
|
|
|
if (process.env.TEST_DEBUG === '1') console.log(` Without caching: ${qps1.toFixed(2)} QPS (${queryCount1} queries)`);
|
|
if (process.env.TEST_DEBUG === '1') console.log(` Positive results: ${positiveResults1} (${positiveRate1.toFixed(1)}%)`);
|
|
|
|
// Test 2: With caching (let cache accumulate)
|
|
if (process.env.TEST_DEBUG === '1') console.log(' Test 2: With caching...');
|
|
rule.chainResultCache.clear();
|
|
rule.chainPathCache.clear();
|
|
|
|
const start2 = Date.now();
|
|
let queryCount2 = 0;
|
|
let positiveResults2 = 0;
|
|
const end2 = start2 + 3000; // 3 seconds
|
|
|
|
while (Date.now() < end2) {
|
|
for (const pair of realisticPairs) {
|
|
const result = rule.evaluate(
|
|
arbiter.nodeIdByKey.get(pair.user),
|
|
pair.user,
|
|
arbiter.nodeIdByKey.get(pair.object),
|
|
pair.object,
|
|
chainRule,
|
|
new Set(),
|
|
'can_read_via_membership',
|
|
{}
|
|
);
|
|
|
|
if (result.possibility > 0) {
|
|
positiveResults2++;
|
|
}
|
|
queryCount2++;
|
|
}
|
|
}
|
|
|
|
const duration2 = Date.now() - start2;
|
|
const qps2 = (queryCount2 / duration2) * 1000;
|
|
const positiveRate2 = (positiveResults2 / queryCount2) * 100;
|
|
|
|
if (process.env.TEST_DEBUG === '1') console.log(` With caching: ${qps2.toFixed(2)} QPS (${queryCount2} queries)`);
|
|
if (process.env.TEST_DEBUG === '1') console.log(` Positive results: ${positiveResults2} (${positiveRate2.toFixed(1)}%)`);
|
|
if (process.env.TEST_DEBUG === '1') console.log(` Cache size: ${rule.chainResultCache.size} results, ${rule.chainPathCache.size} paths`);
|
|
|
|
// Calculate improvement
|
|
const improvement = qps2 / qps1;
|
|
if (process.env.TEST_DEBUG === '1') console.log(` QPS improvement: ${improvement.toFixed(2)}x faster`);
|
|
|
|
// Verify improvement
|
|
assert.ok(improvement > 1, `Caching should improve QPS (${improvement.toFixed(2)}x)`);
|
|
assert.ok(positiveRate1 > 0, 'Should have some positive results');
|
|
assert.ok(positiveRate2 > 0, 'Should have some positive results');
|
|
|
|
if (process.env.TEST_DEBUG === '1') console.log(' ✅ Realistic chain caching improves QPS');
|
|
});
|
|
|
|
test('measures chain QPS with role inheritance', async () => {
|
|
if (process.env.TEST_DEBUG === '1') console.log('🔗 Testing chain QPS with role inheritance...');
|
|
|
|
const generator = new BigGraphGenerator();
|
|
const graphData = generator.generateGraph('enterprise');
|
|
const arbiter = generator.loadIntoArbiter(graphData);
|
|
|
|
const rule = new ChainRule(arbiter);
|
|
|
|
// Create a chain rule for role inheritance
|
|
const chainRule = {
|
|
type: 'chain',
|
|
steps: [
|
|
{ relation: 'member_of', direction: 'out' },
|
|
{ relation: 'inherits_from', direction: 'out' }
|
|
],
|
|
collectValues: false,
|
|
valueAggregation: 'sum'
|
|
};
|
|
|
|
// Find actual role inheritance chains
|
|
if (process.env.TEST_DEBUG === '1') console.log(' Finding role inheritance chains...');
|
|
|
|
const roleChains = [];
|
|
|
|
// Look for users with role memberships
|
|
const roleMemberships = graphData.relations.filter(r => r.relation === 'member_of' && r.dst.startsWith('role:'));
|
|
|
|
for (const membership of roleMemberships.slice(0, 5)) {
|
|
const userId = membership.src;
|
|
const roleId = membership.dst;
|
|
|
|
// Find inheritance chains for this role
|
|
const inheritanceChains = graphData.relations.filter(r =>
|
|
r.relation === 'inherits_from' && r.src === roleId
|
|
);
|
|
|
|
for (const inheritance of inheritanceChains) {
|
|
roleChains.push({
|
|
user: userId,
|
|
role: roleId,
|
|
inheritedRole: inheritance.dst
|
|
});
|
|
}
|
|
}
|
|
|
|
if (process.env.TEST_DEBUG === '1') console.log(` Found ${roleChains.length} role inheritance chains`);
|
|
|
|
if (roleChains.length === 0) {
|
|
if (process.env.TEST_DEBUG === '1') console.log(' No role inheritance chains found, skipping test');
|
|
return;
|
|
}
|
|
|
|
// Test with role inheritance chains
|
|
if (process.env.TEST_DEBUG === '1') console.log(' Testing with role inheritance chains...');
|
|
|
|
// Test 1: Without caching
|
|
if (process.env.TEST_DEBUG === '1') console.log(' Test 1: Without caching...');
|
|
const start1 = Date.now();
|
|
let queryCount1 = 0;
|
|
let successfulChains1 = 0;
|
|
const end1 = start1 + 2000; // 2 seconds
|
|
|
|
while (Date.now() < end1) {
|
|
for (const chain of roleChains) {
|
|
// Clear cache to simulate no caching
|
|
rule.chainResultCache.clear();
|
|
rule.chainPathCache.clear();
|
|
|
|
const result = rule.evaluate(
|
|
arbiter.nodeIdByKey.get(chain.user),
|
|
chain.user,
|
|
arbiter.nodeIdByKey.get(chain.inheritedRole),
|
|
chain.inheritedRole,
|
|
chainRule,
|
|
new Set(),
|
|
'inherits_via_membership',
|
|
{}
|
|
);
|
|
|
|
if (result.possibility > 0) {
|
|
successfulChains1++;
|
|
}
|
|
queryCount2++;
|
|
}
|
|
}
|
|
|
|
const duration1 = Date.now() - start1;
|
|
const qps1 = (queryCount1 / duration1) * 1000;
|
|
const successRate1 = (successfulChains1 / queryCount1) * 100;
|
|
|
|
if (process.env.TEST_DEBUG === '1') console.log(` Without caching: ${qps1.toFixed(2)} QPS (${queryCount1} queries)`);
|
|
if (process.env.TEST_DEBUG === '1') console.log(` Successful chains: ${successfulChains1} (${successRate1.toFixed(1)}%)`);
|
|
|
|
// Test 2: With caching
|
|
if (process.env.TEST_DEBUG === '1') console.log(' Test 2: With caching...');
|
|
rule.chainResultCache.clear();
|
|
rule.chainPathCache.clear();
|
|
|
|
const start2 = Date.now();
|
|
let queryCount2 = 0;
|
|
let successfulChains2 = 0;
|
|
const end2 = start2 + 2000; // 2 seconds
|
|
|
|
while (Date.now() < end2) {
|
|
for (const chain of roleChains) {
|
|
const result = rule.evaluate(
|
|
arbiter.nodeIdByKey.get(chain.user),
|
|
chain.user,
|
|
arbiter.nodeIdByKey.get(chain.inheritedRole),
|
|
chain.inheritedRole,
|
|
chainRule,
|
|
new Set(),
|
|
'inherits_via_membership',
|
|
{}
|
|
);
|
|
|
|
if (result.possibility > 0) {
|
|
successfulChains2++;
|
|
}
|
|
queryCount2++;
|
|
}
|
|
}
|
|
|
|
const duration2 = Date.now() - start2;
|
|
const qps2 = (queryCount2 / duration2) * 1000;
|
|
const successRate2 = (successfulChains2 / queryCount2) * 100;
|
|
|
|
if (process.env.TEST_DEBUG === '1') console.log(` With caching: ${qps2.toFixed(2)} QPS (${queryCount2} queries)`);
|
|
if (process.env.TEST_DEBUG === '1') console.log(` Successful chains: ${successfulChains2} (${successRate2.toFixed(1)}%)`);
|
|
if (process.env.TEST_DEBUG === '1') console.log(` Cache size: ${rule.chainResultCache.size} results, ${rule.chainPathCache.size} paths`);
|
|
|
|
// Calculate improvement
|
|
const improvement = qps2 / qps1;
|
|
if (process.env.TEST_DEBUG === '1') console.log(` QPS improvement: ${improvement.toFixed(2)}x faster`);
|
|
|
|
// Verify improvement
|
|
assert.ok(improvement > 1, `Caching should improve QPS (${improvement.toFixed(2)}x)`);
|
|
assert.ok(successRate1 > 0, 'Should have some successful chains');
|
|
assert.ok(successRate2 > 0, 'Should have some successful chains');
|
|
|
|
if (process.env.TEST_DEBUG === '1') console.log(' ✅ Role inheritance chain caching improves QPS');
|
|
});
|
|
|
|
test('measures chain QPS with realistic workload', async () => {
|
|
if (process.env.TEST_DEBUG === '1') console.log('📊 Testing chain QPS with realistic workload...');
|
|
|
|
const generator = new BigGraphGenerator();
|
|
const graphData = generator.generateGraph('enterprise');
|
|
const arbiter = generator.loadIntoArbiter(graphData);
|
|
|
|
const rule = new ChainRule(arbiter);
|
|
|
|
// Create a realistic chain rule
|
|
const chainRule = {
|
|
type: 'chain',
|
|
steps: [
|
|
{ relation: 'member_of', direction: 'out' },
|
|
{ relation: 'can_read', direction: 'out' }
|
|
],
|
|
collectValues: false,
|
|
valueAggregation: 'sum'
|
|
};
|
|
|
|
// Create a realistic workload: 70% positive, 30% negative
|
|
if (process.env.TEST_DEBUG === '1') console.log(' Creating realistic workload (70% positive, 30% negative)...');
|
|
|
|
const workload = [];
|
|
|
|
// 70% positive queries (should succeed)
|
|
const positiveQueries = [];
|
|
const memberOfRelations = graphData.relations.filter(r => r.relation === 'member_of');
|
|
const canReadRelations = graphData.relations.filter(r => r.relation === 'can_read');
|
|
|
|
for (let i = 0; i < Math.min(7, memberOfRelations.length, canReadRelations.length); i++) {
|
|
positiveQueries.push({
|
|
user: memberOfRelations[i].src,
|
|
object: canReadRelations[i].dst,
|
|
expected: 'positive'
|
|
});
|
|
}
|
|
|
|
// 30% negative queries (should fail)
|
|
const negativeQueries = [];
|
|
const users = graphData.users.slice(0, 3);
|
|
const objects = graphData.documents.slice(0, 3);
|
|
|
|
for (let i = 0; i < 3; i++) {
|
|
negativeQueries.push({
|
|
user: users[i].key,
|
|
object: objects[i].key,
|
|
expected: 'negative'
|
|
});
|
|
}
|
|
|
|
workload.push(...positiveQueries, ...negativeQueries);
|
|
|
|
if (process.env.TEST_DEBUG === '1') console.log(` Workload: ${positiveQueries.length} positive, ${negativeQueries.length} negative queries`);
|
|
|
|
// Test 1: Without caching
|
|
if (process.env.TEST_DEBUG === '1') console.log(' Test 1: Without caching...');
|
|
const start1 = Date.now();
|
|
let queryCount1 = 0;
|
|
let positiveCount1 = 0;
|
|
const end1 = start1 + 3000; // 3 seconds
|
|
|
|
while (Date.now() < end1) {
|
|
for (const query of workload) {
|
|
// Clear cache to simulate no caching
|
|
rule.chainResultCache.clear();
|
|
rule.chainPathCache.clear();
|
|
|
|
const result = rule.evaluate(
|
|
arbiter.nodeIdByKey.get(query.user),
|
|
query.user,
|
|
arbiter.nodeIdByKey.get(query.object),
|
|
query.object,
|
|
chainRule,
|
|
new Set(),
|
|
'can_read_via_membership',
|
|
{}
|
|
);
|
|
|
|
if (result.possibility > 0) {
|
|
positiveCount1++;
|
|
}
|
|
queryCount1++;
|
|
}
|
|
}
|
|
|
|
const duration1 = Date.now() - start1;
|
|
const qps1 = (queryCount1 / duration1) * 1000;
|
|
const positiveRate1 = (positiveCount1 / queryCount1) * 100;
|
|
|
|
if (process.env.TEST_DEBUG === '1') console.log(` Without caching: ${qps1.toFixed(2)} QPS (${queryCount1} queries)`);
|
|
if (process.env.TEST_DEBUG === '1') console.log(` Positive rate: ${positiveRate1.toFixed(1)}%`);
|
|
|
|
// Test 2: With caching
|
|
if (process.env.TEST_DEBUG === '1') console.log(' Test 2: With caching...');
|
|
rule.chainResultCache.clear();
|
|
rule.chainPathCache.clear();
|
|
|
|
const start2 = Date.now();
|
|
let queryCount2 = 0;
|
|
let positiveCount2 = 0;
|
|
const end2 = start2 + 3000; // 3 seconds
|
|
|
|
while (Date.now() < end2) {
|
|
for (const query of workload) {
|
|
const result = rule.evaluate(
|
|
arbiter.nodeIdByKey.get(query.user),
|
|
query.user,
|
|
arbiter.nodeIdByKey.get(query.object),
|
|
query.object,
|
|
chainRule,
|
|
new Set(),
|
|
'can_read_via_membership',
|
|
{}
|
|
);
|
|
|
|
if (result.possibility > 0) {
|
|
positiveCount2++;
|
|
}
|
|
queryCount2++;
|
|
}
|
|
}
|
|
|
|
const duration2 = Date.now() - start2;
|
|
const qps2 = (queryCount2 / duration2) * 1000;
|
|
const positiveRate2 = (positiveCount2 / queryCount2) * 100;
|
|
|
|
if (process.env.TEST_DEBUG === '1') console.log(` With caching: ${qps2.toFixed(2)} QPS (${queryCount2} queries)`);
|
|
if (process.env.TEST_DEBUG === '1') console.log(` Positive rate: ${positiveRate2.toFixed(1)}%`);
|
|
if (process.env.TEST_DEBUG === '1') console.log(` Cache size: ${rule.chainResultCache.size} results, ${rule.chainPathCache.size} paths`);
|
|
|
|
// Calculate improvement
|
|
const improvement = qps2 / qps1;
|
|
if (process.env.TEST_DEBUG === '1') console.log(` QPS improvement: ${improvement.toFixed(2)}x faster`);
|
|
|
|
// Verify improvement
|
|
assert.ok(improvement > 1, `Caching should improve QPS (${improvement.toFixed(2)}x)`);
|
|
assert.ok(positiveRate1 > 0, 'Should have some positive results');
|
|
assert.ok(positiveRate2 > 0, 'Should have some positive results');
|
|
|
|
if (process.env.TEST_DEBUG === '1') console.log(' ✅ Realistic workload caching improves QPS');
|
|
});
|