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,167 @@
|
||||
/**
|
||||
* Predicate Resolver
|
||||
*
|
||||
* Maps predicate names to existing compiled DSL rules across all graph scopes.
|
||||
* Does NOT create new rules - only looks up existing ones.
|
||||
*/
|
||||
|
||||
export class PredicateResolver {
|
||||
constructor(context) {
|
||||
this.context = context;
|
||||
this.graphStores = context.graphStores || {};
|
||||
this.cache = new Map();
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a predicate name to its DSL rule
|
||||
*
|
||||
* @param {string} predicateName - Name of the predicate
|
||||
* @returns {Object|null} Rule info or null if not found
|
||||
*/
|
||||
resolve(predicateName) {
|
||||
// Check cache first
|
||||
if (this.cache.has(predicateName)) {
|
||||
return this.cache.get(predicateName);
|
||||
}
|
||||
|
||||
// Look up in all graph scopes
|
||||
const rule = this.findRule(predicateName);
|
||||
|
||||
if (rule) {
|
||||
this.cache.set(predicateName, rule);
|
||||
}
|
||||
|
||||
return rule;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a rule across all graph scopes
|
||||
* Prefers logical rules over direct rules for evidence predicates
|
||||
*/
|
||||
findRule(predicateName) {
|
||||
const scopes = [
|
||||
'tenantExternal',
|
||||
'tenantInternal',
|
||||
'rootExternal',
|
||||
'rootInternal',
|
||||
'masterExternal',
|
||||
'masterInternal'
|
||||
];
|
||||
|
||||
let directRule = null;
|
||||
let directScope = null;
|
||||
|
||||
for (const scopeName of scopes) {
|
||||
const graphStore = this.graphStores[scopeName];
|
||||
if (!graphStore) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Handle both: Arbiter directly (has .check()) or wrapper with .arbiter
|
||||
const arbiter = graphStore.arbiter || graphStore;
|
||||
const config = arbiter.relationConfigs?.get(predicateName);
|
||||
|
||||
if (config) {
|
||||
// Prefer logical rules (intersection/union) over direct rules
|
||||
// This ensures evidence rules work correctly across all scopes
|
||||
if (config.type === 'intersection' || config.type === 'union' || config.type === 'logical') {
|
||||
return {
|
||||
name: predicateName,
|
||||
scope: scopeName,
|
||||
config: config,
|
||||
arity: this.inferArity(config)
|
||||
};
|
||||
}
|
||||
|
||||
// Remember the first direct rule as fallback
|
||||
if (!directRule && config.type === 'direct') {
|
||||
directRule = config;
|
||||
directScope = scopeName;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Return direct rule if no logical rule found
|
||||
if (directRule) {
|
||||
return {
|
||||
name: predicateName,
|
||||
scope: directScope,
|
||||
config: directRule,
|
||||
arity: this.inferArity(directRule)
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Infer the arity (parameter count) from rule config
|
||||
*/
|
||||
inferArity(config) {
|
||||
// Most DSL evidence rules have 1 or 2 parameters:
|
||||
// - 1 param: just the subject (user)
|
||||
// - 2 params: subject (user) + object
|
||||
|
||||
if (config.arity) {
|
||||
return config.arity;
|
||||
}
|
||||
|
||||
// Default to checking if it's a relation that typically needs an object
|
||||
// This is a heuristic - in practice, the DSL defines this explicitly
|
||||
if (config.type === 'tuple_to_userset' || config.type === 'direct') {
|
||||
return 2; // Likely needs subject + object
|
||||
}
|
||||
|
||||
return 1; // Default to 1 param
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a predicate exists without full resolution
|
||||
*/
|
||||
exists(predicateName) {
|
||||
return this.resolve(predicateName) !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all available predicates across all scopes
|
||||
*/
|
||||
getAllPredicates() {
|
||||
const predicates = [];
|
||||
const scopes = [
|
||||
'tenantExternal',
|
||||
'tenantInternal',
|
||||
'rootExternal',
|
||||
'rootInternal',
|
||||
'masterExternal',
|
||||
'masterInternal'
|
||||
];
|
||||
|
||||
for (const scopeName of scopes) {
|
||||
const graphStore = this.graphStores[scopeName];
|
||||
if (!graphStore) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Handle both: Arbiter directly (has .check()) or wrapper with .arbiter
|
||||
const arbiter = graphStore.arbiter || graphStore;
|
||||
if (arbiter.relationConfigs) {
|
||||
for (const [name, config] of arbiter.relationConfigs) {
|
||||
predicates.push({
|
||||
name,
|
||||
scope: scopeName,
|
||||
arity: this.inferArity(config)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return predicates;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the cache (useful for testing or when rules change)
|
||||
*/
|
||||
clearCache() {
|
||||
this.cache.clear();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user