Files
core/tests/rules/computed-rule.test.js
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

363 lines
12 KiB
JavaScript

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}`);
});
});
});