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