Skip to main content
Glama
gam-executor.js7.33 kB
import { spawn } from 'child_process'; import { z } from 'zod'; /** * GAM Command Executor * Handles execution of GAM commands with proper validation, error handling, and output parsing */ export class GamExecutor { gamPath; timeout; allowedCommands; constructor(gamPath = 'gam', timeout = 30000) { this.gamPath = gamPath; this.timeout = timeout; // Whitelist of allowed GAM commands for security this.allowedCommands = new Set([ 'version', 'info', 'print', 'create', 'update', 'delete', 'add', 'remove', 'move', 'suspend', 'unsuspend', 'show', 'list' ]); } /** * Validates if a GAM command is allowed */ validateCommand(command) { const baseCommand = command.split(' ')[0]; return baseCommand ? this.allowedCommands.has(baseCommand) : false; } /** * Sanitizes command parameters to prevent injection attacks */ sanitizeParams(params) { return params.map(param => { // Remove any characters that could be used for command injection return param.replace(/[;&|`$(){}[\]<>]/g, ''); }); } /** * Executes a GAM command and returns the result */ async executeCommand(args) { const startTime = Date.now(); // Validate command const firstArg = args[0]; if (!firstArg || !this.validateCommand(firstArg)) { throw new Error(`Command not allowed: ${firstArg}`); } // Sanitize parameters const sanitizedArgs = this.sanitizeParams(args); // Log command execution (without sensitive data) console.log(`[GAM] Executing: ${this.gamPath} ${sanitizedArgs.join(' ')}`); return new Promise((resolve, reject) => { const options = { timeout: this.timeout, stdio: ['pipe', 'pipe', 'pipe'] }; const gamProcess = spawn(this.gamPath, sanitizedArgs, options); let stdout = ''; let stderr = ''; let exitCode = null; // Handle stdout gamProcess.stdout?.on('data', (data) => { stdout += data.toString(); }); // Handle stderr gamProcess.stderr?.on('data', (data) => { stderr += data.toString(); }); // Handle process completion gamProcess.on('close', (code) => { exitCode = code; const executionTime = Date.now() - startTime; console.log(`[GAM] Command completed in ${executionTime}ms with exit code: ${code}`); if (code === 0) { resolve({ success: true, stdout: stdout.trim(), stderr: stderr.trim(), executionTime, exitCode }); } else { resolve({ success: false, stdout: stdout.trim(), stderr: stderr.trim(), executionTime, exitCode, error: this.parseGamError(stderr) }); } }); // Handle process errors gamProcess.on('error', (error) => { console.error(`[GAM] Process error: ${error.message}`); reject(new Error(`GAM execution failed: ${error.message}`)); }); // Handle timeout gamProcess.on('timeout', () => { gamProcess.kill(); reject(new Error(`GAM command timed out after ${this.timeout}ms`)); }); }); } /** * Parses GAM error messages to provide more helpful error information */ parseGamError(stderr) { if (stderr.includes('Authentication failed')) { return 'GAM authentication failed. Please check your credentials and run "gam oauth create".'; } if (stderr.includes('Quota exceeded')) { return 'Google API quota exceeded. Please try again later.'; } if (stderr.includes('Rate limit exceeded')) { return 'Rate limit exceeded. Please wait before retrying.'; } if (stderr.includes('Not found')) { return 'Resource not found. Please check the provided identifier.'; } if (stderr.includes('Permission denied')) { return 'Permission denied. Please check your Google Workspace admin privileges.'; } return stderr || 'Unknown GAM error occurred'; } /** * Checks if GAM is properly installed and configured */ async checkGamInstallation() { try { const result = await this.executeCommand(['version']); return result.success; } catch (error) { console.error('[GAM] Installation check failed:', error); return false; } } /** * Parses CSV output from GAM commands */ parseCsvOutput(output) { if (!output.trim()) { return []; } const lines = output.trim().split('\n'); if (lines.length < 2) { return []; } const firstLine = lines[0]; if (!firstLine) { return []; } const headers = firstLine.split(',').map(h => h.trim().replace(/"/g, '')); const results = []; for (let i = 1; i < lines.length; i++) { const line = lines[i]; if (!line) continue; const values = line.split(',').map(v => v.trim().replace(/"/g, '')); const row = {}; headers.forEach((header, index) => { row[header] = values[index] || ''; }); results.push(row); } return results; } /** * Executes a GAM command and returns parsed CSV output */ async executeCommandWithCsv(args) { const result = await this.executeCommand(args); if (!result.success) { return { ...result, parsedData: [] }; } const parsedData = this.parseCsvOutput(result.stdout); return { ...result, parsedData }; } } /** * Validation schemas for common GAM parameters */ export const GamSchemas = { email: z.string().email('Invalid email format'), domain: z.string().regex(/^[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/, 'Invalid domain format'), orgUnitPath: z.string().regex(/^\/.*/, 'Org unit path must start with /'), maxResults: z.number().int().min(1).max(1000).optional(), query: z.string().max(1000).optional(), role: z.enum(['MEMBER', 'MANAGER', 'OWNER']).optional(), firstName: z.string().min(1).max(100), lastName: z.string().min(1).max(100), password: z.string().min(8).max(100).optional(), groupName: z.string().min(1).max(100), description: z.string().max(500).optional() }; //# sourceMappingURL=gam-executor.js.map

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/dbarks/mcp-gam'

If you have feedback or need assistance with the MCP directory API, please join our Discord server