UNPKG

@noony-serverless/core

Version:

A Middy base framework compatible with Firebase and GCP Cloud Functions with TypeScript

377 lines 14.5 kB
"use strict"; /** * Wildcard Permission Resolver * * Configurable permission resolver supporting hierarchical wildcard patterns * with two distinct strategies for optimal performance in different scenarios: * * 1. PRE_EXPANSION Strategy: * - Expand wildcards at user context load time * - Store expanded permissions in user context * - Runtime: O(1) set membership checks (fastest) * - Memory: Higher usage due to expanded permission sets * - Best for: Production environments with predictable permission sets * * 2. ON_DEMAND Strategy: * - Pattern matching at permission check time * - Cache pattern matching results * - Runtime: Pattern matching cost with caching benefits * - Memory: Lower usage, only caches results * - Best for: Development, dynamic permissions, memory-constrained environments * * Supported Patterns: * - 2 levels: "admin.users", "org.reports" * - 3 levels: "admin.users.create", "org.reports.view" * - Wildcards: "admin.*", "org.reports.*" * * @author Noony Framework Team * @version 1.0.0 */ Object.defineProperty(exports, "__esModule", { value: true }); exports.WildcardPermissionResolver = void 0; const PermissionResolver_1 = require("./PermissionResolver"); const CacheAdapter_1 = require("../cache/CacheAdapter"); const GuardConfiguration_1 = require("../config/GuardConfiguration"); /** * Wildcard permission resolver with configurable resolution strategies */ class WildcardPermissionResolver extends PermissionResolver_1.PermissionResolver { strategy; permissionRegistry; cache; maxPatternDepth; // Performance tracking checkCount = 0; totalResolutionTimeUs = 0; cacheHits = 0; cacheMisses = 0; constructor(strategy, permissionRegistry, cache, maxPatternDepth = 3) { super(); this.strategy = strategy; this.permissionRegistry = permissionRegistry; this.cache = cache; this.maxPatternDepth = maxPatternDepth; if (maxPatternDepth < 2 || maxPatternDepth > 3) { throw new Error('Max pattern depth must be 2 or 3'); } } /** * Check if user permissions satisfy wildcard patterns * * @param userPermissions - Set of user's permissions (may be pre-expanded) * @param wildcardPatterns - Array of wildcard patterns to check * @returns Promise resolving to true if user matches any pattern */ async check(userPermissions, wildcardPatterns) { const startTime = process.hrtime.bigint(); try { // Validate inputs if (!userPermissions || userPermissions.size === 0) { return false; } if (!wildcardPatterns || wildcardPatterns.length === 0) { return false; } // Validate patterns for (const pattern of wildcardPatterns) { if (!this.isValidWildcardPattern(pattern)) { throw new Error(`Invalid wildcard pattern: ${pattern}`); } } // Route to appropriate strategy if (this.strategy === GuardConfiguration_1.PermissionResolutionStrategy.PRE_EXPANSION) { return await this.checkPreExpanded(userPermissions, wildcardPatterns); } else { return await this.checkOnDemand(userPermissions, wildcardPatterns); } } finally { // Track performance metrics const endTime = process.hrtime.bigint(); const resolutionTimeUs = Number(endTime - startTime) / 1000; this.checkCount++; this.totalResolutionTimeUs += resolutionTimeUs; } } /** * Pre-expansion strategy: O(1) set membership checks * * Assumes user permissions have been pre-expanded to include all * concrete permissions that match wildcard patterns in their roles. * This provides the fastest runtime performance. */ async checkPreExpanded(userPermissions, wildcardPatterns) { // For pre-expanded permissions, we check both: // 1. Exact pattern matches (if user was granted the wildcard directly) // 2. Concrete permission matches (if user has specific permissions) for (const pattern of wildcardPatterns) { // Check if user has the wildcard permission directly if (userPermissions.has(pattern)) { return true; } // If it's a wildcard pattern, check for any matching concrete permissions if (pattern.includes('*')) { const concretePermissions = this.permissionRegistry.getMatchingPermissions(pattern); for (const concretePermission of concretePermissions) { if (userPermissions.has(concretePermission)) { return true; } } } } return false; } /** * On-demand strategy: Pattern matching with caching * * Performs pattern matching at runtime but caches results to avoid * repeated pattern matching for the same user/pattern combinations. */ async checkOnDemand(userPermissions, wildcardPatterns) { // Create cache key for this specific check const userPermissionArray = Array.from(userPermissions).sort(); const cacheKey = CacheAdapter_1.CacheKeyBuilder.wildcardPattern(wildcardPatterns, userPermissionArray); // Check cache first const cachedResult = await this.cache.get(cacheKey); if (cachedResult !== null) { this.cacheHits++; return cachedResult; } this.cacheMisses++; // Perform pattern matching let result = false; for (const pattern of wildcardPatterns) { if (this.matchesAnyUserPermission(userPermissions, pattern)) { result = true; break; // Short-circuit on first match } } // Cache the result for 1 minute (configurable) await this.cache.set(cacheKey, result, 60 * 1000); return result; } /** * Check if any user permission matches the given pattern */ matchesAnyUserPermission(userPermissions, pattern) { // Direct exact match if (userPermissions.has(pattern)) { return true; } // If not a wildcard pattern, no further matching needed if (!pattern.includes('*')) { return false; } // Pattern matching for wildcard for (const userPermission of userPermissions) { if (PermissionResolver_1.PermissionUtils.matchesWildcard(userPermission, pattern)) { return true; } } return false; } /** * Expand wildcard patterns to concrete permissions * * Used by the user context service when pre-expansion strategy is enabled. * Converts wildcard patterns to all matching concrete permissions. * * @param patterns - Array of wildcard patterns * @returns Set of concrete permissions */ async expandWildcardPatterns(patterns) { const expandedPermissions = new Set(); for (const pattern of patterns) { if (!this.isValidWildcardPattern(pattern)) { throw new Error(`Invalid wildcard pattern: ${pattern}`); } if (pattern.includes('*')) { // Expand wildcard to concrete permissions const concretePermissions = this.permissionRegistry.getMatchingPermissions(pattern); concretePermissions.forEach((permission) => expandedPermissions.add(permission)); } else { // Add concrete permission as-is expandedPermissions.add(pattern); } } return expandedPermissions; } /** * Check permissions with detailed result information */ async checkWithResult(userPermissions, wildcardPatterns) { const startTime = process.hrtime.bigint(); const cached = false; const matchedPermissions = []; try { // Validate inputs if (!userPermissions || userPermissions.size === 0) { return { allowed: false, resolverType: this.getType(), resolutionTimeUs: 0, cached: false, reason: 'User has no permissions', }; } if (!wildcardPatterns || wildcardPatterns.length === 0) { return { allowed: false, resolverType: this.getType(), resolutionTimeUs: 0, cached: false, reason: 'No patterns specified', }; } // Find all matching patterns/permissions for (const pattern of wildcardPatterns) { if (!this.isValidWildcardPattern(pattern)) { throw new Error(`Invalid wildcard pattern: ${pattern}`); } if (this.strategy === GuardConfiguration_1.PermissionResolutionStrategy.PRE_EXPANSION) { // Check pre-expanded permissions if (userPermissions.has(pattern)) { matchedPermissions.push(pattern); } else if (pattern.includes('*')) { const concretePermissions = this.permissionRegistry.getMatchingPermissions(pattern); for (const concretePermission of concretePermissions) { if (userPermissions.has(concretePermission)) { matchedPermissions.push(concretePermission); } } } } else { // On-demand pattern matching if (this.matchesAnyUserPermission(userPermissions, pattern)) { matchedPermissions.push(pattern); } } } const allowed = matchedPermissions.length > 0; const endTime = process.hrtime.bigint(); const resolutionTimeUs = Number(endTime - startTime) / 1000; return { allowed, resolverType: this.getType(), resolutionTimeUs, cached, reason: allowed ? undefined : `No matching patterns: ${wildcardPatterns.join(', ')}`, matchedPermissions: allowed ? matchedPermissions : 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 error', }; } } /** * Validate wildcard pattern format */ isValidWildcardPattern(pattern) { if (!pattern || typeof pattern !== 'string') { return false; } // Check basic permission format first if (!PermissionResolver_1.PermissionUtils.isValidPermission(pattern)) { return false; } // Count depth levels const parts = pattern.split('.'); if (parts.length < 2 || parts.length > this.maxPatternDepth) { return false; } // If contains wildcard, it should be at the end if (pattern.includes('*')) { if (!pattern.endsWith('*') || pattern.indexOf('*') !== pattern.length - 1) { return false; } } return true; } /** * Get resolver type for identification */ getType() { return PermissionResolver_1.PermissionResolverType.WILDCARD; } /** * Get performance characteristics for monitoring */ getPerformanceCharacteristics() { const isPreExpansion = this.strategy === GuardConfiguration_1.PermissionResolutionStrategy.PRE_EXPANSION; return { timeComplexity: isPreExpansion ? 'O(1) per pattern' : 'O(n*m) with caching', memoryUsage: isPreExpansion ? 'high' : 'medium', cacheUtilization: isPreExpansion ? 'none' : 'high', recommendedFor: isPreExpansion ? [ 'Production environments', 'Predictable permission sets', 'Maximum performance', ] : [ 'Development environments', 'Dynamic permissions', 'Memory-constrained scenarios', ], }; } /** * Get performance statistics */ getStats() { const totalCacheRequests = this.cacheHits + this.cacheMisses; return { strategy: this.strategy, 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, }; } /** * Reset performance statistics */ resetStats() { this.checkCount = 0; this.totalResolutionTimeUs = 0; this.cacheHits = 0; this.cacheMisses = 0; } /** * Get resolver name for debugging */ getName() { return `WildcardPermissionResolver(${this.strategy})`; } /** * Check if this resolver can handle the given requirement type */ canHandle(requirement) { return (Array.isArray(requirement) && requirement.length > 0 && requirement.every((item) => typeof item === 'string' && this.isValidWildcardPattern(item))); } } exports.WildcardPermissionResolver = WildcardPermissionResolver; //# sourceMappingURL=WildcardPermissionResolver.js.map