Skip to content

Commit 8c32ebe

Browse files
committed
Add InstanceApiClient and incorporate getAuthorizationHeaderFor into it
1 parent e8380e6 commit 8c32ebe

7 files changed

Lines changed: 157 additions & 130 deletions

File tree

‎lib/entry-points.js‎

Lines changed: 36 additions & 20 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎src/api-client.test.ts‎

Lines changed: 31 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -309,70 +309,45 @@ test.serial(
309309
test("getAuthorizationHeaderFor - returns undefined if there's no token", (t) => {
310310
const logger = new RecordingLogger();
311311

312-
t.is(
313-
api.getAuthorizationHeaderFor(
314-
logger,
315-
{ ...SAMPLE_DOTCOM_API_DETAILS, auth: undefined },
316-
`${GITHUB_DOTCOM_URL}/github`,
317-
),
318-
undefined,
319-
);
320-
t.is(
321-
api.getAuthorizationHeaderFor(
322-
logger,
323-
{ ...SAMPLE_DOTCOM_API_DETAILS, auth: "" },
324-
`${GITHUB_DOTCOM_URL}/github`,
325-
),
326-
undefined,
327-
);
328-
t.is(
329-
api.getAuthorizationHeaderFor(
330-
logger,
331-
{ ...SAMPLE_DOTCOM_API_DETAILS, auth: " " },
332-
`${GITHUB_DOTCOM_URL}/github`,
333-
),
334-
undefined,
335-
);
312+
for (const authValue of [undefined, "", " "]) {
313+
const client = new api.InstanceApiClient({
314+
...SAMPLE_DOTCOM_API_DETAILS,
315+
auth: authValue,
316+
});
317+
t.is(
318+
client.getAuthorizationHeaderFor(logger, `${GITHUB_DOTCOM_URL}/github`),
319+
undefined,
320+
);
321+
}
336322
});
337323

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

341-
t.is(
342-
api.getAuthorizationHeaderFor(
343-
logger,
344-
SAMPLE_DOTCOM_API_DETAILS,
345-
`${GITHUB_DOTCOM_URL}/github`,
346-
),
347-
`token ${SAMPLE_DOTCOM_API_DETAILS.auth}`,
348-
);
349-
t.is(
350-
api.getAuthorizationHeaderFor(
351-
logger,
352-
SAMPLE_DOTCOM_API_DETAILS,
353-
`${GITHUB_DOTCOM_API_URL}/github`,
354-
),
355-
`token ${SAMPLE_DOTCOM_API_DETAILS.auth}`,
356-
);
330+
for (const url of [
331+
`${GITHUB_DOTCOM_URL}/github`,
332+
`${GITHUB_DOTCOM_API_URL}/github`,
333+
]) {
334+
t.is(
335+
client.getAuthorizationHeaderFor(logger, url),
336+
`token ${SAMPLE_DOTCOM_API_DETAILS.auth}`,
337+
);
338+
}
357339
});
358340

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

362-
t.is(
363-
api.getAuthorizationHeaderFor(
364-
logger,
365-
SAMPLE_DOTCOM_API_DETAILS,
366-
`https://localhost/github`,
367-
),
368-
undefined,
369-
);
370-
t.is(
371-
api.getAuthorizationHeaderFor(
372-
logger,
373-
SAMPLE_DOTCOM_API_DETAILS,
374-
`https://api.localhost/github`,
375-
),
376-
undefined,
377-
);
347+
for (const url of [
348+
`https://localhost/github`,
349+
`https://api.localhost/github`,
350+
]) {
351+
t.is(client.getAuthorizationHeaderFor(logger, url), undefined);
352+
}
378353
});

‎src/api-client.ts‎

Lines changed: 55 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,61 @@ export function makeProxyRequestOptions(
122122
/** The type of GitHub API client we use. */
123123
export type ApiClient = InstanceType<typeof githubUtils.GitHub>;
124124

125+
/**
126+
* Encapsulates a {@link GitHubApiCombinedDetails} along with operations which ensure that any credentials
127+
* are only used with the GitHub instance that is represented by the {@link GitHubApiCombinedDetails} value.
128+
*/
129+
export class InstanceApiClient {
130+
private readonly client: ApiClient;
131+
private readonly apiDetails: GitHubApiCombinedDetails;
132+
private readonly authorizationHeader: string | undefined;
133+
134+
constructor(
135+
apiDetails: GitHubApiCombinedDetails,
136+
clientOptions: CreateApiClientOptions = {},
137+
) {
138+
this.apiDetails = apiDetails;
139+
this.client = createApiClientWithDetails(apiDetails, clientOptions);
140+
141+
// Compute the Authorization header value if we have a token.
142+
const token = this.apiDetails.auth?.trim();
143+
if (token !== undefined && token !== "") {
144+
this.authorizationHeader = `token ${token}`;
145+
}
146+
}
147+
148+
/**
149+
* Gets a value for the `Authorization` header for a request to `url`; or `undefined` if the
150+
* `Authorization` header should not be set for `url`.
151+
*
152+
* @param logger The logger to use for debugging messages.
153+
* @param url The URL for which we want to add an `Authorization` header.
154+
*/
155+
public getAuthorizationHeaderFor(
156+
logger: Logger,
157+
url: string,
158+
): string | undefined {
159+
// We don't have to check the `url` if we don't have a token to begin with.
160+
if (this.authorizationHeader === undefined) {
161+
return undefined;
162+
}
163+
164+
// We only want to provide an authorization header if we are downloading
165+
// from the same GitHub instance the Action is running on.
166+
// This avoids leaking Enterprise tokens to dotcom.
167+
if (
168+
url.startsWith(`${this.apiDetails.url}/`) ||
169+
(this.apiDetails.apiURL && url.startsWith(`${this.apiDetails.apiURL}/`))
170+
) {
171+
logger.debug(`Providing an authorization token.`);
172+
return this.authorizationHeader;
173+
}
174+
175+
logger.debug(`Not using an authorization token.`);
176+
return undefined;
177+
}
178+
}
179+
125180
/** Options for `createApiClientWithDetails`. */
126181
interface CreateApiClientOptions {
127182
allowExternal?: boolean;
@@ -207,42 +262,6 @@ export function getApiClientWithExternalAuth(
207262
return createApiClientWithDetails(apiDetails, { allowExternal: true, proxy });
208263
}
209264

210-
/**
211-
* Gets a value for the `Authorization` header for a request to `url`; or `undefined` if the
212-
* `Authorization` header should not be set for `url`.
213-
*
214-
* @param logger The logger to use for debugging messages.
215-
* @param apiDetails Details of the GitHub API we are using.
216-
* @param url The URL for which we want to add an `Authorization` header.
217-
*
218-
* @returns The value for the `Authorization` header or `undefined` if it shouldn't be populated.
219-
*/
220-
export function getAuthorizationHeaderFor(
221-
logger: Logger,
222-
apiDetails: GitHubApiDetails,
223-
url: string,
224-
): string | undefined {
225-
// We don't have to check the `url` if we don't have a token to begin with.
226-
const token = apiDetails.auth?.trim();
227-
if (token === undefined || token === "") {
228-
return undefined;
229-
}
230-
231-
// We only want to provide an authorization header if we are downloading
232-
// from the same GitHub instance the Action is running on.
233-
// This avoids leaking Enterprise tokens to dotcom.
234-
if (
235-
url.startsWith(`${apiDetails.url}/`) ||
236-
(apiDetails.apiURL && url.startsWith(`${apiDetails.apiURL}/`))
237-
) {
238-
logger.debug(`Providing an authorization token.`);
239-
return `token ${token}`;
240-
}
241-
242-
logger.debug(`Not using an authorization token.`);
243-
return undefined;
244-
}
245-
246265
let cachedGitHubVersion: GitHubVersion | undefined = undefined;
247266

248267
export async function getGitHubVersionFromApi(

‎src/setup-codeql.test.ts‎

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1290,7 +1290,14 @@ for (const fallback of [false, true]) {
12901290
request: { fetch: fetchRelease },
12911291
});
12921292
sinon.stub(api, "getApiClient").value(() => client);
1293-
const authorizationSpy = sinon.spy(api, "getAuthorizationHeaderFor");
1293+
1294+
const spyClient = new api.InstanceApiClient(SAMPLE_DOTCOM_API_DETAILS);
1295+
const authorizationSpy = sinon.spy(
1296+
spyClient,
1297+
"getAuthorizationHeaderFor",
1298+
);
1299+
sinon.stub(api, "InstanceApiClient").returns(spyClient);
1300+
12941301
const extractStub = stubDownloadAndExtract();
12951302
if (fallback) {
12961303
extractStub.onFirstCall().rejects(new HTTPError("Not Found", 404));
@@ -1323,9 +1330,9 @@ for (const fallback of [false, true]) {
13231330
t.is(extractStub.firstCall.args[0], assetURL);
13241331
t.is(extractStub.lastCall.args[0], fallback ? combinedURL : assetURL);
13251332
t.is(authorizationSpy.callCount, extractStub.callCount);
1326-
t.is(authorizationSpy.firstCall.args[2], assetURL);
1333+
t.is(authorizationSpy.firstCall.args[1], assetURL);
13271334
t.is(
1328-
authorizationSpy.lastCall.args[2],
1335+
authorizationSpy.lastCall.args[1],
13291336
fallback ? combinedURL : assetURL,
13301337
);
13311338
t.is(extractStub.lastCall.args[3], "token token");

‎src/setup-codeql.ts‎

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -767,11 +767,8 @@ export const downloadCodeQL = async function (
767767
if (searchParams.has("token")) {
768768
logger.debug("CodeQL tools URL contains an authorization token.");
769769
} else {
770-
authorization = api.getAuthorizationHeaderFor(
771-
logger,
772-
source.apiDetails,
773-
codeqlURL,
774-
);
770+
const client = new api.InstanceApiClient(source.apiDetails);
771+
authorization = client.getAuthorizationHeaderFor(logger, codeqlURL);
775772
}
776773

777774
const toolcacheDestination = getToolcacheDestination({ logger }, source);

0 commit comments

Comments
 (0)