2dc478f5a3
A chain step that references a derived evidence is now expanded at compile
time, keeping the engine a flat edge-traversal evaluator:
- DIRECT evidence step -> renamed to its underlying relation
(member_of(user,*g){ group_read(g,doc) } where group_read = can_view
becomes step 'can_view').
- CHAIN evidence step -> its steps are spliced into the parent chain
(a sub-path flattens into the linear source->...->object traversal).
- Any other evidence type (defeasible/logical/comparator) as a step is a
compile-time error: it is a condition, not an edge traversal.
- Cycles and self-references through chain steps are compile-time errors
(the existing composition cycle guard now covers steps).
Rigor: oracle campaign gains a chain_step_composition construct; illegal
mutations gain a non-lowerable-chain-step case. Fixture suites updated to
retarget the self-recursive 'canRead/canAccess/...' terminals (an unsupported
recursion pattern that now fails loudly) to an any-typed 'reachable' fact,
preserving the nested-pattern parsing intent.
420 lines
12 KiB
JavaScript
420 lines
12 KiB
JavaScript
import { describe, test } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { DSLCompiler } from '../src/DSLCompiler.js';
|
|
|
|
function createMockArbiter() {
|
|
const relationConfigs = new Map();
|
|
return {
|
|
relationConfigs,
|
|
setRelationConfig(relation, config) {
|
|
relationConfigs.set(relation, config);
|
|
}
|
|
};
|
|
}
|
|
|
|
const DSL_SUPPORT = `
|
|
definition Employee {
|
|
role: string
|
|
isActive: boolean
|
|
isTrusted: boolean
|
|
hasRecentActivity: boolean
|
|
lastActive: timestamp
|
|
isBlacklisted: boolean
|
|
session: string
|
|
}
|
|
|
|
definition Document {
|
|
level: string
|
|
isPublic: boolean
|
|
isEditable: boolean
|
|
}
|
|
|
|
definition Resource {
|
|
level: string
|
|
isPublic: boolean
|
|
}
|
|
|
|
fact hasRole(user: any, role: string)
|
|
fact hasClearance(user: any, level: string)
|
|
fact owns(user: any, doc: any)
|
|
fact isSuspended(user: any)
|
|
fact isActive(user: any)
|
|
fact isTrusted(user: any)
|
|
fact hasRecentActivity(user: any)
|
|
fact isBlacklisted(user: any)
|
|
fact isMember(user: any, group: any)
|
|
fact isFriend(user: any, friend: any)
|
|
fact similar(a: any, b: any)
|
|
fact reachable(user: any, doc: any)
|
|
fact parentOf(user: any, parent: any)
|
|
fact isEditable(doc: any)
|
|
fact isPublic(doc: any)
|
|
fact recentlyActive(user: any)
|
|
fact reputationScore(user: any)
|
|
fact activityScore(user: any)
|
|
fact verificationLevel(user: any)
|
|
`;
|
|
|
|
describe('Evidence Rules', () => {
|
|
const arbiter = createMockArbiter();
|
|
const compiler = new DSLCompiler(arbiter);
|
|
|
|
test('Basic evidence', () => {
|
|
const testCases = [
|
|
{
|
|
input: `evidence canRead(user: Employee, doc: Document) {
|
|
hasRole(user, 'admin')
|
|
}`,
|
|
description: 'Simple evidence with function call'
|
|
},
|
|
{
|
|
input: `evidence canAccess(user: Employee, resource: Resource) {
|
|
isActive(user)
|
|
}`,
|
|
description: 'Evidence with attribute access'
|
|
},
|
|
{
|
|
input: `evidence canModify(user: Employee, doc: Document) {
|
|
isActive(user)
|
|
hasRole(user, 'admin')
|
|
}`,
|
|
description: 'Evidence with multiple conditions'
|
|
},
|
|
{
|
|
input: `evidence canDelete(user: Employee, doc: Document) {
|
|
owns(user, doc)
|
|
isActive(user)
|
|
}`,
|
|
description: 'Evidence with ownership and status'
|
|
}
|
|
];
|
|
|
|
testCases.forEach(({ input, description }) => {
|
|
const result = compiler.compile(DSL_SUPPORT + 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: Employee, resource: Resource) {
|
|
ALWAYS isActive(user)
|
|
}`,
|
|
description: 'ALWAYS rule - strict requirement'
|
|
},
|
|
{
|
|
input: `evidence canAccess(user: Employee, resource: Resource) {
|
|
WHEN hasRole(user, 'admin')
|
|
}`,
|
|
description: 'WHEN rule - defeasible condition'
|
|
},
|
|
{
|
|
input: `evidence canAccess(user: Employee, resource: Resource) {
|
|
WHEN hasRole(user, 'admin') UNLESS isSuspended(user)
|
|
}`,
|
|
description: 'WHEN/UNLESS rule - defeasible with defeater'
|
|
},
|
|
{
|
|
input: `evidence canAccess(user: Employee, resource: Resource) {
|
|
REQUIRES hasClearance(user, resource.level)
|
|
}`,
|
|
description: 'REQUIRES rule - inverse defeater'
|
|
},
|
|
{
|
|
input: `evidence canAccessCritical(user: Employee, resource: Resource) {
|
|
ALWAYS isActive(user)
|
|
|
|
WHEN hasRole(user, 'admin') UNLESS isSuspended(user)
|
|
|
|
REQUIRES hasClearance(user, resource.level)
|
|
}`,
|
|
description: 'Complex defeasible logic with all rule types'
|
|
},
|
|
{
|
|
input: `evidence canAccessSensitive(user: Employee, doc: Document) {
|
|
ALWAYS isActive(user)
|
|
|
|
WHEN hasRole(user, 'admin') UNLESS isSuspended(user)
|
|
|
|
REQUIRES hasClearance(user, doc.level)
|
|
|
|
fusion majority {
|
|
isTrusted(user),
|
|
hasRecentActivity(user)
|
|
}
|
|
}`,
|
|
description: 'Defeasible logic with fusion'
|
|
}
|
|
];
|
|
|
|
testCases.forEach(({ input, description }) => {
|
|
const result = compiler.compile(DSL_SUPPORT + input, `test-defeasible-${Date.now()}`);
|
|
assert.ok(result.success, `${description} should parse successfully`);
|
|
});
|
|
});
|
|
|
|
test('Pattern matching', () => {
|
|
const testCases = [
|
|
{
|
|
input: `evidence canRead(user: Employee, doc: Document) {
|
|
isMember(user, *group) {
|
|
reachable(group, doc)
|
|
}
|
|
}`,
|
|
description: 'Basic pattern matching with wildcard'
|
|
},
|
|
{
|
|
input: `evidence canRead(user: Employee, doc: Document) {
|
|
isMember(user, *group) {
|
|
reachable(group, doc)
|
|
} limit 5
|
|
}`,
|
|
description: 'Pattern matching with limit'
|
|
},
|
|
{
|
|
input: `evidence canRead(user: Employee, doc: Document) {
|
|
similar(doc, *similar) |similarity| {
|
|
reachable(user, similar)
|
|
} with similarity > 0.7
|
|
}`,
|
|
description: 'Pattern matching with binding and condition'
|
|
},
|
|
{
|
|
input: `evidence canRead(user: Employee, doc: Document) {
|
|
similar(doc, *similar) |similarity| {
|
|
reachable(user, similar)
|
|
} limit 5 with similarity > 0.7
|
|
}`,
|
|
description: 'Pattern matching with binding, condition, and limit'
|
|
},
|
|
{
|
|
input: `evidence canRead(user: Employee, doc: Document) {
|
|
isMember(user, *group) {
|
|
isMember(group, *parentGroup) {
|
|
reachable(parentGroup, doc)
|
|
} limit 2
|
|
} limit 3
|
|
}`,
|
|
description: 'Nested pattern matching'
|
|
},
|
|
{
|
|
input: `evidence canRead(user: Employee, doc: Document) {
|
|
isFriend(user, *friend) {
|
|
isMember(friend, *group) {
|
|
reachable(group, doc)
|
|
} limit 1
|
|
} limit 5
|
|
}`,
|
|
description: 'Multi-hop pattern matching'
|
|
}
|
|
];
|
|
|
|
testCases.forEach(({ input, description }) => {
|
|
const result = compiler.compile(DSL_SUPPORT + input, `test-pattern-${Date.now()}`);
|
|
assert.ok(result.success, `${description} should parse successfully`);
|
|
});
|
|
});
|
|
|
|
test('Fusion', () => {
|
|
const testCases = [
|
|
{
|
|
input: `evidence canAccess(user: Employee, resource: Resource) {
|
|
fusion min {
|
|
hasClearance(user, resource.level),
|
|
isActive(user)
|
|
}
|
|
}`,
|
|
description: 'Min fusion - all conditions must be true'
|
|
},
|
|
{
|
|
input: `evidence canAccess(user: Employee, resource: Resource) {
|
|
fusion max {
|
|
hasRole(user, 'admin'),
|
|
hasRole(user, 'superuser')
|
|
}
|
|
}`,
|
|
description: 'Max fusion - any condition can be true'
|
|
},
|
|
{
|
|
input: `evidence canAccess(user: Employee, resource: Resource) {
|
|
fusion majority {
|
|
hasClearance(user, 'secret'),
|
|
isTrusted(user),
|
|
hasRecentActivity(user)
|
|
}
|
|
}`,
|
|
description: 'Majority fusion - most conditions must be true'
|
|
},
|
|
{
|
|
input: `evidence canAccessCritical(user: Employee, resource: Resource) {
|
|
fusion min {
|
|
hasClearance(user, resource.level),
|
|
isActive(user),
|
|
NOT isBlacklisted(user)
|
|
}
|
|
|
|
fusion max {
|
|
hasRole(user, 'admin')
|
|
}
|
|
|
|
fusion majority {
|
|
hasClearance(user, 'secret'),
|
|
isTrusted(user),
|
|
recentlyActive(user)
|
|
}
|
|
}`,
|
|
description: 'Nested fusion with different strategies'
|
|
},
|
|
{
|
|
input: `evidence canAccess(user: Employee, resource: Resource) {
|
|
fusion average {
|
|
reputationScore(user),
|
|
activityScore(user),
|
|
verificationLevel(user)
|
|
}
|
|
}`,
|
|
description: 'Average fusion for numeric values'
|
|
}
|
|
];
|
|
|
|
testCases.forEach(({ input, description }) => {
|
|
const result = compiler.compile(DSL_SUPPORT + input, `test-fusion-${Date.now()}`);
|
|
assert.ok(result.success, `${description} should parse successfully`);
|
|
});
|
|
});
|
|
|
|
test('Complex evidence', () => {
|
|
const testCases = [
|
|
{
|
|
input: `evidence canRead(user: Employee, doc: Document) {
|
|
owns(user, doc)
|
|
|
|
isMember(user, *group) {
|
|
reachable(group, doc)
|
|
} limit 5
|
|
|
|
parentOf(user, *parent) {
|
|
reachable(parent, doc)
|
|
} limit 3
|
|
|
|
similar(doc, *similar) |similarity| {
|
|
reachable(user, similar)
|
|
} limit 5 with similarity > 0.7
|
|
|
|
WHEN hasRole(user, 'admin') UNLESS isSuspended(user)
|
|
}`,
|
|
description: 'Complex evidence with all features'
|
|
},
|
|
{
|
|
input: `evidence canAccessCritical(user: Employee, resource: Resource) {
|
|
ALWAYS isActive(user)
|
|
|
|
WHEN hasRole(user, 'admin') UNLESS isSuspended(user)
|
|
|
|
REQUIRES hasClearance(user, resource.level)
|
|
|
|
fusion min {
|
|
hasClearance(user, resource.level),
|
|
isActive(user),
|
|
NOT isBlacklisted(user)
|
|
}
|
|
|
|
fusion max {
|
|
hasRole(user, 'admin')
|
|
}
|
|
|
|
fusion majority {
|
|
hasClearance(user, 'secret'),
|
|
isTrusted(user),
|
|
recentlyActive(user)
|
|
}
|
|
}`,
|
|
description: 'Critical access with all rule types and fusion'
|
|
},
|
|
{
|
|
input: `evidence canModify(user: Employee, doc: Document) {
|
|
owns(user, doc)
|
|
|
|
isMember(user, *group) {
|
|
reachable(group, doc)
|
|
} limit 3
|
|
|
|
similar(doc, *similar) |similarity| {
|
|
reachable(user, similar)
|
|
isEditable(similar)
|
|
} limit 2 with similarity > 0.8
|
|
|
|
fusion majority {
|
|
isTrusted(user),
|
|
hasRecentActivity(user),
|
|
isPublic(doc)
|
|
}
|
|
}`,
|
|
description: 'Modification access with similarity and fusion'
|
|
}
|
|
];
|
|
|
|
testCases.forEach(({ input, description }) => {
|
|
const result = compiler.compile(DSL_SUPPORT + input, `test-complex-evidence-${Date.now()}`);
|
|
assert.ok(result.success, `${description} should parse successfully`);
|
|
});
|
|
});
|
|
|
|
test('Evidence error handling', () => {
|
|
const testCases = [
|
|
{
|
|
input: `evidence canRead(user: Employee, doc: Document) {
|
|
hasRole(user, 'admin'
|
|
}`,
|
|
description: 'Missing closing parenthesis should fail'
|
|
},
|
|
{
|
|
input: `evidence canRead(user: Employee, doc: Document) {
|
|
WHEN hasRole(user, 'admin') UNLESS
|
|
}`,
|
|
description: 'Incomplete UNLESS condition should fail'
|
|
},
|
|
{
|
|
input: `evidence canRead(user: Employee, doc: Document) {
|
|
fusion min {
|
|
hasRole(user, 'admin')
|
|
}`,
|
|
description: 'Incomplete fusion should fail'
|
|
},
|
|
{
|
|
input: `evidence canRead(user: Employee, doc: Document) {
|
|
isMember(user, *group) {
|
|
reachable(group, doc)
|
|
} with
|
|
}`,
|
|
description: 'Incomplete with clause should fail'
|
|
},
|
|
{
|
|
input: `evidence canRead(user: Employee, doc: Document) {
|
|
isMember(user, *group) {
|
|
reachable(group, doc)
|
|
} limit
|
|
}`,
|
|
description: 'Incomplete limit should fail'
|
|
},
|
|
{
|
|
input: `evidence canRead(user: Employee, doc: Document) {
|
|
invalid syntax here
|
|
}`,
|
|
description: 'Invalid syntax should fail'
|
|
}
|
|
];
|
|
|
|
testCases.forEach(({ input, description }) => {
|
|
try {
|
|
const result = compiler.compile(DSL_SUPPORT + input, `test-evidence-error-${Date.now()}`);
|
|
assert.ok(!result.success, `${description} should fail to parse`);
|
|
} catch {
|
|
// Expected to fail
|
|
}
|
|
});
|
|
});
|
|
});
|