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,129 @@
|
||||
import { BaseRule } from './BaseRule.js';
|
||||
import { Arbiter } from '../../core/Arbiter.js';
|
||||
|
||||
/**
|
||||
* DirectRule - Checks for direct relationships between user and object
|
||||
*
|
||||
* This rule evaluates direct relations in the graph, optionally in reverse direction.
|
||||
* It supports efficient batch processing and early exit optimizations.
|
||||
*
|
||||
* Returns standardized results with raw possibility values:
|
||||
* - possibility_allow: The strength/possibility of the relation (0.0 to 1.0)
|
||||
* - possibility_deny: Always 0 (DirectRule only reports relation strength, not polarity)
|
||||
* - Collected Values: Values from relations with full path metadata
|
||||
*
|
||||
* Note: This rule returns raw relation strength. The logical context (defeater, strict,
|
||||
* defeasible, etc.) determines how this strength is interpreted as positive or negative evidence.
|
||||
*
|
||||
* Configuration:
|
||||
* {
|
||||
* type: 'direct',
|
||||
* relation: string, // Optional: relation to check (uses currentRelation if not specified)
|
||||
* reverse: boolean, // Optional: check in reverse direction (default: false)
|
||||
* collectValues: boolean // Optional: collect values from relations (default: true if relation has values)
|
||||
* }
|
||||
*/
|
||||
export class DirectRule extends BaseRule {
|
||||
constructor(arbiter) {
|
||||
super(arbiter);
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluate method returns raw relation strength
|
||||
*/
|
||||
evaluate(userId, userKey, objectId, objectKey, rule, visited, currentRelation, options = {}) {
|
||||
const { fastPath = false, minPossibility = null, collectValues: collectValuesOption, includeMeta = true } = options;
|
||||
|
||||
const relName = rule.relation || rule.rel || rule.label || rule.name || currentRelation;
|
||||
const reverse = rule.reverse;
|
||||
const collectValues = collectValuesOption !== undefined ? collectValuesOption : rule.collectValues !== false;
|
||||
|
||||
let directRel;
|
||||
if (reverse) {
|
||||
directRel = this.arbiter.relationManager.getDirectRelation(objectId, relName, userId, options);
|
||||
} else {
|
||||
directRel = this.arbiter.relationManager.getDirectRelation(userId, relName, objectId, options);
|
||||
}
|
||||
|
||||
if (!directRel) {
|
||||
return this._createStandardResult({
|
||||
possibility: 0,
|
||||
...(includeMeta && {
|
||||
meta: {
|
||||
ruleType: 'direct',
|
||||
reason: 'no_relation'
|
||||
}
|
||||
})
|
||||
}, []);
|
||||
}
|
||||
|
||||
const relationStrength = directRel.possibility;
|
||||
const _source = directRel.source || 'persistent';
|
||||
const _allowMeta = {
|
||||
ruleType: 'direct',
|
||||
reason: 'direct',
|
||||
source: _source,
|
||||
layer_name: directRel.layer_name || null,
|
||||
source_class: directRel.source_class || null,
|
||||
reducer_applied: directRel.reducer_applied || null
|
||||
};
|
||||
|
||||
const authResult = {
|
||||
possibility: relationStrength,
|
||||
possibility_allow: relationStrength, // For binary mode
|
||||
possibility_deny: 0, // DirectRule doesn't deny
|
||||
...(includeMeta && {
|
||||
meta: {
|
||||
ruleType: 'direct',
|
||||
reason: 'relation_exists',
|
||||
rule,
|
||||
relation: relName,
|
||||
reverse: reverse || false,
|
||||
strength: relationStrength,
|
||||
source: _source,
|
||||
allow: _allowMeta
|
||||
},
|
||||
meta_allow: _allowMeta
|
||||
}),
|
||||
reason: 'exists'
|
||||
};
|
||||
|
||||
// Collect values if relation has them and collection is enabled
|
||||
let collectedValues = [];
|
||||
if (collectValues && directRel.value !== undefined) {
|
||||
const sourceEntity = reverse ? objectKey : userKey;
|
||||
const targetEntity = reverse ? userKey : objectKey;
|
||||
const path = [sourceEntity, targetEntity];
|
||||
|
||||
collectedValues.push(this._createCollectedValue(
|
||||
directRel.value,
|
||||
directRel.possibility,
|
||||
path,
|
||||
{
|
||||
entityKey: sourceEntity,
|
||||
relation: relName,
|
||||
step: 0
|
||||
},
|
||||
{
|
||||
timestamp: directRel.changed_last_at || directRel.updated_last_at || Date.now(),
|
||||
reliability: 1.0,
|
||||
source: directRel.source || 'persistent'
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
// Apply early exit logic if thresholds are enabled
|
||||
if (fastPath) {
|
||||
// Early exit based on relation strength threshold
|
||||
if (minPossibility !== null && authResult.possibility >= minPossibility) {
|
||||
|
||||
if (authResult.meta) {
|
||||
authResult.meta.earlyExit = true;
|
||||
authResult.meta.earlyExitReason = 'strength_threshold_met';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return this._createStandardResult(authResult, collectedValues);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user