Skip to content

Commit 247df2e

Browse files
authored
Merge pull request #3607 from github/koesie10/ghec-dr-variant-analysis
Add GHEC-DR support
2 parents 616b613 + ba9007e commit 247df2e

25 files changed

+345
-101
lines changed

extensions/ql-vscode/src/common/authentication.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,9 @@ export interface Credentials {
3131
* @returns An OAuth access token, or undefined.
3232
*/
3333
getExistingAccessToken(): Promise<string | undefined>;
34+
35+
/**
36+
* Returns the ID of the authentication provider to use.
37+
*/
38+
authProviderId: string;
3439
}

extensions/ql-vscode/src/common/github-url-identifier-helper.ts

Lines changed: 23 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -29,37 +29,45 @@ function validGitHubNwoOrOwner(
2929

3030
/**
3131
* Extracts an NWO from a GitHub URL.
32-
* @param githubUrl The GitHub repository URL
32+
* @param repositoryUrl The GitHub repository URL
33+
* @param githubUrl The URL of the GitHub instance
3334
* @return The corresponding NWO, or undefined if the URL is not valid
3435
*/
35-
export function getNwoFromGitHubUrl(githubUrl: string): string | undefined {
36-
return getNwoOrOwnerFromGitHubUrl(githubUrl, "nwo");
36+
export function getNwoFromGitHubUrl(
37+
repositoryUrl: string,
38+
githubUrl: URL,
39+
): string | undefined {
40+
return getNwoOrOwnerFromGitHubUrl(repositoryUrl, githubUrl, "nwo");
3741
}
3842

3943
/**
4044
* Extracts an owner from a GitHub URL.
41-
* @param githubUrl The GitHub repository URL
45+
* @param repositoryUrl The GitHub repository URL
46+
* @param githubUrl The URL of the GitHub instance
4247
* @return The corresponding Owner, or undefined if the URL is not valid
4348
*/
44-
export function getOwnerFromGitHubUrl(githubUrl: string): string | undefined {
45-
return getNwoOrOwnerFromGitHubUrl(githubUrl, "owner");
49+
export function getOwnerFromGitHubUrl(
50+
repositoryUrl: string,
51+
githubUrl: URL,
52+
): string | undefined {
53+
return getNwoOrOwnerFromGitHubUrl(repositoryUrl, githubUrl, "owner");
4654
}
4755

4856
function getNwoOrOwnerFromGitHubUrl(
49-
githubUrl: string,
57+
repositoryUrl: string,
58+
githubUrl: URL,
5059
kind: "owner" | "nwo",
5160
): string | undefined {
61+
const validHostnames = [githubUrl.hostname, `www.${githubUrl.hostname}`];
62+
5263
try {
5364
let paths: string[];
54-
const urlElements = githubUrl.split("/");
55-
if (
56-
urlElements[0] === "github.com" ||
57-
urlElements[0] === "www.github.com"
58-
) {
59-
paths = githubUrl.split("/").slice(1);
65+
const urlElements = repositoryUrl.split("/");
66+
if (validHostnames.includes(urlElements[0])) {
67+
paths = repositoryUrl.split("/").slice(1);
6068
} else {
61-
const uri = new URL(githubUrl);
62-
if (uri.hostname !== "github.com" && uri.hostname !== "www.github.com") {
69+
const uri = new URL(repositoryUrl);
70+
if (!validHostnames.includes(uri.hostname)) {
6371
return;
6472
}
6573
paths = uri.pathname.split("/").filter((segment: string) => segment);

extensions/ql-vscode/src/common/vscode/authentication.ts

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ import { authentication } from "vscode";
22
import type { Octokit } from "@octokit/rest";
33
import type { Credentials } from "../authentication";
44
import { AppOctokit } from "../octokit";
5-
6-
export const GITHUB_AUTH_PROVIDER_ID = "github";
5+
import { hasGhecDrUri } from "../../config";
6+
import { getOctokitBaseUrl } from "./octokit";
77

88
// We need 'repo' scope for triggering workflows, 'gist' scope for exporting results to Gist,
99
// and 'read:packages' for reading private CodeQL packages.
@@ -16,30 +16,24 @@ const SCOPES = ["repo", "gist", "read:packages"];
1616
*/
1717
export class VSCodeCredentials implements Credentials {
1818
/**
19-
* A specific octokit to return, otherwise a new authenticated octokit will be created when needed.
20-
*/
21-
private octokit: Octokit | undefined;
22-
23-
/**
24-
* Creates or returns an instance of Octokit.
19+
* Creates or returns an instance of Octokit. The returned instance should
20+
* not be stored and reused, as it may become out-of-date with the current
21+
* authentication session.
2522
*
2623
* @returns An instance of Octokit.
2724
*/
2825
async getOctokit(): Promise<Octokit> {
29-
if (this.octokit) {
30-
return this.octokit;
31-
}
32-
3326
const accessToken = await this.getAccessToken();
3427

3528
return new AppOctokit({
3629
auth: accessToken,
30+
baseUrl: getOctokitBaseUrl(),
3731
});
3832
}
3933

4034
async getAccessToken(): Promise<string> {
4135
const session = await authentication.getSession(
42-
GITHUB_AUTH_PROVIDER_ID,
36+
this.authProviderId,
4337
SCOPES,
4438
{ createIfNone: true },
4539
);
@@ -49,11 +43,18 @@ export class VSCodeCredentials implements Credentials {
4943

5044
async getExistingAccessToken(): Promise<string | undefined> {
5145
const session = await authentication.getSession(
52-
GITHUB_AUTH_PROVIDER_ID,
46+
this.authProviderId,
5347
SCOPES,
5448
{ createIfNone: false },
5549
);
5650

5751
return session?.accessToken;
5852
}
53+
54+
public get authProviderId(): string {
55+
if (hasGhecDrUri()) {
56+
return "github-enterprise";
57+
}
58+
return "github";
59+
}
5960
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { getGitHubInstanceApiUrl } from "../../config";
2+
3+
/**
4+
* Returns the Octokit base URL to use based on the GitHub instance URL.
5+
*
6+
* This is necessary because the Octokit base URL should not have a trailing
7+
* slash, but this is included by default in a URL.
8+
*/
9+
export function getOctokitBaseUrl(): string {
10+
let apiUrl = getGitHubInstanceApiUrl().toString();
11+
if (apiUrl.endsWith("/")) {
12+
apiUrl = apiUrl.slice(0, -1);
13+
}
14+
return apiUrl;
15+
}

extensions/ql-vscode/src/config.ts

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,12 +108,55 @@ export function hasEnterpriseUri(): boolean {
108108
return getEnterpriseUri() !== undefined;
109109
}
110110

111+
/**
112+
* Does the uri look like GHEC-DR?
113+
*/
114+
function isGhecDrUri(uri: Uri | undefined): boolean {
115+
return uri !== undefined && uri.authority.toLowerCase().endsWith(".ghe.com");
116+
}
117+
111118
/**
112119
* Is the GitHub Enterprise URI set to something that looks like GHEC-DR?
113120
*/
114121
export function hasGhecDrUri(): boolean {
115122
const uri = getEnterpriseUri();
116-
return uri !== undefined && uri.authority.toLowerCase().endsWith(".ghe.com");
123+
return isGhecDrUri(uri);
124+
}
125+
126+
/**
127+
* The URI for GitHub.com.
128+
*/
129+
export const GITHUB_URL = new URL("https://github.com");
130+
export const GITHUB_API_URL = new URL("https://api.github.com");
131+
132+
/**
133+
* If the GitHub Enterprise URI is set to something that looks like GHEC-DR, return it.
134+
*/
135+
export function getGhecDrUri(): Uri | undefined {
136+
const uri = getEnterpriseUri();
137+
if (isGhecDrUri(uri)) {
138+
return uri;
139+
} else {
140+
return undefined;
141+
}
142+
}
143+
144+
export function getGitHubInstanceUrl(): URL {
145+
const ghecDrUri = getGhecDrUri();
146+
if (ghecDrUri) {
147+
return new URL(ghecDrUri.toString());
148+
}
149+
return GITHUB_URL;
150+
}
151+
152+
export function getGitHubInstanceApiUrl(): URL {
153+
const ghecDrUri = getGhecDrUri();
154+
if (ghecDrUri) {
155+
const url = new URL(ghecDrUri.toString());
156+
url.hostname = `api.${url.hostname}`;
157+
return url;
158+
}
159+
return GITHUB_API_URL;
117160
}
118161

119162
const ROOT_SETTING = new Setting("codeQL");
@@ -570,6 +613,11 @@ export async function setRemoteControllerRepo(repo: string | undefined) {
570613
export interface VariantAnalysisConfig {
571614
controllerRepo: string | undefined;
572615
showSystemDefinedRepositoryLists: boolean;
616+
/**
617+
* This uses a URL instead of a URI because the URL class is available in
618+
* unit tests and is fully browser-compatible.
619+
*/
620+
githubUrl: URL;
573621
onDidChangeConfiguration?: Event<void>;
574622
}
575623

@@ -591,6 +639,10 @@ export class VariantAnalysisConfigListener
591639
public get showSystemDefinedRepositoryLists(): boolean {
592640
return !hasEnterpriseUri();
593641
}
642+
643+
public get githubUrl(): URL {
644+
return getGitHubInstanceUrl();
645+
}
594646
}
595647

596648
const VARIANT_ANALYSIS_FILTER_RESULTS = new Setting(

extensions/ql-vscode/src/databases/code-search-api.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { AppOctokit } from "../common/octokit";
77
import type { ProgressCallback } from "../common/vscode/progress";
88
import { UserCancellationException } from "../common/vscode/progress";
99
import type { EndpointDefaults } from "@octokit/types";
10+
import { getOctokitBaseUrl } from "../common/vscode/octokit";
1011

1112
export async function getCodeSearchRepositories(
1213
query: string,
@@ -54,6 +55,7 @@ async function provideOctokitWithThrottling(
5455

5556
const octokit = new MyOctokit({
5657
auth,
58+
baseUrl: getOctokitBaseUrl(),
5759
throttle: {
5860
onRateLimit: (retryAfter: number, options: EndpointDefaults): boolean => {
5961
void logger.log(

extensions/ql-vscode/src/databases/database-fetcher.ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ import {
2929
addDatabaseSourceToWorkspace,
3030
allowHttp,
3131
downloadTimeout,
32+
getGitHubInstanceUrl,
33+
hasGhecDrUri,
3234
isCanary,
3335
} from "../config";
3436
import { showAndLogInformationMessage } from "../common/logging";
@@ -150,10 +152,11 @@ export class DatabaseFetcher {
150152
maxStep: 2,
151153
});
152154

155+
const instanceUrl = getGitHubInstanceUrl();
156+
153157
const options: InputBoxOptions = {
154-
title:
155-
'Enter a GitHub repository URL or "name with owner" (e.g. https://github.com/github/codeql or github/codeql)',
156-
placeHolder: "https://github.com/<owner>/<repo> or <owner>/<repo>",
158+
title: `Enter a GitHub repository URL or "name with owner" (e.g. ${new URL("/github/codeql", instanceUrl).toString()} or github/codeql)`,
159+
placeHolder: `${new URL("/", instanceUrl).toString()}<owner>/<repo> or <owner>/<repo>`,
157160
ignoreFocusOut: true,
158161
};
159162

@@ -180,12 +183,14 @@ export class DatabaseFetcher {
180183
makeSelected = true,
181184
addSourceArchiveFolder = addDatabaseSourceToWorkspace(),
182185
): Promise<DatabaseItem | undefined> {
183-
const nwo = getNwoFromGitHubUrl(githubRepo) || githubRepo;
186+
const nwo =
187+
getNwoFromGitHubUrl(githubRepo, getGitHubInstanceUrl()) || githubRepo;
184188
if (!isValidGitHubNwo(nwo)) {
185189
throw new Error(`Invalid GitHub repository: ${githubRepo}`);
186190
}
187191

188-
const credentials = isCanary() ? this.app.credentials : undefined;
192+
const credentials =
193+
isCanary() || hasGhecDrUri() ? this.app.credentials : undefined;
189194

190195
const octokit = credentials
191196
? await credentials.getOctokit()

extensions/ql-vscode/src/databases/github-databases/api.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type { Octokit } from "@octokit/rest";
33
import type { RestEndpointMethodTypes } from "@octokit/plugin-rest-endpoint-methods";
44
import { showNeverAskAgainDialog } from "../../common/vscode/dialog";
55
import type { GitHubDatabaseConfig } from "../../config";
6+
import { hasGhecDrUri } from "../../config";
67
import type { Credentials } from "../../common/authentication";
78
import { AppOctokit } from "../../common/octokit";
89
import type { ProgressCallback } from "../../common/vscode/progress";
@@ -67,7 +68,10 @@ export async function listDatabases(
6768
credentials: Credentials,
6869
config: GitHubDatabaseConfig,
6970
): Promise<ListDatabasesResult | undefined> {
70-
const hasAccessToken = !!(await credentials.getExistingAccessToken());
71+
// On GHEC-DR, unauthenticated requests will never work, so we should always ask
72+
// for authentication.
73+
const hasAccessToken =
74+
!!(await credentials.getExistingAccessToken()) || hasGhecDrUri();
7175

7276
let octokit = hasAccessToken
7377
? await credentials.getOctokit()

extensions/ql-vscode/src/databases/ui/db-panel.ts

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import type { App } from "../../common/app";
2525
import { QueryLanguage } from "../../common/query-language";
2626
import { getCodeSearchRepositories } from "../code-search-api";
2727
import { showAndLogErrorMessage } from "../../common/logging";
28+
import { getGitHubInstanceUrl } from "../../config";
2829

2930
export interface RemoteDatabaseQuickPickItem extends QuickPickItem {
3031
remoteDatabaseKind: string;
@@ -146,16 +147,19 @@ export class DbPanel extends DisposableObject {
146147
}
147148

148149
private async addNewRemoteRepo(parentList?: string): Promise<void> {
150+
const instanceUrl = getGitHubInstanceUrl();
151+
149152
const repoName = await window.showInputBox({
150153
title: "Add a repository",
151154
prompt: "Insert a GitHub repository URL or name with owner",
152-
placeHolder: "<owner>/<repo> or https://github.com/<owner>/<repo>",
155+
placeHolder: `<owner>/<repo> or ${new URL("/", instanceUrl).toString()}<owner>/<repo>`,
153156
});
154157
if (!repoName) {
155158
return;
156159
}
157160

158-
const nwo = getNwoFromGitHubUrl(repoName) || repoName;
161+
const nwo =
162+
getNwoFromGitHubUrl(repoName, getGitHubInstanceUrl()) || repoName;
159163
if (!isValidGitHubNwo(nwo)) {
160164
void showAndLogErrorMessage(
161165
this.app.logger,
@@ -176,17 +180,20 @@ export class DbPanel extends DisposableObject {
176180
}
177181

178182
private async addNewRemoteOwner(): Promise<void> {
183+
const instanceUrl = getGitHubInstanceUrl();
184+
179185
const ownerName = await window.showInputBox({
180186
title: "Add all repositories of a GitHub org or owner",
181187
prompt: "Insert a GitHub organization or owner name",
182-
placeHolder: "<owner> or https://github.com/<owner>",
188+
placeHolder: `<owner> or ${new URL("/", instanceUrl).toString()}<owner>`,
183189
});
184190

185191
if (!ownerName) {
186192
return;
187193
}
188194

189-
const owner = getOwnerFromGitHubUrl(ownerName) || ownerName;
195+
const owner =
196+
getOwnerFromGitHubUrl(ownerName, getGitHubInstanceUrl()) || ownerName;
190197
if (!isValidGitHubOwner(owner)) {
191198
void showAndLogErrorMessage(
192199
this.app.logger,
@@ -411,7 +418,7 @@ export class DbPanel extends DisposableObject {
411418
if (treeViewItem.dbItem === undefined) {
412419
throw new Error("Unable to open on GitHub. Please select a valid item.");
413420
}
414-
const githubUrl = getGitHubUrl(treeViewItem.dbItem);
421+
const githubUrl = getGitHubUrl(treeViewItem.dbItem, getGitHubInstanceUrl());
415422
if (!githubUrl) {
416423
throw new Error(
417424
"Unable to open on GitHub. Please select a variant analysis repository or owner.",

extensions/ql-vscode/src/databases/ui/db-tree-view-item-action.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,12 +62,15 @@ function canImportCodeSearch(dbItem: DbItem): boolean {
6262
return DbItemKind.RemoteUserDefinedList === dbItem.kind;
6363
}
6464

65-
export function getGitHubUrl(dbItem: DbItem): string | undefined {
65+
export function getGitHubUrl(
66+
dbItem: DbItem,
67+
githubUrl: URL,
68+
): string | undefined {
6669
switch (dbItem.kind) {
6770
case DbItemKind.RemoteOwner:
68-
return `https://github.com/${dbItem.ownerName}`;
71+
return new URL(`/${dbItem.ownerName}`, githubUrl).toString();
6972
case DbItemKind.RemoteRepo:
70-
return `https://github.com/${dbItem.repoFullName}`;
73+
return new URL(`/${dbItem.repoFullName}`, githubUrl).toString();
7174
default:
7275
return undefined;
7376
}

0 commit comments

Comments
 (0)