/** * 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); }