@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
JavaScript
/**
* 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;
}