Skip to content

makes exec_auth spawn the command asynchronously #2046

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 46 additions & 16 deletions src/exec_auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Buffer> = 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) {
Expand All @@ -41,7 +41,7 @@ export class ExecAuth implements Authenticator {
}

public async applyAuthentication(user: User, opts: https.RequestOptions): Promise<void> {
const credential = this.getCredential(user);
const credential = await this.getCredential(user);
if (!credential) {
return;
}
Expand Down Expand Up @@ -70,7 +70,7 @@ export class ExecAuth implements Authenticator {
return null;
}

private getCredential(user: User): Credential | null {
private async getCredential(user: User): Promise<Credential | null> {
// TODO: Add a unit test for token caching.
const cachedToken = this.tokenCache[user.name];
if (cachedToken) {
Expand Down Expand Up @@ -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);
}
});
});
}
}
206 changes: 158 additions & 48 deletions src/exec_auth_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Buffer> => {
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<Buffer>;
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: {
Expand All @@ -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<Buffer> => {
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<Buffer>;
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 = {
Expand All @@ -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');
Expand All @@ -136,18 +166,33 @@ describe('ExecAuth', () => {
var tokenValue = 'foo';
(auth as any).execFn = (
command: string,
args: string[],
opts: child_process.SpawnOptions,
): child_process.SpawnSyncReturns<Buffer> => {
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<Buffer>;
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 = {
Expand Down Expand Up @@ -207,6 +252,28 @@ describe('ExecAuth', () => {
} as child_process.SpawnSyncReturns<Buffer>;
};

(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: {
Expand All @@ -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<Buffer> => {
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<Buffer>;
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 = {
Expand All @@ -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<Buffer> => {
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<Buffer>;
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;
Expand Down Expand Up @@ -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<Buffer> => {
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<Buffer>;
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: {
Expand Down
Loading