diff --git a/src/authorization/CompiledEvaluator.js b/src/authorization/CompiledEvaluator.js deleted file mode 100644 index 0f1882e..0000000 --- a/src/authorization/CompiledEvaluator.js +++ /dev/null @@ -1,898 +0,0 @@ -import { buildValidity, normalizeValidity, DEFAULT_VALIDITY } from '../core/validity.js'; -import { OWAFusion, getOWAWeightsFromRule } from '../utils/OWAFusion.js'; -import { buildRemediation, extractRemediation, mergeRemediationOptions } from './remediation.js'; - -export class CompiledEvaluator { - constructor(arbiter, ruleEvaluator, logicalOperators) { - this.arbiter = arbiter; - this.ruleEvaluator = ruleEvaluator; - this.logicalOperators = logicalOperators; - } - - evaluate(compiled, userId, userKey, objectId, objectKey, visited, currentRelation, options = {}) { - if (!compiled || typeof compiled !== 'object') { - return { possibility: 0, reason: 'missing_compiled_rule' }; - } - - switch (compiled.type) { - case 'direct': - return this._evaluateDirect(compiled, userId, userKey, objectId, objectKey, options, currentRelation); - case 'computed': - return this._evaluateComputed(compiled, userKey, objectKey, options, visited, currentRelation); - case 'tuple_to_userset': - return this._evaluateTupleToUserset(compiled, userId, userKey, objectId, objectKey, visited, currentRelation, options); - case 'chain': - return this._evaluateChain(compiled, userId, userKey, objectId, objectKey, visited, currentRelation, options); - case 'multi_hop': - return this._evaluateMultiHop(compiled, userId, userKey, objectId, objectKey, visited, currentRelation, options); - case 'logical': - return this._evaluateLogical(compiled, userId, userKey, objectId, objectKey, visited, currentRelation, options); - case 'defeasible': - return this._evaluateDefeasible(compiled, userId, userKey, objectId, objectKey, visited, currentRelation, options); - case 'relational_comparator': - return this._evaluateRelationalComparator(compiled, userId, userKey, objectId, objectKey, visited, currentRelation, options); - case 'challenge': - return this.ruleEvaluator.evaluateRule( - userId, - userKey, - objectId, - objectKey, - compiled, - visited, - currentRelation, - { ...options, useCompiled: false } - ); - default: - if (compiled.source) { - return this.ruleEvaluator.evaluateRule( - userId, - userKey, - objectId, - objectKey, - compiled.source, - visited, - currentRelation, - { ...options, useCompiled: false } - ); - } - return { possibility: 0, reason: 'unsupported_compiled_rule' }; - } - } - - _evaluateRelationalComparator(compiled, userId, userKey, objectId, objectKey, visited, currentRelation, options) { - if (compiled.source) { - return this.ruleEvaluator.evaluateRule( - userId, - userKey, - objectId, - objectKey, - compiled.source, - visited, - currentRelation, - { ...options, useCompiled: false } - ); - } - - const toOperandConfig = (operand) => { - if (!operand) return null; - return { - rule: operand.rule || null, - extractValue: operand.extractValue !== false, - valueRelation: operand.valueRelationResolved || operand.valueRelation || null, - evaluateFrom: operand.evaluateFrom || 'auto', - aggregator: operand.aggregator, - owaWeights: operand.owaWeights, - _owaSparseCount: operand._owaSparseCount, - minOperandPossibility: operand.minOperandPossibility - }; - }; - - const fallbackConfig = { - type: 'relational_comparator', - comparator: compiled.comparator, - marginOfSafety: compiled.marginOfSafety, - fallbackBehavior: compiled.fallbackBehavior, - minRulePossibility: compiled.minRulePossibility, - epsilon: compiled.epsilon, - left: toOperandConfig(compiled.left), - right: toOperandConfig(compiled.right) - }; - - return this.ruleEvaluator.evaluateRule( - userId, - userKey, - objectId, - objectKey, - fallbackConfig, - visited, - currentRelation, - { ...options, useCompiled: false } - ); - } - - _evaluateDirect(compiled, userId, userKey, objectId, objectKey, options, currentRelation) { - const { fastPath = false, minPossibility = null, includeMeta = true } = options; - const relName = compiled.relation || currentRelation; - const reverse = compiled.reverse; - const collectValues = options.collectValues !== undefined ? options.collectValues : compiled.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 { - possibility: 0, - ...(includeMeta && { meta: { ruleType: 'direct', reason: 'no_relation' } }), - 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 result = { - possibility: relationStrength, - reliability: directRel.reliability !== undefined ? directRel.reliability : 1.0, - validity: buildValidity('identity', [relName], [directRel.validity !== undefined ? normalizeValidity(directRel.validity) : 'heuristic'], 1, relationStrength), - possibility_allow: relationStrength, - possibility_deny: 0, - ...(includeMeta && { - meta: { - ruleType: 'direct', - reason: 'relation_exists', - relation: relName, - reverse: reverse || false, - strength: relationStrength, - source: _source, - allow: _allowMeta - }, - meta_allow: _allowMeta - }), - reason: 'exists' - }; - - if (fastPath && minPossibility !== null && result.possibility >= minPossibility) { - if (result.meta) { - result.meta.earlyExit = true; - result.meta.earlyExitReason = 'strength_threshold_met'; - } - } - - if (collectValues && directRel.value !== undefined) { - const sourceEntity = reverse ? objectKey : userKey; - const targetEntity = reverse ? userKey : objectKey; - const path = [sourceEntity, targetEntity]; - result.collectedValues = [ - this.ruleEvaluator.ruleHandlers.direct._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' - } - ) - ]; - } - - return result; - } - - _evaluateComputed(compiled, userKey, objectKey, options, visited, currentRelation) { - return this.arbiter.authChecker.check(userKey, compiled.relation, objectKey, { - ...options, - _visited: visited, - _currentRelation: currentRelation - }); - } - - _evaluateTupleToUserset(compiled, userId, userKey, objectId, objectKey, visited, currentRelation, options) { - if (compiled._optimized?.kind === 'direct_join' && !options.collectValues && !options.trackEvaluation) { - const optimized = compiled._optimized; - const resolvePossibility = (value) => value !== undefined ? value : 1.0; - const resolveReliability = (value) => value !== undefined ? value : 1.0; - const tuplesetDirection = optimized.tuplesetDirection || 'out'; - let tuples; - if (optimized.reverse) { - tuples = tuplesetDirection === 'in' - ? this.arbiter.relationManager.getRelationsToDst(userId, optimized.tuplesetRelation, options) - : this.arbiter.relationManager.getRelationsFromSrc(userId, optimized.tuplesetRelation, options); - } else { - tuples = tuplesetDirection === 'in' - ? this.arbiter.relationManager.getRelationsToDst(objectId, optimized.tuplesetRelation, options) - : this.arbiter.relationManager.getRelationsFromSrc(objectId, optimized.tuplesetRelation, options); - } - - const maxIntermediates = optimized.maxIntermediates !== undefined ? optimized.maxIntermediates : 20; - if (tuples.length > maxIntermediates * 3) { - tuples.sort((a, b) => resolvePossibility(b.possibility) - resolvePossibility(a.possibility)); - tuples = tuples.slice(0, maxIntermediates); - } - - const checkingId = optimized.reverse ? objectId : userId; - const computedEdges = this.arbiter.relationManager.getRelationsFromSrc(checkingId, optimized.computedRelation, options); - const computedByIntermediate = new Map(computedEdges.map(edge => [edge.dst, edge])); - - let processed = 0; - let bestPossibility = 0; - let bestReliability = 1.0; - const earlyExitThreshold = optimized.earlyExitThreshold !== undefined ? optimized.earlyExitThreshold : 0.95; - const minPossibility = optimized.minPossibility !== undefined ? optimized.minPossibility : 0; - - for (const t of tuples) { - if (processed >= maxIntermediates) break; - processed++; - const intermediateId = tuplesetDirection === 'in' ? t.src : t.dst; - const directRel = computedByIntermediate.get(intermediateId); - if (!directRel) continue; - const combinedPossibility = Math.min(resolvePossibility(t.possibility), resolvePossibility(directRel.possibility)); - const finalPossibility = combinedPossibility >= minPossibility ? combinedPossibility : 0; - if (finalPossibility > bestPossibility) { - bestPossibility = finalPossibility; - bestReliability = resolveReliability(t.reliability) * (directRel.reliability !== undefined ? directRel.reliability : 1.0); - if (bestPossibility >= earlyExitThreshold) break; - } - } - - return { - possibility: bestPossibility, - reliability: bestReliability, - reason: bestPossibility > 0 ? 'direct_join' : 'no_match' - }; - } - - if (compiled.source) { - return this.ruleEvaluator.evaluateRule( - userId, - userKey, - objectId, - objectKey, - compiled.source, - visited, - currentRelation, - { ...options, useCompiled: false } - ); - } - const fallbackConfig = { - type: 'tuple_to_userset', - tuplesetRelation: compiled.tuplesetRelation, - tuplesetDirection: compiled.tuplesetDirection, - computedRelation: compiled.computedRelation - }; - return this.ruleEvaluator.evaluateRule( - userId, - userKey, - objectId, - objectKey, - fallbackConfig, - visited, - currentRelation, - { ...options, useCompiled: false } - ); - } - - _evaluateChain(compiled, userId, userKey, objectId, objectKey, visited, currentRelation, options) { - if (compiled._optimized?.kind === 'direct' && !options.collectValues && !options.trackEvaluation) { - const direct = { - type: 'direct', - relation: compiled._optimized.relation, - reverse: compiled._optimized.reverse, - collectValues: false - }; - return this._evaluateDirect(direct, userId, userKey, objectId, objectKey, options, currentRelation); - } - if (compiled.source) { - return this.ruleEvaluator.evaluateRule( - userId, - userKey, - objectId, - objectKey, - compiled.source, - visited, - currentRelation, - { ...options, useCompiled: false } - ); - } - const fallbackConfig = { - type: 'chain', - steps: compiled.steps, - reverse: compiled.reverse, - collectValues: compiled.collectValues, - valueFilters: compiled.valueFilters, - valueAggregation: compiled.valueAggregation - }; - return this.ruleEvaluator.evaluateRule( - userId, - userKey, - objectId, - objectKey, - fallbackConfig, - visited, - currentRelation, - { ...options, useCompiled: false } - ); - } - - _evaluateMultiHop(compiled, userId, userKey, objectId, objectKey, visited, currentRelation, options) { - if (compiled._optimized?.kind === 'direct' && !options.collectValues && !options.trackEvaluation) { - const direct = { - type: 'direct', - relation: compiled._optimized.relation, - reverse: compiled._optimized.reverse, - collectValues: false - }; - return this._evaluateDirect(direct, userId, userKey, objectId, objectKey, options, currentRelation); - } - if (compiled.source) { - return this.ruleEvaluator.evaluateRule( - userId, - userKey, - objectId, - objectKey, - compiled.source, - visited, - currentRelation, - { ...options, useCompiled: false } - ); - } - return { possibility: 0, reason: 'unsupported_multi_hop' }; - } - - _evaluateLogical(compiled, userId, userKey, objectId, objectKey, visited, currentRelation, options) { - if (compiled.source?._subjectAsObject) { objectId = userId; objectKey = userKey; } - let result; - switch (compiled.op) { - case 'union': - result = this._evaluateUnion(compiled, userId, userKey, objectId, objectKey, visited, currentRelation, options); - break; - case 'intersection': - result = this._evaluateIntersection(compiled, userId, userKey, objectId, objectKey, visited, currentRelation, options); - break; - case 'exclusion': - result = this._evaluateExclusion(compiled, userId, userKey, objectId, objectKey, visited, currentRelation, options); - break; - default: - result = { possibility: 0, reason: 'unknown_logical_op' }; - } - - if (compiled.negate) { - result = { - ...result, - possibility: Math.max(0, 1 - (result.possibility || 0)), - reason: result.possibility === 0 ? result.reason : 'negated' - }; - } - - return result; - } - - _evaluateUnion(compiled, userId, userKey, objectId, objectKey, visited, currentRelation, options) { - const { fastPath = false, minPossibility = null, valueContext = null, collectValues = false, includeMeta = true, trackEvaluation = false } = options; - const includeOwaTrace = trackEvaluation && includeMeta; - const owaTraceOptions = includeOwaTrace ? { includeTrace: true } : null; - - if (compiled.useBilattice && compiled.source) { - return this.ruleEvaluator.evaluateRule( - userId, - userKey, - objectId, - objectKey, - compiled.source, - visited, - currentRelation, - { ...options, useCompiled: false } - ); - } - - if (compiled._optimized?.kind === 'direct_list') { - return this._evaluateDirectList(compiled, userId, userKey, objectId, objectKey, visited, currentRelation, options, 'union'); - } - - const children = compiled.children || [compiled]; - const possibilities = []; - const reliabilities = []; - const metas = []; - const resChildrenValidity = []; - const remediationOptions = []; - const allCollectedValues = collectValues ? [] : null; - - for (const child of children) { - const childVisited = new Set(visited); - let res = this.evaluate(child, userId, userKey, objectId, objectKey, childVisited, currentRelation, options); - if (!res || typeof res.possibility !== 'number') { - res = { possibility: 0, meta: { reason: 'missing_rule' }, collectedValues: [] }; - } - if (res.validity) resChildrenValidity.push(res.validity); - - mergeRemediationOptions(remediationOptions, extractRemediation(res)); - - possibilities.push(res.possibility); - reliabilities.push(res.reliability !== undefined ? res.reliability : 1.0); - metas.push(includeMeta ? res.meta : null); - - if (collectValues && res.collectedValues && Array.isArray(res.collectedValues)) { - allCollectedValues.push(...res.collectedValues); - if (valueContext) { - valueContext.addCollectedValues(res.collectedValues, child.type, child); - } - } - - if (fastPath && minPossibility !== null && res.possibility >= minPossibility) { - const remediation = buildRemediation(extractRemediation(res)); - return { - possibility: res.possibility, - reliability: res.reliability !== undefined ? res.reliability : 1.0, - ...(collectValues && { collectedValues: allCollectedValues }), - ...(includeMeta && { meta: res.meta }), - ...(remediation ? { remediation } : {}) - }; - } - } - - if (!possibilities.length) { - return { - possibility: 0, - reliability: 1.0, - ...(collectValues && { collectedValues: allCollectedValues }), - ...(includeMeta && { meta: { operation: 'union', childCount: 0 } }) - }; - } - - const unionOWAWeights = compiled._precomputedWeights && compiled._precomputedWeights.length === possibilities.length - ? compiled._precomputedWeights - : getOWAWeightsFromRule(compiled, possibilities.length, metas); - let result; - if (compiled.reliabilityWeighting) { - const weighted = possibilities.map((poss, i) => poss * (reliabilities[i] ?? 1.0)); - result = OWAFusion.fuseWithMeta(weighted, metas, unionOWAWeights, compiled.aggregator || 'max', true, owaTraceOptions); - } else if (unionOWAWeights.some(w => w > 0)) { - result = OWAFusion.fuseWithMeta(possibilities, metas, unionOWAWeights, compiled.aggregator || 'max', true, owaTraceOptions); - } else { - result = OWAFusion.fuseWithMeta(possibilities, metas, null, 'max', true, owaTraceOptions); - } - - const remediation = result.value === 0 - ? buildRemediation(remediationOptions.length > 0 ? { status: 'required', options: remediationOptions } : null, { status: 'required' }) - : null; - let unionReliability = 1.0; - if (includeOwaTrace && result.trace && typeof result.trace.selectedIndex === 'number') { - unionReliability = reliabilities[result.trace.selectedIndex] ?? 1.0; - } else if ((compiled.aggregator || 'max') === 'min') { - const minP = Math.min(...possibilities); - unionReliability = reliabilities[possibilities.indexOf(minP)] ?? 1.0; - } else { - const maxP = Math.max(...possibilities); - unionReliability = reliabilities[possibilities.indexOf(maxP)] ?? 1.0; - } - const unionOperator = (compiled.aggregator || 'max') === 'max' && (!compiled.owaWeights || (compiled.owaWeights.length <= 1 || (compiled.owaWeights[0] === 1 && compiled.owaWeights.slice(1).every(w => w === 0)))) - ? 'max' : 'owa'; - const unionSourceLabels = (resChildrenValidity || []).map(v => v?.label).filter(Boolean); - const unionSources = []; - for (const v of resChildrenValidity || []) for (const s of v?.sources || []) if (!unionSources.includes(s)) unionSources.push(s); - return { - possibility: result.value, - reliability: unionReliability, - validity: buildValidity(unionOperator, unionSources, unionSourceLabels, possibilities.length, result.value), - ...(collectValues && { collectedValues: allCollectedValues }), - ...(includeMeta && { - meta: { - ...result.meta, - operation: 'union', - childCount: possibilities.length, - aggregator: compiled.aggregator || 'max', - ...(includeOwaTrace && result.trace ? { - owa: { - level: null, - aggregator: compiled.aggregator || 'max', - weights: result.trace.weights, - sortedValues: result.trace.sortedValues, - contributions: result.trace.contributions, - selectedIndex: result.trace.selectedIndex - } - } : {}), - ...(remediation ? { remediation } : {}) - } - }), - ...(remediation ? { remediation } : {}) - }; - } - - _evaluateIntersection(compiled, userId, userKey, objectId, objectKey, visited, currentRelation, options) { - const { fastPath = false, minPossibility = null, valueContext = null, collectValues = false, includeMeta = true, trackEvaluation = false } = options; - const includeOwaTrace = trackEvaluation && includeMeta; - const owaTraceOptions = includeOwaTrace ? { includeTrace: true } : null; - const effectiveObjectId = compiled.source?._subjectAsObject ? userId : objectId; - const effectiveObjectKey = compiled.source?._subjectAsObject ? userKey : objectKey; - if (compiled._optimized?.kind === 'direct_list') { - return this._evaluateDirectList(compiled, userId, userKey, effectiveObjectId, effectiveObjectKey, visited, currentRelation, options, 'intersection'); - } - const possibilities = []; - const reliabilities = []; - const metas = []; - const remediationOptions = []; - const allCollectedValues = collectValues ? [] : null; - - // A defeasible `when` component may compile to a single direct node - // (no children); treat it as a one-element conjunction, matching the - // union path's `compiled.children || [compiled]` fallback. - const children = compiled.children || [compiled]; - const resChildrenValidity = []; - - for (const child of children) { - const childVisited = new Set(visited); - const res = this.evaluate(child, userId, userKey, objectId, objectKey, childVisited, currentRelation, options); - if (res.validity) resChildrenValidity.push(res.validity); - possibilities.push(res.possibility); - reliabilities.push(res.reliability !== undefined ? res.reliability : 1.0); - metas.push(includeMeta ? res.meta : null); - - mergeRemediationOptions(remediationOptions, extractRemediation(res)); - - if (collectValues && res.collectedValues && Array.isArray(res.collectedValues)) { - allCollectedValues.push(...res.collectedValues); - if (valueContext) { - valueContext.addCollectedValues(res.collectedValues, child.type, child); - } - } - - if (fastPath && minPossibility !== null && res.possibility < minPossibility) { - return { - possibility: res.possibility, - reliability: res.reliability !== undefined ? res.reliability : 1.0, - ...(collectValues && { collectedValues: allCollectedValues }), - ...(includeMeta && { meta: res.meta }) - }; - } - } - - if (!possibilities.length) { - return { - possibility: 0, - reliability: 1.0, - ...(collectValues && { collectedValues: allCollectedValues }), - ...(includeMeta && { meta: { operation: 'intersection', childCount: 0 } }) - }; - } - - const intersectionOWAWeights = compiled._precomputedWeights && compiled._precomputedWeights.length === possibilities.length - ? compiled._precomputedWeights - : getOWAWeightsFromRule(compiled, possibilities.length, metas); - const defaultMode = compiled.aggregator || 'min'; - const finalWeights = compiled.aggregator || compiled.owaWeights ? intersectionOWAWeights : - [...Array(Math.max(0, possibilities.length - 1)).fill(0), 1]; - - let result; - if (compiled.reliabilityWeighting) { - const reliabilityWeightedPossibilities = possibilities.map((poss, i) => poss * (reliabilities[i] ?? 1.0)); - result = OWAFusion.fuseWithMeta(reliabilityWeightedPossibilities, metas, finalWeights, defaultMode, true, owaTraceOptions); - } else { - result = OWAFusion.fuseWithMeta(possibilities, metas, finalWeights, defaultMode, true, owaTraceOptions); - } - - const remediation = result.value === 0 - ? buildRemediation(remediationOptions.length > 0 ? { status: 'required', options: remediationOptions } : null, { status: 'required' }) - : null; - let intersectionReliability = 1.0; - if (includeOwaTrace && result.trace && typeof result.trace.selectedIndex === 'number') { - intersectionReliability = reliabilities[result.trace.selectedIndex] ?? 1.0; - } else if ((compiled.aggregator || 'min') === 'max') { - const maxP = Math.max(...possibilities); - intersectionReliability = reliabilities[possibilities.indexOf(maxP)] ?? 1.0; - } else { - const minP = Math.min(...possibilities); - intersectionReliability = reliabilities[possibilities.indexOf(minP)] ?? 1.0; - } - const interSourceLabels = (resChildrenValidity || []).map(v => v?.label).filter(Boolean); - const interSources = []; - for (const v of resChildrenValidity || []) for (const s of v?.sources || []) if (!interSources.includes(s)) interSources.push(s); - return { - possibility: result.value, - reliability: intersectionReliability, - validity: buildValidity('min', interSources, interSourceLabels, possibilities.length, result.value), - ...(collectValues && { collectedValues: allCollectedValues }), - ...(includeMeta && { - meta: { - ...result.meta, - operation: 'intersection', - childCount: possibilities.length, - aggregator: defaultMode, - ...(includeOwaTrace && result.trace ? { - owa: { - level: null, - aggregator: defaultMode, - weights: result.trace.weights, - sortedValues: result.trace.sortedValues, - contributions: result.trace.contributions, - selectedIndex: result.trace.selectedIndex - } - } : {}), - ...(remediation ? { remediation } : {}) - } - }), - ...(remediation ? { remediation } : {}) - }; - } - - _evaluateExclusion(compiled, userId, userKey, objectId, objectKey, visited, currentRelation, options) { - const { valueContext = null, collectValues = false, includeMeta = true, trackEvaluation = false } = options; - const includeOwaTrace = trackEvaluation && includeMeta; - const owaTraceOptions = includeOwaTrace ? { includeTrace: true } : null; - if (compiled.children.length !== 2) { - return { - possibility: 0, - ...(collectValues && { collectedValues: [] }), - ...(includeMeta && { meta: { operation: 'exclusion', error: 'exclusion_requires_exactly_two_rules' } }), - reason: 'exclusion_requires_exactly_two_rules' - }; - } - - const visitedA = new Set(visited); - const visitedB = new Set(visited); - const a = this.evaluate(compiled.children[0], userId, userKey, objectId, objectKey, visitedA, currentRelation, options); - const b = this.evaluate(compiled.children[1], userId, userKey, objectId, objectKey, visitedB, currentRelation, options); - - const allCollectedValues = collectValues ? [] : null; - if (collectValues && a.collectedValues && Array.isArray(a.collectedValues)) { - allCollectedValues.push(...a.collectedValues); - if (valueContext) valueContext.addCollectedValues(a.collectedValues, compiled.children[0].type, compiled.children[0]); - } - if (collectValues && b.collectedValues && Array.isArray(b.collectedValues)) { - allCollectedValues.push(...b.collectedValues); - if (valueContext) valueContext.addCollectedValues(b.collectedValues, compiled.children[1].type, compiled.children[1]); - } - - let possibility; - let result; - if (compiled.aggregator || compiled.owaWeights) { - const possibilities = [a.possibility, 1 - b.possibility]; - const metas = [a.meta, { exclusion_complement: b.meta }]; - const reliabilityWeights = [ - a.reliability !== undefined ? a.reliability : 1.0, - b.reliability !== undefined ? b.reliability : 1.0 - ]; - const exclusionOWAWeights = compiled._precomputedWeights && compiled._precomputedWeights.length === 2 - ? compiled._precomputedWeights - : getOWAWeightsFromRule(compiled, 2, metas); - const fusedInputs = compiled.reliabilityWeighting - ? possibilities.map((poss, i) => poss * (reliabilityWeights[i] ?? 1.0)) - : possibilities; - result = OWAFusion.fuseWithMeta(fusedInputs, metas, exclusionOWAWeights, compiled.aggregator || 'min', true, owaTraceOptions); - possibility = result.value; - } else { - possibility = a.possibility * (1 - b.possibility); - } - - return { - possibility, - // Both legs contribute to the decision, so their reliabilities - // multiply (consistent with TTU/chain path semantics). - reliability: (a.reliability !== undefined ? a.reliability : 1.0) * (b.reliability !== undefined ? b.reliability : 1.0), - validity: buildValidity('product', [a.validity?.sources?.[0], b.validity?.sources?.[0]].filter(Boolean), [a.validity?.label, b.validity?.label].filter(Boolean), 2, possibility), - ...(collectValues && { collectedValues: allCollectedValues }), - ...(includeMeta && { - meta: { - operation: 'exclusion', - childA: a.meta, - childB: b.meta, - aggregator: compiled.aggregator || 'standard', - ...(includeOwaTrace && result?.trace ? { - owa: { - level: null, - aggregator: compiled.aggregator || 'standard', - weights: result.trace.weights, - sortedValues: result.trace.sortedValues, - contributions: result.trace.contributions, - selectedIndex: result.trace.selectedIndex - } - } : {}) - } - }) - }; - } - - _evaluateDirectList(compiled, userId, userKey, objectId, objectKey, visited, currentRelation, options, op) { - const { fastPath = false, minPossibility = null, valueContext = null, collectValues = false, includeMeta = true, trackEvaluation = false } = options; - const includeOwaTrace = trackEvaluation && includeMeta; - const owaTraceOptions = includeOwaTrace ? { includeTrace: true } : null; - - const possibilities = []; - const reliabilities = []; - const metas = []; - const listChildrenValidity = []; - const allCollectedValues = collectValues ? [] : null; - const directRules = compiled._optimized.direct; - - for (const direct of directRules) { - let directRel; - if (direct.reverse) { - directRel = this.arbiter.relationManager.getDirectRelation(objectId, direct.relation, userId, options); - } else { - directRel = this.arbiter.relationManager.getDirectRelation(userId, direct.relation, objectId, options); - } - - const possibility = directRel ? directRel.possibility : 0; - possibilities.push(possibility); - reliabilities.push(directRel ? (directRel.reliability !== undefined ? directRel.reliability : 1.0) : 1.0); - if (directRel) { - listChildrenValidity.push(buildValidity('identity', [direct.relation], [normalizeValidity(directRel.validity !== undefined ? directRel.validity : null)], 1, possibility)); - } - if (includeMeta) { - const _src = directRel ? (directRel.source || 'persistent') : null; - const _allowMeta = directRel ? { - ruleType: 'direct', - reason: 'direct', - source: _src, - layer_name: directRel.layer_name || null, - source_class: directRel.source_class || null, - reducer_applied: directRel.reducer_applied || null - } : null; - metas.push(directRel ? { - ruleType: 'direct', - relation: direct.relation, - reverse: direct.reverse || false, - strength: possibility, - source: _src, - allow: _allowMeta - } : { ruleType: 'direct', relation: direct.relation, reason: 'no_relation' }); - } else { - metas.push(null); - } - - if (collectValues && direct.collectValues !== false && directRel && directRel.value !== undefined) { - const sourceEntity = direct.reverse ? objectKey : userKey; - const targetEntity = direct.reverse ? userKey : objectKey; - const path = [sourceEntity, targetEntity]; - const collected = this.ruleEvaluator.ruleHandlers.direct._createCollectedValue( - directRel.value, - directRel.possibility, - path, - { - entityKey: sourceEntity, - relation: direct.relation, - step: 0 - }, - { - timestamp: directRel.changed_last_at || directRel.updated_last_at || Date.now(), - reliability: 1.0, - source: directRel.source || 'persistent' - } - ); - allCollectedValues.push(collected); - if (valueContext) { - valueContext.addCollectedValues([collected], 'direct', direct); - } - } - - if (fastPath && minPossibility !== null) { - if (op === 'union' && possibility >= minPossibility) { - return { - possibility, - reliability: directRel ? (directRel.reliability !== undefined ? directRel.reliability : 1.0) : 1.0, - ...(collectValues && { collectedValues: allCollectedValues }), - ...(includeMeta && { meta: metas[metas.length - 1] }) - }; - } - if (op === 'intersection' && possibility < minPossibility) { - return { - possibility, - reliability: directRel ? (directRel.reliability !== undefined ? directRel.reliability : 1.0) : 1.0, - ...(collectValues && { collectedValues: allCollectedValues }), - ...(includeMeta && { meta: metas[metas.length - 1] }) - }; - } - } - } - - if (!possibilities.length) { - return { - possibility: 0, - reliability: 1.0, - ...(collectValues && { collectedValues: allCollectedValues }), - ...(includeMeta && { meta: { operation: op, childCount: 0 } }) - }; - } - - const weights = compiled._precomputedWeights && compiled._precomputedWeights.length === possibilities.length - ? compiled._precomputedWeights - : getOWAWeightsFromRule(compiled, possibilities.length, metas); - const aggregator = op === 'intersection' ? (compiled.aggregator || 'min') : (compiled.aggregator || 'max'); - const fusedInputs = compiled.reliabilityWeighting - ? possibilities.map((poss, i) => poss * (reliabilities[i] ?? 1.0)) - : possibilities; - const result = OWAFusion.fuseWithMeta(fusedInputs, metas, weights, aggregator, true, owaTraceOptions); - - let listReliability = 1.0; - if (includeOwaTrace && result.trace && typeof result.trace.selectedIndex === 'number') { - listReliability = reliabilities[result.trace.selectedIndex] ?? 1.0; - } else if (aggregator === 'min') { - const minP = Math.min(...possibilities); - listReliability = reliabilities[possibilities.indexOf(minP)] ?? 1.0; - } else { - const maxP = Math.max(...possibilities); - listReliability = reliabilities[possibilities.indexOf(maxP)] ?? 1.0; - } - - const listOperator = aggregator === 'min' ? 'min' : (aggregator === 'max' ? 'max' : 'owa'); - const listLabels = listChildrenValidity.map(v => v.label).filter(Boolean); - const listSources = []; - for (const v of listChildrenValidity) for (const s of v.sources || []) if (!listSources.includes(s)) listSources.push(s); - return { - possibility: result.value, - reliability: listReliability, - validity: buildValidity(listOperator, listSources, listLabels, possibilities.length, result.value), - ...(collectValues && { collectedValues: allCollectedValues }), - ...(includeMeta && { - meta: { - ...result.meta, - operation: op, - childCount: possibilities.length, - aggregator, - ...(includeOwaTrace && result.trace ? { - owa: { - level: null, - aggregator, - weights: result.trace.weights, - sortedValues: result.trace.sortedValues, - contributions: result.trace.contributions, - selectedIndex: result.trace.selectedIndex - } - } : {}) - } - }) - }; - } - - _evaluateDefeasible(compiled, userId, userKey, objectId, objectKey, visited, currentRelation, options) { - const { binary = false, fastPath = false, minPossibility = 0.0 } = options; - const mode = compiled.mode || (binary ? 'binary' : (fastPath ? 'threshold' : 'normal')); - const allCollectedValues = []; - let neverResult = null, defeatersResult = null, strictResult = null, defeasibleResult = null, requiresResult = null; - - if (compiled.never) { - neverResult = this._evaluateUnion(compiled.never, userId, userKey, objectId, objectKey, visited, currentRelation, options); - if (neverResult.collectedValues) allCollectedValues.push(...neverResult.collectedValues); - } - if (compiled.always) { - strictResult = this.evaluate(compiled.always, userId, userKey, objectId, objectKey, visited, currentRelation, options); - if (strictResult.collectedValues) allCollectedValues.push(...strictResult.collectedValues); - } - if (compiled.requires) { - requiresResult = this._evaluateUnion(compiled.requires, userId, userKey, objectId, objectKey, visited, currentRelation, options); - if (requiresResult.collectedValues) allCollectedValues.push(...requiresResult.collectedValues); - } - if (compiled.unless) { - defeatersResult = this._evaluateUnion(compiled.unless, userId, userKey, objectId, objectKey, visited, currentRelation, options); - if (defeatersResult.collectedValues) allCollectedValues.push(...defeatersResult.collectedValues); - } - if (compiled.when) { - defeasibleResult = this._evaluateIntersection(compiled.when, userId, userKey, objectId, objectKey, visited, currentRelation, options); - if (defeasibleResult.collectedValues) allCollectedValues.push(...defeasibleResult.collectedValues); - } - - const ruleMeta = { - minPossibility: compiled.minPossibility, - priority: compiled.priority, - mode - }; - - const finalResult = this.logicalOperators._applyDefeasibleLogic(mode, ruleMeta, neverResult, defeatersResult, strictResult, defeasibleResult, requiresResult, allCollectedValues); - return finalResult; - } -} diff --git a/src/authorization/RuleEvaluator.js b/src/authorization/RuleEvaluator.js index 5a66dc9..b2153b7 100644 --- a/src/authorization/RuleEvaluator.js +++ b/src/authorization/RuleEvaluator.js @@ -8,7 +8,6 @@ import { RelationalComparatorRouter } from './rules/RelationalComparatorRouter.j import { ChainRule } from './rules/ChainRule.js'; import { ChallengeRule } from './rules/ChallengeRule.js'; import { ValueContext } from './ValueContext.js'; -import { CompiledEvaluator } from './CompiledEvaluator.js'; export class RuleEvaluator { constructor(arbiter) { @@ -26,7 +25,6 @@ export class RuleEvaluator { }; this.logicalOperators = new LogicalOperators(arbiter, this); - this.compiledEvaluator = new CompiledEvaluator(arbiter, this, this.logicalOperators); this._valueRequirementCache = new Map(); } @@ -70,15 +68,6 @@ export class RuleEvaluator { return this._evaluateRuleBinary(numericUserId, userKey, numericObjectId, objectKey, rule, visited, currentRelation, enhancedOptions); } - if (rule && rule._compiled && options.useCompiled !== false && !(rule._compileErrors && rule._compileErrors.length)) { - const result = this.compiledEvaluator.evaluate(rule._compiled, userId, userKey, objectId, objectKey, visited, currentRelation, enhancedOptions); - if (finalValueContext && result.collectedValues && result.collectedValues.length > 0) { - const ruleType = rule?.type || (rule?.union ? 'union' : rule?.intersection ? 'intersection' : rule?.exclusion ? 'exclusion' : 'rule'); - finalValueContext.addCollectedValues(result.collectedValues, ruleType, rule); - } - return this._maybeCacheRuleResult(result, currentRelation, ruleCacheKey, canCacheRuleResult); - } - if (rule.union) { const result = this.logicalOperators.evaluateUnion(numericUserId, userKey, numericObjectId, objectKey, rule, visited, currentRelation, enhancedOptions); return this._maybeCacheRuleResult(result, currentRelation, ruleCacheKey, canCacheRuleResult); @@ -94,6 +83,17 @@ export class RuleEvaluator { return this._maybeCacheRuleResult(result, currentRelation, ruleCacheKey, canCacheRuleResult); } + // Defeasible configs ({ type: 'defeasible', when/unless/never/always }) + // have no rule-handler entry; route them to the defeasible evaluator + // with the components normalized to the union/intersection format. + if (this.logicalOperators._isDefeasibleLogic(rule)) { + const result = this.logicalOperators.evaluateDefeasible( + numericUserId, userKey, numericObjectId, objectKey, + wrapDefeasibleComponents(rule), visited, currentRelation, enhancedOptions + ); + return this._maybeCacheRuleResult(result, currentRelation, ruleCacheKey, canCacheRuleResult); + } + // For other rule types, evaluate normally first // Shorthand operand objects ({ relation: 'owner' } inside logical rules, // or caller-supplied raw configs) carry no type: treat them as direct @@ -292,17 +292,17 @@ export class RuleEvaluator { // defeasible config). Route them through the defeasible evaluator, // which implements binary/threshold modes natively. if (this.logicalOperators._isDefeasibleLogic(rule)) { - if (rule._compiled && !(rule._compileErrors && rule._compileErrors.length)) { - return this.compiledEvaluator.evaluate(rule._compiled, userId, userKey, objectId, objectKey, visited, currentRelation, { - ...enhancedOptions, - binary: true, - fastPath: true - }); - } - return this.logicalOperators.evaluateDefeasible(userId, userKey, objectId, objectKey, wrapDefeasibleComponents(rule), visited, currentRelation, { + // The compiled path ran defeasible rules in normal mode regardless of + // the binary flag (compiled.mode defaulted to 'normal'), so the binary + // decision was always the thresholded normal combination. Preserve + // that contract: force normal mode; the checker applies the binary + // thresholds to the result. + const wrapped = wrapDefeasibleComponents(rule); + wrapped.mode = 'normal'; + return this.logicalOperators.evaluateDefeasible(userId, userKey, objectId, objectKey, wrapped, visited, currentRelation, { ...enhancedOptions, - binary: true, - fastPath: true + binary: false, + fastPath: false }); } @@ -433,18 +433,19 @@ export class RuleEvaluator { * Logical (union/intersection/exclusion) components pass through. */ function wrapDefeasibleComponents(rule) { - const wrapSingle = (component) => ({ rules: [component] }); + // The defeasible evaluator's normalize expects the component format + // when.intersection.rules[] / unless.union.rules[] (not bare {rules}). const wrapUnion = (component) => { if (!component) return component; - if (Array.isArray(component)) return { rules: component }; - if (component.union || component.intersection || component.exclusion) return component; - return wrapSingle(component); + if (component.union) return component; + if (Array.isArray(component)) return { union: { rules: component } }; + return { union: { rules: [component] } }; }; const wrapIntersection = (component) => { if (!component) return component; - if (Array.isArray(component)) return { rules: component }; - if (component.union || component.intersection || component.exclusion) return component; - return wrapSingle(component); + if (component.intersection) return component; + if (Array.isArray(component)) return { intersection: { rules: component } }; + return { intersection: { rules: [component] } }; }; return { ...rule, diff --git a/src/authorization/rules/LogicalOperators.js b/src/authorization/rules/LogicalOperators.js index 5ad3032..de22b5b 100644 --- a/src/authorization/rules/LogicalOperators.js +++ b/src/authorization/rules/LogicalOperators.js @@ -888,6 +888,7 @@ export class LogicalOperators extends BaseRule { const childRules = unionConfig.rules || []; let possibilities = [], reliabilities = [], metas = [], reasons = []; + const resChildrenValidity = []; const remediationOptions = []; const { fastPath = false, minPossibility = null, valueContext = null, collectValues = false, includeMeta = true, trackEvaluation = false } = options; const allCollectedValues = collectValues ? [] : null; // Track collected values from all child rules @@ -901,7 +902,7 @@ export class LogicalOperators extends BaseRule { if (!res || typeof res.possibility !== 'number') { res = { possibility: 0, meta: { reason: 'missing_rule' }, collectedValues: [] }; } - + if (res.validity) resChildrenValidity.push(res.validity); if (res.reason === 'cycle') reasons.push('cycle'); mergeRemediationOptions(remediationOptions, extractRemediation(res)); @@ -1034,9 +1035,14 @@ export class LogicalOperators extends BaseRule { const maxP = Math.max(...possibilities); unionReliability = reliabilities[possibilities.indexOf(maxP)] ?? 1.0; } + const unionOperator = (unionConfig.aggregator || 'max') === 'max' && (!unionConfig.owaWeights || (unionConfig.owaWeights.length <= 1 || (unionConfig.owaWeights[0] === 1 && unionConfig.owaWeights.slice(1).every(w => w === 0)))) + ? 'max' : 'owa'; + const unionSources = []; + for (const v of resChildrenValidity) for (const s of v.sources || []) if (!unionSources.includes(s)) unionSources.push(s); return { possibility: result.value, reliability: unionReliability, + validity: buildValidity(unionOperator, unionSources, resChildrenValidity.map(v => v.label).filter(Boolean), possibilities.length, result.value), ...(collectValues && { collectedValues: allCollectedValues }), // Include all collected values from child rules ...(includeMeta && { meta: { @@ -1073,6 +1079,7 @@ export class LogicalOperators extends BaseRule { const childRules = intersectionConfig.rules || []; let possibilities = [], reliabilities = [], metas = [], reasons = []; + const resChildrenValidity = []; const remediationOptions = []; const { fastPath = false, minPossibility = null, valueContext = null, collectValues = false, includeMeta = true, trackEvaluation = false } = options; const allCollectedValues = collectValues ? [] : null; // Track collected values from all child rules @@ -1090,6 +1097,7 @@ export class LogicalOperators extends BaseRule { for (const child of childRules) { const res = this.ruleEvaluator.evaluateRule(userId, userKey, objectId, objectKey, child, visited, currentRelation, options); + if (res.validity) resChildrenValidity.push(res.validity); if (res.reason === 'cycle') reasons.push('cycle'); possibilities.push(res.possibility); reliabilities.push(res.reliability !== undefined ? res.reliability : 1.0); @@ -1174,9 +1182,12 @@ export class LogicalOperators extends BaseRule { const minP = Math.min(...possibilities); intersectionReliability = reliabilities[possibilities.indexOf(minP)] ?? 1.0; } + const interSources = []; + for (const v of resChildrenValidity) for (const s of v.sources || []) if (!interSources.includes(s)) interSources.push(s); return { possibility: result.value, reliability: intersectionReliability, + validity: buildValidity('min', interSources, resChildrenValidity.map(v => v.label).filter(Boolean), possibilities.length, result.value), ...(collectValues && { collectedValues: allCollectedValues }), // Include all collected values from child rules ...(includeMeta && { meta: { @@ -1305,6 +1316,7 @@ export class LogicalOperators extends BaseRule { // Both legs contribute to the decision, so their reliabilities // multiply (consistent with TTU/chain path semantics). reliability: (a.reliability !== undefined ? a.reliability : 1.0) * (b.reliability !== undefined ? b.reliability : 1.0), + validity: buildValidity('product', [a.validity?.sources?.[0], b.validity?.sources?.[0]].filter(Boolean), [a.validity?.label, b.validity?.label].filter(Boolean), 2, possibility), // In binary mode the negated child's strength is the deny side of the // dual-threshold contract: deny fires when P(B) >= maxDenyPossibility. ...(options.binary && { possibility_deny: b.possibility ?? 0 }),