@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
JavaScript
// 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