@noony-serverless/core
Version:
A Middy base framework compatible with Firebase and GCP Cloud Functions with TypeScript
377 lines • 14.5 kB
JavaScript
"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