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,435 @@
|
||||
/**
|
||||
* Test PLTC Correctness with Zanzibar Rules
|
||||
*
|
||||
* Verifies that PLTC (with SCC condensation) correctly answers queries
|
||||
* when complex Zanzibar rules (ChainRule, ParentRule, etc.) are used.
|
||||
*
|
||||
* Key test: PLTC only sees "agnostic" base edges, not logical edges.
|
||||
* We verify that PLTC's transitive closure on base edges matches
|
||||
* the authorization logic results.
|
||||
*/
|
||||
|
||||
import { Arbiter } from '../src/index.js';
|
||||
|
||||
/**
|
||||
* Test Case 1: ChainRule with cycles
|
||||
*/
|
||||
function testCase1_ChainRuleWithCycles() {
|
||||
console.log('\n' + '='.repeat(80));
|
||||
console.log('Test Case 1: ChainRule with Friend Cycles');
|
||||
console.log('='.repeat(80));
|
||||
|
||||
const arbiter = new Arbiter({
|
||||
fastConstructionMode: true,
|
||||
enableInference: false
|
||||
});
|
||||
|
||||
// Create users with friend relationships (bidirectional = cycles)
|
||||
arbiter.addNode('user:alice', 'user');
|
||||
arbiter.addNode('user:bob', 'user');
|
||||
arbiter.addNode('user:charlie', 'user');
|
||||
arbiter.addNode('file:doc1', 'file');
|
||||
arbiter.addNode('file:doc2', 'file');
|
||||
|
||||
// Friend relationships (bidirectional = creates cycles)
|
||||
arbiter.addRelation('user:alice', 'friend', 'user:bob');
|
||||
arbiter.addRelation('user:bob', 'friend', 'user:alice');
|
||||
arbiter.addRelation('user:bob', 'friend', 'user:charlie');
|
||||
arbiter.addRelation('user:charlie', 'friend', 'user:bob');
|
||||
|
||||
// Ownership
|
||||
arbiter.addRelation('user:bob', 'owns', 'file:doc1');
|
||||
arbiter.addRelation('user:charlie', 'owns', 'file:doc2');
|
||||
|
||||
// Configure rules
|
||||
arbiter.setRelationConfig('friend', { type: 'direct' });
|
||||
arbiter.setRelationConfig('owns', { type: 'direct' });
|
||||
|
||||
// Chain rule: friend_file = friend -> owns
|
||||
arbiter.setRelationConfig('friend_file', {
|
||||
type: 'chain',
|
||||
steps: [
|
||||
{ relation: 'friend', direction: 'out' },
|
||||
{ relation: 'owns', direction: 'out' }
|
||||
]
|
||||
});
|
||||
|
||||
console.log('Graph structure:');
|
||||
console.log(' alice <-> bob <-> charlie (friend cycles)');
|
||||
console.log(' bob -> doc1 (owns)');
|
||||
console.log(' charlie -> doc2 (owns)');
|
||||
console.log(' Chain rule: friend_file = friend -> owns');
|
||||
|
||||
// Initialize PLTC
|
||||
arbiter.graphManager.initializeReachabilityChecker();
|
||||
const stats = arbiter.getReachabilityStats();
|
||||
console.log(`\nPLTC Stats: ${stats.pltcInitialized ? '✓' : '✗'}`);
|
||||
console.log(` SCCs: ${stats.sccCount || 'N/A'}`);
|
||||
|
||||
// Test queries
|
||||
const testQueries = [
|
||||
{ source: 'user:alice', relation: 'friend_file', target: 'file:doc1', expected: true, desc: 'alice -> friend(bob) -> owns(doc1)' },
|
||||
{ source: 'user:alice', relation: 'friend_file', target: 'file:doc2', expected: true, desc: 'alice -> friend(bob) -> friend(charlie) -> owns(doc2)' },
|
||||
{ source: 'user:bob', relation: 'friend_file', target: 'file:doc1', expected: true, desc: 'bob -> owns(doc1) (direct)' },
|
||||
{ source: 'user:bob', relation: 'friend_file', target: 'file:doc2', expected: true, desc: 'bob -> friend(charlie) -> owns(doc2)' },
|
||||
{ source: 'user:charlie', relation: 'friend_file', target: 'file:doc1', expected: true, desc: 'charlie -> friend(bob) -> owns(doc1)' },
|
||||
{ source: 'user:charlie', relation: 'friend_file', target: 'file:doc2', expected: true, desc: 'charlie -> owns(doc2) (direct)' },
|
||||
];
|
||||
|
||||
console.log('\nTesting ChainRule queries:');
|
||||
let correct = 0;
|
||||
let incorrect = 0;
|
||||
|
||||
for (const query of testQueries) {
|
||||
// Get ground truth using authorization logic (bypassPLTC to get true result)
|
||||
const groundTruth = arbiter.check(query.source, query.relation, query.target, {
|
||||
bypassPLTC: true,
|
||||
noInfer: true
|
||||
});
|
||||
const groundTruthBool = groundTruth && groundTruth.possibility > 0;
|
||||
|
||||
// Get PLTC result (via isReachable - PLTC is used internally)
|
||||
const pltcReachable = arbiter.isReachable(query.source, query.target);
|
||||
|
||||
// Get authorization result (uses PLTC internally for fast-fail)
|
||||
const authResult = arbiter.check(query.source, query.relation, query.target, {
|
||||
noInfer: true
|
||||
});
|
||||
const authResultBool = authResult && authResult.possibility > 0;
|
||||
|
||||
// For chain rules, PLTC should detect reachability, but authorization logic determines access
|
||||
// We expect: groundTruth === authResult (authorization should be correct)
|
||||
// And: pltcReachable should be true if groundTruth is true (PLTC should not have false negatives)
|
||||
|
||||
const pltcCorrect = !groundTruthBool || pltcReachable; // PLTC should not have false negatives
|
||||
const authCorrect = groundTruthBool === authResultBool;
|
||||
|
||||
if (pltcCorrect && authCorrect) {
|
||||
correct++;
|
||||
console.log(` ✓ ${query.desc}`);
|
||||
console.log(` Ground truth: ${groundTruthBool}, PLTC reachable: ${pltcReachable}, Auth: ${authResultBool}`);
|
||||
} else {
|
||||
incorrect++;
|
||||
console.log(` ❌ ${query.desc}`);
|
||||
console.log(` Ground truth: ${groundTruthBool}, PLTC reachable: ${pltcReachable}, Auth: ${authResultBool}`);
|
||||
if (!pltcCorrect) {
|
||||
console.log(` ERROR: PLTC false negative!`);
|
||||
}
|
||||
if (!authCorrect) {
|
||||
console.log(` ERROR: Authorization logic incorrect!`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`\nResults: ${correct} correct, ${incorrect} incorrect`);
|
||||
return incorrect === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test Case 2: Multi-hop ChainRule
|
||||
*/
|
||||
function testCase2_MultiHopChain() {
|
||||
console.log('\n' + '='.repeat(80));
|
||||
console.log('Test Case 2: Multi-hop ChainRule');
|
||||
console.log('='.repeat(80));
|
||||
|
||||
const arbiter = new Arbiter({
|
||||
fastConstructionMode: true,
|
||||
enableInference: false
|
||||
});
|
||||
|
||||
// Create hierarchy: user -> team -> project -> resource
|
||||
arbiter.addNode('user:alice', 'user');
|
||||
arbiter.addNode('team:eng', 'team');
|
||||
arbiter.addNode('project:web', 'project');
|
||||
arbiter.addNode('resource:server1', 'resource');
|
||||
|
||||
// Relations
|
||||
arbiter.addRelation('user:alice', 'member_of', 'team:eng');
|
||||
arbiter.addRelation('team:eng', 'owns', 'project:web');
|
||||
arbiter.addRelation('project:web', 'has_access', 'resource:server1');
|
||||
|
||||
// Configure rules
|
||||
arbiter.setRelationConfig('member_of', { type: 'direct' });
|
||||
arbiter.setRelationConfig('owns', { type: 'direct' });
|
||||
arbiter.setRelationConfig('has_access', { type: 'direct' });
|
||||
|
||||
// Multi-hop chain: user_resource = member_of -> owns -> has_access
|
||||
arbiter.setRelationConfig('user_resource', {
|
||||
type: 'chain',
|
||||
steps: [
|
||||
{ relation: 'member_of', direction: 'out' },
|
||||
{ relation: 'owns', direction: 'out' },
|
||||
{ relation: 'has_access', direction: 'out' }
|
||||
]
|
||||
});
|
||||
|
||||
console.log('Graph structure:');
|
||||
console.log(' alice -> eng -> web -> server1');
|
||||
console.log(' Chain rule: user_resource = member_of -> owns -> has_access');
|
||||
|
||||
// Initialize PLTC
|
||||
arbiter.graphManager.initializeReachabilityChecker();
|
||||
|
||||
// Test query
|
||||
const groundTruth = arbiter.check('user:alice', 'user_resource', 'resource:server1', {
|
||||
bypassPLTC: true,
|
||||
noInfer: true
|
||||
});
|
||||
const groundTruthBool = groundTruth && groundTruth.possibility > 0;
|
||||
|
||||
const pltcReachable = arbiter.isReachable('user:alice', 'resource:server1');
|
||||
const authResult = arbiter.check('user:alice', 'user_resource', 'resource:server1', {
|
||||
noInfer: true
|
||||
});
|
||||
const authResultBool = authResult && authResult.possibility > 0;
|
||||
|
||||
console.log('\nTesting multi-hop chain:');
|
||||
console.log(` Ground truth: ${groundTruthBool}`);
|
||||
console.log(` PLTC reachable: ${pltcReachable}`);
|
||||
console.log(` Auth result: ${authResultBool}`);
|
||||
|
||||
const pltcCorrect = !groundTruthBool || pltcReachable;
|
||||
const authCorrect = groundTruthBool === authResultBool;
|
||||
|
||||
if (pltcCorrect && authCorrect) {
|
||||
console.log(' ✓ PASS');
|
||||
return true;
|
||||
} else {
|
||||
console.log(' ❌ FAIL');
|
||||
if (!pltcCorrect) console.log(' PLTC false negative!');
|
||||
if (!authCorrect) console.log(' Authorization incorrect!');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test Case 3: ChainRule with cycles and multiple paths
|
||||
*/
|
||||
function testCase3_ChainRuleMultiplePaths() {
|
||||
console.log('\n' + '='.repeat(80));
|
||||
console.log('Test Case 3: ChainRule with Cycles and Multiple Paths');
|
||||
console.log('='.repeat(80));
|
||||
|
||||
const arbiter = new Arbiter({
|
||||
fastConstructionMode: true,
|
||||
enableInference: false
|
||||
});
|
||||
|
||||
// Create users in friend cycles
|
||||
arbiter.addNode('user:alice', 'user');
|
||||
arbiter.addNode('user:bob', 'user');
|
||||
arbiter.addNode('user:charlie', 'user');
|
||||
arbiter.addNode('user:dave', 'user');
|
||||
arbiter.addNode('file:doc1', 'file');
|
||||
arbiter.addNode('file:doc2', 'file');
|
||||
|
||||
// Friend network (creates cycles)
|
||||
arbiter.addRelation('user:alice', 'friend', 'user:bob');
|
||||
arbiter.addRelation('user:bob', 'friend', 'user:alice');
|
||||
arbiter.addRelation('user:bob', 'friend', 'user:charlie');
|
||||
arbiter.addRelation('user:charlie', 'friend', 'user:bob');
|
||||
arbiter.addRelation('user:charlie', 'friend', 'user:dave');
|
||||
arbiter.addRelation('user:dave', 'friend', 'user:charlie');
|
||||
|
||||
// Ownership
|
||||
arbiter.addRelation('user:bob', 'owns', 'file:doc1');
|
||||
arbiter.addRelation('user:dave', 'owns', 'file:doc2');
|
||||
|
||||
// Configure rules
|
||||
arbiter.setRelationConfig('friend', { type: 'direct' });
|
||||
arbiter.setRelationConfig('owns', { type: 'direct' });
|
||||
|
||||
// Chain rule: friend_file = friend -> owns
|
||||
arbiter.setRelationConfig('friend_file', {
|
||||
type: 'chain',
|
||||
steps: [
|
||||
{ relation: 'friend', direction: 'out' },
|
||||
{ relation: 'owns', direction: 'out' }
|
||||
]
|
||||
});
|
||||
|
||||
console.log('Graph structure:');
|
||||
console.log(' alice <-> bob <-> charlie <-> dave (friend cycles)');
|
||||
console.log(' bob -> doc1, dave -> doc2 (ownership)');
|
||||
|
||||
// Initialize PLTC
|
||||
arbiter.graphManager.initializeReachabilityChecker();
|
||||
const stats = arbiter.getReachabilityStats();
|
||||
console.log(`\nPLTC Stats: SCCs: ${stats.sccCount || 'N/A'}`);
|
||||
|
||||
// Test queries with multiple possible paths
|
||||
const testQueries = [
|
||||
{ source: 'user:alice', target: 'file:doc1', expected: true, desc: 'alice -> bob -> doc1' },
|
||||
{ source: 'user:alice', target: 'file:doc2', expected: true, desc: 'alice -> bob -> charlie -> dave -> doc2' },
|
||||
{ source: 'user:charlie', target: 'file:doc1', expected: true, desc: 'charlie -> bob -> doc1' },
|
||||
{ source: 'user:charlie', target: 'file:doc2', expected: true, desc: 'charlie -> dave -> doc2' },
|
||||
];
|
||||
|
||||
console.log('\nTesting queries with multiple paths:');
|
||||
let correct = 0;
|
||||
let incorrect = 0;
|
||||
|
||||
for (const query of testQueries) {
|
||||
const groundTruth = arbiter.check(query.source, 'friend_file', query.target, {
|
||||
bypassPLTC: true,
|
||||
noInfer: true
|
||||
});
|
||||
const groundTruthBool = groundTruth && groundTruth.possibility > 0;
|
||||
|
||||
const pltcReachable = arbiter.isReachable(query.source, query.target);
|
||||
const authResult = arbiter.check(query.source, 'friend_file', query.target, {
|
||||
noInfer: true
|
||||
});
|
||||
const authResultBool = authResult && authResult.possibility > 0;
|
||||
|
||||
const pltcCorrect = !groundTruthBool || pltcReachable;
|
||||
const authCorrect = groundTruthBool === authResultBool;
|
||||
|
||||
if (pltcCorrect && authCorrect) {
|
||||
correct++;
|
||||
console.log(` ✓ ${query.desc}: PLTC=${pltcReachable}, Auth=${authResultBool}`);
|
||||
} else {
|
||||
incorrect++;
|
||||
console.log(` ❌ ${query.desc}: PLTC=${pltcReachable}, Auth=${authResultBool}, Truth=${groundTruthBool}`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`\nResults: ${correct} correct, ${incorrect} incorrect`);
|
||||
return incorrect === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test Case 4: ParentRule (hierarchical)
|
||||
*
|
||||
* ParentRule semantics:
|
||||
* - Hierarchical relation: 'contains' (parent contains child)
|
||||
* - Access relation: 'can_access' (user can access object)
|
||||
* - Rule: user has access to object if user has access to object's parent
|
||||
*
|
||||
* Query: check(alice, can_access, readme)
|
||||
* Logic: find readme's parent (docs), check check(alice, can_access, docs)
|
||||
*/
|
||||
function testCase4_ParentRule() {
|
||||
console.log('\n' + '='.repeat(80));
|
||||
console.log('Test Case 4: ParentRule (Hierarchical)');
|
||||
console.log('='.repeat(80));
|
||||
|
||||
const arbiter = new Arbiter({
|
||||
fastConstructionMode: true,
|
||||
enableInference: false
|
||||
});
|
||||
|
||||
// Create folder hierarchy
|
||||
arbiter.addNode('user:alice', 'user');
|
||||
arbiter.addNode('folder:root', 'folder');
|
||||
arbiter.addNode('folder:docs', 'folder');
|
||||
arbiter.addNode('folder:projects', 'folder');
|
||||
arbiter.addNode('file:readme', 'file');
|
||||
|
||||
// Hierarchical relationships (containment): parent contains child
|
||||
arbiter.addRelation('folder:root', 'contains', 'folder:docs');
|
||||
arbiter.addRelation('folder:root', 'contains', 'folder:projects');
|
||||
arbiter.addRelation('folder:docs', 'contains', 'file:readme');
|
||||
|
||||
// Access relationships: user can access folder
|
||||
arbiter.addRelation('user:alice', 'can_access', 'folder:root');
|
||||
arbiter.addRelation('user:alice', 'can_access', 'folder:docs');
|
||||
// Note: alice has access to root, so by ParentRule semantics, alice has access to everything under root
|
||||
// This includes projects (since projects's parent is root)
|
||||
|
||||
// Configure rules
|
||||
// 'can_access' uses ParentRule, which looks for 'contains' as the parent relation
|
||||
arbiter.setRelationConfig('can_access', {
|
||||
type: 'parent',
|
||||
parentRelation: 'contains' // The hierarchical relation name
|
||||
});
|
||||
|
||||
console.log('Graph structure:');
|
||||
console.log(' Hierarchical: root contains docs, docs contains readme');
|
||||
console.log(' Access: alice -> can_access -> root, alice -> can_access -> docs');
|
||||
console.log(' Parent rule: user has access to object if user has access to object\'s parent');
|
||||
|
||||
// Initialize PLTC
|
||||
arbiter.graphManager.initializeReachabilityChecker();
|
||||
|
||||
// Test queries
|
||||
// Query: check(alice, can_access, readme)
|
||||
// Logic: readme's parent is docs (via contains), check check(alice, can_access, docs)
|
||||
// Since alice -> can_access -> docs exists, should return true
|
||||
const testQueries = [
|
||||
{ source: 'user:alice', target: 'file:readme', expected: true, desc: 'alice -> readme (via docs parent)' },
|
||||
{ source: 'user:alice', target: 'folder:docs', expected: true, desc: 'alice -> docs (direct access)' },
|
||||
{ source: 'user:alice', target: 'folder:projects', expected: true, desc: 'alice -> projects (via root parent - ParentRule grants access)' },
|
||||
];
|
||||
|
||||
console.log('\nTesting ParentRule queries:');
|
||||
let correct = 0;
|
||||
let incorrect = 0;
|
||||
|
||||
for (const query of testQueries) {
|
||||
const groundTruth = arbiter.check(query.source, 'can_access', query.target, {
|
||||
bypassPLTC: true,
|
||||
noInfer: true
|
||||
});
|
||||
const groundTruthBool = groundTruth && groundTruth.possibility > 0;
|
||||
|
||||
const pltcReachable = arbiter.isReachable(query.source, query.target);
|
||||
const authResult = arbiter.check(query.source, 'can_access', query.target, {
|
||||
noInfer: true
|
||||
});
|
||||
const authResultBool = authResult && authResult.possibility > 0;
|
||||
|
||||
const pltcCorrect = !groundTruthBool || pltcReachable;
|
||||
const authCorrect = groundTruthBool === authResultBool;
|
||||
|
||||
if (pltcCorrect && authCorrect && groundTruthBool === query.expected) {
|
||||
correct++;
|
||||
console.log(` ✓ ${query.desc}: ${authResultBool}`);
|
||||
} else {
|
||||
incorrect++;
|
||||
console.log(` ❌ ${query.desc}: PLTC=${pltcReachable}, Auth=${authResultBool}, Truth=${groundTruthBool}, Expected=${query.expected}`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`\nResults: ${correct} correct, ${incorrect} incorrect`);
|
||||
return incorrect === 0;
|
||||
}
|
||||
|
||||
// Run all tests
|
||||
console.log('='.repeat(80));
|
||||
console.log('PLTC with Zanzibar Rules Correctness Tests');
|
||||
console.log('='.repeat(80));
|
||||
console.log('\nTesting that PLTC (with SCC) correctly answers queries');
|
||||
console.log('when complex Zanzibar rules are used (ChainRule, ParentRule, etc.)');
|
||||
console.log('\nKey verification: PLTC only sees base edges, not logical edges.');
|
||||
console.log('We verify that PLTC\'s transitive closure matches authorization logic.');
|
||||
|
||||
const results = {
|
||||
test1: testCase1_ChainRuleWithCycles(),
|
||||
test2: testCase2_MultiHopChain(),
|
||||
test3: testCase3_ChainRuleMultiplePaths(),
|
||||
test4: testCase4_ParentRule()
|
||||
};
|
||||
|
||||
console.log('\n' + '='.repeat(80));
|
||||
console.log('Summary');
|
||||
console.log('='.repeat(80));
|
||||
console.log(`Test 1 (ChainRule with cycles): ${results.test1 ? '✅ PASS' : '❌ FAIL'}`);
|
||||
console.log(`Test 2 (Multi-hop chain): ${results.test2 ? '✅ PASS' : '❌ FAIL'}`);
|
||||
console.log(`Test 3 (Multiple paths): ${results.test3 ? '✅ PASS' : '❌ FAIL'}`);
|
||||
console.log(`Test 4 (ParentRule): ${results.test4 ? '✅ PASS' : '❌ FAIL'}`);
|
||||
|
||||
const allPassed = Object.values(results).every(r => r);
|
||||
if (allPassed) {
|
||||
console.log('\n✅ ALL TESTS PASSED: PLTC with SCC correctly handles Zanzibar rules!');
|
||||
console.log('\nConclusion:');
|
||||
console.log(' - PLTC only sees base edges (agnostic relations)');
|
||||
console.log(' - SCC condensation preserves reachability correctly');
|
||||
console.log(' - PLTC\'s transitive closure matches authorization logic');
|
||||
console.log(' - Complex rules (ChainRule, ParentRule) work correctly with SCC');
|
||||
} else {
|
||||
console.log('\n❌ SOME TESTS FAILED: PLTC with SCC has issues with Zanzibar rules!');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user