diff --git a/src/exec_auth.ts b/src/exec_auth.ts index 34425af5b6..d6eb0a0bf7 100644 --- a/src/exec_auth.ts +++ b/src/exec_auth.ts @@ -20,10 +20,10 @@ export interface Credential { export class ExecAuth implements Authenticator { private readonly tokenCache: { [key: string]: Credential | null } = {}; private execFn: ( - cmd: string, - args: string[], - opts: child_process.SpawnOptions, - ) => child_process.SpawnSyncReturns = child_process.spawnSync; + command: string, + args?: readonly string[], + options?: child_process.SpawnOptionsWithoutStdio, + ) => child_process.ChildProcessWithoutNullStreams = child_process.spawn; public isAuthProvider(user: User): boolean { if (!user) { @@ -41,7 +41,7 @@ export class ExecAuth implements Authenticator { } public async applyAuthentication(user: User, opts: https.RequestOptions): Promise { - const credential = this.getCredential(user); + const credential = await this.getCredential(user); if (!credential) { return; } @@ -70,7 +70,7 @@ export class ExecAuth implements Authenticator { return null; } - private getCredential(user: User): Credential | null { + private async getCredential(user: User): Promise { // TODO: Add a unit test for token caching. const cachedToken = this.tokenCache[user.name]; if (cachedToken) { @@ -99,15 +99,45 @@ export class ExecAuth implements Authenticator { exec.env.forEach((elt) => (env[elt.name] = elt.value)); opts = { ...opts, env }; } - const result = this.execFn(exec.command, exec.args, opts); - if (result.error) { - throw result.error; - } - if (result.status === 0) { - const obj = JSON.parse(result.stdout.toString('utf8')) as Credential; - this.tokenCache[user.name] = obj; - return obj; - } - throw new Error(result.stderr.toString('utf8')); + + return new Promise((resolve, reject) => { + let stdoutData: string = ''; + let stderrData: string = ''; + let savedError: Error | undefined = undefined; + + const subprocess = this.execFn(exec.command, exec.args, opts); + subprocess.stdout.setEncoding('utf8'); + subprocess.stderr.setEncoding('utf8'); + + subprocess.stdout.on('data', (data: string) => { + stdoutData += data; + }); + + subprocess.stderr.on('data', (data: string) => { + stderrData += data; + }); + + subprocess.on('error', (error) => { + savedError = error; + }); + + subprocess.on('close', (code) => { + if (savedError) { + reject(savedError); + return; + } + if (code !== 0) { + reject(new Error(stderrData)); + return; + } + try { + const obj = JSON.parse(stdoutData) as Credential; + this.tokenCache[user.name] = obj; + resolve(obj); + } catch (error) { + reject(error); + } + }); + }); } } diff --git a/src/exec_auth_test.ts b/src/exec_auth_test.ts index 759c05e91b..fc512310ce 100644 --- a/src/exec_auth_test.ts +++ b/src/exec_auth_test.ts @@ -60,17 +60,30 @@ describe('ExecAuth', () => { const auth = new ExecAuth(); (auth as any).execFn = ( command: string, - args: string[], - opts: child_process.SpawnOptions, - ): child_process.SpawnSyncReturns => { + args?: readonly string[], + options?: child_process.SpawnOptionsWithoutStdio, + ): child_process.ChildProcessWithoutNullStreams => { return { - status: 0, - stdout: Buffer.from(JSON.stringify({ status: { token: 'foo' } })), - } as child_process.SpawnSyncReturns; + stdout: { + setEncoding: () => {}, + on: (_data: string, f: (data: Buffer | string) => void) => { + f(Buffer.from(JSON.stringify({ status: { token: 'foo' } }))); + }, + }, + stderr: { + setEncoding: () => {}, + on: () => {}, + }, + on: (op: string, f: any) => { + if (op === 'close') { + f(0); + } + }, + } as unknown as child_process.ChildProcessWithoutNullStreams; }; const opts = {} as https.RequestOptions; opts.headers = {} as OutgoingHttpHeaders; - auth.applyAuthentication( + await auth.applyAuthentication( { name: 'user', authProvider: { @@ -94,15 +107,32 @@ describe('ExecAuth', () => { const auth = new ExecAuth(); (auth as any).execFn = ( command: string, - args: string[], - opts: child_process.SpawnOptions, - ): child_process.SpawnSyncReturns => { + args?: readonly string[], + options?: child_process.SpawnOptionsWithoutStdio, + ): child_process.ChildProcessWithoutNullStreams => { return { - status: 0, - stdout: Buffer.from( - JSON.stringify({ status: { clientCertificateData: 'foo', clientKeyData: 'bar' } }), - ), - } as child_process.SpawnSyncReturns; + stdout: { + setEncoding: () => {}, + on: (_data: string, f: (data: Buffer | string) => void) => { + f( + Buffer.from( + JSON.stringify({ + status: { clientCertificateData: 'foo', clientKeyData: 'bar' }, + }), + ), + ); + }, + }, + stderr: { + setEncoding: () => {}, + on: () => {}, + }, + on: (op: string, f: any) => { + if (op === 'close') { + f(0); + } + }, + } as unknown as child_process.ChildProcessWithoutNullStreams; }; const user = { @@ -119,7 +149,7 @@ describe('ExecAuth', () => { opts.headers = {} as OutgoingHttpHeaders; opts.headers = {} as OutgoingHttpHeaders; - auth.applyAuthentication(user, opts); + await auth.applyAuthentication(user, opts); expect(opts.headers.Authorization).to.be.undefined; expect(opts.cert).to.equal('foo'); expect(opts.key).to.equal('bar'); @@ -136,18 +166,33 @@ describe('ExecAuth', () => { var tokenValue = 'foo'; (auth as any).execFn = ( command: string, - args: string[], - opts: child_process.SpawnOptions, - ): child_process.SpawnSyncReturns => { + args?: readonly string[], + options?: child_process.SpawnOptionsWithoutStdio, + ): child_process.ChildProcessWithoutNullStreams => { execCount++; return { - status: 0, - stdout: Buffer.from( - JSON.stringify({ - status: { token: tokenValue, expirationTimestamp: expire }, - }), - ), - } as child_process.SpawnSyncReturns; + stdout: { + setEncoding: () => {}, + on: (_data: string, f: (data: Buffer | string) => void) => { + f( + Buffer.from( + JSON.stringify({ + status: { token: tokenValue, expirationTimestamp: expire }, + }), + ), + ); + }, + }, + stderr: { + setEncoding: () => {}, + on: () => {}, + }, + on: (op: string, f: any) => { + if (op === 'close') { + f(0); + } + }, + } as unknown as child_process.ChildProcessWithoutNullStreams; }; const user = { @@ -207,6 +252,28 @@ describe('ExecAuth', () => { } as child_process.SpawnSyncReturns; }; + (auth as any).execFn = ( + command: string, + args?: readonly string[], + options?: child_process.SpawnOptionsWithoutStdio, + ): child_process.ChildProcessWithoutNullStreams => { + return { + stdout: { + setEncoding: () => {}, + on: (_data: string, f: (data: Buffer | string) => void) => {}, + }, + stderr: { + setEncoding: () => {}, + on: () => {}, + }, + on: (op: string, f: any) => { + if (op === 'error') { + throw new Error('Error: spawnSync /path/to/bin ENOENT'); + } + }, + } as unknown as child_process.ChildProcessWithoutNullStreams; + }; + const user = { name: 'user', authProvider: { @@ -230,16 +297,31 @@ describe('ExecAuth', () => { return; } const auth = new ExecAuth(); + (auth as any).execFn = ( command: string, - args: string[], - opts: child_process.SpawnOptions, - ): child_process.SpawnSyncReturns => { + args?: readonly string[], + options?: child_process.SpawnOptionsWithoutStdio, + ): child_process.ChildProcessWithoutNullStreams => { return { - status: 100, - stdout: Buffer.from(JSON.stringify({ status: { token: 'foo' } })), - stderr: Buffer.from('Some error!'), - } as child_process.SpawnSyncReturns; + stdout: { + setEncoding: () => {}, + on: (_data: string, f: (data: Buffer | string) => void) => { + f(Buffer.from(JSON.stringify({ status: { token: 'foo' } }))); + }, + }, + stderr: { + setEncoding: () => {}, + on: (_data: string, f: (data: Buffer | string) => void) => { + f(Buffer.from('Some error!')); + }, + }, + on: (op: string, f: any) => { + if (op === 'close') { + f(100); + } + }, + } as unknown as child_process.ChildProcessWithoutNullStreams; }; const user = { @@ -265,18 +347,32 @@ describe('ExecAuth', () => { return; } const auth = new ExecAuth(); - let optsOut: child_process.SpawnOptions = {}; + let optsOut: child_process.SpawnOptions | undefined = {}; (auth as any).execFn = ( command: string, - args: string[], - opts: child_process.SpawnOptions, - ): child_process.SpawnSyncReturns => { - optsOut = opts; + args?: readonly string[], + options?: child_process.SpawnOptionsWithoutStdio, + ): child_process.ChildProcessWithoutNullStreams => { + optsOut = options; return { - status: 0, - stdout: Buffer.from(JSON.stringify({ status: { token: 'foo' } })), - } as child_process.SpawnSyncReturns; + stdout: { + setEncoding: () => {}, + on: (_data: string, f: (data: Buffer | string) => void) => { + f(Buffer.from(JSON.stringify({ status: { token: 'foo' } }))); + }, + }, + stderr: { + setEncoding: () => {}, + on: () => {}, + }, + on: (op: string, f: any) => { + if (op === 'close') { + f(0); + } + }, + } as unknown as child_process.ChildProcessWithoutNullStreams; }; + process.env.BLABBLE = 'flubble'; const opts = {} as https.RequestOptions; opts.headers = {} as OutgoingHttpHeaders; @@ -313,16 +409,30 @@ describe('ExecAuth', () => { const auth = new ExecAuth(); (auth as any).execFn = ( command: string, - args: string[], - opts: child_process.SpawnOptions, - ): child_process.SpawnSyncReturns => { + args?: readonly string[], + options?: child_process.SpawnOptionsWithoutStdio, + ): child_process.ChildProcessWithoutNullStreams => { return { - status: 0, - stdout: Buffer.from(JSON.stringify({ status: { token: 'foo' } })), - } as child_process.SpawnSyncReturns; + stdout: { + setEncoding: () => {}, + on: (_data: string, f: (data: Buffer | string) => void) => { + f(Buffer.from(JSON.stringify({ status: { token: 'foo' } }))); + }, + }, + stderr: { + setEncoding: () => {}, + on: () => {}, + }, + on: (op: string, f: any) => { + if (op === 'close') { + f(0); + } + }, + } as unknown as child_process.ChildProcessWithoutNullStreams; }; + const opts = {} as https.RequestOptions; - auth.applyAuthentication( + await auth.applyAuthentication( { name: 'user', authProvider: {