@noony-serverless/core
Version:
A Middy base framework compatible with Firebase and GCP Cloud Functions with TypeScript
451 lines • 17.3 kB
JavaScript
"use strict";
/**
* Expression Permission Resolver
*
* Advanced permission resolver supporting complex boolean expressions with
* AND, OR, and NOT operations. Limited to 2-level nesting to prevent
* performance degradation while maintaining flexibility for complex
* authorization scenarios.
*
* Supported Expression Structure:
* - Leaf permissions: { permission: "admin.users" }
* - AND operations: { and: [expr1, expr2, ...] }
* - OR operations: { or: [expr1, expr2, ...] }
* - NOT operations: { not: expr }
* - Maximum 2-level nesting depth
*
* Performance Features:
* - Result caching for expensive expression evaluations
* - Short-circuit evaluation (AND stops at first false, OR stops at first true)
* - Expression normalization for consistent cache keys
* - Performance metrics and monitoring
*
* Example Expressions:
* ```
* // Simple OR: user needs admin OR manager role
* { or: [{ permission: "admin.platform" }, { permission: "role.manager" }] }
*
* // Complex AND/OR: (admin OR (manager AND finance))
* {
* or: [
* { permission: "admin.platform" },
* { and: [{ permission: "role.manager" }, { permission: "department.finance" }] }
* ]
* }
* ```
*
* @author Noony Framework Team
* @version 1.0.0
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.ExpressionPermissionResolver = void 0;
const PermissionResolver_1 = require("./PermissionResolver");
const CacheAdapter_1 = require("../cache/CacheAdapter");
/**
* Expression permission resolver for complex boolean logic
*/
class ExpressionPermissionResolver extends PermissionResolver_1.PermissionResolver {
cache;
maxNestingDepth = 2;
// Performance tracking
checkCount = 0;
totalResolutionTimeUs = 0;
cacheHits = 0;
cacheMisses = 0;
expressionComplexityStats = {
simple: 0, // Single permission checks
moderate: 0, // 1-level nesting
complex: 0, // 2-level nesting
};
constructor(cache) {
super();
this.cache = cache;
}
/**
* Check if user permissions satisfy the permission expression
*
* @param userPermissions - Set of user's permissions for O(1) lookup
* @param expression - Permission expression to evaluate
* @returns Promise resolving to true if expression evaluates to true
*/
async check(userPermissions, expression) {
const startTime = process.hrtime.bigint();
try {
// Validate expression structure
if (!PermissionResolver_1.PermissionUtils.isValidExpression(expression)) {
throw new Error('Invalid permission expression structure');
}
// Generate cache key for this evaluation
const userPermissionArray = Array.from(userPermissions).sort();
const cacheKey = CacheAdapter_1.CacheKeyBuilder.expressionResult(expression, userPermissionArray);
// Check cache first
const cachedResult = await this.cache.get(cacheKey);
if (cachedResult !== null) {
this.cacheHits++;
return cachedResult;
}
this.cacheMisses++;
// Evaluate expression
const result = this.evaluateExpression(userPermissions, expression, 0);
// Track complexity
this.trackComplexity(expression);
// Cache result for 1 minute (configurable)
await this.cache.set(cacheKey, result, 60 * 1000);
return result;
}
finally {
// Track performance metrics
const endTime = process.hrtime.bigint();
const resolutionTimeUs = Number(endTime - startTime) / 1000;
this.checkCount++;
this.totalResolutionTimeUs += resolutionTimeUs;
}
}
/**
* Check permissions with detailed result information
*/
async checkWithResult(userPermissions, expression) {
const startTime = process.hrtime.bigint();
let cached = false;
try {
// Validate inputs
if (!userPermissions || userPermissions.size === 0) {
return {
allowed: false,
resolverType: this.getType(),
resolutionTimeUs: 0,
cached: false,
reason: 'User has no permissions',
};
}
if (!expression) {
return {
allowed: false,
resolverType: this.getType(),
resolutionTimeUs: 0,
cached: false,
reason: 'No expression provided',
};
}
// Validate expression structure
if (!PermissionResolver_1.PermissionUtils.isValidExpression(expression)) {
return {
allowed: false,
resolverType: this.getType(),
resolutionTimeUs: 0,
cached: false,
reason: 'Invalid expression structure',
};
}
// Check cache
const userPermissionArray = Array.from(userPermissions).sort();
const cacheKey = CacheAdapter_1.CacheKeyBuilder.expressionResult(expression, userPermissionArray);
const cachedResult = await this.cache.get(cacheKey);
if (cachedResult !== null) {
cached = true;
const endTime = process.hrtime.bigint();
const resolutionTimeUs = Number(endTime - startTime) / 1000;
return {
allowed: cachedResult,
resolverType: this.getType(),
resolutionTimeUs,
cached: true,
reason: cachedResult ? undefined : 'Expression evaluation failed',
};
}
// Evaluate with detailed result
const evaluationResult = this.evaluateExpressionWithDetails(userPermissions, expression, 0);
// Cache result
await this.cache.set(cacheKey, evaluationResult.result, 60 * 1000);
const endTime = process.hrtime.bigint();
const resolutionTimeUs = Number(endTime - startTime) / 1000;
return {
allowed: evaluationResult.result,
resolverType: this.getType(),
resolutionTimeUs,
cached: false,
reason: evaluationResult.result
? undefined
: 'Expression requirements not met',
matchedPermissions: evaluationResult.result
? evaluationResult.evaluatedPaths
: undefined,
};
}
catch (error) {
const endTime = process.hrtime.bigint();
const resolutionTimeUs = Number(endTime - startTime) / 1000;
return {
allowed: false,
resolverType: this.getType(),
resolutionTimeUs,
cached,
reason: error instanceof Error ? error.message : 'Unknown evaluation error',
};
}
}
/**
* Evaluate permission expression recursively
*
* @param userPermissions - User's permissions as Set for O(1) lookup
* @param expression - Expression to evaluate
* @param depth - Current nesting depth
* @returns Boolean result of expression evaluation
*/
evaluateExpression(userPermissions, expression, depth) {
// Enforce maximum nesting depth
if (depth > this.maxNestingDepth) {
throw new Error(`Expression nesting depth exceeds maximum of ${this.maxNestingDepth}`);
}
// Leaf node: direct permission check
if (expression.permission) {
return userPermissions.has(expression.permission);
}
// AND operation: all sub-expressions must be true
if (expression.and && Array.isArray(expression.and)) {
for (const subExpression of expression.and) {
if (!this.evaluateExpression(userPermissions, subExpression, depth + 1)) {
return false; // Short-circuit: AND fails on first false
}
}
return true;
}
// OR operation: at least one sub-expression must be true
if (expression.or && Array.isArray(expression.or)) {
for (const subExpression of expression.or) {
if (this.evaluateExpression(userPermissions, subExpression, depth + 1)) {
return true; // Short-circuit: OR succeeds on first true
}
}
return false;
}
// NOT operation: sub-expression must be false
if (expression.not) {
return !this.evaluateExpression(userPermissions, expression.not, depth + 1);
}
// Invalid expression structure
throw new Error('Invalid expression: must have exactly one operation (permission, and, or, not)');
}
/**
* Evaluate expression with detailed result information
*/
evaluateExpressionWithDetails(userPermissions, expression, depth) {
const startTime = process.hrtime.bigint();
const evaluatedPaths = [];
let shortCircuited = false;
// Enforce maximum nesting depth
if (depth > this.maxNestingDepth) {
throw new Error(`Expression nesting depth exceeds maximum of ${this.maxNestingDepth}`);
}
// Leaf node: direct permission check
if (expression.permission) {
const hasPermission = userPermissions.has(expression.permission);
evaluatedPaths.push(`permission:${expression.permission}=${hasPermission}`);
const endTime = process.hrtime.bigint();
return {
result: hasPermission,
evaluatedPaths,
shortCircuited: false,
evaluationTimeUs: Number(endTime - startTime) / 1000,
};
}
// AND operation: all sub-expressions must be true
if (expression.and && Array.isArray(expression.and)) {
for (let i = 0; i < expression.and.length; i++) {
const subResult = this.evaluateExpressionWithDetails(userPermissions, expression.and[i], depth + 1);
evaluatedPaths.push(`and[${i}]=${subResult.result}`);
if (!subResult.result) {
shortCircuited = true;
const endTime = process.hrtime.bigint();
return {
result: false,
evaluatedPaths,
shortCircuited,
evaluationTimeUs: Number(endTime - startTime) / 1000,
};
}
}
const endTime = process.hrtime.bigint();
return {
result: true,
evaluatedPaths,
shortCircuited: false,
evaluationTimeUs: Number(endTime - startTime) / 1000,
};
}
// OR operation: at least one sub-expression must be true
if (expression.or && Array.isArray(expression.or)) {
for (let i = 0; i < expression.or.length; i++) {
const subResult = this.evaluateExpressionWithDetails(userPermissions, expression.or[i], depth + 1);
evaluatedPaths.push(`or[${i}]=${subResult.result}`);
if (subResult.result) {
shortCircuited = true;
const endTime = process.hrtime.bigint();
return {
result: true,
evaluatedPaths,
shortCircuited,
evaluationTimeUs: Number(endTime - startTime) / 1000,
};
}
}
const endTime = process.hrtime.bigint();
return {
result: false,
evaluatedPaths,
shortCircuited: false,
evaluationTimeUs: Number(endTime - startTime) / 1000,
};
}
// NOT operation: sub-expression must be false
if (expression.not) {
const subResult = this.evaluateExpressionWithDetails(userPermissions, expression.not, depth + 1);
evaluatedPaths.push(`not=${!subResult.result}`);
const endTime = process.hrtime.bigint();
return {
result: !subResult.result,
evaluatedPaths,
shortCircuited: false,
evaluationTimeUs: Number(endTime - startTime) / 1000,
};
}
throw new Error('Invalid expression: must have exactly one operation');
}
/**
* Track expression complexity for analytics
*/
trackComplexity(expression) {
const depth = this.getExpressionDepth(expression);
if (depth === 0) {
this.expressionComplexityStats.simple++;
}
else if (depth === 1) {
this.expressionComplexityStats.moderate++;
}
else {
this.expressionComplexityStats.complex++;
}
}
/**
* Get the depth of an expression
*/
getExpressionDepth(expression) {
if (expression.permission) {
return 0; // Leaf node
}
let maxDepth = 0;
if (expression.and) {
for (const subExpr of expression.and) {
maxDepth = Math.max(maxDepth, this.getExpressionDepth(subExpr) + 1);
}
}
if (expression.or) {
for (const subExpr of expression.or) {
maxDepth = Math.max(maxDepth, this.getExpressionDepth(subExpr) + 1);
}
}
if (expression.not) {
maxDepth = Math.max(maxDepth, this.getExpressionDepth(expression.not) + 1);
}
return maxDepth;
}
/**
* Get resolver type for identification
*/
getType() {
return PermissionResolver_1.PermissionResolverType.EXPRESSION;
}
/**
* Get performance characteristics for monitoring
*/
getPerformanceCharacteristics() {
return {
timeComplexity: 'O(n) with caching, bounded by expression complexity',
memoryUsage: 'medium',
cacheUtilization: 'high',
recommendedFor: [
'Complex authorization scenarios',
'Multi-criteria permission checks',
'Role-based access with conditions',
'Hierarchical permission systems',
],
};
}
/**
* Get performance statistics
*/
getStats() {
const totalCacheRequests = this.cacheHits + this.cacheMisses;
return {
checkCount: this.checkCount,
averageResolutionTimeUs: this.checkCount > 0 ? this.totalResolutionTimeUs / this.checkCount : 0,
totalResolutionTimeUs: this.totalResolutionTimeUs,
cacheHitRate: totalCacheRequests > 0
? (this.cacheHits / totalCacheRequests) * 100
: 0,
cacheHits: this.cacheHits,
cacheMisses: this.cacheMisses,
complexityDistribution: { ...this.expressionComplexityStats },
};
}
/**
* Reset performance statistics
*/
resetStats() {
this.checkCount = 0;
this.totalResolutionTimeUs = 0;
this.cacheHits = 0;
this.cacheMisses = 0;
this.expressionComplexityStats = {
simple: 0,
moderate: 0,
complex: 0,
};
}
/**
* Get resolver name for debugging
*/
getName() {
return 'ExpressionPermissionResolver';
}
/**
* Check if this resolver can handle the given requirement type
*/
canHandle(requirement) {
return (requirement &&
typeof requirement === 'object' &&
PermissionResolver_1.PermissionUtils.isValidExpression(requirement));
}
/**
* Normalize expression for consistent cache keys
*
* Sorts arrays and standardizes structure for reliable caching
*/
static normalizeExpression(expression) {
if (expression.permission) {
return { permission: expression.permission };
}
if (expression.and) {
return {
and: expression.and
.map((subExpr) => this.normalizeExpression(subExpr))
.sort((a, b) => JSON.stringify(a).localeCompare(JSON.stringify(b))),
};
}
if (expression.or) {
return {
or: expression.or
.map((subExpr) => this.normalizeExpression(subExpr))
.sort((a, b) => JSON.stringify(a).localeCompare(JSON.stringify(b))),
};
}
if (expression.not) {
return {
not: this.normalizeExpression(expression.not),
};
}
throw new Error('Invalid expression structure');
}
}
exports.ExpressionPermissionResolver = ExpressionPermissionResolver;
//# sourceMappingURL=ExpressionPermissionResolver.js.map