UNPKG

@fastmcp-oauth/sql-delegation

Version:

SQL delegation module for FastMCP OAuth framework - PostgreSQL and SQL Server support

1,046 lines (1,045 loc) 36 kB
// src/postgresql-module.ts import pg from "pg"; import { createSecurityError } from "fastmcp-oauth/core"; var { Pool } = pg; var PostgreSQLDelegationModule = class { name; type = "database"; pool = null; config = null; isConnected = false; tokenExchangeConfig = null; /** * Create a new PostgreSQL delegation module * * @param name - Module name (e.g., 'postgresql', 'postgresql1', 'postgresql2') * Defaults to 'postgresql' for backward compatibility */ constructor(name = "postgresql") { this.name = name; } /** * Initialize PostgreSQL connection pool * * Per-Module Token Exchange (Phase 2): * - Token exchange config comes from config.tokenExchange (not injected separately) * - Token exchange happens on-demand in delegate() method * - TokenExchangeService can be injected via context parameter in delegate() * * @param config - PostgreSQL configuration (includes optional tokenExchange) * @throws Error if connection fails */ async initialize(config) { console.log("[PostgreSQLModule] VERSION: Phase2-Fix-2025-01-06-v3 - Per-module token exchange implementation"); if (this.isConnected) { return; } this.config = config; if (config.tokenExchange) { this.tokenExchangeConfig = config.tokenExchange; console.log("[PostgreSQLModule] Token exchange enabled with IDP:", config.tokenExchange.idpName); } try { this.pool = new Pool({ host: config.host, port: config.port ?? 5432, database: config.database, user: config.user, password: config.password, ssl: config.options?.ssl ?? false, max: config.pool?.max ?? 10, min: config.pool?.min ?? 0, idleTimeoutMillis: config.pool?.idleTimeoutMillis ?? 3e4, connectionTimeoutMillis: config.pool?.connectionTimeoutMillis ?? 5e3 }); const client = await this.pool.connect(); client.release(); this.isConnected = true; } catch (error) { throw createSecurityError( "POSTGRESQL_CONNECTION_FAILED", `Failed to initialize PostgreSQL connection: ${error instanceof Error ? error.message : error}`, 500 ); } } /** * Delegate PostgreSQL operation on behalf of user * * **Phase 2 Enhancement:** Now accepts optional context parameter with CoreContext. * This enables access to framework services like TokenExchangeService. * * @param session - User session * @param action - Action to perform * @param params - Action parameters * @param context - Optional context with sessionId and coreContext */ async delegate(session, action, params, context) { if (!this.isConnected || !this.pool) { try { if (!this.config) { throw new Error("PostgreSQL module not initialized. Call initialize() first."); } await this.initialize(this.config); } catch (error) { return { success: false, error: error instanceof Error ? error.message : "Initialization failed", auditTrail: { timestamp: /* @__PURE__ */ new Date(), source: "delegation:postgresql", userId: session.userId, action: `postgresql_delegation:${action}`, success: false, error: error instanceof Error ? error.message : "Unknown error" } }; } } console.log("[PostgreSQLModule] delegate() VERSION: Phase2-Fix-2025-01-06-v3"); let effectiveLegacyUsername = session.legacyUsername; let teRoles = []; if (this.tokenExchangeConfig) { try { console.log("[PostgreSQLModule] DEBUG: context type:", typeof context); console.log("[PostgreSQLModule] DEBUG: context is null?", context === null); console.log("[PostgreSQLModule] DEBUG: context is undefined?", context === void 0); console.log("[PostgreSQLModule] DEBUG: context keys:", context ? Object.keys(context) : "N/A"); console.log("[PostgreSQLModule] DEBUG: has coreContext?", !!context?.coreContext); console.log("[PostgreSQLModule] DEBUG: coreContext keys:", context?.coreContext ? Object.keys(context.coreContext) : "N/A"); console.log("[PostgreSQLModule] DEBUG: has tokenExchangeService?", !!context?.coreContext?.tokenExchangeService); console.log("[PostgreSQLModule] DEBUG: tokenExchangeService type:", typeof context?.coreContext?.tokenExchangeService); const tokenExchangeService = context?.coreContext?.tokenExchangeService; if (!tokenExchangeService) { console.error("[PostgreSQLModule] ERROR: TokenExchangeService not available - dumping context structure"); console.error("[PostgreSQLModule] ERROR: Full context:", JSON.stringify(context, null, 2)); return { success: false, error: "Token exchange configured but TokenExchangeService not available in context", auditTrail: { timestamp: /* @__PURE__ */ new Date(), source: "delegation:postgresql", userId: session.userId, action: `postgresql_delegation:${action}`, success: false, reason: "TokenExchangeService not in context" } }; } const requestorJWT = session.requestorJWT; console.log("[PostgreSQLModule] DEBUG: requestorJWT from session:", { exists: !!requestorJWT, type: typeof requestorJWT, length: requestorJWT?.length, first50chars: requestorJWT?.substring(0, 50) }); if (!requestorJWT) { console.error("[PostgreSQLModule] ERROR: Session is missing requestorJWT"); console.error("[PostgreSQLModule] ERROR: Session keys:", Object.keys(session)); console.error("[PostgreSQLModule] ERROR: Session dump:", JSON.stringify(session, null, 2)); return { success: false, error: "Session missing requestorJWT (required for token exchange)", auditTrail: { timestamp: /* @__PURE__ */ new Date(), source: "delegation:postgresql", userId: session.userId, action: `postgresql_delegation:${action}`, success: false, reason: "Missing requestorJWT in session" } }; } console.log("[PostgreSQLModule] Performing token exchange:", { idpName: this.tokenExchangeConfig.idpName, audience: this.tokenExchangeConfig.audience, userId: session.userId, requestorJWTLength: requestorJWT.length }); const exchangeResult = await tokenExchangeService.performExchange({ subjectToken: requestorJWT, subjectTokenType: "urn:ietf:params:oauth:token-type:access_token", audience: this.tokenExchangeConfig.audience || "postgresql-delegation", tokenEndpoint: this.tokenExchangeConfig.tokenEndpoint, clientId: this.tokenExchangeConfig.clientId, clientSecret: this.tokenExchangeConfig.clientSecret, cache: this.tokenExchangeConfig.cache, sessionId: context?.sessionId // For token caching }); if (!exchangeResult.success || !exchangeResult.accessToken) { return { success: false, error: `Token exchange failed: ${exchangeResult.errorDescription || exchangeResult.error}`, auditTrail: { timestamp: /* @__PURE__ */ new Date(), source: "delegation:postgresql", userId: session.userId, action: `postgresql_delegation:${action}`, success: false, reason: `Token exchange error: ${exchangeResult.error}` } }; } const teClaims = tokenExchangeService.decodeTokenClaims(exchangeResult.accessToken); const requiredClaim = this.tokenExchangeConfig.requiredClaim || "legacy_name"; const claimValue = teClaims?.[requiredClaim]; if (!claimValue) { return { success: false, error: `TE-JWT missing required claim: ${requiredClaim}`, auditTrail: { timestamp: /* @__PURE__ */ new Date(), source: "delegation:postgresql", userId: session.userId, action: `postgresql_delegation:${action}`, success: false, reason: `TE-JWT missing ${requiredClaim} claim` } }; } effectiveLegacyUsername = claimValue; const rolesClaimPath = this.tokenExchangeConfig.rolesClaim || "roles"; teRoles = Array.isArray(teClaims?.[rolesClaimPath]) ? teClaims[rolesClaimPath] : []; console.log("[PostgreSQLModule] Token exchange successful:", { legacyUsername: effectiveLegacyUsername, roles: teRoles, rolesClaimPath, idpName: this.tokenExchangeConfig.idpName }); } catch (error) { return { success: false, error: error instanceof Error ? error.message : "Token exchange failed", auditTrail: { timestamp: /* @__PURE__ */ new Date(), source: "delegation:postgresql", userId: session.userId, action: `postgresql_delegation:${action}`, success: false, error: error instanceof Error ? error.message : "Unknown error" } }; } } if (!effectiveLegacyUsername) { return { success: false, error: "Unable to determine legacy username for PostgreSQL delegation (configure tokenExchange or provide legacyUsername in JWT)", auditTrail: { timestamp: /* @__PURE__ */ new Date(), source: "delegation:postgresql", userId: session.userId, action: `postgresql_delegation:${action}`, success: false, reason: "No legacy username available" } }; } console.log("[PostgreSQL] Proceeding with delegation action:", { action, effectiveLegacyUsername, userId: session.userId }); try { let result; switch (action) { case "query": console.log("[PostgreSQL] Routing to executeQuery handler with roles:", teRoles); result = await this.executeQuery(effectiveLegacyUsername, params, teRoles); break; case "schema": console.log("[PostgreSQL] Routing to getSchema handler"); result = await this.getSchema(effectiveLegacyUsername, params); break; case "table-details": result = await this.getTableDetails(effectiveLegacyUsername, params); break; default: return { success: false, error: `Unknown action: ${action}`, auditTrail: { timestamp: /* @__PURE__ */ new Date(), source: "delegation:postgresql", userId: session.userId, action: `postgresql_delegation:${action}`, success: false, reason: `Unknown action type: ${action}` } }; } return { success: true, data: result, auditTrail: { timestamp: /* @__PURE__ */ new Date(), source: "delegation:postgresql", userId: session.userId, action: `postgresql_delegation:${action}`, success: true, metadata: { legacyUsername: effectiveLegacyUsername, action, tokenExchangeUsed: !!this.tokenExchangeConfig } } }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : "PostgreSQL operation failed", auditTrail: { timestamp: /* @__PURE__ */ new Date(), source: "delegation:postgresql", userId: session.userId, action: `postgresql_delegation:${action}`, success: false, error: error instanceof Error ? error.message : "Unknown error", metadata: { legacyUsername: effectiveLegacyUsername, tokenExchangeUsed: !!this.tokenExchangeConfig } } }; } } /** * Validate user has access to PostgreSQL delegation */ async validateAccess(session) { return !!session.legacyUsername; } /** * Health check - verify PostgreSQL connection */ async healthCheck() { if (!this.pool || !this.isConnected) { return false; } try { const client = await this.pool.connect(); await client.query("SELECT 1"); client.release(); return true; } catch { return false; } } /** * Clean up resources */ async destroy() { if (this.pool) { await this.pool.end(); this.pool = null; this.isConnected = false; } } // ========================================================================== // Private Methods - PostgreSQL Operation Handlers // ========================================================================== /** * Execute SQL query with SET ROLE */ async executeQuery(roleName, params, teJwtRoles) { if (!this.pool) { throw new Error("PostgreSQL pool not initialized"); } this.validateSQL(params.sql, teJwtRoles); this.validateIdentifier(roleName); const client = await this.pool.connect(); try { await client.query(`SET ROLE ${this.escapeIdentifier(roleName)}`); const result = await client.query(params.sql, params.params || []); await client.query("RESET ROLE"); client.release(); const dataModificationCommands = ["INSERT", "UPDATE", "DELETE"]; if (dataModificationCommands.includes(result.command)) { return { success: true, rowCount: result.rowCount || 0, command: result.command, message: this.getOperationMessage(result.command, result.rowCount || 0) }; } else { return result.rows; } } catch (error) { try { await client.query("RESET ROLE"); } catch { } client.release(); throw error; } } /** * Generate user-friendly message for SQL operations */ getOperationMessage(command, rowCount) { switch (command) { case "INSERT": return `Successfully inserted ${rowCount} row${rowCount !== 1 ? "s" : ""}`; case "UPDATE": return `Successfully updated ${rowCount} row${rowCount !== 1 ? "s" : ""}`; case "DELETE": return `Successfully deleted ${rowCount} row${rowCount !== 1 ? "s" : ""}`; default: return `Operation completed successfully. Rows affected: ${rowCount}`; } } /** * Get database schema (tables list) */ async getSchema(roleName, params) { if (!this.pool) { throw new Error("PostgreSQL pool not initialized"); } this.validateIdentifier(roleName); const schemaName = params.schemaName || "public"; this.validateIdentifier(schemaName); const client = await this.pool.connect(); try { await client.query(`SET ROLE ${this.escapeIdentifier(roleName)}`); const result = await client.query( `SELECT table_name, table_type FROM information_schema.tables WHERE table_schema = $1 ORDER BY table_name`, [schemaName] ); await client.query("RESET ROLE"); client.release(); return result.rows; } catch (error) { try { await client.query("RESET ROLE"); } catch { } client.release(); throw error; } } /** * Get table details (columns, types, etc.) */ async getTableDetails(roleName, params) { if (!this.pool) { throw new Error("PostgreSQL pool not initialized"); } this.validateIdentifier(roleName); this.validateIdentifier(params.tableName); const schemaName = params.schemaName || "public"; this.validateIdentifier(schemaName); const client = await this.pool.connect(); try { await client.query(`SET ROLE ${this.escapeIdentifier(roleName)}`); const result = await client.query( `SELECT column_name, data_type, is_nullable, column_default, character_maximum_length FROM information_schema.columns WHERE table_schema = $1 AND table_name = $2 ORDER BY ordinal_position`, [schemaName, params.tableName] ); await client.query("RESET ROLE"); client.release(); return result.rows; } catch (error) { try { await client.query("RESET ROLE"); } catch { } client.release(); throw error; } } // ========================================================================== // Security Validators // ========================================================================== /** * Validate SQL query based on user roles from TE-JWT * * Role-based command authorization: * - sql-read: SELECT only * - sql-write: SELECT, INSERT, UPDATE, DELETE * - sql-admin: All commands except dangerous operations * - admin: All commands including dangerous operations * * @param sqlQuery - SQL query to validate * @param roles - User roles from TE-JWT (optional, defaults to no restrictions) */ validateSQL(sqlQuery, roles) { const upperSQL = sqlQuery.trim().toUpperCase(); const commandMatch = upperSQL.match(/^(SELECT|INSERT|UPDATE|DELETE|DROP|CREATE|ALTER|TRUNCATE|GRANT|REVOKE|WITH|EXPLAIN|SHOW|DESCRIBE)/); const command = commandMatch ? commandMatch[1] : null; if (!command) { throw createSecurityError( "POSTGRESQL_INVALID_SQL", "Unable to determine SQL command type", 400 ); } if (roles && roles.length > 0) { const hasReadAccess = roles.some( (role) => ["sql-read", "sql-write", "sql-admin", "admin"].includes(role) ); const hasWriteAccess = roles.some( (role) => ["sql-write", "sql-admin", "admin"].includes(role) ); const hasAdminAccess = roles.some( (role) => ["sql-admin", "admin"].includes(role) ); const hasSuperAdminAccess = roles.some( (role) => ["admin"].includes(role) ); const readCommands = ["SELECT", "WITH", "EXPLAIN", "SHOW", "DESCRIBE"]; const writeCommands = ["INSERT", "UPDATE", "DELETE"]; const adminCommands = ["CREATE", "ALTER", "GRANT", "REVOKE"]; const dangerousCommands = ["DROP", "TRUNCATE"]; if (readCommands.includes(command)) { if (!hasReadAccess) { throw createSecurityError( "POSTGRESQL_INSUFFICIENT_PERMISSIONS", `Insufficient permissions to execute ${command} operation.`, 403 ); } } else if (writeCommands.includes(command)) { if (!hasWriteAccess) { throw createSecurityError( "POSTGRESQL_INSUFFICIENT_PERMISSIONS", `Insufficient permissions to execute ${command} operation.`, 403 ); } } else if (adminCommands.includes(command)) { if (!hasAdminAccess) { throw createSecurityError( "POSTGRESQL_INSUFFICIENT_PERMISSIONS", `Insufficient permissions to execute ${command} operation.`, 403 ); } } else if (dangerousCommands.includes(command)) { if (!hasSuperAdminAccess) { throw createSecurityError( "POSTGRESQL_DANGEROUS_OPERATION", `Insufficient permissions to execute ${command} operation.`, 403 ); } } else { if (!hasAdminAccess) { throw createSecurityError( "POSTGRESQL_UNKNOWN_COMMAND", `Insufficient permissions to execute ${command} operation.`, 403 ); } } } else { const dangerous = [ "DROP", "CREATE", "ALTER", "TRUNCATE", "GRANT", "REVOKE" ]; for (const keyword of dangerous) { if (upperSQL.includes(keyword)) { throw createSecurityError( "POSTGRESQL_DANGEROUS_OPERATION", `Dangerous SQL operation blocked: ${keyword}`, 403 ); } } } } /** * Validate SQL identifier format */ validateIdentifier(identifier) { const pattern = /^[a-zA-Z_][a-zA-Z0-9_]*$/; if (!pattern.test(identifier)) { throw createSecurityError( "POSTGRESQL_INVALID_IDENTIFIER", `Invalid PostgreSQL identifier: ${identifier}`, 400 ); } } /** * Escape identifier for PostgreSQL */ escapeIdentifier(identifier) { return `"${identifier.replace(/"/g, '""')}"`; } }; // src/sql-module.ts import sql from "mssql"; import { createSecurityError as createSecurityError2 } from "fastmcp-oauth/core"; var SQLDelegationModule = class { name; type = "database"; pool = null; config = null; isConnected = false; tokenExchangeService = null; tokenExchangeConfig = null; /** * Create a new SQL Server delegation module * * @param name - Module name (e.g., 'sql', 'sql1', 'sql2') * Defaults to 'sql' for backward compatibility */ constructor(name = "sql") { this.name = name; } /** * Initialize SQL connection pool * * Per-Module Token Exchange (Phase 2): * - Token exchange config comes from config.tokenExchange (not injected separately) * - Token exchange happens on-demand in delegate() method * - TokenExchangeService can be injected via context parameter in delegate() * * @param config - SQL Server configuration (includes optional tokenExchange) * @throws Error if connection fails */ async initialize(config) { if (this.isConnected) { return; } this.config = config; if (config.tokenExchange) { this.tokenExchangeConfig = config.tokenExchange; console.log(`[SQLModule:${this.name}] Token exchange enabled with IDP:`, config.tokenExchange.idpName); } try { const connectionConfig = { server: config.server, database: config.database, options: { ...config.options, trustServerCertificate: config.options?.trustServerCertificate ?? false, encrypt: true // MANDATORY: Always encrypt connections }, pool: { max: config.pool?.max ?? 10, min: config.pool?.min ?? 0, idleTimeoutMillis: config.pool?.idleTimeoutMillis ?? 3e4 }, connectionTimeout: config.connectionTimeout ?? 5e3, requestTimeout: config.requestTimeout ?? 3e4 }; this.pool = new sql.ConnectionPool(connectionConfig); await this.pool.connect(); this.isConnected = true; } catch (error) { throw createSecurityError2( "SQL_CONNECTION_FAILED", `Failed to initialize SQL connection: ${error instanceof Error ? error.message : error}`, 500 ); } } /** * Delegate SQL operation on behalf of user * * Per-Module Token Exchange (Phase 2): * - Gets TokenExchangeService from context.coreContext * - Uses session.requestorJWT (not session.claims.access_token) * - Validates TE-JWT with module's specific IDP (idpName) * * @param session - User session with requestorJWT * @param action - Action type: 'query', 'procedure', 'function' * @param params - Action parameters * @param context - Context with sessionId and coreContext * @returns Delegation result with audit trail */ async delegate(session, action, params, context) { if (!this.isConnected || !this.pool) { try { if (!this.config) { throw new Error("SQL module not initialized. Call initialize() first."); } await this.initialize(this.config); } catch (error) { return { success: false, error: error instanceof Error ? error.message : "Initialization failed", auditTrail: { timestamp: /* @__PURE__ */ new Date(), source: `delegation:${this.name}`, userId: session.userId, action: `${this.name}_delegation:${action}`, success: false, error: error instanceof Error ? error.message : "Unknown error" } }; } } let effectiveLegacyUsername = session.legacyUsername; if (this.tokenExchangeConfig) { try { const tokenExchangeService = context?.coreContext?.tokenExchangeService; if (!tokenExchangeService) { return { success: false, error: "Token exchange configured but TokenExchangeService not available in context", auditTrail: { timestamp: /* @__PURE__ */ new Date(), source: `delegation:${this.name}`, userId: session.userId, action: `${this.name}_delegation:${action}`, success: false, reason: "TokenExchangeService not in context" } }; } const requestorJWT = session.requestorJWT; if (!requestorJWT) { return { success: false, error: "Session missing requestorJWT (required for token exchange)", auditTrail: { timestamp: /* @__PURE__ */ new Date(), source: `delegation:${this.name}`, userId: session.userId, action: `${this.name}_delegation:${action}`, success: false, reason: "Missing requestorJWT in session" } }; } console.log("[SQLModule] Performing token exchange:", { idpName: this.tokenExchangeConfig.idpName, audience: this.tokenExchangeConfig.audience, userId: session.userId }); const exchangeResult = await tokenExchangeService.performExchange({ subjectToken: requestorJWT, subjectTokenType: "urn:ietf:params:oauth:token-type:jwt", audience: this.tokenExchangeConfig.audience || "sql-delegation", tokenEndpoint: this.tokenExchangeConfig.tokenEndpoint, clientId: this.tokenExchangeConfig.clientId, clientSecret: this.tokenExchangeConfig.clientSecret, sessionId: context?.sessionId // For token caching }); if (!exchangeResult.success || !exchangeResult.accessToken) { return { success: false, error: `Token exchange failed: ${exchangeResult.errorDescription || exchangeResult.error}`, auditTrail: { timestamp: /* @__PURE__ */ new Date(), source: `delegation:${this.name}`, userId: session.userId, action: `${this.name}_delegation:${action}`, success: false, reason: `Token exchange error: ${exchangeResult.error}` } }; } const teClaims = tokenExchangeService.decodeTokenClaims(exchangeResult.accessToken); const requiredClaim = this.tokenExchangeConfig.requiredClaim || "legacy_name"; const claimValue = teClaims?.[requiredClaim]; if (!claimValue) { return { success: false, error: `TE-JWT missing required claim: ${requiredClaim}`, auditTrail: { timestamp: /* @__PURE__ */ new Date(), source: `delegation:${this.name}`, userId: session.userId, action: `${this.name}_delegation:${action}`, success: false, reason: `TE-JWT missing ${requiredClaim} claim` } }; } effectiveLegacyUsername = claimValue; console.log("[SQLModule] Token exchange successful:", { legacyUsername: effectiveLegacyUsername, idpName: this.tokenExchangeConfig.idpName }); } catch (error) { return { success: false, error: error instanceof Error ? error.message : "Token exchange failed", auditTrail: { timestamp: /* @__PURE__ */ new Date(), source: `delegation:${this.name}`, userId: session.userId, action: `sql_delegation:${action}`, success: false, error: error instanceof Error ? error.message : "Unknown error" } }; } } if (!effectiveLegacyUsername) { return { success: false, error: "Unable to determine legacy username for SQL delegation (configure tokenExchange or provide legacyUsername in JWT)", auditTrail: { timestamp: /* @__PURE__ */ new Date(), source: `delegation:${this.name}`, userId: session.userId, action: `sql_delegation:${action}`, success: false, reason: "No legacy username available" } }; } try { let result; switch (action) { case "query": result = await this.executeQuery(effectiveLegacyUsername, params); break; case "procedure": result = await this.executeProcedure(effectiveLegacyUsername, params); break; case "function": result = await this.executeFunction(effectiveLegacyUsername, params); break; default: return { success: false, error: `Unknown action: ${action}`, auditTrail: { timestamp: /* @__PURE__ */ new Date(), source: `delegation:${this.name}`, userId: session.userId, action: `${this.name}_delegation:${action}`, success: false, reason: `Unknown action type: ${action}` } }; } return { success: true, data: result, auditTrail: { timestamp: /* @__PURE__ */ new Date(), source: `delegation:${this.name}`, userId: session.userId, action: `sql_delegation:${action}`, success: true, metadata: { legacyUsername: effectiveLegacyUsername, action, tokenExchangeUsed: !!this.tokenExchangeService } } }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : "SQL operation failed", auditTrail: { timestamp: /* @__PURE__ */ new Date(), source: `delegation:${this.name}`, userId: session.userId, action: `sql_delegation:${action}`, success: false, error: error instanceof Error ? error.message : "Unknown error", metadata: { legacyUsername: effectiveLegacyUsername, tokenExchangeUsed: !!this.tokenExchangeService } } }; } } /** * Validate user has access to SQL delegation * * @param session - User session * @returns true if user has legacyUsername (required for delegation) */ async validateAccess(session) { return !!session.legacyUsername; } /** * Health check - verify SQL connection * * @returns true if connected and healthy */ async healthCheck() { if (!this.pool || !this.isConnected) { return false; } try { const request = this.pool.request(); await request.query("SELECT 1"); return true; } catch { return false; } } /** * Clean up resources */ async destroy() { if (this.pool) { await this.pool.close(); this.pool = null; this.isConnected = false; } } // ========================================================================== // Private Methods - SQL Operation Handlers // ========================================================================== /** * Execute SQL query with EXECUTE AS USER * * @param legacyUsername - Legacy SAM account name * @param params - Query parameters * @returns Query results */ async executeQuery(legacyUsername, params) { if (!this.pool) { throw new Error("SQL pool not initialized"); } this.validateSQL(params.sql); this.validateIdentifier(legacyUsername); const request = this.pool.request(); try { await request.query(`EXECUTE AS USER = '${this.escapeSingleQuotes(legacyUsername)}'`); if (params.params) { for (const [key, value] of Object.entries(params.params)) { request.input(key, value); } } const result = await request.query(params.sql); await request.query("REVERT"); return result.recordset; } catch (error) { try { await request.query("REVERT"); } catch { } throw error; } } /** * Execute stored procedure with EXECUTE AS USER */ async executeProcedure(legacyUsername, params) { if (!this.pool) { throw new Error("SQL pool not initialized"); } this.validateIdentifier(legacyUsername); this.validateIdentifier(params.procedure); const request = this.pool.request(); try { await request.query(`EXECUTE AS USER = '${this.escapeSingleQuotes(legacyUsername)}'`); if (params.params) { for (const [key, value] of Object.entries(params.params)) { request.input(key, value); } } const result = await request.execute(params.procedure); await request.query("REVERT"); return result.recordset; } catch (error) { try { await request.query("REVERT"); } catch { } throw error; } } /** * Execute scalar function with EXECUTE AS USER */ async executeFunction(legacyUsername, params) { if (!this.pool) { throw new Error("SQL pool not initialized"); } this.validateIdentifier(legacyUsername); this.validateIdentifier(params.functionName); const request = this.pool.request(); try { await request.query(`EXECUTE AS USER = '${this.escapeSingleQuotes(legacyUsername)}'`); if (params.params) { for (const [key, value] of Object.entries(params.params)) { request.input(key, value); } } const paramList = params.params ? Object.keys(params.params).map((k) => `@${k}`).join(", ") : ""; const result = await request.query(`SELECT ${params.functionName}(${paramList}) AS result`); await request.query("REVERT"); return result.recordset[0].result; } catch (error) { try { await request.query("REVERT"); } catch { } throw error; } } // ========================================================================== // Security Validators // ========================================================================== /** * Validate SQL query for dangerous operations */ validateSQL(sqlQuery) { const dangerous = [ "DROP", "CREATE", "ALTER", "TRUNCATE", "xp_cmdshell", "sp_executesql" ]; const upperSQL = sqlQuery.toUpperCase(); for (const keyword of dangerous) { if (upperSQL.includes(keyword)) { throw createSecurityError2( "SQL_DANGEROUS_OPERATION", `Dangerous SQL operation blocked: ${keyword}`, 403 ); } } } /** * Validate SQL identifier format */ validateIdentifier(identifier) { const pattern = /^[a-zA-Z_][a-zA-Z0-9_\\]*$/; if (!pattern.test(identifier)) { throw createSecurityError2( "SQL_INVALID_IDENTIFIER", `Invalid SQL identifier: ${identifier}`, 400 ); } } /** * Escape single quotes in string for SQL */ escapeSingleQuotes(str) { return str.replace(/'/g, "''"); } }; export { PostgreSQLDelegationModule, SQLDelegationModule }; //# sourceMappingURL=index.js.map