Skip to content

Commit

Permalink
Merge pull request #696 from SquirrelCorporation/chore-remove-sudo-rs…
Browse files Browse the repository at this point in the history
…i-on-start

[CHORE] Refactor SSH command execution to streamline privilege elevation
  • Loading branch information
SquirrelDeveloper authored Feb 3, 2025
2 parents b1f4712 + c5bd4a2 commit 23ad62d
Show file tree
Hide file tree
Showing 3 changed files with 36 additions and 78 deletions.
24 changes: 12 additions & 12 deletions server/src/helpers/sudo/sudo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,51 +18,51 @@ export async function generateSudoCommand(deviceUuid: string): Promise<string> {
switch (method) {
case SsmAnsible.AnsibleBecomeMethod.SUDO:
if (user && password) {
return `echo ${JSON.stringify(password)} | su - ${user}`;
return `echo ${JSON.stringify(password)} | su - ${user} -c "%command%"`;
}
// Sudo command with password
if (password) {
return `echo ${JSON.stringify(password)} | sudo -S -p "" true`;
return `echo ${JSON.stringify(password)} | sudo -S -p "" %command%`;
}
// Sudo without password
return user ? `sudo -u ${user} true` : `sudo true`;
return user ? `sudo -u ${user} %command%` : `sudo %command%`;

case SsmAnsible.AnsibleBecomeMethod.SU:
// su command with password
if (password) {
return `echo ${JSON.stringify(password)} | su ${user || ''}`;
return `echo ${JSON.stringify(password)} | su ${user || ''} -c "%command%"`;
}
// su without password
return `su ${user || ''}`;

case SsmAnsible.AnsibleBecomeMethod.PBRUN:
// pbrun doesn't natively support password passing; use user if available
return user ? `pbrun -u ${user}` : `pbrun`;
return user ? `pbrun -u ${user} %command%` : `pbrun %command%`;

case SsmAnsible.AnsibleBecomeMethod.PFEXEC:
// pfexec (no password support; only provide user if available)
return user ? `pfexec -u ${user}` : `pfexec`;
return user ? `pfexec -u ${user} %command%` : `pfexec %command%`;

case SsmAnsible.AnsibleBecomeMethod.DOAS:
// doas (openbsd tool, may support user config in doas.conf)
return user ? `doas -u ${user}` : `doas`;
return user ? `doas -u ${user} %command%` : `doas %command%`;

case SsmAnsible.AnsibleBecomeMethod.DZDO:
// dzdo (Powerbroker; usually interactive, user pre-configured)
if (user) {
return password
? `echo ${JSON.stringify(password)} | dzdo -u ${user} true`
: `dzdo -u ${user} true`;
? `echo ${JSON.stringify(password)} | dzdo -u ${user} %command%`
: `dzdo -u ${user} %command%`;
}
return `dzdo true`;
return `dzdo %command%`;

case SsmAnsible.AnsibleBecomeMethod.KSU:
// ksu (Kerberos su, user optional; may not support password)
return user ? `ksu ${user}` : `ksu`;
return user ? `ksu ${user} %command%` : `ksu %command%`;

case SsmAnsible.AnsibleBecomeMethod.RUNAS:
// runas (Windows-specific, password likely provided interactively)
return user ? `runas /user:${user}` : `runas`;
return user ? `runas /user:${user} %command%` : `runas %command%`;

default:
throw new Error(`Unsupported method: ${method}`);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import Component from './Component';
export default class RemoteSSHExecutorComponent extends Component {
private sshClient: Client | null = null;
private connectionConfig!: ConnectConfig;
private isElevated: boolean = false; // To track if privilege elevation was successful
private keepAliveInterval: NodeJS.Timeout | undefined;
private reconnectDelay = 5000; // Delay before retrying connection in case of failure
private isExecuting: boolean = false;
Expand All @@ -31,18 +30,30 @@ export default class RemoteSSHExecutorComponent extends Component {
return new Promise((resolve, reject) => {
process.nextTick(async () => {
try {
// If we're not connected or elevated, wait for reconnection
if (!this.sshClient || !this.isElevated) {
// If we're not connected, wait for reconnection
if (!this.sshClient) {
await this.reconnect();
if (!this.sshClient || !this.isElevated) {
return reject(new Error('SSH Client not connected or privilege elevation failed'));
if (!this.sshClient) {
return reject(new Error('SSH Client not connected'));
}
}

const maxBuffer = options?.maxBuffer ?? Infinity;
const encoding: BufferEncoding = options?.encoding ?? 'utf8';

this.logger.debug(`Running command: ${command}`);
// Get the sudo command and prepend it to the actual command if needed
let finalCommand = command;
if (options?.elevatePrivilege) {
try {
const sudoCmd = await generateSudoCommand(this.configuration.deviceUuid);
finalCommand = sudoCmd.replace('%command%', command);
} catch (error) {
this.logger.error('Failed to generate sudo command:', error);
return reject(new Error('Failed to generate sudo command'));
}
}

this.logger.debug(`Running command: ${finalCommand}`);

let result = '';
let errorOutput = '';
Expand All @@ -52,7 +63,7 @@ export default class RemoteSSHExecutorComponent extends Component {
let isResolved = false; // To ensure no double resolve/reject

try {
this.sshClient.exec(command, (err, stream) => {
this.sshClient.exec(finalCommand, (err, stream) => {
if (err) {
return reject({ err, result: '' });
}
Expand All @@ -64,11 +75,11 @@ export default class RemoteSSHExecutorComponent extends Component {
isResolved = true;

if (exitCode === 0) {
this.logger.debug(`Command executed successfully: ${command}`);
this.logger.debug(`Command executed successfully: ${finalCommand}`);
resolve(result.trim());
} else {
const error = new Error(
`Command "${command}" failed with code ${exitCode}, signal: ${exitSignal}, stderr: "${errorOutput.trim()}"`,
`Command "${finalCommand}" failed with code ${exitCode}, signal: ${exitSignal}, stderr: "${errorOutput.trim()}"`,
);
this.logger.debug(error.message);
reject({ err: error, result: result.trim() });
Expand Down Expand Up @@ -101,18 +112,18 @@ export default class RemoteSSHExecutorComponent extends Component {
exitCode = code;
exitSignal = signal;
this.logger.debug(
`Command "${command}" exited with code ${code} and signal ${signal}. Awaiting close event to finalize...`,
`Command "${finalCommand}" exited with code ${code} and signal ${signal}. Awaiting close event to finalize...`,
);
})
.on('close', () => {
this.logger.debug(`Command "${command}" stream closed. Finalizing...`);
this.logger.debug(`Command "${finalCommand}" stream closed. Finalizing...`);

if (exitCode === null) {
// If `exit` signal was not received before `close`
if (!isResolved) {
isResolved = true;
this.logger.warn(
`Command "${command}" closed without receiving an exit event. Assuming output is complete...`,
`Command "${finalCommand}" closed without receiving an exit event. Assuming output is complete...`,
);
reject({
err: new Error(
Expand Down Expand Up @@ -178,11 +189,6 @@ export default class RemoteSSHExecutorComponent extends Component {
conn
.on('ready', async () => {
this.logger.info('SSH Connection established');
try {
await this.elevatePrivilege();
} catch (err) {
reject(err);
}
retryAttempt = 0;
resolve(); // Connection successful
})
Expand All @@ -202,62 +208,13 @@ export default class RemoteSSHExecutorComponent extends Component {
});
}

// Sudo with password
private async elevatePrivilege() {
// Command to execute sudo with a TTY and password
try {
const command = await generateSudoCommand(this.configuration.deviceUuid);
this.logger.debug(`Elevation command: ${command}`);
return new Promise<void>((resolve, reject) => {
if (!this.sshClient) {
return reject(new Error('SSH Client not connected'));
}
this.sshClient.exec(command, { pty: true }, (err, stream) => {
if (err) {
this.logger.error(`Error during privilege elevation with password: ${err.message}`);
return reject(err);
}
stream
.on('close', (code: number) => {
if (code === 0) {
this.logger.info('Privilege elevation successful (with sudo password)');
this.isElevated = true;
this.startKeepAlive();
return resolve();
} else {
const msg = `Privilege elevation with password failed with code ${code}`;
this.logger.error(msg);
return reject(new Error(msg));
}
})
.stdout.on('data', (data: any) => {
this.logger.debug(`Privilege elevation stdout: ${data.toString()}`);
})
.stderr.on('data', (data) => {
this.logger.error(`Privilege elevation stderr: ${data.toString()}`);
});
});
});
} catch (err: any) {
this.logger.error(`Error during privilege elevation with password: ${err?.message}`);
}
}

// Reconnection logic
private async reconnect(retryAttempt = 0): Promise<void> {
// If already reconnecting, return the existing promise
if (this.reconnectPromise) {
return this.reconnectPromise;
}

this.isElevated = false;

if (retryAttempt > 3) {
this.reconnectPromise = null;
this.logger.error('Maximum reconnection attempts reached, shutting down SSH Manager');
throw new Error('Maximum reconnection attempts reached');
}

this.logger.info(`Reconnecting in ${this.reconnectDelay / 1000} seconds...`);

// Create and store the reconnection promise
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export interface RemoteExecOptions {
maxBuffer?: number | undefined;
encoding?: BufferEncoding | null | undefined;
env?: NodeJS.ProcessEnv & { LANG: string };
elevatePrivilege?: boolean;
}

export type RemoteExecutorType = (cmd: string, options?: RemoteExecOptions) => Promise<string>;
Expand Down

0 comments on commit 23ad62d

Please sign in to comment.