@noony-serverless/core
Version:
A Middy base framework compatible with Firebase and GCP Cloud Functions with TypeScript
448 lines • 18.3 kB
JavaScript
"use strict";
/**
* Fast User Context Service
*
* High-performance user context management with configurable permission resolution.
* This service orchestrates the permission resolution strategies, manages caching,
* and provides sub-millisecond user permission checks for serverless environments.
*
* Key Features:
* - Configurable permission resolution (pre-expansion vs on-demand)
* - Multi-layer caching (L1 memory + L2 distributed)
* - Conservative cache invalidation for security
* - Permission expansion and validation
* - Performance monitoring and metrics
* - TypeDI integration for dependency injection
*
* Architecture:
* - Uses strategy pattern for different resolution approaches
* - Implements repository pattern for user context storage
* - Follows single responsibility principle with focused methods
* - Provides comprehensive error handling and logging
*
* @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.FastUserContextService = void 0;
const typedi_1 = require("typedi");
const CacheAdapter_1 = require("../cache/CacheAdapter");
const GuardConfiguration_1 = require("../config/GuardConfiguration");
const PlainPermissionResolver_1 = require("../resolvers/PlainPermissionResolver");
const WildcardPermissionResolver_1 = require("../resolvers/WildcardPermissionResolver");
const ExpressionPermissionResolver_1 = require("../resolvers/ExpressionPermissionResolver");
const PermissionResolver_1 = require("../resolvers/PermissionResolver");
const NoopCacheAdapter_1 = require("../cache/NoopCacheAdapter");
/**
* Fast User Context Service Implementation
*/
let FastUserContextService = class FastUserContextService {
cache;
config;
permissionSource;
_permissionRegistry;
// Permission resolvers
plainResolver;
wildcardResolver;
expressionResolver;
// Performance tracking
contextLoads = 0;
cacheHits = 0;
cacheMisses = 0;
permissionChecks = 0;
totalResolutionTimeUs = 0;
constructor(cache, config, permissionSource, permissionRegistry) {
this.cache = cache;
this.config = config;
this.permissionSource = permissionSource;
this._permissionRegistry = permissionRegistry;
// Initialize permission resolvers
this.plainResolver = new PlainPermissionResolver_1.PlainPermissionResolver();
this.wildcardResolver = new WildcardPermissionResolver_1.WildcardPermissionResolver(config.security.permissionResolutionStrategy ??
GuardConfiguration_1.PermissionResolutionStrategy.PRE_EXPANSION, this._permissionRegistry, cache);
this.expressionResolver = new ExpressionPermissionResolver_1.ExpressionPermissionResolver(cache);
}
/**
* Check if caching is effectively disabled
*
* @returns true if caching is disabled (either by environment variable or NoopCacheAdapter)
*/
isCachingDisabled() {
return (!GuardConfiguration_1.GuardConfiguration.isCachingEnabled() ||
this.cache instanceof NoopCacheAdapter_1.NoopCacheAdapter);
}
/**
* Get or load user context with permissions
*
* This is the primary method for retrieving user contexts with caching.
* It handles both pre-expansion and on-demand permission strategies.
*
* @param userId - Unique user identifier
* @param forceRefresh - Skip cache and force reload
* @returns User context with permissions or null if user not found
*/
async getUserContext(userId, forceRefresh = false) {
const startTime = process.hrtime.bigint();
this.contextLoads++;
try {
const cachingDisabled = this.isCachingDisabled();
// Check cache first unless forced refresh or caching is disabled
if (!forceRefresh && !cachingDisabled) {
const cachedContext = await this.loadFromCache(userId);
if (cachedContext) {
this.cacheHits++;
return cachedContext;
}
}
this.cacheMisses++;
// Load from permission source
const userData = await this.permissionSource.getUserPermissions(userId);
if (!userData) {
return null;
}
// Build user context
const context = await this.buildUserContext(userId, userData);
// Cache the context only if caching is enabled
if (!cachingDisabled) {
await this.saveToCache(context);
}
return context;
}
finally {
const endTime = process.hrtime.bigint();
this.totalResolutionTimeUs += Number(endTime - startTime) / 1000;
}
}
/**
* Check user permission using appropriate resolver
*
* Routes permission checks to the optimal resolver based on requirement type.
* Provides detailed results including performance metrics and cache status.
*
* @param userId - User identifier
* @param requirement - Permission requirement (string[], wildcard pattern, or expression)
* @param options - Check options
* @returns Detailed permission check result
*/
async checkPermission(userId, requirement, options = {}) {
const startTime = process.hrtime.bigint();
this.permissionChecks++;
try {
// Load user context
const userContext = await this.getUserContext(userId, !options.useCache);
if (!userContext) {
return {
allowed: false,
resolverType: PermissionResolver_1.PermissionResolverType.PLAIN,
resolutionTimeUs: 0,
cached: false,
reason: 'User not found',
};
}
// Select appropriate resolver
const resolver = this.selectResolver(requirement, options.resolverType);
if (!resolver) {
return {
allowed: false,
resolverType: PermissionResolver_1.PermissionResolverType.PLAIN,
resolutionTimeUs: 0,
cached: false,
reason: 'No suitable resolver found',
};
}
// Perform permission check
const permissions = userContext.expandedPermissions || userContext.permissions;
const result = await resolver.checkWithResult(permissions, requirement);
// Add audit trail if enabled
if (options.auditTrail && result.allowed) {
await this.recordAuditTrail(userId, requirement, result);
}
return result;
}
finally {
const endTime = process.hrtime.bigint();
this.totalResolutionTimeUs += Number(endTime - startTime) / 1000;
}
}
/**
* Batch check multiple permissions for a user
*
* Optimized for checking multiple permissions at once.
* Uses the same user context for all checks to minimize overhead.
*
* @param userId - User identifier
* @param requirements - Array of permission requirements
* @param options - Check options
* @returns Array of permission check results
*/
async checkPermissions(userId, requirements, options = {}) {
const startTime = process.hrtime.bigint();
try {
// Load user context once for all checks
const userContext = await this.getUserContext(userId, !options.useCache);
if (!userContext) {
const failResult = {
allowed: false,
resolverType: PermissionResolver_1.PermissionResolverType.PLAIN,
resolutionTimeUs: 0,
cached: false,
reason: 'User not found',
};
return requirements.map(() => ({ ...failResult }));
}
// Process all requirements
const results = [];
const permissions = userContext.expandedPermissions || userContext.permissions;
for (const { requirement, resolverType } of requirements) {
const resolver = this.selectResolver(requirement, resolverType);
if (!resolver) {
results.push({
allowed: false,
resolverType: PermissionResolver_1.PermissionResolverType.PLAIN,
resolutionTimeUs: 0,
cached: false,
reason: 'No suitable resolver found',
});
continue;
}
const result = await resolver.checkWithResult(permissions, requirement);
results.push(result);
// Add audit trail for allowed permissions
if (options.auditTrail && result.allowed) {
await this.recordAuditTrail(userId, requirement, result);
}
}
return results;
}
finally {
const endTime = process.hrtime.bigint();
this.totalResolutionTimeUs += Number(endTime - startTime) / 1000;
this.permissionChecks += requirements.length;
}
}
/**
* Invalidate user context cache
*
* Removes user context from cache when permissions change.
* Uses conservative approach by also clearing related cached data.
*
* @param userId - User identifier
* @param clearRelated - Also clear permission-related caches
*/
async invalidateUserContext(userId, clearRelated = true) {
const cacheKey = CacheAdapter_1.CacheKeyBuilder.userContext(userId);
await this.cache.delete(cacheKey);
if (clearRelated && this.config.security.conservativeCacheInvalidation) {
// Clear permission check caches that might be affected
await this.cache.deletePattern(`perm:*:${userId}:*`);
await this.cache.deletePattern(`expr:*:${userId}:*`);
await this.cache.deletePattern(`wild:*:${userId}:*`);
}
console.log(`🔄 Invalidated user context cache`, {
userId,
clearRelated,
timestamp: new Date().toISOString(),
});
}
/**
* Pre-expand wildcard permissions for user context
*
* Used when pre-expansion strategy is enabled to convert
* wildcard permissions to concrete permission sets.
*
* @param permissions - Raw permissions from user/roles
* @returns Expanded permission set
*/
async expandPermissions(permissions) {
const expanded = new Set();
for (const permission of permissions) {
if (permission.includes('*')) {
// Expand wildcard
const concretePermissions = await this.wildcardResolver.expandWildcardPatterns([permission]);
concretePermissions.forEach((p) => expanded.add(p));
}
else {
// Add concrete permission
expanded.add(permission);
}
}
return expanded;
}
/**
* Get service performance statistics
*/
getStats() {
const totalCacheRequests = this.cacheHits + this.cacheMisses;
return {
contextLoads: this.contextLoads,
permissionChecks: this.permissionChecks,
cacheHitRate: totalCacheRequests > 0
? (this.cacheHits / totalCacheRequests) * 100
: 0,
cacheHits: this.cacheHits,
cacheMisses: this.cacheMisses,
averageResolutionTimeUs: this.contextLoads > 0
? this.totalResolutionTimeUs / this.contextLoads
: 0,
totalResolutionTimeUs: this.totalResolutionTimeUs,
resolverStats: {
plain: this.plainResolver.getStats(),
wildcard: this.wildcardResolver.getStats(),
expression: this.expressionResolver.getStats(),
},
};
}
/**
* Reset performance statistics
*/
resetStats() {
this.contextLoads = 0;
this.cacheHits = 0;
this.cacheMisses = 0;
this.permissionChecks = 0;
this.totalResolutionTimeUs = 0;
this.plainResolver.resetStats();
this.wildcardResolver.resetStats();
this.expressionResolver.resetStats();
}
/**
* Load user context from cache
*/
async loadFromCache(userId) {
const cacheKey = CacheAdapter_1.CacheKeyBuilder.userContext(userId);
const cachedData = await this.cache.get(cacheKey);
if (!cachedData) {
return null;
}
// Check if context is stale
if (await this.permissionSource.isUserContextStale(userId, cachedData.context.lastUpdated)) {
await this.cache.delete(cacheKey);
return null;
}
// Reconstruct context with Set objects
return {
...cachedData.context,
permissions: new Set(cachedData.permissions),
expandedPermissions: cachedData.expandedPermissions
? new Set(cachedData.expandedPermissions)
: undefined,
};
}
/**
* Save user context to cache
*/
async saveToCache(context) {
const cacheKey = CacheAdapter_1.CacheKeyBuilder.userContext(context.userId);
// Serialize Sets to arrays for caching
const cacheData = {
context: {
userId: context.userId,
roles: context.roles,
metadata: context.metadata,
lastUpdated: context.lastUpdated,
expiresAt: context.expiresAt,
},
permissions: Array.from(context.permissions),
expandedPermissions: context.expandedPermissions
? Array.from(context.expandedPermissions)
: undefined,
};
const ttlMs = this.config.cache.userContextTtlMs || 15 * 60 * 1000; // 15 minutes default
await this.cache.set(cacheKey, cacheData, ttlMs);
}
/**
* Build user context from raw user data
*/
async buildUserContext(userId, userData) {
const now = new Date().toISOString();
// Combine user and role permissions
const rolePermissions = await this.permissionSource.getRolePermissions(userData.roles);
const allPermissions = [
...new Set([...userData.permissions, ...rolePermissions]),
];
// Create base context
const context = {
userId,
permissions: new Set(allPermissions),
roles: userData.roles,
metadata: userData.metadata || {},
lastUpdated: now,
};
// Add expanded permissions for pre-expansion strategy
if (this.config.security.permissionResolutionStrategy ===
GuardConfiguration_1.PermissionResolutionStrategy.PRE_EXPANSION) {
context.expandedPermissions =
await this.expandPermissions(allPermissions);
}
return context;
}
/**
* Select appropriate permission resolver
*/
selectResolver(requirement, preferredType) {
// Use preferred type if specified and resolver can handle it
if (preferredType) {
const resolver = this.getResolverByType(preferredType);
if (resolver && resolver.canHandle(requirement)) {
return resolver;
}
}
// Auto-select based on requirement type
if (this.expressionResolver.canHandle(requirement)) {
return this.expressionResolver;
}
if (this.wildcardResolver.canHandle(requirement)) {
return this.wildcardResolver;
}
if (this.plainResolver.canHandle(requirement)) {
return this.plainResolver;
}
return null;
}
/**
* Get resolver by type
*/
getResolverByType(type) {
switch (type) {
case PermissionResolver_1.PermissionResolverType.PLAIN:
return this.plainResolver;
case PermissionResolver_1.PermissionResolverType.WILDCARD:
return this.wildcardResolver;
case PermissionResolver_1.PermissionResolverType.EXPRESSION:
return this.expressionResolver;
default:
return null;
}
}
/**
* Record audit trail for permission checks
*/
async recordAuditTrail(userId, requirement, result) {
// In production, this would write to an audit log
console.log(`🔍 Permission granted`, {
userId,
requirement: typeof requirement === 'object'
? JSON.stringify(requirement)
: requirement,
resolverType: result.resolverType,
resolutionTimeUs: result.resolutionTimeUs,
matchedPermissions: result.matchedPermissions,
timestamp: new Date().toISOString(),
});
}
};
exports.FastUserContextService = FastUserContextService;
exports.FastUserContextService = FastUserContextService = __decorate([
(0, typedi_1.Service)(),
__metadata("design:paramtypes", [Object, GuardConfiguration_1.GuardConfiguration, Object, Object])
], FastUserContextService);
//# sourceMappingURL=FastUserContextService.js.map