Files
core/src/authorization/rules/BaseRule.js
T

687 lines
23 KiB
JavaScript
Raw Normal View History

import { OWAFusion } from '../../utils/OWAFusion.js';
import { BilatticeOrderings } from '../../qualitative/BilatticeOrderings.js';
import { QualitativeCapacity } from '../../qualitative/QualitativeCapacity.js';
import { QualitativeScale } from '../../qualitative/QualitativeScale.js';
import { UnifiedEvidenceFusion } from '../../qualitative/UnifiedEvidenceFusion.js';
/**
* BaseRule - Standard interface for all authorization rules
*
* This abstract base class defines the standard API that all rule implementations
* must follow to ensure consistency, performance, and maintainability.
*
* STANDARDIZED RESULT STRUCTURE:
* All rules return both authorization results AND collected values:
* {
* // AUTHORIZATION (core purpose)
* possibility_allow: number, // 0-1, aggregated possibility across paths
* possibility_deny: number, // 0-1, denial confidence
* reliability: number, // 0-1, confidence in evaluation
*
* // VALUE COLLECTION (optional metadata)
* collectedValues: [ // Array of individual values with paths
* {
* value: number, // The actual value
* possibility: number, // Individual confidence in this value
* path: string[], // Full traversal path to this value
* source: { // Where value came from
* entityKey: string,
* relation: string,
* step: number
* },
* metadata: { // Rich metadata
* timestamp: number,
* reliability: number,
* decay: object
* }
* }
* ],
*
* // EXISTING FIELDS
* meta_allow: object,
* meta_deny: object,
* reason: string
* }
*/
export class BaseRule {
constructor(arbiter) {
if (new.target === BaseRule) {
throw new Error('BaseRule is abstract and cannot be instantiated directly');
}
this.arbiter = arbiter;
this.ruleType = this.constructor.name.replace('Rule', '').toLowerCase();
// Performance tracking
this.evaluationCount = 0;
this.totalEvaluationTime = 0;
}
/**
* Standard rule evaluation interface
*
* @param {number} userId - Internal user node ID
* @param {string} userKey - User key (e.g., "user:alice")
* @param {number} objectId - Internal object node ID
* @param {string} objectKey - Object key (e.g., "doc:secret")
* @param {Object} rule - Rule configuration object
* @param {Set} visited - Set of visited nodes for cycle detection
* @param {string} currentRelation - Current relation being evaluated
* @param {Object} options - Evaluation options
* @returns {RuleEvaluationResult} Standardized result object
*/
evaluate(userId, userKey, objectId, objectKey, rule, visited, currentRelation, options = {}) {
const startTime = Date.now();
this.evaluationCount++;
try {
// Validate inputs
const validation = this._validateInputs(userId, userKey, objectId, objectKey, rule, visited, currentRelation, options);
if (!validation.valid) {
return this._createErrorResult(validation.reason, validation.details);
}
// Normalize options
const normalizedOptions = this._normalizeOptions(options, rule);
// Check for early termination conditions
const earlyExit = this._checkEarlyExit(normalizedOptions, rule);
if (earlyExit) {
return earlyExit;
}
// Delegate to concrete implementation
const result = this._evaluateRule(userId, userKey, objectId, objectKey, rule, visited, currentRelation, normalizedOptions);
// Post-process result
const finalResult = this._postProcessResult(result, rule, normalizedOptions);
// Track performance
this.totalEvaluationTime += Date.now() - startTime;
return finalResult;
} catch (error) {
this.totalEvaluationTime += Date.now() - startTime;
return this._createErrorResult('evaluation_error', { error: error.message, stack: error.stack });
}
}
/**
* Get performance statistics for this rule
* @returns {Object} Performance metrics
*/
getPerformanceStats() {
return {
ruleType: this.ruleType,
evaluationCount: this.evaluationCount,
totalEvaluationTime: this.totalEvaluationTime,
averageEvaluationTime: this.evaluationCount > 0 ? this.totalEvaluationTime / this.evaluationCount : 0
};
}
/**
* Reset performance counters
*/
resetPerformanceStats() {
this.evaluationCount = 0;
this.totalEvaluationTime = 0;
}
// ========== HELPER METHODS FOR STANDARDIZED RESULTS ==========
/**
* Create standardized rule result with collected values
* @protected
*/
_createStandardResult(authResult, collectedValues = []) {
const result = {
// AUTHORIZATION
possibility: authResult.possibility || 0,
reliability: authResult.reliability !== undefined ? authResult.reliability : 1.0,
// BINARY MODE FIELDS
possibility_allow: authResult.possibility_allow !== undefined ? authResult.possibility_allow : (authResult.possibility || 0),
possibility_deny: authResult.possibility_deny !== undefined ? authResult.possibility_deny : 0,
// VALUE COLLECTION
collectedValues: collectedValues,
// EXISTING FIELDS
meta: authResult.meta || null,
meta_allow: authResult.meta_allow,
meta_deny: authResult.meta_deny,
remediation: authResult.remediation,
reason: authResult.reason
};
return result;
}
/**
* Create a collected value entry with full metadata
* @protected
*/
_createCollectedValue(value, possibility, path, source, metadata = {}) {
return {
value: value,
possibility: possibility ?? 1.0,
path: Array.isArray(path) ? [...path] : [path],
source: {
...source,
entityKey: source.entityKey,
relation: source.relation,
step: source.step !== undefined ? source.step : 0
},
metadata: {
timestamp: metadata.timestamp || Date.now(),
reliability: metadata.reliability !== undefined ? metadata.reliability : 1.0,
decay: metadata.decay || null,
...metadata
}
};
}
/**
* Quick reachability failure check for early termination
* @protected
* @param {string} sourceKey - Source entity key
* @param {string} targetKey - Target entity key
* @param {Object} options - Options (direction: 'forward' | 'backward')
* @returns {boolean|null} - true if reachable, false if not reachable, null if no reachability checker
*/
_quickReachabilityCheck(sourceKey, targetKey, options = {}) {
if (!this.arbiter.reachabilityChecker) {
return null; // No reachability checker available
}
try {
// Get node IDs for PLTC query
const sourceId = this.arbiter.resolveNodeId(sourceKey, options);
const targetId = this.arbiter.resolveNodeId(targetKey, options);
if (sourceId === undefined || targetId === undefined) {
return false; // Nodes don't exist
}
// Use reachability checker with direction option for PLTC
return this.arbiter.reachabilityChecker.isReachable(sourceId, targetId, options);
} catch (error) {
console.warn(`Reachability check failed for ${sourceKey} -> ${targetKey}:`, error);
return null;
}
}
/**
* Quick reachability failure check with early termination
* Returns a standardized result for unreachable cases
* @protected
* @param {string} sourceKey - Source entity key
* @param {string} targetKey - Target entity key
* @param {string} reason - Reason for the check
* @returns {Object|null} - Standardized result if unreachable, null if reachable or no checker
*/
_quickReachabilityFailure(sourceKey, targetKey, reason = 'Not reachable via reachability index', options = {}) {
const isReachable = this._quickReachabilityCheck(sourceKey, targetKey, options);
if (isReachable === false) {
// Quick failure - not reachable
return this._createStandardResult({
possibility: 0,
possibility_allow: 0,
possibility_deny: 1.0,
reliability: 1.0,
reason: reason,
meta: {
method: 'reachability_quick_fail',
sourceKey,
targetKey,
timestamp: Date.now()
}
});
}
return null; // Reachable or no checker available
}
/**
* Batch reachability check for multiple source-target pairs
* @protected
* @param {Array} pairs - Array of {sourceKey, targetKey} objects
* @returns {Object} - Map of reachability results
*/
_batchReachabilityCheck(pairs) {
if (!this.arbiter.reachabilityChecker) {
return null;
}
const results = {};
for (const {sourceKey, targetKey} of pairs) {
try {
results[`${sourceKey}->${targetKey}`] = this.arbiter.isReachable(sourceKey, targetKey);
} catch (error) {
console.warn(`Batch reachability check failed for ${sourceKey} -> ${targetKey}:`, error);
results[`${sourceKey}->${targetKey}`] = null;
}
}
return results;
}
/**
* Get reachable nodes for a source with quick failure check
* @protected
* @param {string} sourceKey - Source entity key
* @param {number} maxResults - Maximum number of results
* @returns {Array|null} - Array of reachable node keys or null if no checker
*/
_getReachableNodes(sourceKey, maxResults = 1000) {
if (!this.arbiter.reachabilityChecker) {
return null;
}
try {
return this.arbiter.getReachableNodes(sourceKey, maxResults);
} catch (error) {
console.warn(`Get reachable nodes failed for ${sourceKey}:`, error);
return null;
}
}
/**
* Get nodes that can reach a target with quick failure check
* @protected
* @param {string} targetKey - Target entity key
* @param {number} maxResults - Maximum number of results
* @returns {Array|null} - Array of reaching node keys or null if no checker
*/
_getReachingNodes(targetKey, maxResults = 1000) {
if (!this.arbiter.reachabilityChecker) {
return null;
}
try {
return this.arbiter.getReachingNodes(targetKey, maxResults);
} catch (error) {
console.warn(`Get reaching nodes failed for ${targetKey}:`, error);
return null;
}
}
/**
* Aggregate collected values for consumption by other rules using OWAFusion
* @protected
*/
_aggregateCollectedValues(collectedValues, method = 'sum', options = {}) {
if (!collectedValues || collectedValues.length === 0) {
return { value: null, hasValue: false };
}
const values = collectedValues.map(cv => cv.value);
const possibilities = collectedValues.map(cv => cv.possibility);
const metas = collectedValues.map(cv => ({
path: cv.path,
source: cv.source,
reliability: cv.metadata?.reliability || 1.0,
timestamp: cv.metadata?.timestamp,
originalValue: cv.value,
originalPossibility: cv.possibility
}));
// Use OWAFusion for aggregation
const valueResult = OWAFusion.fuseWithMeta(values, metas, null, method);
const possibilityResult = OWAFusion.fuseWithMeta(possibilities, metas, null, method);
return {
value: valueResult.value,
possibility: possibilityResult.value,
hasValue: true,
aggregationMeta: {
method: method,
sourceCount: collectedValues.length,
sourcePaths: collectedValues.map(cv => cv.path),
selectedSource: valueResult.meta,
valueContribution: valueResult.value,
possibilityContribution: possibilityResult.value,
allValues: values,
allPossibilities: possibilities
}
};
}
/**
* Bilattice-enhanced evidence combination for rigorous epistemic reasoning
*
* This method provides sophisticated evidence fusion using bilattice orderings
* while maintaining compatibility with existing possibilistic infrastructure.
*
* @param {Array} collectedValues - Array of collected values with metadata
* @param {Object} options - Combination options
* @param {string} options.method - Fusion method ('max', 'min', 'majority', etc.)
* @param {boolean} options.useBilattice - Whether to use bilattice reasoning (default: false)
* @param {QualitativeCapacity} options.capacity - Capacity for bilattice analysis
* @param {QualitativeScale} options.scale - Qualitative scale for bilattice operations
* @param {string} options.epistemicMode - 'information', 'truth', or 'hybrid' (default: 'hybrid')
* @returns {Object} Enhanced fusion result with epistemic analysis
* @protected
*/
_combineEvidenceWithBilattice(collectedValues, options = {}) {
const {
method = 'max',
useBilattice = false,
capacity = null,
scale = null,
epistemicMode = 'hybrid',
reconciliationMethod = 'bilattice',
capacityType = 'simple_support',
mode = 'qualitative'
} = options;
if (!useBilattice) {
return this._aggregateCollectedValues(collectedValues, method, options);
}
try {
// Use the unified evidence fusion system
const fusionResult = UnifiedEvidenceFusion.fuse(collectedValues, {
mode,
aggregationMethod: method,
reconciliationMethod: useBilattice ? reconciliationMethod : 'none',
epistemicMode,
capacityType,
scale: scale || QualitativeScale.fivePoint(),
useReconciliation: useBilattice,
reliabilityWeighting: options.reliabilityWeighting || false
});
return {
value: fusionResult.value,
possibility: fusionResult.possibility,
hasValue: fusionResult.hasValue,
aggregationMeta: {
method: fusionResult.fusionMethod,
aggregationMethod: fusionResult.aggregationMethod,
reconciliationMethod: fusionResult.reconciliationMethod,
selectedSource: {
type: 'unified_evidence_fusion',
mode,
epistemicMode,
capacityType
}
},
epistemicAnalysis: fusionResult.epistemicAnalysis
};
} catch (error) {
console.warn('Unified evidence fusion failed, falling back to standard aggregation:', error);
return this._aggregateCollectedValues(collectedValues, method, options);
}
}
/**
* Create a qualitative capacity from collected values for bilattice analysis
*
* @param {Array} collectedValues - Array of collected values
* @param {QualitativeScale} scale - Qualitative scale to use
* @param {string} capacityType - Type of capacity ('simple_support', 'possibility', 'necessity')
* @returns {QualitativeCapacity} Capacity for bilattice analysis
* @protected
*/
_createCapacityFromValues(collectedValues, scale, capacityType = 'simple_support') {
if (!collectedValues || collectedValues.length === 0) {
return null;
}
// Create state space from unique values
const uniqueValues = [...new Set(collectedValues.map(cv => cv.value))];
const stateSpace = uniqueValues.map((_, index) => `evidence_${index}`);
switch (capacityType) {
case 'simple_support':
// Create simple support capacity for each piece of evidence
const qmt = new Map();
collectedValues.forEach((cv, index) => {
const evidenceSet = new Set([`evidence_${index}`]);
qmt.set(evidenceSet, cv.possibility);
});
return new QualitativeCapacity(stateSpace, scale, qmt);
case 'possibility':
// Create possibility measure (all focal sets are singletons)
const possibilityQmt = new Map();
collectedValues.forEach((cv, index) => {
const singletonSet = new Set([`evidence_${index}`]);
possibilityQmt.set(singletonSet, cv.possibility);
});
return new QualitativeCapacity(stateSpace, scale, possibilityQmt);
case 'necessity':
// Create necessity measure (nested focal sets)
const necessityQmt = new Map();
const sortedValues = collectedValues
.map((cv, index) => ({ value: cv.value, possibility: cv.possibility, index }))
.sort((a, b) => b.possibility - a.possibility);
sortedValues.forEach((item, rank) => {
const nestedSet = new Set(sortedValues.slice(0, rank + 1).map(sv => `evidence_${sv.index}`));
necessityQmt.set(nestedSet, item.possibility);
});
return new QualitativeCapacity(stateSpace, scale, necessityQmt);
default:
throw new Error(`Unknown capacity type: ${capacityType}`);
}
}
// ========== PROTECTED METHODS (to be implemented by subclasses) ==========
/**
* Concrete rule evaluation implementation
* @protected
*/
_evaluateRule(userId, userKey, objectId, objectKey, rule, visited, currentRelation, options) {
throw new Error(`${this.constructor.name} must implement _evaluateRule()`);
}
// ========== PRIVATE HELPER METHODS ==========
/**
* Validate input parameters
* @private
*/
_validateInputs(userId, userKey, objectId, objectKey, rule, visited, currentRelation, options) {
// Check for required parameters
if (userId === undefined || userId === null) {
return { valid: false, reason: 'missing_user_id', details: { userKey } };
}
if (objectId === undefined || objectId === null) {
return { valid: false, reason: 'missing_object_id', details: { objectKey } };
}
if (!userKey || typeof userKey !== 'string') {
return { valid: false, reason: 'invalid_user_key', details: { userKey } };
}
if (!objectKey || typeof objectKey !== 'string') {
return { valid: false, reason: 'invalid_object_key', details: { objectKey } };
}
if (!rule || typeof rule !== 'object') {
return { valid: false, reason: 'invalid_rule', details: { rule } };
}
if (!visited || typeof visited.has !== 'function') {
return { valid: false, reason: 'invalid_visited_set', details: { visited } };
}
return { valid: true };
}
/**
* Normalize and validate options
* @private
*/
_normalizeOptions(options, rule) {
const normalized = {
// Performance options
fastPath: options.fastPath || false,
minPossibility: options.minPossibility || null,
maxPossibility: options.maxPossibility || null,
// Inference options
noInfer: options.noInfer || options.no_infer || rule.no_infer || false,
allowInference: options.allowInference !== false && !options.noInfer && !options.no_infer && !rule.no_infer,
// Binary mode
binary: options.binary || false,
// Evaluation tracking
trackEvaluation: options.trackEvaluation !== false,
// Rule-specific options (pass through)
...options
};
// Validate threshold values
if (normalized.minPossibility !== null) {
normalized.minPossibility = Math.max(0, Math.min(1, normalized.minPossibility));
}
if (normalized.maxPossibility !== null) {
normalized.maxPossibility = Math.max(0, Math.min(1, normalized.maxPossibility));
}
return normalized;
}
/**
* Check for early exit conditions
* @private
*/
_checkEarlyExit(options, rule) {
// Cycle detection is handled at a higher level
// Rule-specific early exits should be implemented in _evaluateRule
return null;
}
/**
* Post-process evaluation result
* @private
*/
_postProcessResult(result, rule, options) {
const includeMeta = options.includeMeta !== undefined ? options.includeMeta : true;
const includeValues = options.collectValues !== undefined ? options.collectValues : true;
const { meta, meta_allow, meta_deny, collectedValues, ...rest } = result;
// Ensure result has required fields with standardized structure
const processed = {
// AUTHORIZATION
possibility: result.possibility || 0,
reliability: result.reliability !== undefined ? result.reliability : 1.0,
// EXISTING FIELDS
reason: result.reason,
// PRESERVE OTHER FIELDS (for backward compatibility)
...rest
};
if (includeValues) {
processed.collectedValues = collectedValues || [];
}
if (includeMeta) {
processed.meta = meta || null;
if (meta_allow !== undefined) {
processed.meta_allow = meta_allow;
}
if (meta_deny !== undefined) {
processed.meta_deny = meta_deny;
}
}
// Add evaluation metadata if tracking is enabled
if (options.trackEvaluation && includeMeta && result.evaluation) {
processed.evaluation = {
ruleType: this.ruleType,
evaluationTime: Date.now() - (result.evaluation.evaluationStarted || Date.now()),
...result.evaluation
};
}
// Apply early exit metadata if applicable
if (options.fastPath && includeMeta && this._shouldMarkEarlyExit(processed, options)) {
if (processed.meta) {
processed.meta.earlyExit = true;
processed.meta.earlyExitReason = this._getEarlyExitReason(processed, options);
}
}
return processed;
}
/**
* Create standardized error result
* @private
*/
_createErrorResult(reason, details = {}) {
return {
possibility_allow: 0,
possibility_deny: 0,
reliability: 1.0,
meta_allow: null,
meta_deny: null,
reason,
error: true,
details
};
}
/**
* Check if result should be marked as early exit
* @private
*/
_shouldMarkEarlyExit(result, options) {
if (options.minAllowPossibility !== null && result.possibility_allow >= options.minAllowPossibility) {
return true;
}
if (options.maxDenyPossibility !== null && result.possibility_deny >= options.maxDenyPossibility) {
return true;
}
return false;
}
/**
* Get early exit reason
* @private
*/
_getEarlyExitReason(result, options) {
if (options.minAllowPossibility !== null && result.possibility_allow >= options.minAllowPossibility) {
return 'allow_threshold_met';
}
if (options.maxDenyPossibility !== null && result.possibility_deny >= options.maxDenyPossibility) {
return 'deny_threshold_met';
}
return 'unknown';
}
}
/**
* Standard result structure for rule evaluations
* @typedef {Object} RuleEvaluationResult
* @property {number} possibility_allow - Possibility of allowing access (0-1)
* @property {number} possibility_deny - Possibility of denying access (0-1)
* @property {number} reliability - Reliability of the evaluation (0-1)
* @property {Object|null} meta_allow - Metadata for allow decision
* @property {Object|null} meta_deny - Metadata for deny decision
* @property {string} reason - Reason for the result (optional)
* @property {boolean} error - Whether this is an error result (optional)
* @property {Object} details - Additional details (optional)
* @property {Object} evaluation - Evaluation tracking data (optional)
*/