UNPKG

@taukala/xs-ctrl

Version:

A flexible and powerful access control library for JavaScript applications with dynamic validation support

96 lines (83 loc) 3.18 kB
/** * Validates user claims against a set of access rules using OR/AND logic. * Supports both static claim validation and dynamic resource-based validation. * * The validation follows these rules: * 1. Access rules are organized in groups (outer array) * 2. Each group can contain: * - Only static conditions * - Only dynamic conditions * - Both static and dynamic conditions * 3. Groups are combined with OR logic (user needs to match any group) * 4. Conditions within a group use AND logic (user needs to match all conditions) * 5. Empty access rules array means no restrictions (returns true) * * Example: * Empty rules means no restrictions: * validateClaim([], userClaims) // Returns true * * @param {Array<Object>} accessRules - Array of rule groups * @param {Object} userClaims - Object containing user's claims * @param {Object} [context] - Context object passed to dynamic validators * * @throws {Error} If accessRules is not an array * @throws {Error} If user's claims don't match any rule group * @returns {Promise<boolean>} Returns true if validation passes */ export async function validateClaim(accessRules, userClaims, context = {}) { if (!Array.isArray(accessRules)) { throw new Error('Access rules must be an array of condition groups.'); } // Empty rules means no restrictions if (accessRules.length === 0) { return true; } const validateStaticConditions = (conditions) => { return conditions.every(([key, validValues]) => { const userValues = userClaims[key]; if (!userValues) return false; return userValues.some((value) => validValues.includes(value)); }); }; const validateDynamicConditions = async (validators) => { const results = await Promise.all( validators.map(validator => validator(context)) ); return results.every(Boolean); }; const validateRuleGroup = async (ruleGroup) => { const hasStaticConditions = ruleGroup.conditions?.length > 0; const hasDynamicValidators = ruleGroup.dynamicValidators?.length > 0; // No conditions - pass through if (!hasStaticConditions && !hasDynamicValidators) { return true; } // Static only if (hasStaticConditions && !hasDynamicValidators) { return validateStaticConditions(ruleGroup.conditions); } // Dynamic only if (!hasStaticConditions && hasDynamicValidators) { return await validateDynamicConditions(ruleGroup.dynamicValidators); } // Mixed - both static and dynamic return validateStaticConditions(ruleGroup.conditions) && await validateDynamicConditions(ruleGroup.dynamicValidators); }; // Use reduce to process rules sequentially const isAuthorized = await accessRules.reduce(async (promiseAcc, ruleGroup) => { // Wait for the previous promise to resolve const acc = await promiseAcc; // If we already found a valid rule, short circuit if (acc) return true; try { return await validateRuleGroup(ruleGroup); } catch (error) { return false; } }, Promise.resolve(false)); if (!isAuthorized) { throw new Error('Forbidden: Access denied.'); } return true; }