@noony-serverless/core
Version:
A Middy base framework compatible with Firebase and GCP Cloud Functions with TypeScript
176 lines • 6.63 kB
JavaScript
;
/**
* Permission Resolution System
*
* Base interfaces and types for the three distinct permission resolution strategies:
* - PlainPermissionResolver: Direct O(1) set membership checks
* - WildcardPermissionResolver: Pattern matching with configurable strategies
* - ExpressionPermissionResolver: Complex AND/OR/NOT expression evaluation
*
* Each resolver operates independently and cannot be combined on a single endpoint,
* ensuring clear performance characteristics and simplified reasoning.
*
* @author Noony Framework Team
* @version 1.0.0
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.PermissionUtils = exports.PermissionResolverType = exports.PermissionResolver = void 0;
/**
* Base interface for all permission resolvers
*
* Each resolver implements this interface with specific logic for handling
* different types of permission requirements. The generic type T represents
* the specific requirement format for each resolver.
*/
class PermissionResolver {
}
exports.PermissionResolver = PermissionResolver;
/**
* Permission resolver types for identification
*/
var PermissionResolverType;
(function (PermissionResolverType) {
PermissionResolverType["PLAIN"] = "plain";
PermissionResolverType["WILDCARD"] = "wildcard";
PermissionResolverType["EXPRESSION"] = "expression";
})(PermissionResolverType || (exports.PermissionResolverType = PermissionResolverType = {}));
/**
* Utility functions for working with permissions
*/
class PermissionUtils {
/**
* Convert array of permissions to Set for O(1) lookups
*
* @param permissions - Array of permission strings
* @returns Set of permissions for fast membership testing
*/
static arrayToSet(permissions) {
return new Set(permissions);
}
/**
* Validate permission string format
*
* Ensures permissions follow expected naming conventions:
* - 2-3 levels separated by dots
* - Only alphanumeric characters, dots, and hyphens
* - No leading/trailing dots
*
* @param permission - Permission string to validate
* @returns true if valid, false otherwise
*/
static isValidPermission(permission) {
if (!permission || typeof permission !== 'string') {
return false;
}
// Check for valid format: 2-3 levels, alphanumeric + dots + hyphens
const validPattern = /^[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+){1,2}$/;
// Check for wildcard format: ends with .* for wildcards
const wildcardPattern = /^[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*\.\*$/;
return validPattern.test(permission) || wildcardPattern.test(permission);
}
/**
* Extract base permission from wildcard pattern
*
* @param wildcardPermission - Wildcard permission like "admin.*"
* @returns Base permission like "admin"
*/
static extractWildcardBase(wildcardPermission) {
if (wildcardPermission.endsWith('.*')) {
return wildcardPermission.slice(0, -2);
}
return wildcardPermission;
}
/**
* Check if a permission matches a wildcard pattern
*
* @param permission - Specific permission to test
* @param wildcardPattern - Wildcard pattern to match against
* @returns true if permission matches the pattern
*/
static matchesWildcard(permission, wildcardPattern) {
if (!wildcardPattern.includes('*')) {
return permission === wildcardPattern;
}
// Convert wildcard pattern to regex
// "admin.*" becomes /^admin\.[^.]+$/
// "admin.users.*" becomes /^admin\.users\.[^.]+$/
const regexPattern = wildcardPattern
.replace(/\./g, '\\.') // Escape dots
.replace(/\*/g, '[^.]+'); // Replace * with non-dot characters
const regex = new RegExp(`^${regexPattern}$`);
return regex.test(permission);
}
/**
* Validate permission expression structure
*
* Ensures expressions follow the 2-level nesting limit and contain
* valid permission strings.
*
* @param expression - Permission expression to validate
* @param depth - Current nesting depth (internal use)
* @returns true if valid, false otherwise
*/
static isValidExpression(expression, depth = 0) {
if (depth > 2) {
return false; // Exceeds maximum nesting depth
}
// Must have exactly one operation type
const operationCount = [
expression.and ? 1 : 0,
expression.or ? 1 : 0,
expression.not ? 1 : 0,
expression.permission ? 1 : 0,
].reduce((sum, count) => sum + count, 0);
if (operationCount !== 1) {
return false; // Must have exactly one operation
}
// Validate leaf permission
if (expression.permission) {
return this.isValidPermission(expression.permission);
}
// Validate AND operation
if (expression.and) {
if (!Array.isArray(expression.and) || expression.and.length === 0) {
return false;
}
return expression.and.every((subExpr) => this.isValidExpression(subExpr, depth + 1));
}
// Validate OR operation
if (expression.or) {
if (!Array.isArray(expression.or) || expression.or.length === 0) {
return false;
}
return expression.or.every((subExpr) => this.isValidExpression(subExpr, depth + 1));
}
// Validate NOT operation
if (expression.not) {
return this.isValidExpression(expression.not, depth + 1);
}
return false;
}
/**
* Generate a stable hash for caching permission expressions
*
* @param expression - Permission expression to hash
* @returns Stable string hash for cache keys
*/
static hashExpression(expression) {
// Sort keys to ensure consistent serialization
const sortedJson = JSON.stringify(expression, Object.keys(expression).sort());
return this.simpleHash(sortedJson);
}
/**
* Simple hash function for string data (not cryptographically secure)
*/
static simpleHash(str) {
let hash = 0;
for (let i = 0; i < str.length; i++) {
const char = str.charCodeAt(i);
hash = (hash << 5) - hash + char;
hash = hash & hash; // Convert to 32-bit integer
}
return hash.toString(36);
}
}
exports.PermissionUtils = PermissionUtils;
//# sourceMappingURL=PermissionResolver.js.map