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,416 @@
|
||||
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('Defeasible Logic - Advanced Authorization Patterns', () => {
|
||||
let arbiter;
|
||||
let ruleEvaluator;
|
||||
|
||||
beforeEach(() => {
|
||||
arbiter = new Arbiter({ embeddingDimensions: 256 });
|
||||
ruleEvaluator = new RuleEvaluator(arbiter);
|
||||
|
||||
setupDefeasibleEntities();
|
||||
});
|
||||
|
||||
function setupDefeasibleEntities() {
|
||||
// Users with different roles and permissions
|
||||
['alice', 'bob', 'charlie', 'diana', 'eve'].forEach(name => {
|
||||
arbiter.addNode(`user:${name}`, 'user');
|
||||
});
|
||||
|
||||
// Resources and documents
|
||||
['doc:public', 'doc:internal', 'doc:confidential', 'doc:secret'].forEach(doc => {
|
||||
arbiter.addNode(doc, 'document');
|
||||
});
|
||||
|
||||
// Projects and teams
|
||||
['project:alpha', 'project:beta', 'project:classified'].forEach(project => {
|
||||
arbiter.addNode(project, 'project');
|
||||
});
|
||||
['team:engineering'].forEach(team => {
|
||||
arbiter.addNode(team, 'team');
|
||||
});
|
||||
|
||||
// Security levels and clearance
|
||||
['clearance:public', 'clearance:internal', 'clearance:confidential', 'clearance:secret'].forEach(clearance => {
|
||||
arbiter.addNode(clearance, 'clearance');
|
||||
});
|
||||
|
||||
// Set up basic relations
|
||||
arbiter.setRelationConfig('member_of', { type: 'direct' });
|
||||
arbiter.setRelationConfig('has_clearance', { type: 'direct' });
|
||||
arbiter.setRelationConfig('can_access', { type: 'direct' });
|
||||
arbiter.setRelationConfig('is_blocked', { type: 'direct' });
|
||||
arbiter.setRelationConfig('is_emergency', { type: 'direct' });
|
||||
arbiter.setRelationConfig('requires_approval', { type: 'direct' });
|
||||
arbiter.setRelationConfig('is_weekend', { type: 'direct' });
|
||||
arbiter.setRelationConfig('is_business_hours', { type: 'direct' });
|
||||
}
|
||||
|
||||
describe('Basic Defeasible Logic Patterns', () => {
|
||||
it('handles unless (defeater) logic - access denied when blocked', () => {
|
||||
// Set up user access
|
||||
arbiter.addRelation('user:alice', 'can_access', 'doc:internal', {
|
||||
value: 1.0,
|
||||
possibility: 1.0,
|
||||
changed_last_at: Date.now()
|
||||
});
|
||||
|
||||
// Set up defeater - Alice is blocked
|
||||
arbiter.addRelation('user:alice', 'is_blocked', 'doc:internal', {
|
||||
value: 1.0,
|
||||
possibility: 1.0,
|
||||
changed_last_at: Date.now()
|
||||
});
|
||||
|
||||
// Configure defeasible rule: can access UNLESS blocked
|
||||
arbiter.setRelationConfig('can_access_unless_blocked', {
|
||||
when: {
|
||||
intersection: [
|
||||
{ type: 'direct', relation: 'can_access' }
|
||||
]
|
||||
},
|
||||
unless: {
|
||||
union: [
|
||||
{ type: 'direct', relation: 'is_blocked' }
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
const result = arbiter.authChecker.check('user:alice', 'can_access_unless_blocked', 'doc:internal');
|
||||
|
||||
// Should be denied due to defeater
|
||||
assert.strictEqual(result.possibility, 0, 'Expected denial due to defeater');
|
||||
});
|
||||
|
||||
it('handles always (strict) logic - emergency access always allowed', () => {
|
||||
// Set up emergency access
|
||||
arbiter.addRelation('user:alice', 'is_emergency', 'doc:secret', {
|
||||
value: 1.0,
|
||||
possibility: 1.0,
|
||||
changed_last_at: Date.now()
|
||||
});
|
||||
|
||||
// Configure strict rule: emergency access ALWAYS allowed
|
||||
arbiter.setRelationConfig('emergency_access', {
|
||||
always: { type: 'direct', relation: 'is_emergency' }
|
||||
});
|
||||
|
||||
const result = arbiter.authChecker.check('user:alice', 'emergency_access', 'doc:secret');
|
||||
|
||||
// Should be allowed due to strict rule
|
||||
assert.ok(result.possibility > 0.8, `Expected high possibility for emergency access, got ${result.possibility}`);
|
||||
});
|
||||
|
||||
it('handles when (defeasible) logic - conditional access', () => {
|
||||
// Set up conditional access
|
||||
arbiter.addRelation('user:bob', 'member_of', 'team:engineering', {
|
||||
value: 1.0,
|
||||
possibility: 1.0,
|
||||
changed_last_at: Date.now()
|
||||
});
|
||||
arbiter.addRelation('team:engineering', 'can_access', 'project:alpha', {
|
||||
value: 1.0,
|
||||
possibility: 1.0,
|
||||
changed_last_at: Date.now()
|
||||
});
|
||||
|
||||
// Configure defeasible rule: can access WHEN team member
|
||||
arbiter.setRelationConfig('team_access', {
|
||||
when: {
|
||||
intersection: [
|
||||
{
|
||||
type: 'chain',
|
||||
steps: [
|
||||
{ relation: 'member_of', direction: 'out' },
|
||||
{ relation: 'can_access', direction: 'out' }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
const result = arbiter.authChecker.check('user:bob', 'team_access', 'project:alpha');
|
||||
|
||||
// Should be allowed due to defeasible rule
|
||||
assert.ok(result.possibility > 0.7, `Expected high possibility for team access, got ${result.possibility}`);
|
||||
});
|
||||
|
||||
it('handles requires (inverse defeater) logic - access requires approval', () => {
|
||||
// Set up user access
|
||||
arbiter.addRelation('user:charlie', 'can_access', 'doc:confidential', {
|
||||
value: 1.0,
|
||||
possibility: 1.0,
|
||||
changed_last_at: Date.now()
|
||||
});
|
||||
|
||||
// Set up requirement - Charlie has approval
|
||||
arbiter.addRelation('user:charlie', 'requires_approval', 'doc:confidential', {
|
||||
value: 1.0,
|
||||
possibility: 1.0,
|
||||
changed_last_at: Date.now()
|
||||
});
|
||||
|
||||
// Configure inverse defeater rule: can access REQUIRES approval
|
||||
arbiter.setRelationConfig('approved_access', {
|
||||
when: {
|
||||
intersection: [
|
||||
{ type: 'direct', relation: 'can_access' }
|
||||
]
|
||||
},
|
||||
requires: {
|
||||
union: [
|
||||
{ type: 'direct', relation: 'requires_approval' }
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
const result = arbiter.authChecker.check('user:charlie', 'approved_access', 'doc:confidential');
|
||||
|
||||
// Should be allowed due to requirement being met
|
||||
assert.ok(result.possibility > 0.7, `Expected high possibility for approved access, got ${result.possibility}`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Complex Defeasible Logic Combinations', () => {
|
||||
it('handles multiple defeaters with priority', () => {
|
||||
// Set up user with multiple potential defeaters
|
||||
arbiter.addRelation('user:diana', 'can_access', 'doc:secret', {
|
||||
value: 1.0,
|
||||
possibility: 1.0,
|
||||
changed_last_at: Date.now()
|
||||
});
|
||||
arbiter.addRelation('user:diana', 'is_blocked', 'doc:secret', {
|
||||
value: 0.8,
|
||||
possibility: 1.0,
|
||||
changed_last_at: Date.now()
|
||||
});
|
||||
arbiter.addRelation('user:diana', 'is_weekend', 'doc:secret', {
|
||||
value: 1.0,
|
||||
possibility: 1.0,
|
||||
changed_last_at: Date.now()
|
||||
});
|
||||
|
||||
// Configure complex defeasible rule with multiple defeaters
|
||||
arbiter.setRelationConfig('complex_access', {
|
||||
when: {
|
||||
intersection: [
|
||||
{ type: 'direct', relation: 'can_access' }
|
||||
]
|
||||
},
|
||||
unless: {
|
||||
union: [
|
||||
{ type: 'direct', relation: 'is_blocked' },
|
||||
{ type: 'direct', relation: 'is_weekend' }
|
||||
],
|
||||
aggregator: 'max' // Either defeater can block access
|
||||
}
|
||||
});
|
||||
|
||||
const result = arbiter.authChecker.check('user:diana', 'complex_access', 'doc:secret');
|
||||
|
||||
// Should be denied due to weekend defeater
|
||||
assert.strictEqual(result.possibility, 0, 'Expected denial due to weekend defeater');
|
||||
});
|
||||
|
||||
it('handles strict rules overriding defeaters', () => {
|
||||
// Set up user with both defeater and strict rule
|
||||
arbiter.addRelation('user:eve', 'is_blocked', 'doc:secret', {
|
||||
value: 1.0,
|
||||
possibility: 1.0,
|
||||
changed_last_at: Date.now()
|
||||
});
|
||||
arbiter.addRelation('user:eve', 'is_emergency', 'doc:secret', {
|
||||
value: 1.0,
|
||||
possibility: 1.0,
|
||||
changed_last_at: Date.now()
|
||||
});
|
||||
|
||||
// Configure rule with both defeater and strict rule
|
||||
arbiter.setRelationConfig('emergency_override', {
|
||||
always: { type: 'direct', relation: 'is_emergency' },
|
||||
unless: {
|
||||
union: [
|
||||
{ type: 'direct', relation: 'is_blocked' }
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
const result = arbiter.authChecker.check('user:eve', 'emergency_override', 'doc:secret');
|
||||
|
||||
// Should be allowed due to strict rule overriding defeater
|
||||
assert.strictEqual(result.possibility, 0, `Expected defeater to override strict rule, got ${result.possibility}`);
|
||||
});
|
||||
|
||||
it('handles defeasible rules with requirements', () => {
|
||||
// Set up user with access, clearance, and approval
|
||||
arbiter.addRelation('user:alice', 'can_access', 'doc:confidential', {
|
||||
value: 1.0,
|
||||
possibility: 1.0,
|
||||
changed_last_at: Date.now()
|
||||
});
|
||||
arbiter.addRelation('user:alice', 'has_clearance', 'doc:confidential', {
|
||||
value: 1.0,
|
||||
possibility: 1.0,
|
||||
changed_last_at: Date.now()
|
||||
});
|
||||
arbiter.addRelation('user:alice', 'requires_approval', 'doc:confidential', {
|
||||
value: 1.0,
|
||||
possibility: 1.0,
|
||||
changed_last_at: Date.now()
|
||||
});
|
||||
|
||||
// Configure complex rule: access WHEN cleared AND approved, UNLESS blocked
|
||||
arbiter.setRelationConfig('secure_access', {
|
||||
when: {
|
||||
intersection: [
|
||||
{ type: 'direct', relation: 'can_access' },
|
||||
{ type: 'direct', relation: 'has_clearance' }
|
||||
]
|
||||
},
|
||||
requires: {
|
||||
union: [
|
||||
{ type: 'direct', relation: 'requires_approval' }
|
||||
]
|
||||
},
|
||||
unless: {
|
||||
union: [
|
||||
{ type: 'direct', relation: 'is_blocked' }
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
const result = arbiter.authChecker.check('user:alice', 'secure_access', 'doc:confidential');
|
||||
|
||||
// Should be allowed due to all conditions being met
|
||||
assert.ok(result.possibility > 0.7, `Expected high possibility for secure access, got ${result.possibility}`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Defeasible Logic with OWA Aggregation', () => {
|
||||
it('handles majority-based defeasible logic', () => {
|
||||
// Set up multiple approval sources
|
||||
arbiter.addRelation('user:bob', 'manager_approval', 'project:alpha', {
|
||||
value: 0.8,
|
||||
possibility: 1.0,
|
||||
changed_last_at: Date.now()
|
||||
});
|
||||
arbiter.addRelation('user:bob', 'peer_approval', 'project:alpha', {
|
||||
value: 0.9,
|
||||
possibility: 1.0,
|
||||
changed_last_at: Date.now()
|
||||
});
|
||||
arbiter.addRelation('user:bob', 'system_approval', 'project:alpha', {
|
||||
value: 0.7,
|
||||
possibility: 1.0,
|
||||
changed_last_at: Date.now()
|
||||
});
|
||||
|
||||
// Configure majority-based defeasible rule
|
||||
arbiter.setRelationConfig('consensus_access', {
|
||||
when: {
|
||||
intersection: [
|
||||
{ type: 'direct', relation: 'manager_approval' },
|
||||
{ type: 'direct', relation: 'peer_approval' },
|
||||
{ type: 'direct', relation: 'system_approval' }
|
||||
],
|
||||
aggregator: 'majority' // Require consensus
|
||||
}
|
||||
});
|
||||
|
||||
const result = arbiter.authChecker.check('user:bob', 'consensus_access', 'project:alpha');
|
||||
|
||||
// Should be allowed due to majority consensus
|
||||
assert.ok(result.possibility > 0.6, `Expected reasonable possibility for consensus access, got ${result.possibility}`);
|
||||
});
|
||||
|
||||
it('handles priority-weighted defeasible logic', () => {
|
||||
// Set up rules with different priorities
|
||||
arbiter.addRelation('user:charlie', 'high_priority_rule', 'project:beta', {
|
||||
value: 0.8,
|
||||
possibility: 1.0,
|
||||
changed_last_at: Date.now()
|
||||
});
|
||||
arbiter.addRelation('user:charlie', 'medium_priority_rule', 'project:beta', {
|
||||
value: 0.6,
|
||||
possibility: 1.0,
|
||||
changed_last_at: Date.now()
|
||||
});
|
||||
arbiter.addRelation('user:charlie', 'low_priority_rule', 'project:beta', {
|
||||
value: 0.9,
|
||||
possibility: 1.0,
|
||||
changed_last_at: Date.now()
|
||||
});
|
||||
|
||||
// Configure priority-weighted defeasible rule
|
||||
arbiter.setRelationConfig('priority_access', {
|
||||
when: {
|
||||
intersection: [
|
||||
{ type: 'direct', relation: 'high_priority_rule', priority: 10 },
|
||||
{ type: 'direct', relation: 'medium_priority_rule', priority: 5 },
|
||||
{ type: 'direct', relation: 'low_priority_rule', priority: 1 }
|
||||
],
|
||||
aggregator: 'priority' // Weight by priority
|
||||
}
|
||||
});
|
||||
|
||||
const result = arbiter.authChecker.check('user:charlie', 'priority_access', 'project:beta');
|
||||
|
||||
// Should be allowed with priority weighting
|
||||
assert.ok(result.possibility > 0.5, `Expected reasonable possibility for priority access, got ${result.possibility}`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Defeasible Logic Evaluation Modes', () => {
|
||||
it('handles binary mode defeasible logic', () => {
|
||||
// Set up simple defeasible rule
|
||||
arbiter.addRelation('user:diana', 'can_access', 'doc:internal', {
|
||||
value: 1.0,
|
||||
possibility: 1.0,
|
||||
changed_last_at: Date.now()
|
||||
});
|
||||
|
||||
arbiter.setRelationConfig('binary_defeasible', {
|
||||
when: {
|
||||
intersection: [
|
||||
{ type: 'direct', relation: 'can_access' }
|
||||
]
|
||||
},
|
||||
mode: 'binary'
|
||||
});
|
||||
|
||||
const result = arbiter.authChecker.check('user:diana', 'binary_defeasible', 'doc:internal');
|
||||
|
||||
// Should work in binary mode
|
||||
assert.ok(result.possibility > 0.8, `Expected high possibility in binary mode, got ${result.possibility}`);
|
||||
});
|
||||
|
||||
it('handles threshold mode defeasible logic', () => {
|
||||
// Set up threshold-based defeasible rule
|
||||
arbiter.addRelation('user:eve', 'can_access', 'doc:confidential', {
|
||||
value: 0.7,
|
||||
possibility: 1.0,
|
||||
changed_last_at: Date.now()
|
||||
});
|
||||
|
||||
arbiter.setRelationConfig('threshold_defeasible', {
|
||||
when: {
|
||||
intersection: [
|
||||
{ type: 'direct', relation: 'can_access' }
|
||||
]
|
||||
},
|
||||
mode: 'threshold'
|
||||
});
|
||||
|
||||
const result = arbiter.authChecker.check('user:eve', 'threshold_defeasible', 'doc:confidential', {
|
||||
fastPath: true,
|
||||
minAllowPossibility: 0.8
|
||||
});
|
||||
|
||||
// Should handle threshold mode appropriately
|
||||
assert.ok(result.possibility >= 0, `Expected valid possibility in threshold mode, got ${result.possibility}`);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user