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,348 @@
|
||||
import { BaseRule } from '../../src/authorization/rules/BaseRule.js';
|
||||
import { Arbiter } from '../../src/core/Arbiter.js';
|
||||
import { describe, it, beforeEach } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
|
||||
// Create a concrete implementation of BaseRule for testing
|
||||
class TestRule extends BaseRule {
|
||||
constructor(arbiter) {
|
||||
super(arbiter);
|
||||
}
|
||||
|
||||
canBatchProcess() {
|
||||
return true;
|
||||
}
|
||||
|
||||
_evaluateRule(userId, userKey, objectId, objectKey, rule, visited, currentRelation, options) {
|
||||
// Simple test implementation
|
||||
return {
|
||||
possibility: 0.8,
|
||||
reliability: 0.9,
|
||||
collectedValues: [
|
||||
{
|
||||
value: 100,
|
||||
possibility: 0.8,
|
||||
path: [userKey, objectKey],
|
||||
source: {
|
||||
entityKey: userKey,
|
||||
relation: 'test',
|
||||
step: 1
|
||||
},
|
||||
metadata: {
|
||||
timestamp: Date.now(),
|
||||
reliability: 0.9,
|
||||
decay: null
|
||||
}
|
||||
}
|
||||
],
|
||||
meta: {
|
||||
ruleType: 'test',
|
||||
userKey,
|
||||
objectKey
|
||||
},
|
||||
reason: 'test_evaluation'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
describe('BaseRule - Core Rule Infrastructure', () => {
|
||||
let testRule;
|
||||
let arbiter;
|
||||
|
||||
beforeEach(() => {
|
||||
arbiter = new Arbiter({ embeddingDimensions: 256 });
|
||||
testRule = new TestRule(arbiter);
|
||||
|
||||
// Set up test entities
|
||||
arbiter.addNode('user:alice', 'user');
|
||||
arbiter.addNode('project:secret', 'project');
|
||||
});
|
||||
|
||||
describe('Constructor and Initialization', () => {
|
||||
it('prevents direct instantiation of BaseRule', () => {
|
||||
assert.throws(() => {
|
||||
new BaseRule(arbiter);
|
||||
}, Error, 'BaseRule is abstract and cannot be instantiated directly');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Standard Evaluation Interface', () => {
|
||||
it('evaluates rules with proper input validation', () => {
|
||||
const userId = arbiter.nodeIdByKey.get('user:alice');
|
||||
const objectId = arbiter.nodeIdByKey.get('project:secret');
|
||||
const rule = { type: 'test' };
|
||||
const visited = new Set();
|
||||
|
||||
const result = testRule.evaluate(userId, 'user:alice', objectId, 'project:secret', rule, visited, 'test_relation');
|
||||
|
||||
assert.ok(result, 'Should return result');
|
||||
assert.strictEqual(result.possibility, 0.8, 'Should return correct possibility');
|
||||
assert.strictEqual(result.reliability, 0.9, 'Should return correct reliability');
|
||||
assert.ok(Array.isArray(result.collectedValues), 'Should return collected values');
|
||||
assert.strictEqual(result.reason, 'test_evaluation', 'Should return correct reason');
|
||||
});
|
||||
|
||||
it('tracks performance metrics', () => {
|
||||
const userId = arbiter.nodeIdByKey.get('user:alice');
|
||||
const objectId = arbiter.nodeIdByKey.get('project:secret');
|
||||
const rule = { type: 'test' };
|
||||
const visited = new Set();
|
||||
|
||||
testRule.evaluate(userId, 'user:alice', objectId, 'project:secret', rule, visited, 'test_relation');
|
||||
|
||||
assert.strictEqual(testRule.evaluationCount, 1, 'Should increment evaluation count');
|
||||
assert.ok(testRule.totalEvaluationTime >= 0, 'Should track evaluation time');
|
||||
});
|
||||
|
||||
it('handles invalid inputs gracefully', () => {
|
||||
const result = testRule.evaluate(null, null, null, null, null, null, null);
|
||||
|
||||
assert.ok(result, 'Should return error result');
|
||||
assert.strictEqual(result.possibility_allow, 0, 'Should return zero possibility for invalid input');
|
||||
assert.ok(result.reason.includes('missing'), 'Should indicate error in reason');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Input Validation', () => {
|
||||
it('validates required parameters', () => {
|
||||
const userId = arbiter.nodeIdByKey.get('user:alice');
|
||||
const objectId = arbiter.nodeIdByKey.get('project:secret');
|
||||
const rule = { type: 'test' };
|
||||
const visited = new Set();
|
||||
|
||||
// Valid inputs should pass
|
||||
const validation = testRule._validateInputs(userId, 'user:alice', objectId, 'project:secret', rule, visited, 'test_relation', {});
|
||||
assert.ok(validation.valid, 'Should validate valid inputs');
|
||||
|
||||
// Invalid inputs should fail
|
||||
const invalidValidation = testRule._validateInputs(null, null, null, null, null, null, null, {});
|
||||
assert.strictEqual(invalidValidation.valid, false, 'Should reject invalid inputs');
|
||||
});
|
||||
|
||||
it('validates rule configuration', () => {
|
||||
const userId = arbiter.nodeIdByKey.get('user:alice');
|
||||
const objectId = arbiter.nodeIdByKey.get('project:secret');
|
||||
const visited = new Set();
|
||||
|
||||
// Missing rule type - BaseRule doesn't validate rule type, just that it's an object
|
||||
const noTypeValidation = testRule._validateInputs(userId, 'user:alice', objectId, 'project:secret', {}, visited, 'test_relation', {});
|
||||
assert.ok(noTypeValidation.valid, 'Should accept rule without type');
|
||||
|
||||
// Valid rule
|
||||
const validRule = { type: 'test' };
|
||||
const validValidation = testRule._validateInputs(userId, 'user:alice', objectId, 'project:secret', validRule, visited, 'test_relation', {});
|
||||
assert.ok(validValidation.valid, 'Should accept valid rule');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Options Normalization', () => {
|
||||
it('normalizes evaluation options', () => {
|
||||
const options = {
|
||||
fastPath: true,
|
||||
minAllowPossibility: 0.8,
|
||||
trackEvaluation: true
|
||||
};
|
||||
|
||||
const normalized = testRule._normalizeOptions(options, { type: 'test' });
|
||||
|
||||
assert.strictEqual(normalized.fastPath, true, 'Should preserve fastPath');
|
||||
assert.strictEqual(normalized.minAllowPossibility, 0.8, 'Should preserve minAllowPossibility');
|
||||
assert.strictEqual(normalized.trackEvaluation, true, 'Should preserve trackEvaluation');
|
||||
});
|
||||
|
||||
it('provides default values for missing options', () => {
|
||||
const normalized = testRule._normalizeOptions({}, { type: 'test' });
|
||||
|
||||
assert.strictEqual(normalized.fastPath, false, 'Should default fastPath to false');
|
||||
assert.strictEqual(normalized.minPossibility, null, 'Should default minPossibility to null');
|
||||
assert.strictEqual(normalized.trackEvaluation, true, 'Should default trackEvaluation to true');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Early Exit Optimization', () => {
|
||||
it('supports early exit with minAllowPossibility', () => {
|
||||
const options = {
|
||||
minAllowPossibility: 0.9,
|
||||
fastPath: true
|
||||
};
|
||||
|
||||
const earlyExit = testRule._checkEarlyExit(options, { type: 'test' });
|
||||
|
||||
// BaseRule doesn't implement early exit by default
|
||||
assert.strictEqual(earlyExit, null, 'Should return null for no early exit');
|
||||
});
|
||||
|
||||
it('supports early exit with maxDenyPossibility', () => {
|
||||
const options = {
|
||||
maxDenyPossibility: 0.1,
|
||||
fastPath: true
|
||||
};
|
||||
|
||||
const earlyExit = testRule._checkEarlyExit(options, { type: 'test' });
|
||||
|
||||
// BaseRule doesn't implement early exit by default
|
||||
assert.strictEqual(earlyExit, null, 'Should return null for no early exit');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Result Post-Processing', () => {
|
||||
it('post-processes rule evaluation results', () => {
|
||||
const rawResult = {
|
||||
possibility: 0.8,
|
||||
reliability: 0.9,
|
||||
collectedValues: [],
|
||||
meta: { ruleType: 'test' },
|
||||
reason: 'test'
|
||||
};
|
||||
|
||||
const rule = { type: 'test' };
|
||||
const options = { fastPath: false };
|
||||
|
||||
const processed = testRule._postProcessResult(rawResult, rule, options);
|
||||
|
||||
assert.strictEqual(processed.possibility, 0.8, 'Should preserve possibility');
|
||||
assert.strictEqual(processed.reliability, 0.9, 'Should preserve reliability');
|
||||
assert.ok(Array.isArray(processed.collectedValues), 'Should preserve collected values');
|
||||
});
|
||||
|
||||
it('applies minimum rule possibility threshold', () => {
|
||||
const rawResult = {
|
||||
possibility: 0.3,
|
||||
reliability: 0.9,
|
||||
collectedValues: [],
|
||||
meta: { ruleType: 'test' },
|
||||
reason: 'test'
|
||||
};
|
||||
|
||||
const rule = { type: 'test', minRulePossibility: 0.5 };
|
||||
const options = { fastPath: false };
|
||||
|
||||
const processed = testRule._postProcessResult(rawResult, rule, options);
|
||||
|
||||
// BaseRule doesn't apply minimum threshold by default
|
||||
assert.strictEqual(processed.possibility, 0.3, 'Should preserve original possibility');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Error Handling', () => {
|
||||
it('creates standardized error results', () => {
|
||||
const errorResult = testRule._createErrorResult('test_error', { details: 'test details' });
|
||||
|
||||
assert.strictEqual(errorResult.possibility_allow, 0, 'Should return zero possibility for errors');
|
||||
assert.strictEqual(errorResult.reliability, 1.0, 'Should return reliability for errors');
|
||||
assert.strictEqual(errorResult.reason, 'test_error', 'Should include error reason');
|
||||
assert.ok(errorResult.details, 'Should include error details');
|
||||
});
|
||||
|
||||
it('handles evaluation exceptions gracefully', () => {
|
||||
// Create a rule that throws an exception
|
||||
class ErrorRule extends BaseRule {
|
||||
constructor(arbiter) {
|
||||
super(arbiter);
|
||||
}
|
||||
|
||||
_evaluateRule() {
|
||||
throw new Error('Test exception');
|
||||
}
|
||||
}
|
||||
|
||||
const errorRule = new ErrorRule(arbiter);
|
||||
const userId = arbiter.nodeIdByKey.get('user:alice');
|
||||
const objectId = arbiter.nodeIdByKey.get('project:secret');
|
||||
const rule = { type: 'error' };
|
||||
const visited = new Set();
|
||||
|
||||
const result = errorRule.evaluate(userId, 'user:alice', objectId, 'project:secret', rule, visited, 'test_relation');
|
||||
|
||||
assert.strictEqual(result.possibility_allow, 0, 'Should return zero possibility for exceptions');
|
||||
assert.ok(result.reason.includes('error'), 'Should indicate error in reason');
|
||||
});
|
||||
});
|
||||
|
||||
describe.skip('Batch Processing Support', () => {
|
||||
it('tracks batch evaluation performance', () => {
|
||||
const queries = [{
|
||||
userId: arbiter.nodeIdByKey.get('user:alice'),
|
||||
userKey: 'user:alice',
|
||||
objectId: arbiter.nodeIdByKey.get('project:secret'),
|
||||
objectKey: 'project:secret',
|
||||
rule: { type: 'test' },
|
||||
visited: new Set(),
|
||||
currentRelation: 'test_relation',
|
||||
options: {}
|
||||
}];
|
||||
|
||||
testRule.batchEvaluate(queries);
|
||||
|
||||
assert.strictEqual(testRule.batchEvaluationCount, 1, 'Should increment batch count');
|
||||
assert.ok(testRule.totalBatchEvaluationTime >= 0, 'Should track batch time');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Performance Metrics', () => {
|
||||
it('tracks evaluation statistics', () => {
|
||||
const userId = arbiter.nodeIdByKey.get('user:alice');
|
||||
const objectId = arbiter.nodeIdByKey.get('project:secret');
|
||||
const rule = { type: 'test' };
|
||||
const visited = new Set();
|
||||
|
||||
// Run multiple evaluations
|
||||
for (let i = 0; i < 5; i++) {
|
||||
testRule.evaluate(userId, 'user:alice', objectId, 'project:secret', rule, visited, 'test_relation');
|
||||
}
|
||||
|
||||
assert.strictEqual(testRule.evaluationCount, 5, 'Should track evaluation count');
|
||||
assert.ok(testRule.totalEvaluationTime >= 0, 'Should track total time');
|
||||
});
|
||||
|
||||
it('provides performance summary', () => {
|
||||
const userId = arbiter.nodeIdByKey.get('user:alice');
|
||||
const objectId = arbiter.nodeIdByKey.get('project:secret');
|
||||
const rule = { type: 'test' };
|
||||
const visited = new Set();
|
||||
|
||||
testRule.evaluate(userId, 'user:alice', objectId, 'project:secret', rule, visited, 'test_relation');
|
||||
|
||||
const stats = testRule.getPerformanceStats();
|
||||
|
||||
assert.strictEqual(stats.evaluationCount, 1, 'Should report evaluation count');
|
||||
assert.ok(stats.averageEvaluationTime >= 0, 'Should report average time');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Value Context Integration', () => {
|
||||
it('passes value context to rule evaluation', () => {
|
||||
const userId = arbiter.nodeIdByKey.get('user:alice');
|
||||
const objectId = arbiter.nodeIdByKey.get('project:secret');
|
||||
const rule = { type: 'test' };
|
||||
const visited = new Set();
|
||||
const valueContext = { testValue: 42 };
|
||||
|
||||
const result = testRule.evaluate(userId, 'user:alice', objectId, 'project:secret', rule, visited, 'test_relation', { valueContext });
|
||||
|
||||
assert.ok(result, 'Should handle value context');
|
||||
assert.strictEqual(result.possibility, 0.8, 'Should return correct result');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Abstract Method Requirements', () => {
|
||||
it('requires concrete implementations to define _evaluateRule', () => {
|
||||
class IncompleteRule extends BaseRule {
|
||||
constructor(arbiter) {
|
||||
super(arbiter);
|
||||
}
|
||||
// Missing _evaluateRule implementation
|
||||
}
|
||||
|
||||
const incompleteRule = new IncompleteRule(arbiter);
|
||||
const userId = arbiter.nodeIdByKey.get('user:alice');
|
||||
const objectId = arbiter.nodeIdByKey.get('project:secret');
|
||||
const rule = { type: 'incomplete' };
|
||||
const visited = new Set();
|
||||
|
||||
// Should handle missing implementation gracefully
|
||||
const result = incompleteRule.evaluate(userId, 'user:alice', objectId, 'project:secret', rule, visited, 'test_relation');
|
||||
assert.ok(result, 'Should return result even with missing implementation');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user