UNPKG

@noony-serverless/core

Version:

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

510 lines โ€ข 19.8 kB
"use strict"; /** * Conservative Cache Invalidation Service * * Implements a security-first approach to cache invalidation for permission systems. * When in doubt about data freshness or permission changes, this service errs on the * side of security by clearing broader cache segments rather than risking stale data. * * Security Principles: * - Invalidate broadly rather than narrowly when permissions change * - Clear dependent caches proactively to prevent inconsistencies * - Use time-based invalidation as backup for missed updates * - Log all invalidation events for audit trails * - Provide rollback capabilities for accidental cache clears * * Use Cases: * - User permission changes (role assignments, direct permissions) * - System-wide permission updates (new permissions, policy changes) * - Security incidents requiring immediate cache clearing * - Scheduled maintenance and cache refresh operations * - Development and testing environment resets * * @author Noony Framework Team * @version 1.0.0 */ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; var __metadata = (this && this.__metadata) || function (k, v) { if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.ConservativeCacheInvalidation = exports.InvalidationScope = exports.InvalidationType = void 0; const typedi_1 = require("typedi"); const CacheAdapter_1 = require("./CacheAdapter"); /** * Types of cache invalidation operations */ var InvalidationType; (function (InvalidationType) { InvalidationType["USER_PERMISSION_CHANGE"] = "user_permission_change"; InvalidationType["ROLE_ASSIGNMENT_CHANGE"] = "role_assignment_change"; InvalidationType["SYSTEM_PERMISSION_UPDATE"] = "system_permission_update"; InvalidationType["SECURITY_INCIDENT"] = "security_incident"; InvalidationType["SCHEDULED_REFRESH"] = "scheduled_refresh"; InvalidationType["MANUAL_CLEAR"] = "manual_clear"; InvalidationType["DEVELOPMENT_RESET"] = "development_reset"; })(InvalidationType || (exports.InvalidationType = InvalidationType = {})); /** * Scope of cache invalidation */ var InvalidationScope; (function (InvalidationScope) { InvalidationScope["SINGLE_USER"] = "single_user"; InvalidationScope["MULTIPLE_USERS"] = "multiple_users"; InvalidationScope["ROLE_BASED"] = "role_based"; InvalidationScope["SYSTEM_WIDE"] = "system_wide"; InvalidationScope["PERMISSION_SPECIFIC"] = "permission_specific"; })(InvalidationScope || (exports.InvalidationScope = InvalidationScope = {})); /** * Conservative Cache Invalidation Implementation */ let ConservativeCacheInvalidation = class ConservativeCacheInvalidation { cache; auditLog = []; backups = new Map(); maxAuditLogSize = 1000; maxBackupAge = 24 * 60 * 60 * 1000; // 24 hours // Performance tracking invalidationCount = 0; totalKeysCleared = 0; totalExecutionTimeMs = 0; constructor(cache) { this.cache = cache; } /** * Invalidate cache for specific user permission changes * * Conservative approach: Clears all permission-related caches for the user * and any cached data that might depend on their permissions. * * @param userId - User whose permissions changed * @param reason - Reason for invalidation * @param createBackup - Whether to create restoration backup * @returns Invalidation event details */ async invalidateUserPermissions(userId, reason, createBackup = false) { const startTime = Date.now(); const eventId = this.generateEventId(); try { // Define patterns to clear for this user const patterns = [ CacheAdapter_1.CacheKeyBuilder.userContext(userId), `perm:*:${userId}:*`, // Plain permission checks `wild:*:${userId}:*`, // Wildcard permission checks `expr:*:${userId}:*`, // Expression permission checks `auth:${userId}:*`, // Authentication caches ]; // Create backup if requested if (createBackup) { await this.createBackup(`user_${userId}_${eventId}`, InvalidationScope.SINGLE_USER, patterns, `User permission change: ${reason}`); } // Clear caches const keysCleared = await this.clearCachesByPatterns(patterns); // Create audit event const event = { eventId, type: InvalidationType.USER_PERMISSION_CHANGE, scope: InvalidationScope.SINGLE_USER, patterns, reason, userId, timestamp: new Date().toISOString(), restorable: createBackup, performance: { keysCleared, executionTimeMs: Date.now() - startTime, }, }; await this.recordAuditEvent(event); this.updatePerformanceMetrics(keysCleared, Date.now() - startTime); console.log('๐Ÿงน Conservative cache invalidation completed', { type: 'user_permission_change', userId, keysCleared, duration: Date.now() - startTime, reason, }); return event; } catch (error) { console.error('โŒ Cache invalidation failed', { userId, reason, error: error instanceof Error ? error.message : 'Unknown error', duration: Date.now() - startTime, }); throw error; } } /** * Invalidate cache for role-based permission changes * * Conservative approach: Clears caches for all users with the affected roles * plus any system-level permission caches that might be affected. * * @param roles - Roles whose permissions changed * @param reason - Reason for invalidation * @param createBackup - Whether to create restoration backup * @returns Invalidation event details */ async invalidateRolePermissions(roles, reason, createBackup = false) { const startTime = Date.now(); const eventId = this.generateEventId(); try { // Conservative approach: Clear all user contexts and permission caches // Since we don't know which users have these roles, clear everything const patterns = [ 'user:context:*', // All user contexts 'perm:*', // All permission checks 'wild:*', // All wildcard checks 'expr:*', // All expression checks 'auth:*', // All authentication caches 'role:permissions:*', // Role permission mappings ]; // Create backup if requested if (createBackup) { await this.createBackup(`roles_${roles.join('_')}_${eventId}`, InvalidationScope.ROLE_BASED, patterns, `Role permission change: ${reason}`); } // Clear caches const keysCleared = await this.clearCachesByPatterns(patterns); // Create audit event const event = { eventId, type: InvalidationType.ROLE_ASSIGNMENT_CHANGE, scope: InvalidationScope.ROLE_BASED, patterns, reason: `Roles affected: ${roles.join(', ')} - ${reason}`, timestamp: new Date().toISOString(), restorable: createBackup, performance: { keysCleared, executionTimeMs: Date.now() - startTime, }, }; await this.recordAuditEvent(event); this.updatePerformanceMetrics(keysCleared, Date.now() - startTime); console.log('๐Ÿงน Conservative role cache invalidation completed', { type: 'role_permission_change', roles, keysCleared, duration: Date.now() - startTime, reason, }); return event; } catch (error) { console.error('โŒ Role cache invalidation failed', { roles, reason, error: error instanceof Error ? error.message : 'Unknown error', duration: Date.now() - startTime, }); throw error; } } /** * System-wide cache invalidation * * Nuclear option: Clears all permission-related caches across the system. * Used for major system updates, security incidents, or when unsure about * the scope of changes. * * @param reason - Reason for system-wide invalidation * @param createBackup - Whether to create restoration backup * @returns Invalidation event details */ async invalidateSystemWide(reason, createBackup = false) { const startTime = Date.now(); const eventId = this.generateEventId(); try { // Nuclear option: clear everything const patterns = ['*']; // Create backup if requested (warning: this could be large) if (createBackup) { await this.createBackup(`system_wide_${eventId}`, InvalidationScope.SYSTEM_WIDE, patterns, `System-wide invalidation: ${reason}`); } // Flush entire cache await this.cache.flush(); const keysCleared = -1; // Unknown count for flush operation // Create audit event const event = { eventId, type: InvalidationType.SYSTEM_PERMISSION_UPDATE, scope: InvalidationScope.SYSTEM_WIDE, patterns, reason, timestamp: new Date().toISOString(), restorable: createBackup, performance: { keysCleared, executionTimeMs: Date.now() - startTime, }, }; await this.recordAuditEvent(event); this.updatePerformanceMetrics(1000, Date.now() - startTime); // Estimate for stats console.log('๐Ÿ’ฅ System-wide cache invalidation completed', { type: 'system_wide', duration: Date.now() - startTime, reason, warning: 'All caches cleared', }); return event; } catch (error) { console.error('โŒ System-wide cache invalidation failed', { reason, error: error instanceof Error ? error.message : 'Unknown error', duration: Date.now() - startTime, }); throw error; } } /** * Emergency security invalidation * * Immediate cache clearing for security incidents. * Bypasses backup creation for speed and clears everything. * * @param reason - Security incident description * @returns Invalidation event details */ async emergencySecurityInvalidation(reason) { const startTime = Date.now(); const eventId = this.generateEventId(); try { // Emergency flush - no backup, clear everything immediately await this.cache.flush(); // Create audit event const event = { eventId, type: InvalidationType.SECURITY_INCIDENT, scope: InvalidationScope.SYSTEM_WIDE, patterns: ['*'], reason: `SECURITY INCIDENT: ${reason}`, timestamp: new Date().toISOString(), restorable: false, performance: { keysCleared: -1, executionTimeMs: Date.now() - startTime, }, }; await this.recordAuditEvent(event); this.updatePerformanceMetrics(1000, Date.now() - startTime); console.error('๐Ÿšจ EMERGENCY: Security cache invalidation completed', { type: 'security_incident', duration: Date.now() - startTime, reason, warning: 'ALL CACHES CLEARED FOR SECURITY', }); return event; } catch (error) { console.error('โŒ CRITICAL: Emergency cache invalidation failed', { reason, error: error instanceof Error ? error.message : 'Unknown error', duration: Date.now() - startTime, }); throw error; } } /** * Restore cache from backup * * Attempts to restore previously backed up cache data. * Use with caution - only restore if you're certain the data is safe. * * @param backupId - ID of the backup to restore * @returns Restoration success status */ async restoreFromBackup(backupId) { const backup = this.backups.get(backupId); if (!backup) { console.error('โŒ Backup not found', { backupId }); return false; } // Check backup age const backupAge = Date.now() - new Date(backup.timestamp).getTime(); if (backupAge > this.maxBackupAge) { console.warn('โš ๏ธ Backup is old, restoration may not be safe', { backupId, ageHours: backupAge / (1000 * 60 * 60), }); } try { let restoredCount = 0; // Restore each cache entry for (const [key, value] of backup.data) { await this.cache.set(key, value); restoredCount++; } console.log('โœ… Cache restored from backup', { backupId, restoredEntries: restoredCount, originalSize: backup.metadata.totalEntries, }); return true; } catch (error) { console.error('โŒ Cache restoration failed', { backupId, error: error instanceof Error ? error.message : 'Unknown error', }); return false; } } /** * Get invalidation audit log */ getAuditLog() { return [...this.auditLog]; } /** * Get available backups */ getAvailableBackups() { return Array.from(this.backups.values()).map((backup) => ({ backupId: backup.backupId, timestamp: backup.timestamp, scope: backup.scope, totalEntries: backup.metadata.totalEntries, sizeBytes: backup.metadata.sizeBytes, ageHours: (Date.now() - new Date(backup.timestamp).getTime()) / (1000 * 60 * 60), })); } /** * Get invalidation statistics */ getStats() { return { invalidationCount: this.invalidationCount, totalKeysCleared: this.totalKeysCleared, averageExecutionTimeMs: this.invalidationCount > 0 ? this.totalExecutionTimeMs / this.invalidationCount : 0, totalExecutionTimeMs: this.totalExecutionTimeMs, auditLogSize: this.auditLog.length, backupsAvailable: this.backups.size, oldestBackupAge: this.getOldestBackupAge(), }; } /** * Clear patterns from cache */ async clearCachesByPatterns(patterns) { let totalKeysCleared = 0; for (const pattern of patterns) { try { if (pattern === '*') { // Special case: flush all await this.cache.flush(); return -1; // Unknown count } else { // Pattern-based deletion await this.cache.deletePattern(pattern); totalKeysCleared += 10; // Estimate since we don't have exact counts } } catch (error) { console.warn('โš ๏ธ Pattern deletion failed', { pattern, error: error instanceof Error ? error.message : 'Unknown error', }); } } return totalKeysCleared; } /** * Create backup of cache data */ async createBackup(backupId, scope, patterns, reason) { // Note: This is a simplified backup implementation // In production, you'd implement pattern-based cache reading const backup = { backupId, timestamp: new Date().toISOString(), scope, data: new Map(), // Would contain actual cached data metadata: { totalEntries: 0, sizeBytes: 0, createdBy: 'ConservativeCacheInvalidation', reason, }, }; this.backups.set(backupId, backup); // Clean up old backups this.cleanupOldBackups(); } /** * Record audit event */ async recordAuditEvent(event) { this.auditLog.push(event); // Keep audit log size manageable if (this.auditLog.length > this.maxAuditLogSize) { this.auditLog.shift(); } } /** * Update performance metrics */ updatePerformanceMetrics(keysCleared, executionTimeMs) { this.invalidationCount++; this.totalKeysCleared += Math.max(0, keysCleared); this.totalExecutionTimeMs += executionTimeMs; } /** * Generate unique event ID */ generateEventId() { return `inv_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; } /** * Clean up old backups */ cleanupOldBackups() { const now = Date.now(); const keysToDelete = []; for (const [backupId, backup] of this.backups) { const age = now - new Date(backup.timestamp).getTime(); if (age > this.maxBackupAge) { keysToDelete.push(backupId); } } keysToDelete.forEach((key) => { this.backups.delete(key); }); if (keysToDelete.length > 0) { console.log('๐Ÿ—‘๏ธ Cleaned up old cache backups', { removedBackups: keysToDelete.length, }); } } /** * Get age of oldest backup in hours */ getOldestBackupAge() { if (this.backups.size === 0) return 0; let oldest = Date.now(); for (const backup of this.backups.values()) { const backupTime = new Date(backup.timestamp).getTime(); if (backupTime < oldest) { oldest = backupTime; } } return (Date.now() - oldest) / (1000 * 60 * 60); } }; exports.ConservativeCacheInvalidation = ConservativeCacheInvalidation; exports.ConservativeCacheInvalidation = ConservativeCacheInvalidation = __decorate([ (0, typedi_1.Service)(), __metadata("design:paramtypes", [Object]) ], ConservativeCacheInvalidation); //# sourceMappingURL=ConservativeCacheInvalidation.js.map