Add capability to specify auth from env var or stdin

This commit adds two new ways of specifying GitHub auth:

1. from the GITHUB_TOKEN environment variable
2. from standard input

This commit does not include any documentation changes and the
descriptions of new command line options will need to be tweaked.
This commit is contained in:
Andrew Eisenberg 2021-02-12 14:31:38 -08:00 committed by Andrew Eisenberg
parent 1d92248672
commit 88714e3a60
9 changed files with 267 additions and 20 deletions

View file

@ -20,6 +20,7 @@ import {
getMemoryFlag,
getThreadsFlag,
parseGithubUrl,
getGitHubAuth,
} from "./util";
const program = new Command();
@ -96,6 +97,7 @@ interface InitArgs {
repository: string;
githubUrl: string;
githubAuth: string;
githubAuthStdin: boolean;
externalRepositoryToken: string | undefined;
debug: boolean;
}
@ -105,9 +107,13 @@ program
.description("Initializes CodeQL")
.requiredOption("--repository <repository>", "Repository name. (Required)")
.requiredOption("--github-url <url>", "URL of GitHub instance. (Required)")
.requiredOption(
.option(
"--github-auth <auth>",
"GitHub Apps token or personal access token. (Required)"
"GitHub Apps token or personal access token. This option is insecure and deprecated, please use `--github-auth-stdin` instead."
)
.option(
"--github-auth-stdin",
"Read GitHub Apps token or personal access token from stdin."
)
.option(
"--external-repository-token <token>",
@ -153,8 +159,14 @@ program
fs.rmdirSync(tempDir, { recursive: true });
fs.mkdirSync(tempDir, { recursive: true });
const auth = await getGitHubAuth(
logger,
cmd.githubAuth,
cmd.githubAuthStdin
);
const apiDetails = {
auth: cmd.githubAuth,
auth,
externalRepoAuth: cmd.externalRepositoryToken,
url: parseGithubUrl(cmd.githubUrl),
};
@ -315,6 +327,7 @@ interface AnalyzeArgs {
ref: string;
githubUrl: string;
githubAuth: string;
githubAuthStdin: boolean;
checkoutPath: string | undefined;
upload: boolean;
outputDir: string | undefined;
@ -335,9 +348,13 @@ program
)
.requiredOption("--ref <ref>", "Name of ref that was analyzed. (Required)")
.requiredOption("--github-url <url>", "URL of GitHub instance. (Required)")
.requiredOption(
.option(
"--github-auth <auth>",
"GitHub Apps token or personal access token. (Required)"
"GitHub Apps token or personal access token. This option is insecure and deprecated, please use `--github-auth-stdin` instead."
)
.option(
"--github-auth-stdin",
"Read GitHub Apps token or personal access token from stdin."
)
.option(
"--checkout-path <path>",
@ -379,8 +396,14 @@ program
);
}
const auth = await getGitHubAuth(
logger,
cmd.githubAuth,
cmd.githubAuthStdin
);
const apiDetails = {
auth: cmd.githubAuth,
auth,
url: parseGithubUrl(cmd.githubUrl),
};
@ -421,6 +444,7 @@ interface UploadArgs {
commit: string;
ref: string;
githubUrl: string;
githubAuthStdin: boolean;
githubAuth: string;
checkoutPath: string | undefined;
debug: boolean;
@ -442,9 +466,13 @@ program
)
.requiredOption("--ref <ref>", "Name of ref that was analyzed. (Required)")
.requiredOption("--github-url <url>", "URL of GitHub instance. (Required)")
.requiredOption(
.option(
"--github-auth <auth>",
"GitHub Apps token or personal access token. (Required)"
"GitHub Apps token or personal access token. This option is insecure and deprecated, please use `--github-auth-stdin` instead."
)
.option(
"--github-auth-stdin",
"Read GitHub Apps token or personal access token from stdin."
)
.option(
"--checkout-path <path>",
@ -453,8 +481,13 @@ program
.option("--debug", "Print more verbose output", false)
.action(async (cmd: UploadArgs) => {
const logger = getRunnerLogger(cmd.debug);
const auth = await getGitHubAuth(
logger,
cmd.githubAuth,
cmd.githubAuthStdin
);
const apiDetails = {
auth: cmd.githubAuth,
auth,
url: parseGithubUrl(cmd.githubUrl),
};
try {

View file

@ -1,12 +1,13 @@
import * as fs from "fs";
import * as os from "os";
import * as stream from "stream";
import * as github from "@actions/github";
import test from "ava";
import test, { ExecutionContext } from "ava";
import sinon from "sinon";
import * as api from "./api-client";
import { getRunnerLogger } from "./logging";
import { getRunnerLogger, Logger } from "./logging";
import { setupTests } from "./testing-utils";
import * as util from "./util";
@ -244,3 +245,62 @@ test("getGitHubVersion", async (t) => {
});
t.deepEqual({ type: util.GitHubVariant.DOTCOM }, v3);
});
test("getGitHubAuth", async (t) => {
const msgs: string[] = [];
const mockLogger = ({
warning: (msg: string) => msgs.push(msg),
} as unknown) as Logger;
// eslint-disable-next-line @typescript-eslint/no-floating-promises
t.throwsAsync(async () => util.getGitHubAuth(mockLogger, "abc", true));
process.env.GITHUB_TOKEN = "123";
t.is("123", await util.getGitHubAuth(mockLogger, undefined, undefined));
t.is(msgs.length, 0);
t.is("abc", await util.getGitHubAuth(mockLogger, "abc", undefined));
t.is(msgs.length, 1); // warning expected
msgs.length = 0;
await mockStdInForAuth(t, mockLogger, "def", "def");
await mockStdInForAuth(t, mockLogger, "def", "", "def");
await mockStdInForAuth(
t,
mockLogger,
"def",
"def\n some extra garbage",
"ghi"
);
await mockStdInForAuth(t, mockLogger, "defghi", "def", "ghi\n123");
await mockStdInForAuthExpectError(t, mockLogger, "");
await mockStdInForAuthExpectError(t, mockLogger, "", " ", "abc");
await mockStdInForAuthExpectError(
t,
mockLogger,
" def\n some extra garbage",
"ghi"
);
t.is(msgs.length, 0);
});
async function mockStdInForAuth(
t: ExecutionContext<any>,
mockLogger: Logger,
expected: string,
...text: string[]
) {
const stdin = stream.Readable.from(text) as any;
t.is(expected, await util.getGitHubAuth(mockLogger, undefined, true, stdin));
}
async function mockStdInForAuthExpectError(
t: ExecutionContext<unknown>,
mockLogger: Logger,
...text: string[]
) {
const stdin = stream.Readable.from(text) as any;
await t.throwsAsync(async () =>
util.getGitHubAuth(mockLogger, undefined, true, stdin)
);
}

View file

@ -1,6 +1,7 @@
import * as fs from "fs";
import * as os from "os";
import * as path from "path";
import { Readable } from "stream";
import * as core from "@actions/core";
import * as semver from "semver";
@ -311,3 +312,68 @@ export function apiVersionInRange(
}
return undefined;
}
/**
* Retrieves the github auth token for use with the runner. There are
* three possible locations for the token:
*
* 1. from the cli (considered insecure)
* 2. from stdin
* 3. from the GITHUB_TOKEN environment variable
*
* If both 1 & 2 are specified, then an error is thrown.
* If 1 & 3 or 2 & 3 are specified, then the environment variable is ignored.
*
* @param githubAuth a github app token or PAT
* @param fromStdIn read the github app token or PAT from stdin up to, but excluding the first whitespace
* @param readable the readable stream to use for getting the token (defaults to stdin)
*
* @return a promise resolving to the auth token.
*/
export async function getGitHubAuth(
logger: Logger,
githubAuth: string | undefined,
fromStdIn: boolean | undefined,
readable = process.stdin as Readable
): Promise<string> {
if (githubAuth && fromStdIn) {
throw new Error(
"Cannot specify both `--github-auth` and `--github-auth-stdin`. Please use `--github-auth-stdin`, which is more secure."
);
}
if (githubAuth) {
logger.warning(
"Using `--github-auth` via the CLI is insecure. Use `--github-auth-stdin` instead."
);
return githubAuth;
}
if (fromStdIn) {
return new Promise((resolve, reject) => {
let token = "";
readable.on("data", (data) => {
token += data.toString("utf8");
});
readable.on("end", () => {
token = token.split(/\s+/)[0].trim();
if (token) {
resolve(token);
} else {
reject(new Error("Standard input is empty"));
}
});
readable.on("error", (err) => {
reject(err);
});
});
}
if (process.env.GITHUB_TOKEN) {
return process.env.GITHUB_TOKEN;
}
throw new Error(
"No GitHub authentication token was specified. Please provide a token via the GITHUB_TOKEN environment variable, or by adding the `--github-auth-stdin` flag and passing the token via standard input."
);
}