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,374 @@
|
||||
import { describe, test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { DSLCompiler } from '../../src/ast/DSLCompiler.js';
|
||||
|
||||
function createMockArbiter() {
|
||||
const relationConfigs = new Map();
|
||||
return {
|
||||
relationConfigs,
|
||||
setRelationConfig(relation, config) {
|
||||
relationConfigs.set(relation, config);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
describe('Evidence Rules', () => {
|
||||
const arbiter = createMockArbiter();
|
||||
const compiler = new DSLCompiler(arbiter);
|
||||
|
||||
test('Basic evidence', () => {
|
||||
const testCases = [
|
||||
{
|
||||
input: `evidence canRead(user: User, doc: Document) {
|
||||
hasRole(user, 'admin')
|
||||
}`,
|
||||
description: 'Simple evidence with function call'
|
||||
},
|
||||
{
|
||||
input: `evidence canAccess(user: User, resource: Resource) {
|
||||
user.isActive
|
||||
}`,
|
||||
description: 'Evidence with attribute access'
|
||||
},
|
||||
{
|
||||
input: `evidence canModify(user: User, doc: Document) {
|
||||
user.isActive
|
||||
hasRole(user, 'admin')
|
||||
}`,
|
||||
description: 'Evidence with multiple conditions'
|
||||
},
|
||||
{
|
||||
input: `evidence canDelete(user: User, doc: Document) {
|
||||
owns(user, doc)
|
||||
user.isActive
|
||||
}`,
|
||||
description: 'Evidence with ownership and status'
|
||||
}
|
||||
];
|
||||
|
||||
testCases.forEach(({ input, description }) => {
|
||||
const result = compiler.compile(input, `test-basic-evidence-${Date.now()}`);
|
||||
assert.ok(result.success, `${description} should parse successfully`);
|
||||
assert.ok(result.program.evidence.length > 0, 'Should have evidence');
|
||||
});
|
||||
});
|
||||
|
||||
test('Defeasible logic', () => {
|
||||
const testCases = [
|
||||
{
|
||||
input: `evidence canAccess(user: User, resource: Resource) {
|
||||
ALWAYS user.isActive
|
||||
}`,
|
||||
description: 'ALWAYS rule - strict requirement'
|
||||
},
|
||||
{
|
||||
input: `evidence canAccess(user: User, resource: Resource) {
|
||||
WHEN hasRole(user, 'admin')
|
||||
}`,
|
||||
description: 'WHEN rule - defeasible condition'
|
||||
},
|
||||
{
|
||||
input: `evidence canAccess(user: User, resource: Resource) {
|
||||
WHEN hasRole(user, 'admin') UNLESS isSuspended(user)
|
||||
}`,
|
||||
description: 'WHEN/UNLESS rule - defeasible with defeater'
|
||||
},
|
||||
{
|
||||
input: `evidence canAccess(user: User, resource: Resource) {
|
||||
REQUIRES hasClearance(user, resource.level)
|
||||
}`,
|
||||
description: 'REQUIRES rule - inverse defeater'
|
||||
},
|
||||
{
|
||||
input: `evidence canAccessCritical(user: User, resource: Resource) {
|
||||
ALWAYS user.isActive
|
||||
|
||||
WHEN hasRole(user, 'admin') UNLESS isSuspended(user)
|
||||
|
||||
REQUIRES hasClearance(user, resource.level)
|
||||
}`,
|
||||
description: 'Complex defeasible logic with all rule types'
|
||||
},
|
||||
{
|
||||
input: `evidence canAccessSensitive(user: User, doc: Document) {
|
||||
ALWAYS user.isActive
|
||||
|
||||
WHEN hasRole(user, 'admin') UNLESS isSuspended(user)
|
||||
|
||||
REQUIRES hasClearance(user, doc.level)
|
||||
|
||||
fusion majority {
|
||||
user.isTrusted
|
||||
user.hasRecentActivity
|
||||
}
|
||||
}`,
|
||||
description: 'Defeasible logic with fusion'
|
||||
}
|
||||
];
|
||||
|
||||
testCases.forEach(({ input, description }) => {
|
||||
const result = compiler.compile(input, `test-defeasible-${Date.now()}`);
|
||||
assert.ok(result.success, `${description} should parse successfully`);
|
||||
});
|
||||
});
|
||||
|
||||
test('Pattern matching', () => {
|
||||
const testCases = [
|
||||
{
|
||||
input: `evidence canRead(user: User, doc: Document) {
|
||||
isMember(user, *group) {
|
||||
canRead(group, doc)
|
||||
}
|
||||
}`,
|
||||
description: 'Basic pattern matching with wildcard'
|
||||
},
|
||||
{
|
||||
input: `evidence canRead(user: User, doc: Document) {
|
||||
isMember(user, *group) {
|
||||
canRead(group, doc)
|
||||
} limit 5
|
||||
}`,
|
||||
description: 'Pattern matching with limit'
|
||||
},
|
||||
{
|
||||
input: `evidence canRead(user: User, doc: Document) {
|
||||
similar(doc, *similar) |similarity| {
|
||||
canRead(user, similar)
|
||||
} with similarity > 0.7
|
||||
}`,
|
||||
description: 'Pattern matching with binding and condition'
|
||||
},
|
||||
{
|
||||
input: `evidence canRead(user: User, doc: Document) {
|
||||
similar(doc, *similar) |similarity| {
|
||||
canRead(user, similar)
|
||||
} with similarity > 0.7 limit 5
|
||||
}`,
|
||||
description: 'Pattern matching with binding, condition, and limit'
|
||||
},
|
||||
{
|
||||
input: `evidence canRead(user: User, doc: Document) {
|
||||
isMember(user, *group) {
|
||||
isMember(group, *parentGroup) {
|
||||
canRead(parentGroup, doc)
|
||||
} limit 2
|
||||
} limit 3
|
||||
}`,
|
||||
description: 'Nested pattern matching'
|
||||
},
|
||||
{
|
||||
input: `evidence canRead(user: User, doc: Document) {
|
||||
isFriend(user, *friend) {
|
||||
isMember(friend, *group) {
|
||||
canRead(group, doc)
|
||||
} limit 1
|
||||
} limit 5
|
||||
}`,
|
||||
description: 'Multi-hop pattern matching'
|
||||
}
|
||||
];
|
||||
|
||||
testCases.forEach(({ input, description }) => {
|
||||
const result = compiler.compile(input, `test-pattern-${Date.now()}`);
|
||||
assert.ok(result.success, `${description} should parse successfully`);
|
||||
});
|
||||
});
|
||||
|
||||
test('Fusion', () => {
|
||||
const testCases = [
|
||||
{
|
||||
input: `evidence canAccess(user: User, resource: Resource) {
|
||||
fusion min {
|
||||
hasClearance(user, resource.level)
|
||||
user.isActive
|
||||
}
|
||||
}`,
|
||||
description: 'Min fusion - all conditions must be true'
|
||||
},
|
||||
{
|
||||
input: `evidence canAccess(user: User, resource: Resource) {
|
||||
fusion max {
|
||||
hasRole(user, 'admin')
|
||||
hasRole(user, 'superuser')
|
||||
}
|
||||
}`,
|
||||
description: 'Max fusion - any condition can be true'
|
||||
},
|
||||
{
|
||||
input: `evidence canAccess(user: User, resource: Resource) {
|
||||
fusion majority {
|
||||
hasClearance(user, 'secret')
|
||||
user.isTrusted
|
||||
user.hasRecentActivity
|
||||
}
|
||||
}`,
|
||||
description: 'Majority fusion - most conditions must be true'
|
||||
},
|
||||
{
|
||||
input: `evidence canAccessCritical(user: User, resource: Resource) {
|
||||
fusion min {
|
||||
hasClearance(user, resource.level)
|
||||
user.isActive
|
||||
NOT user.isBlacklisted
|
||||
}
|
||||
|
||||
fusion max {
|
||||
hasRole(user, 'admin')
|
||||
fusion majority {
|
||||
hasClearance(user, 'secret')
|
||||
user.isTrusted
|
||||
user.lastActive within 1hr
|
||||
}
|
||||
}
|
||||
}`,
|
||||
description: 'Nested fusion with different strategies'
|
||||
},
|
||||
{
|
||||
input: `evidence canAccess(user: User, resource: Resource) {
|
||||
fusion average {
|
||||
user.reputation
|
||||
user.activityScore
|
||||
user.verificationLevel
|
||||
}
|
||||
}`,
|
||||
description: 'Average fusion for numeric values'
|
||||
}
|
||||
];
|
||||
|
||||
testCases.forEach(({ input, description }) => {
|
||||
const result = compiler.compile(input, `test-fusion-${Date.now()}`);
|
||||
assert.ok(result.success, `${description} should parse successfully`);
|
||||
});
|
||||
});
|
||||
|
||||
test('Complex evidence', () => {
|
||||
const testCases = [
|
||||
{
|
||||
input: `evidence canRead(user: User, doc: Document) {
|
||||
owns(user, doc)
|
||||
|
||||
isMember(user, *group) {
|
||||
canRead(group, doc)
|
||||
} limit 5
|
||||
|
||||
parentOf(user, *parent) {
|
||||
canRead(parent, doc)
|
||||
} limit 3
|
||||
|
||||
similar(doc, *similar) |similarity| {
|
||||
canRead(user, similar)
|
||||
} with similarity > 0.7 limit 5
|
||||
|
||||
WHEN hasRole(user, 'admin') UNLESS isSuspended(user)
|
||||
}`,
|
||||
description: 'Complex evidence with all features'
|
||||
},
|
||||
{
|
||||
input: `evidence canAccessCritical(user: User, resource: Resource) {
|
||||
ALWAYS user.isActive
|
||||
|
||||
WHEN hasRole(user, 'admin') UNLESS isSuspended(user)
|
||||
|
||||
REQUIRES hasClearance(user, resource.level)
|
||||
|
||||
fusion min {
|
||||
hasClearance(user, resource.level)
|
||||
user.isActive
|
||||
NOT user.isBlacklisted
|
||||
}
|
||||
|
||||
fusion max {
|
||||
hasRole(user, 'admin')
|
||||
fusion majority {
|
||||
hasClearance(user, 'secret')
|
||||
user.isTrusted
|
||||
user.lastActive within 1hr
|
||||
}
|
||||
}
|
||||
}`,
|
||||
description: 'Critical access with all rule types and fusion'
|
||||
},
|
||||
{
|
||||
input: `evidence canModify(user: User, doc: Document) {
|
||||
owns(user, doc)
|
||||
|
||||
isMember(user, *group) {
|
||||
canModify(group, doc)
|
||||
} limit 3
|
||||
|
||||
similar(doc, *similar) |similarity| {
|
||||
canModify(user, similar)
|
||||
similar.isEditable
|
||||
} with similarity > 0.8 limit 2
|
||||
|
||||
fusion majority {
|
||||
user.isTrusted
|
||||
user.hasRecentActivity
|
||||
doc.isPublic
|
||||
}
|
||||
}`,
|
||||
description: 'Modification access with similarity and fusion'
|
||||
}
|
||||
];
|
||||
|
||||
testCases.forEach(({ input, description }) => {
|
||||
const result = compiler.compile(input, `test-complex-evidence-${Date.now()}`);
|
||||
assert.ok(result.success, `${description} should parse successfully`);
|
||||
});
|
||||
});
|
||||
|
||||
test('Evidence error handling', () => {
|
||||
const testCases = [
|
||||
{
|
||||
input: `evidence canRead(user: User, doc: Document) {
|
||||
hasRole(user, 'admin'
|
||||
}`,
|
||||
description: 'Missing closing parenthesis should fail'
|
||||
},
|
||||
{
|
||||
input: `evidence canRead(user: User, doc: Document) {
|
||||
WHEN hasRole(user, 'admin') UNLESS
|
||||
}`,
|
||||
description: 'Incomplete UNLESS condition should fail'
|
||||
},
|
||||
{
|
||||
input: `evidence canRead(user: User, doc: Document) {
|
||||
fusion min {
|
||||
hasRole(user, 'admin')
|
||||
}`,
|
||||
description: 'Incomplete fusion should fail'
|
||||
},
|
||||
{
|
||||
input: `evidence canRead(user: User, doc: Document) {
|
||||
isMember(user, *group) {
|
||||
canRead(group, doc)
|
||||
} with
|
||||
}`,
|
||||
description: 'Incomplete with clause should fail'
|
||||
},
|
||||
{
|
||||
input: `evidence canRead(user: User, doc: Document) {
|
||||
isMember(user, *group) {
|
||||
canRead(group, doc)
|
||||
} limit
|
||||
}`,
|
||||
description: 'Incomplete limit should fail'
|
||||
},
|
||||
{
|
||||
input: `evidence canRead(user: User, doc: Document) {
|
||||
invalid syntax here
|
||||
}`,
|
||||
description: 'Invalid syntax should fail'
|
||||
}
|
||||
];
|
||||
|
||||
testCases.forEach(({ input, description }) => {
|
||||
try {
|
||||
const result = compiler.compile(input, `test-evidence-error-${Date.now()}`);
|
||||
assert.ok(!result.success, `${description} should fail to parse`);
|
||||
} catch {
|
||||
// Expected to fail
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user