Skip to content

Commit 20a0623

Browse files
committed
Use stdin for supplying auth to CodeQL
This will supply the GitHub access token to certain CodeQL CLI commands such that private packages can be resolved. It will only do so if the user has an existing auth session. If they don't, they will now get a prompt to login. However, this will only happen for commands which actually use authentication, which is limited to packaging commands.
1 parent f82fde3 commit 20a0623

3 files changed

Lines changed: 156 additions & 15 deletions

File tree

extensions/ql-vscode/src/authentication.ts

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,11 @@ import { retry } from "@octokit/plugin-retry";
44

55
const GITHUB_AUTH_PROVIDER_ID = "github";
66

7-
// We need 'repo' scope for triggering workflows and 'gist' scope for exporting results to Gist.
7+
// We need 'repo' scope for triggering workflows, 'gist' scope for exporting results to Gist,
8+
// and 'read:packages' for reading private CodeQL packages.
89
// For a comprehensive list of scopes, see:
910
// https://docs.github.com/apps/building-oauth-apps/understanding-scopes-for-oauth-apps
10-
const SCOPES = ["repo", "gist"];
11+
const SCOPES = ["repo", "gist", "read:packages"];
1112

1213
/**
1314
* Handles authentication to GitHub, using the VS Code [authentication API](https://code.visualstudio.com/api/references/vscode-api#authentication).
@@ -57,15 +58,31 @@ export class Credentials {
5758
return this.octokit;
5859
}
5960

61+
const accessToken = await this.getAccessToken();
62+
63+
return new Octokit.Octokit({
64+
auth: accessToken,
65+
retry,
66+
});
67+
}
68+
69+
async getAccessToken(): Promise<string> {
6070
const session = await vscode.authentication.getSession(
6171
GITHUB_AUTH_PROVIDER_ID,
6272
SCOPES,
6373
{ createIfNone: true },
6474
);
6575

66-
return new Octokit.Octokit({
67-
auth: session.accessToken,
68-
retry,
69-
});
76+
return session.accessToken;
77+
}
78+
79+
async getExistingAccessToken(): Promise<string | undefined> {
80+
const session = await vscode.authentication.getSession(
81+
GITHUB_AUTH_PROVIDER_ID,
82+
SCOPES,
83+
{ createIfNone: false },
84+
);
85+
86+
return session?.accessToken;
7087
}
7188
}

extensions/ql-vscode/src/cli.ts

Lines changed: 76 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { EOL } from "os";
12
import { spawn } from "child-process-promise";
23
import * as child_process from "child_process";
34
import { readFile } from "fs-extra";
@@ -26,6 +27,7 @@ import { Logger, ProgressReporter } from "./common";
2627
import { CompilationMessage } from "./pure/legacy-messages";
2728
import { sarifParser } from "./sarif-parser";
2829
import { dbSchemeToLanguage, walkDirectory } from "./helpers";
30+
import { Credentials } from "./authentication";
2931

3032
/**
3133
* The version of the SARIF format that we are using.
@@ -156,6 +158,10 @@ interface BqrsDecodeOptions {
156158
entities?: string[];
157159
}
158160

161+
export type OnLineCallback = (
162+
line: string,
163+
) => Promise<string | undefined> | string | undefined;
164+
159165
/**
160166
* This class manages a cli server started by `codeql execute cli-server` to
161167
* run commands without the overhead of starting a new java
@@ -304,6 +310,7 @@ export class CodeQLCliServer implements Disposable {
304310
command: string[],
305311
commandArgs: string[],
306312
description: string,
313+
onLine?: OnLineCallback,
307314
): Promise<string> {
308315
const stderrBuffers: Buffer[] = [];
309316
if (this.commandInProcess) {
@@ -328,6 +335,22 @@ export class CodeQLCliServer implements Disposable {
328335
await new Promise<void>((resolve, reject) => {
329336
// Start listening to stdout
330337
process.stdout.addListener("data", (newData: Buffer) => {
338+
if (onLine) {
339+
void (async () => {
340+
const response = await onLine(newData.toString("utf-8"));
341+
342+
if (!response) {
343+
return;
344+
}
345+
346+
process.stdin.write(`${response}${EOL}`);
347+
348+
// Remove newData from stdoutBuffers because the data has been consumed
349+
// by the onLine callback.
350+
stdoutBuffers.splice(stdoutBuffers.indexOf(newData), 1);
351+
})();
352+
}
353+
331354
stdoutBuffers.push(newData);
332355
// If the buffer ends in '0' then exit.
333356
// We don't have to check the middle as no output will be written after the null until
@@ -487,13 +510,15 @@ export class CodeQLCliServer implements Disposable {
487510
* @param commandArgs The arguments to pass to the `codeql` command.
488511
* @param description Description of the action being run, to be shown in log and error messages.
489512
* @param progressReporter Used to output progress messages, e.g. to the status bar.
513+
* @param onLine Used for responding to interactive output on stdout/stdin.
490514
* @returns The contents of the command's stdout, if the command succeeded.
491515
*/
492516
runCodeQlCliCommand(
493517
command: string[],
494518
commandArgs: string[],
495519
description: string,
496520
progressReporter?: ProgressReporter,
521+
onLine?: OnLineCallback,
497522
): Promise<string> {
498523
if (progressReporter) {
499524
progressReporter.report({ message: description });
@@ -503,10 +528,12 @@ export class CodeQLCliServer implements Disposable {
503528
// Construct the command that actually does the work
504529
const callback = (): void => {
505530
try {
506-
this.runCodeQlCliInternal(command, commandArgs, description).then(
507-
resolve,
508-
reject,
509-
);
531+
this.runCodeQlCliInternal(
532+
command,
533+
commandArgs,
534+
description,
535+
onLine,
536+
).then(resolve, reject);
510537
} catch (err) {
511538
reject(err);
512539
}
@@ -528,6 +555,7 @@ export class CodeQLCliServer implements Disposable {
528555
* @param description Description of the action being run, to be shown in log and error messages.
529556
* @param addFormat Whether or not to add commandline arguments to specify the format as JSON.
530557
* @param progressReporter Used to output progress messages, e.g. to the status bar.
558+
* @param onLine Used for responding to interactive output on stdout/stdin.
531559
* @returns The contents of the command's stdout, if the command succeeded.
532560
*/
533561
async runJsonCodeQlCliCommand<OutputType>(
@@ -536,6 +564,7 @@ export class CodeQLCliServer implements Disposable {
536564
description: string,
537565
addFormat = true,
538566
progressReporter?: ProgressReporter,
567+
onLine?: OnLineCallback,
539568
): Promise<OutputType> {
540569
let args: string[] = [];
541570
if (addFormat)
@@ -547,6 +576,7 @@ export class CodeQLCliServer implements Disposable {
547576
args,
548577
description,
549578
progressReporter,
579+
onLine,
550580
);
551581
try {
552582
return JSON.parse(result) as OutputType;
@@ -559,6 +589,44 @@ export class CodeQLCliServer implements Disposable {
559589
}
560590
}
561591

592+
/**
593+
* Runs a CodeQL CLI command, returning the output as JSON.
594+
* @param command The `codeql` command to be run, provided as an array of command/subcommand names.
595+
* @param commandArgs The arguments to pass to the `codeql` command.
596+
* @param description Description of the action being run, to be shown in log and error messages.
597+
* @param addFormat Whether or not to add commandline arguments to specify the format as JSON.
598+
* @param progressReporter Used to output progress messages, e.g. to the status bar.
599+
* @returns The contents of the command's stdout, if the command succeeded.
600+
*/
601+
async runJsonCodeQlCliCommandWithAuthentication<OutputType>(
602+
command: string[],
603+
commandArgs: string[],
604+
description: string,
605+
addFormat = true,
606+
progressReporter?: ProgressReporter,
607+
): Promise<OutputType> {
608+
const credentials = await Credentials.initialize();
609+
610+
const accessToken = await credentials.getExistingAccessToken();
611+
612+
const extraArgs = accessToken ? ["--github-auth-stdin"] : [];
613+
614+
return this.runJsonCodeQlCliCommand(
615+
command,
616+
[...extraArgs, ...commandArgs],
617+
description,
618+
addFormat,
619+
progressReporter,
620+
async (line) => {
621+
if (line.startsWith("Enter value for --github-auth-stdin")) {
622+
return credentials.getAccessToken();
623+
}
624+
625+
return undefined;
626+
},
627+
);
628+
}
629+
562630
/**
563631
* Resolve the library path and dbscheme for a query.
564632
* @param workspaces The current open workspaces
@@ -1136,7 +1204,7 @@ export class CodeQLCliServer implements Disposable {
11361204
* @param packs The `<package-scope/name[@version]>` of the packs to download.
11371205
*/
11381206
async packDownload(packs: string[]) {
1139-
return this.runJsonCodeQlCliCommand(
1207+
return this.runJsonCodeQlCliCommandWithAuthentication(
11401208
["pack", "download"],
11411209
packs,
11421210
"Downloading packs",
@@ -1148,7 +1216,7 @@ export class CodeQLCliServer implements Disposable {
11481216
if (forceUpdate) {
11491217
args.push("--mode", "update");
11501218
}
1151-
return this.runJsonCodeQlCliCommand(
1219+
return this.runJsonCodeQlCliCommandWithAuthentication(
11521220
["pack", "install"],
11531221
args,
11541222
"Installing pack dependencies",
@@ -1169,7 +1237,7 @@ export class CodeQLCliServer implements Disposable {
11691237
...this.getAdditionalPacksArg(workspaceFolders),
11701238
];
11711239

1172-
return this.runJsonCodeQlCliCommand(
1240+
return this.runJsonCodeQlCliCommandWithAuthentication(
11731241
["pack", "bundle"],
11741242
args,
11751243
"Bundling pack",
@@ -1200,7 +1268,7 @@ export class CodeQLCliServer implements Disposable {
12001268
): Promise<{ [pack: string]: string }> {
12011269
// Uses the default `--mode use-lock`, which creates the lock file if it doesn't exist.
12021270
const results: { [pack: string]: string } =
1203-
await this.runJsonCodeQlCliCommand(
1271+
await this.runJsonCodeQlCliCommandWithAuthentication(
12041272
["pack", "resolve-dependencies"],
12051273
[dir],
12061274
"Resolving pack dependencies",

extensions/ql-vscode/test/vscode-tests/cli-integration/run-cli.test.ts

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { extensions, Uri } from "vscode";
1+
import { authentication, extensions, Uri } from "vscode";
22
import { join } from "path";
33
import { SemVer } from "semver";
44

@@ -12,6 +12,7 @@ import {
1212
} from "../../../src/helpers";
1313
import { resolveQueries } from "../../../src/contextual/queryResolver";
1414
import { KeyType } from "../../../src/contextual/keyType";
15+
import { faker } from "@faker-js/faker";
1516

1617
jest.setTimeout(60_000);
1718

@@ -104,4 +105,59 @@ describe("Use cli", () => {
104105
}
105106
},
106107
);
108+
109+
describe("github authentication", () => {
110+
itWithCodeQL()(
111+
"should not use authentication if there are no credentials",
112+
async () => {
113+
const getSession = jest
114+
.spyOn(authentication, "getSession")
115+
.mockResolvedValue(undefined);
116+
117+
await cli.packDownload(["codeql/tutorial"]);
118+
expect(getSession).toHaveBeenCalledTimes(1);
119+
expect(getSession).toHaveBeenCalledWith(
120+
"github",
121+
expect.arrayContaining(["read:packages"]),
122+
{
123+
createIfNone: false,
124+
},
125+
);
126+
},
127+
);
128+
129+
itWithCodeQL()(
130+
"should use authentication if there are credentials",
131+
async () => {
132+
const getSession = jest
133+
.spyOn(authentication, "getSession")
134+
.mockResolvedValue({
135+
id: faker.datatype.uuid(),
136+
accessToken: "gho_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
137+
account: {
138+
id: faker.datatype.uuid(),
139+
label: "Account",
140+
},
141+
scopes: ["read:packages"],
142+
});
143+
144+
await cli.packDownload(["codeql/tutorial"]);
145+
expect(getSession).toHaveBeenCalledTimes(2);
146+
expect(getSession).toHaveBeenCalledWith(
147+
"github",
148+
expect.arrayContaining(["read:packages"]),
149+
{
150+
createIfNone: false,
151+
},
152+
);
153+
expect(getSession).toHaveBeenCalledWith(
154+
"github",
155+
expect.arrayContaining(["read:packages"]),
156+
{
157+
createIfNone: true,
158+
},
159+
);
160+
},
161+
);
162+
});
107163
});

0 commit comments

Comments
 (0)