import { ComputedRule } from '../../src/authorization/rules/ComputedRule.js'; import { Arbiter } from '../../src/core/Arbiter.js'; import { RuleEvaluator } from '../../src/authorization/RuleEvaluator.js'; import { describe, it, beforeEach } from 'node:test'; import assert from 'node:assert/strict'; describe('ComputedRule - Recursive Authorization Delegation', () => { let computedRule; let arbiter; let ruleEvaluator; // Helper function to evaluate rules function evaluateRule(userKey, objectKey, rule, visited = new Set(), currentRelation = null, options = {}) { const userId = arbiter.nodeIdByKey.get(userKey); const objectId = arbiter.nodeIdByKey.get(objectKey); const finalOptions = { includeMeta: true, ...options }; return computedRule._evaluateRule(userId, userKey, objectId, objectKey, rule, visited, currentRelation, finalOptions); } beforeEach(() => { // Create fresh arbiter for each test arbiter = new Arbiter({ embeddingDimensions: 256 }); ruleEvaluator = new RuleEvaluator(arbiter); computedRule = new ComputedRule(arbiter); // Set up realistic entities arbiter.addNode('user:alice', 'user'); arbiter.addNode('user:bob', 'user'); arbiter.addNode('user:charlie', 'user'); arbiter.addNode('team:engineering', 'team'); arbiter.addNode('team:marketing', 'team'); arbiter.addNode('project:secret', 'project'); arbiter.addNode('project:public', 'project'); arbiter.addNode('document:classified', 'document'); arbiter.addNode('document:public', 'document'); // Configure relations arbiter.setRelationConfig('member_of', { type: 'direct' }); arbiter.setRelationConfig('can_access', { type: 'direct' }); arbiter.setRelationConfig('can_read', { type: 'direct' }); arbiter.setRelationConfig('can_write', { type: 'direct' }); arbiter.setRelationConfig('owns', { type: 'direct' }); }); describe('Basic Computed Relation Evaluation', () => { it('delegates to authorization checker for simple relations', () => { // Alice can access project:secret directly arbiter.addRelation('user:alice', 'can_access', 'project:secret', { value: 1.0, possibility: 1.0, changed_last_at: Date.now() }); const rule = { type: 'computed', relation: 'can_access' }; const result = evaluateRule('user:alice', 'project:secret', rule); assert.ok(result.possibility > 0.9, `Expected high possibility, got ${result.possibility}`); // CI-001 fix: computed rule delegates to fast path → 'direct_match' assert.strictEqual(result.reason, 'direct_match'); assert.ok(result.meta, 'Expected meta information'); assert.strictEqual(result.meta.ruleType, 'computed'); assert.strictEqual(result.meta.computedRelation, 'can_access'); assert.strictEqual(result.meta.delegated, true); }); it('handles denied access through computed relations', () => { // Bob has no access to project:secret const rule = { type: 'computed', relation: 'can_access' }; const result = evaluateRule('user:bob', 'project:secret', rule); assert.strictEqual(result.possibility, 0, 'Expected no access'); // CI-001 fix: fast path → 'no_relation' assert.strictEqual(result.reason, 'no_relation'); }); it('propagates collected values from delegated evaluation', () => { // Alice can access project:secret with a value arbiter.addRelation('user:alice', 'can_access', 'project:secret', { value: 100, possibility: 1.0, changed_last_at: Date.now() }); const rule = { type: 'computed', relation: 'can_access' }; const result = evaluateRule('user:alice', 'project:secret', rule); assert.ok(result.possibility > 0.9, `Expected high possibility, got ${result.possibility}`); // The collectedValues should be propagated from the delegated evaluation assert.ok(Array.isArray(result.collectedValues), 'Expected collectedValues array'); }); }); describe('Complex Authorization Scenarios', () => { it('handles team membership through computed relations', () => { // Alice is member of engineering team, team can access project arbiter.addRelation('user:alice', 'member_of', 'team:engineering', { value: 1.0, possibility: 1.0, changed_last_at: Date.now() }); arbiter.addRelation('team:engineering', 'can_access', 'project:secret', { value: 1.0, possibility: 1.0, changed_last_at: Date.now() }); // Configure can_access to use chain rule for team membership arbiter.setRelationConfig('can_access', { type: 'chain', steps: [ { relation: 'member_of', direction: 'out' }, { relation: 'can_access', direction: 'out' } ] }); const rule = { type: 'computed', relation: 'can_access' }; const result = evaluateRule('user:alice', 'project:secret', rule); assert.ok(result.possibility > 0.9, `Expected high possibility through team membership, got ${result.possibility}`); }); it('handles nested computed relations', () => { // Alice can read documents that she can access arbiter.addRelation('user:alice', 'can_access', 'document:classified', { value: 1.0, possibility: 1.0, changed_last_at: Date.now() }); // Configure can_read to use computed relation arbiter.setRelationConfig('can_read', { type: 'computed', relation: 'can_access' }); const rule = { type: 'computed', relation: 'can_read' }; const result = evaluateRule('user:alice', 'document:classified', rule); assert.ok(result.possibility > 0.9, `Expected high possibility through nested computation, got ${result.possibility}`); }); }); describe('Performance and Optimization', () => { it('tracks evaluation performance', () => { arbiter.addRelation('user:alice', 'can_access', 'project:secret', { value: 1.0, possibility: 1.0, changed_last_at: Date.now() }); const rule = { type: 'computed', relation: 'can_access' }; const result = evaluateRule('user:alice', 'project:secret', rule, new Set(), null, { trackEvaluation: true }); assert.ok(result.evaluation, 'Expected evaluation tracking'); assert.ok(result.evaluation.evaluationStarted, 'Expected start time'); assert.ok(result.evaluation.evaluationCompleted, 'Expected completion time'); assert.ok(result.evaluation.evaluationDuration >= 0, 'Expected positive duration'); assert.strictEqual(result.evaluation.type, 'computed'); assert.strictEqual(result.evaluation.computedRelation, 'can_access'); }); }); describe('Error Handling and Edge Cases', () => { it('handles missing relation configuration', () => { const rule = { type: 'computed', relation: 'nonexistent_relation' }; const result = evaluateRule('user:alice', 'project:secret', rule); assert.strictEqual(result.possibility, 0, 'Expected no access for missing relation'); assert.strictEqual(result.reason, 'no_config'); }); it('handles circular dependencies gracefully', () => { // Set up circular dependency: can_access -> can_read -> can_access arbiter.setRelationConfig('can_access', { type: 'computed', relation: 'can_read' }); arbiter.setRelationConfig('can_read', { type: 'computed', relation: 'can_access' }); const rule = { type: 'computed', relation: 'can_access' }; const result = evaluateRule('user:alice', 'project:secret', rule); // Should handle circular dependency without infinite recursion assert.ok(result.possibility >= 0, 'Expected valid result despite circular dependency'); assert.ok(result.possibility <= 1, 'Expected valid possibility range'); }); it('preserves visited set for cycle detection', () => { const visited = new Set(); visited.add('user:alice:can_access:project:secret'); const rule = { type: 'computed', relation: 'can_access' }; const result = evaluateRule('user:alice', 'project:secret', rule, visited); // Should respect the visited set to prevent cycles assert.ok(result.possibility >= 0, 'Expected valid result with visited set'); }); }); describe('Value Context and Metadata', () => { it('preserves value context through delegation', () => { arbiter.addRelation('user:alice', 'can_access', 'project:secret', { value: 100, possibility: 1.0, changed_last_at: Date.now() }); const valueContext = { someValue: 42 }; const rule = { type: 'computed', relation: 'can_access' }; const result = evaluateRule('user:alice', 'project:secret', rule, new Set(), null, { valueContext, trackEvaluation: true }); assert.ok(result.possibility > 0.9, 'Expected successful evaluation'); assert.ok(result.evaluation, 'Expected evaluation tracking'); }); it('handles different relation types through delegation', () => { // Test with different relation types that might be configured const relations = ['can_access', 'can_read', 'can_write', 'owns']; for (const relation of relations) { arbiter.addRelation('user:alice', relation, 'project:secret', { value: 1.0, possibility: 1.0, changed_last_at: Date.now() }); const rule = { type: 'computed', relation: relation }; const result = evaluateRule('user:alice', 'project:secret', rule); assert.ok(result.possibility > 0.9, `Expected high possibility for ${relation}, got ${result.possibility}`); assert.strictEqual(result.meta.computedRelation, relation); } }); }); describe('Integration with Other Rules', () => { it('works with logical operators', () => { // Alice can access through multiple paths arbiter.addRelation('user:alice', 'can_access', 'project:secret', { value: 1.0, possibility: 0.8, changed_last_at: Date.now() }); arbiter.addRelation('user:alice', 'owns', 'project:secret', { value: 1.0, possibility: 0.9, changed_last_at: Date.now() }); // Configure can_access to use union of direct and computed relations arbiter.setRelationConfig('can_access', { type: 'logical', union: { rules: [ { type: 'direct' }, { type: 'computed', relation: 'owns' } ] } }); const rule = { type: 'computed', relation: 'can_access' }; const result = evaluateRule('user:alice', 'project:secret', rule); assert.ok(result.possibility > 0.8, `Expected high possibility through union, got ${result.possibility}`); }); it('handles complex nested rule configurations', () => { // Set up a complex scenario: user -> team -> project access arbiter.addRelation('user:alice', 'member_of', 'team:engineering', { value: 1.0, possibility: 1.0, changed_last_at: Date.now() }); arbiter.addRelation('team:engineering', 'can_access', 'project:secret', { value: 1.0, possibility: 1.0, changed_last_at: Date.now() }); // Configure a complex rule that combines multiple approaches arbiter.setRelationConfig('can_access', { type: 'logical', union: { rules: [ { type: 'direct' }, { type: 'chain', steps: [ { relation: 'member_of', direction: 'out' }, { relation: 'can_access', direction: 'out' } ] } ] } }); const rule = { type: 'computed', relation: 'can_access' }; const result = evaluateRule('user:alice', 'project:secret', rule); assert.ok(result.possibility > 0.9, `Expected high possibility through complex rule, got ${result.possibility}`); }); }); });