UNPKG

rls-guard

Version:

A CLI tool for managing PostgreSQL Row Level Security (RLS) policies as code

208 lines (179 loc) 6.91 kB
// Expression Parser - Convert SQL expressions back to helper functions // This module analyzes PostgreSQL RLS expressions and tries to map them back to our helper functions /** * Parse a PostgreSQL RLS expression and attempt to convert it to helper function calls * @param {string} expression - SQL expression from RLS policy * @returns {Object} Parsed expression with helper function mapping */ export function parseExpression(expression: string): { helper: string, raw: string, confidence: number, params?: any } { if (!expression || expression.trim() === '') { return { helper: 'publicAccess', raw: expression, confidence: 1.0 }; } const normalized = expression.trim().toLowerCase(); // Handle simple boolean values if (normalized === 'true') { return { helper: 'publicAccess', raw: expression, confidence: 1.0 }; } if (normalized === 'false') { return { helper: 'noAccess', raw: expression, confidence: 1.0 }; } // Try to match against known helper patterns const patterns = [ { name: 'currentUserId', regex: /\(?(\w+)\s*=\s*\(?current_setting\('app\.current_user_id'(?:::text)?\)(?:::uuid)?\)?/i, confidence: 0.9, extract: (match: RegExpMatchArray) => ({ column: match[1] }) }, { name: 'tenantId', regex: /(\w+)\s*=\s*current_setting\('app\.tenant_id'\)::uuid/i, confidence: 0.9, extract: (match: RegExpMatchArray) => ({ column: match[1] }) }, { name: 'roleCheck', regex: /current_setting\('app\.user_role'\)\s*=\s*'([^']+)'/i, confidence: 0.9, extract: (match: RegExpMatchArray) => ({ role: match[1] }) }, { name: 'recentData', regex: /\(?(\w+)\s*>=\s*\(?(?:current_date|CURRENT_DATE)\s*-\s*(?:'(\d+)\s*days?'::interval|interval\s*'(\d+)\s*days?')\)?/i, confidence: 0.8, extract: (match: RegExpMatchArray) => ({ column: match[1], days: parseInt(match[2] || match[3]) }) }, { name: 'timeWindow', regex: /(\w+)\s*>=\s*now\(\)\s*-\s*interval\s*'(\d+)\s*hours?'/i, confidence: 0.8, extract: (match: RegExpMatchArray) => ({ column: match[1], hours: parseInt(match[2]) }) }, { name: 'ownerOnly', regex: /(\w+)\s*=\s*(\w+)/i, confidence: 0.6, extract: (match: RegExpMatchArray) => ({ userColumn: match[1], ownerColumn: match[2] }) } ]; // Try to match against patterns for (const pattern of patterns) { const match = expression.match(pattern.regex); if (match) { return { helper: pattern.name, params: pattern.extract(match), raw: expression, confidence: pattern.confidence }; } } // Check for complex expressions that might combine multiple conditions if (expression.includes(' AND ') || expression.includes(' OR ')) { return parseComplexExpression(expression); } // If no pattern matches, return as raw expression return { helper: 'custom', raw: expression, confidence: 0.0 }; } /** * Parse complex expressions with AND/OR operators * @param {string} expression - Complex SQL expression * @returns {Object} Parsed complex expression */ function parseComplexExpression(expression: string): { helper: string, conditions?: any[], raw: string, confidence: number } { // For now, treat complex expressions as custom // Future enhancement: recursively parse sub-expressions const conditions = []; // Split by AND/OR and try to parse each part const parts = expression.split(/\s+(AND|OR)\s+/i); for (let i = 0; i < parts.length; i += 2) { const part = parts[i]; const parsed = parseExpression(part); if (parsed.helper !== 'custom') { conditions.push(parsed); } } if (conditions.length > 0) { return { helper: 'complex', conditions: conditions, raw: expression, confidence: Math.min(...conditions.map(c => c.confidence)) * 0.8 }; } return { helper: 'custom', raw: expression, confidence: 0.0 }; } /** * Generate helper function call from parsed expression * @param {Object} parsed - Parsed expression object * @returns {string} Helper function call as string */ export function generateHelperCall(parsed: { helper: string, params?: any, raw: string }): string { if (!parsed || parsed.helper === 'custom') { return `"${parsed.raw}"`; } switch (parsed.helper) { case 'publicAccess': return 'publicAccess()'; case 'noAccess': return 'noAccess()'; case 'currentUserId': return parsed.params.column === 'user_id' ? 'currentUserId()' : `currentUserId("${parsed.params.column}")`; case 'tenantId': return parsed.params.column === 'tenant_id' ? 'tenantId()' : `tenantId("${parsed.params.column}")`; case 'roleCheck': return `roleCheck("${parsed.params.role}")`; case 'recentData': return parsed.params.column === 'created_at' && parsed.params.days === 90 ? 'recentData()' : `recentData("${parsed.params.column}", ${parsed.params.days})`; case 'timeWindow': return `timeWindow("${parsed.params.column}", ${parsed.params.hours})`; case 'ownerOnly': return parsed.params.userColumn === 'user_id' && parsed.params.ownerColumn === 'owner_id' ? 'ownerOnly()' : `ownerOnly("${parsed.params.userColumn}", "${parsed.params.ownerColumn}")`; case 'complex': const conditions = (parsed as any).conditions?.map((c: any) => generateHelperCall(c)).join(', '); return `/* Complex: ${parsed.raw} */ "${parsed.raw}"`; default: return `"${parsed.raw}"`; } } /** * Analyze expression and provide suggestions for improvement * @param {Object} parsed - Parsed expression object * @returns {Object} Analysis with suggestions */ export function analyzeExpression(parsed: { confidence: number, helper: string }): { confidence: number, suggestions: string[], warnings: string[] } { const analysis = { confidence: parsed.confidence, suggestions: [], warnings: [] }; if (parsed.confidence < 0.5) { analysis.warnings.push('Low confidence in helper function mapping' as never); analysis.suggestions.push('Consider using a custom expression or updating helper patterns' as never); } if (parsed.helper === 'custom') { analysis.suggestions.push('Expression could not be mapped to helper function' as never); analysis.suggestions.push('Review if this pattern should be added to available helpers' as never); } if (parsed.helper === 'ownerOnly' && parsed.confidence < 0.8) { analysis.warnings.push('Simple column comparison detected - verify this is actually owner-based access' as never); analysis.suggestions.push('Consider if this should use currentUserId() instead' as never); } return analysis; }