Skip to content
Open
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
282 changes: 148 additions & 134 deletions lib/entry-points.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@
"@octokit/plugin-rest-endpoint-methods": "^18.0.0",
"@octokit/plugin-retry": "^8.1.1",
"archiver": "^8.0.0",
"fast-deep-equal": "^3.1.3",
"follow-redirects": "^1.16.1",
"get-folder-size": "^5.0.0",
"https-proxy-agent": "^7.0.6",
Expand Down
4 changes: 2 additions & 2 deletions src/actions-util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ export function getWorkflowEventName(env: Env = getEnv()) {
* Returns whether the current workflow is executing a local copy of the Action, e.g. we're running
* a workflow on the codeql-action repo itself.
*/
export function isRunningLocalAction(env: Env = getEnv()): boolean {
export function isRunningLocalAction(env: ReadOnlyEnv = getEnv()): boolean {
const relativeScriptPath = getRelativeScriptPath(env);
return (
relativeScriptPath.startsWith("..") || path.isAbsolute(relativeScriptPath)
Expand All @@ -113,7 +113,7 @@ export function isRunningLocalAction(env: Env = getEnv()): boolean {
*
* This can be used to get the Action's name or tell if we're running a local Action.
*/
function getRelativeScriptPath(env: Env): string {
function getRelativeScriptPath(env: ReadOnlyEnv): string {
const runnerTemp = env.getRequired(ActionsEnvVars.RUNNER_TEMP);
const actionsDirectory = path.join(path.dirname(runnerTemp), "_actions");
return path.relative(actionsDirectory, __filename);
Expand Down
88 changes: 32 additions & 56 deletions src/api-client.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as github from "@actions/github";
import * as githubUtils from "@actions/github/lib/utils";
import * as retry from "@octokit/plugin-retry";
import test from "ava";
import * as sinon from "sinon";
import { ProxyAgent } from "undici";
Expand Down Expand Up @@ -308,70 +309,45 @@ test.serial(
test("getAuthorizationHeaderFor - returns undefined if there's no token", (t) => {
const logger = new RecordingLogger();

t.is(
api.getAuthorizationHeaderFor(
logger,
{ ...SAMPLE_DOTCOM_API_DETAILS, auth: undefined },
`${GITHUB_DOTCOM_URL}/github`,
),
undefined,
);
t.is(
api.getAuthorizationHeaderFor(
logger,
{ ...SAMPLE_DOTCOM_API_DETAILS, auth: "" },
`${GITHUB_DOTCOM_URL}/github`,
),
undefined,
);
t.is(
api.getAuthorizationHeaderFor(
logger,
{ ...SAMPLE_DOTCOM_API_DETAILS, auth: " " },
`${GITHUB_DOTCOM_URL}/github`,
),
undefined,
);
for (const authValue of [undefined, "", " "]) {
const client = new api.InstanceApiClient({
...SAMPLE_DOTCOM_API_DETAILS,
auth: authValue,
});
t.is(
client.getAuthorizationHeaderFor(logger, `${GITHUB_DOTCOM_URL}/github`),
undefined,
);
}
});

test("getAuthorizationHeaderFor - returns header value with token if url is allowed", (t) => {
const logger = new RecordingLogger();
const client = new api.InstanceApiClient({
...SAMPLE_DOTCOM_API_DETAILS,
});

t.is(
api.getAuthorizationHeaderFor(
logger,
SAMPLE_DOTCOM_API_DETAILS,
`${GITHUB_DOTCOM_URL}/github`,
),
`token ${SAMPLE_DOTCOM_API_DETAILS.auth}`,
);
t.is(
api.getAuthorizationHeaderFor(
logger,
SAMPLE_DOTCOM_API_DETAILS,
`${GITHUB_DOTCOM_API_URL}/github`,
),
`token ${SAMPLE_DOTCOM_API_DETAILS.auth}`,
);
for (const url of [
`${GITHUB_DOTCOM_URL}/github`,
`${GITHUB_DOTCOM_API_URL}/github`,
]) {
t.is(
client.getAuthorizationHeaderFor(logger, url),
`token ${SAMPLE_DOTCOM_API_DETAILS.auth}`,
);
}
});

test("getAuthorizationHeaderFor - returns undefined if url is not allowed", (t) => {
const logger = new RecordingLogger();
const client = new api.InstanceApiClient({
...SAMPLE_DOTCOM_API_DETAILS,
});

t.is(
api.getAuthorizationHeaderFor(
logger,
SAMPLE_DOTCOM_API_DETAILS,
`https://localhost/github`,
),
undefined,
);
t.is(
api.getAuthorizationHeaderFor(
logger,
SAMPLE_DOTCOM_API_DETAILS,
`https://api.localhost/github`,
),
undefined,
);
for (const url of [
`https://localhost/github`,
`https://api.localhost/github`,
]) {
t.is(client.getAuthorizationHeaderFor(logger, url), undefined);
}
});
122 changes: 84 additions & 38 deletions src/api-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,11 @@ import {
RegistryProxyVars,
getEnv,
exportEnvVar,
ActionsEnvVars,
} from "./environment";
import {
getCurrentInstance,
getDotcomInstance,
GITHUB_DOTCOM_URL,
parseGitHubUrl,
type GitHubApiCombinedDetails,
Expand Down Expand Up @@ -120,6 +122,61 @@ export function makeProxyRequestOptions(
/** The type of GitHub API client we use. */
export type ApiClient = InstanceType<typeof githubUtils.GitHub>;

/**
* Encapsulates a {@link GitHubApiCombinedDetails} along with operations which ensure that any credentials
* are only used with the GitHub instance that is represented by the {@link GitHubApiCombinedDetails} value.
*/
export class InstanceApiClient {
private readonly client: ApiClient;
private readonly apiDetails: GitHubApiCombinedDetails;
private readonly authorizationHeader: string | undefined;

constructor(
apiDetails: GitHubApiCombinedDetails,
clientOptions: CreateApiClientOptions = {},
) {
this.apiDetails = apiDetails;
this.client = createApiClientWithDetails(apiDetails, clientOptions);

// Compute the Authorization header value if we have a token.
const token = this.apiDetails.auth?.trim();
if (token !== undefined && token !== "") {
this.authorizationHeader = `token ${token}`;
}
}

/**
* Gets a value for the `Authorization` header for a request to `url`; or `undefined` if the
* `Authorization` header should not be set for `url`.
*
* @param logger The logger to use for debugging messages.
* @param url The URL for which we want to add an `Authorization` header.
*/
public getAuthorizationHeaderFor(
logger: Logger,
url: string,
): string | undefined {
// We don't have to check the `url` if we don't have a token to begin with.
if (this.authorizationHeader === undefined) {
return undefined;
}

// We only want to provide an authorization header if we are downloading
// from the same GitHub instance the Action is running on.
// This avoids leaking Enterprise tokens to dotcom.
if (
url.startsWith(`${this.apiDetails.url}/`) ||
(this.apiDetails.apiURL && url.startsWith(`${this.apiDetails.apiURL}/`))
) {
logger.debug(`Providing an authorization token.`);
return this.authorizationHeader;
}

logger.debug(`Not using an authorization token.`);
return undefined;
}
}

/** Options for `createApiClientWithDetails`. */
interface CreateApiClientOptions {
allowExternal?: boolean;
Expand Down Expand Up @@ -169,8 +226,33 @@ export function getApiDetails(env: ReadOnlyEnv = getEnv()): GitHubApiDetails {
};
}

export function getApiClient(env: ReadOnlyEnv = getEnv()) {
return createApiClientWithDetails(getApiDetails(env));
/**
* Gets a `GitHubApiDetails` object describing the GitHub Dotcom instance.
*
* @param env The environment variables to read.
*/
export function getDotcomApiDetails(env: ReadOnlyEnv): GitHubApiDetails {
// If we are currently running on dotcom, then just call `getApiDetails` which will
// construct the appropriate `GitHubApiDetails` from the environment and include the
// value of the `token` input. The `token` input relates to the GitHub instance that
// we are running on only. Therefore, if we are not currently running on dotcom,
// then we construct `GitHubApiDetails` without the `token` value and using the
// hard-coded instance and API URLs.
if (env.getRequired(ActionsEnvVars.GITHUB_SERVER_URL) === GITHUB_DOTCOM_URL) {
return getApiDetails(env);
} else {
return {
auth: undefined,
...getDotcomInstance(),
};
}
}

export function getApiClient(
env: ReadOnlyEnv = getEnv(),
apiDetails?: GitHubApiDetails,
) {
return createApiClientWithDetails(apiDetails ?? getApiDetails(env));
}

export function getApiClientWithExternalAuth(
Expand All @@ -180,42 +262,6 @@ export function getApiClientWithExternalAuth(
return createApiClientWithDetails(apiDetails, { allowExternal: true, proxy });
}

/**
* Gets a value for the `Authorization` header for a request to `url`; or `undefined` if the
* `Authorization` header should not be set for `url`.
*
* @param logger The logger to use for debugging messages.
* @param apiDetails Details of the GitHub API we are using.
* @param url The URL for which we want to add an `Authorization` header.
*
* @returns The value for the `Authorization` header or `undefined` if it shouldn't be populated.
*/
export function getAuthorizationHeaderFor(
logger: Logger,
apiDetails: GitHubApiDetails,
url: string,
): string | undefined {
// We don't have to check the `url` if we don't have a token to begin with.
const token = apiDetails.auth?.trim();
if (token === undefined || token === "") {
return undefined;
}

// We only want to provide an authorization header if we are downloading
// from the same GitHub instance the Action is running on.
// This avoids leaking Enterprise tokens to dotcom.
if (
url.startsWith(`${apiDetails.url}/`) ||
(apiDetails.apiURL && url.startsWith(`${apiDetails.apiURL}/`))
) {
logger.debug(`Providing an authorization token.`);
return `token ${token}`;
}

logger.debug(`Not using an authorization token.`);
return undefined;
}

let cachedGitHubVersion: GitHubVersion | undefined = undefined;

export async function getGitHubVersionFromApi(
Expand Down
3 changes: 3 additions & 0 deletions src/codeql-bundle.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { GitHubApiDetails } from "./github";
import { BuiltInLanguage, parseBuiltInLanguage } from "./languages";
import { BundlePlatform } from "./platform";
import type { CompressionMethod } from "./tar";
Expand All @@ -17,6 +18,8 @@ export type CodeQLBundle =
export interface CodeQLDownloadSource {
/** Distinguishes downloads from local archives and cached installations. */
sourceType: "download";
/** Information about the GitHub instance that contains the asset. */
apiDetails: GitHubApiDetails;
/** The bundle to download. */
bundle: CodeQLBundle;
/** The compression format of the bundle archive. */
Expand Down
17 changes: 4 additions & 13 deletions src/codeql.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import {
makeVersionInfo,
createTestConfig,
makeMacro,
mockApiDetails,
} from "./testing-utils";
import { ToolsDownloadStatusReport } from "./tools-download";
import * as util from "./util";
Expand Down Expand Up @@ -126,19 +127,6 @@ function mockReleaseApi({
});
}

function mockApiDetails(apiDetails: Required<GitHubApiDetails>) {
// This is a workaround to mock `api.getApiDetails()` since it doesn't seem to be possible to
// mock this directly. The difficulty is that `getApiDetails()` is called locally in
// `api-client.ts`, but `sinon.stub(api, "getApiDetails")` only affects calls to
// `getApiDetails()` via an imported `api` module.
sinon
.stub(actionsUtil, "getRequiredInput")
.withArgs("token")
.returns(apiDetails.auth);
process.env["GITHUB_SERVER_URL"] = apiDetails.url;
process.env["GITHUB_API_URL"] = apiDetails.apiURL || "";
}

async function stubCodeql(): Promise<codeql.CodeQL> {
const codeqlObject = await codeql.getCodeQLForTesting();
sinon.stub(codeqlObject, "getVersion").resolves(makeVersionInfo("2.17.6"));
Expand Down Expand Up @@ -290,6 +278,7 @@ for (const { cliVersion, tagName, expectedToolcacheVersion } of [
async (t) => {
await util.withTmpDir(async (tmpDir) => {
setupActionsVars(tmpDir, tmpDir);
mockApiDetails(SAMPLE_DOTCOM_API_DETAILS);

const result = await installIntoToolcache({
cliVersion,
Expand Down Expand Up @@ -470,6 +459,7 @@ test.serial(

await util.withTmpDir(async (tmpDir) => {
setupActionsVars(tmpDir, tmpDir);
mockApiDetails(SAMPLE_DOTCOM_API_DETAILS);

await installIntoToolcache({
tagName: "codeql-bundle-20200601",
Expand Down Expand Up @@ -517,6 +507,7 @@ test.serial(

await util.withTmpDir(async (tmpDir) => {
setupActionsVars(tmpDir, tmpDir);
mockApiDetails(SAMPLE_DOTCOM_API_DETAILS);

await installIntoToolcache({
tagName: "codeql-bundle-20200601",
Expand Down
5 changes: 4 additions & 1 deletion src/database-upload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -267,7 +267,10 @@ async function uploadBundledDatabase(
bundledDbSize: number,
apiDetails: GitHubApiDetails,
): Promise<number> {
const client = getApiClient();
// Ensure that the `apiDetails` that `getApiClient` uses match the ones we were given here,
// because we use `apiDetails.auth` below which must be used with the same instance that
// `apiDetails` refers to.
const client = getApiClient(util.getEnv(), apiDetails);

const uploadsUrl = new URL(parseGitHubUrl(apiDetails.url));
uploadsUrl.hostname = `uploads.${uploadsUrl.hostname}`;
Expand Down
8 changes: 8 additions & 0 deletions src/repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@ export interface RepositoryNwo {
repo: string;
}

/** An approximation of what a repository NWO string looks like. */
export type RepositoryNwoStr = `${string}/${string}`;

/** A basic check that `val` looks like a `RepositoryNwoStr`. */
export function isRepositoryNwoStr(val: string): val is RepositoryNwoStr {
return val.includes("/");
Comment thread
mbg marked this conversation as resolved.
}

/**
* Get the repository name with owner from the environment variable
* `GITHUB_REPOSITORY`.
Expand Down
Loading
Loading