]> git.wincent.com - wincent.git/commitdiff
feat: add kind-of working sudo functionality
authorGreg Hurrell <greg@hurrell.net>
Mon, 30 Mar 2020 23:24:29 +0000 (01:24 +0200)
committerGreg Hurrell <greg@hurrell.net>
Tue, 31 Mar 2020 00:02:27 +0000 (02:02 +0200)
src/Fig/operations/template.ts
src/capture.ts [deleted file]
src/prompt.ts
src/sudo.ts [new file with mode: 0644]

index 6d7531aca829b9928f6f95586d230373e5e9103a..42c2e943d213d1fe1470740e5e0a98f93f6bb67e 100644 (file)
@@ -2,6 +2,7 @@ import * as fs from 'fs';
 
 import {log} from '../../console';
 import expand from '../../expand';
+import sudo from '../../sudo';
 import {compile, fill} from '../../template';
 import Context from '../Context';
 
@@ -28,9 +29,16 @@ export default async function template({
   if (owner && owner !== Context.attributes.username) {
     log.notice(`needs sudo: ${Context.attributes.username} -> ${owner}`);
     const passphrase = await Context.sudoPassphrase;
-    // obviously not going to log this in real life:
-    log.debug(`got: ${passphrase}`);
+    const result = await sudo('ls', ['-l', '/var/audit'], {passphrase});
+
+    console.log(result);
+
     // chown in node works with numeric uid and gid
+    // TODO: is there a way to check when sudo pass has expired?
+    // can run `sudo -v` to validate, but it may prompt
+    // how to pass password to sudo?
+    // may be able to hack it with askpass option and fake helper
+    // can use -S/--stdin it seems
   } else {
     // open, write, mode
     // can't chown, i think? without uid and gid
diff --git a/src/capture.ts b/src/capture.ts
deleted file mode 100644 (file)
index b5eaef8..0000000
+++ /dev/null
@@ -1,32 +0,0 @@
-import {spawn} from 'child_process';
-
-// TODO decide if we still need this, and we can make it async
-export default async function capture(command: string, ...args: Array<string>) {
-  return new Promise((resolve, reject) => {
-    const child = spawn(command, args, {
-      stdio: ['inherit', 'pipe', 'inherit'],
-    });
-
-    let stdout = '';
-
-    child.stdout.on('data', (data) => (stdout += data.toString()));
-
-    child.on('close', (code, signal) => {
-      if (code) {
-        reject(
-          new Error(
-            `Exit code ${code} returned from: ${[command, ...args].join(' ')}`
-          )
-        );
-      } else if (signal) {
-        reject(
-          new Error(
-            `Signal ${signal} received by: ${[command, ...args].join(' ')}`
-          )
-        );
-      } else {
-        resolve(stdout.trimEnd());
-      }
-    });
-  });
-}
index efa35343d3630a86c2a68c76ccd3f9b9026c3dda..8b7834c97612740165527bfc4f3995cd83f5ae7a 100644 (file)
@@ -14,9 +14,7 @@ export default async function prompt(
   // https://stackoverflow.com/a/33500118/2103996
   const stdout = new Writable({
     write: (chunk, _encoding, callback) => {
-      if (muted) {
-        process.stdout.write('*'.repeat(chunk.toString().length));
-      } else {
+      if (!muted) {
         process.stdout.write(chunk);
       }
       callback();
@@ -31,9 +29,12 @@ export default async function prompt(
   });
 
   try {
-    const response = new Promise<string>((resolve) =>
-      rl.question(text, resolve)
-    );
+    const response = new Promise<string>((resolve) => {
+      rl.question(text, (response) => {
+        process.stdout.write('\n');
+        resolve(response);
+      });
+    });
 
     muted = !!options.private;
 
diff --git a/src/sudo.ts b/src/sudo.ts
new file mode 100644 (file)
index 0000000..9b7685c
--- /dev/null
@@ -0,0 +1,81 @@
+import * as child_process from 'child_process';
+import {randomBytes} from 'crypto';
+
+type Options = {
+  passphrase: string;
+};
+
+type Result = {
+  error: Error | null;
+  signal: string | null;
+  status: number | null;
+  stderr: string;
+  stdout: string;
+};
+
+export default async function sudo(
+  command: string,
+  args: Array<string>,
+  options: Options
+): Promise<Result> {
+  return new Promise((resolve, reject) => {
+    const result = {
+      error: null,
+      signal: null,
+      status: null,
+      stderr: '',
+      stdout: '',
+    };
+
+    const PROMPT_TEXT = `sudo[${randomBytes(16).toString('hex')}]:`;
+
+    const child = child_process.spawn('sudo', [
+      '-S',
+      '-p',
+      PROMPT_TEXT,
+      '--',
+      command,
+      ...args,
+    ]);
+
+    // Sadly, we'll still see "Sorry, try again" if the wrong password is
+    // supplied, because sudo is logging it directly to /dev/tty, not to stderr.
+    //
+    // See: https://github.com/sudo-project/sudo/blob/972670bf/plugins/sudoers/sudo_printf.c#L47
+    child.stderr.on('data', (data) => {
+      if (data.toString() === PROMPT_TEXT) {
+        child.stdin.write(`${options.passphrase}\n`);
+
+        // No point in retrying; by calling `end()` here we'll get one shot.
+        child.stdin.end();
+      } else {
+        result.stderr += data.toString();
+      }
+    });
+
+    child.stdout.on('data', (data) => {
+      result.stdout += data.toString();
+    });
+
+    child.on('error', (error) =>
+      reject({
+        ...result,
+        error,
+      })
+    );
+
+    child.on('exit', (status, signal) => {
+      if (typeof status === 'number') {
+        resolve({
+          ...result,
+          status,
+        });
+      } else if (signal) {
+        resolve({
+          ...result,
+          status,
+        });
+      }
+    });
+  });
+}