Skip to content

Commit

Permalink
Merge pull request #3607 from github/koesie10/ghec-dr-variant-analysis
Browse files Browse the repository at this point in the history
Add GHEC-DR support
  • Loading branch information
koesie10 authored May 14, 2024
2 parents 616b613 + ba9007e commit 247df2e
Show file tree
Hide file tree
Showing 25 changed files with 345 additions and 101 deletions.
5 changes: 5 additions & 0 deletions extensions/ql-vscode/src/common/authentication.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,9 @@ export interface Credentials {
* @returns An OAuth access token, or undefined.
*/
getExistingAccessToken(): Promise<string | undefined>;

/**
* Returns the ID of the authentication provider to use.
*/
authProviderId: string;
}
38 changes: 23 additions & 15 deletions extensions/ql-vscode/src/common/github-url-identifier-helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,37 +29,45 @@ function validGitHubNwoOrOwner(

/**
* Extracts an NWO from a GitHub URL.
* @param githubUrl The GitHub repository URL
* @param repositoryUrl The GitHub repository URL
* @param githubUrl The URL of the GitHub instance
* @return The corresponding NWO, or undefined if the URL is not valid
*/
export function getNwoFromGitHubUrl(githubUrl: string): string | undefined {
return getNwoOrOwnerFromGitHubUrl(githubUrl, "nwo");
export function getNwoFromGitHubUrl(
repositoryUrl: string,
githubUrl: URL,
): string | undefined {
return getNwoOrOwnerFromGitHubUrl(repositoryUrl, githubUrl, "nwo");
}

/**
* Extracts an owner from a GitHub URL.
* @param githubUrl The GitHub repository URL
* @param repositoryUrl The GitHub repository URL
* @param githubUrl The URL of the GitHub instance
* @return The corresponding Owner, or undefined if the URL is not valid
*/
export function getOwnerFromGitHubUrl(githubUrl: string): string | undefined {
return getNwoOrOwnerFromGitHubUrl(githubUrl, "owner");
export function getOwnerFromGitHubUrl(
repositoryUrl: string,
githubUrl: URL,
): string | undefined {
return getNwoOrOwnerFromGitHubUrl(repositoryUrl, githubUrl, "owner");
}

function getNwoOrOwnerFromGitHubUrl(
githubUrl: string,
repositoryUrl: string,
githubUrl: URL,
kind: "owner" | "nwo",
): string | undefined {
const validHostnames = [githubUrl.hostname, `www.${githubUrl.hostname}`];

try {
let paths: string[];
const urlElements = githubUrl.split("/");
if (
urlElements[0] === "github.com" ||
urlElements[0] === "www.github.com"
) {
paths = githubUrl.split("/").slice(1);
const urlElements = repositoryUrl.split("/");
if (validHostnames.includes(urlElements[0])) {
paths = repositoryUrl.split("/").slice(1);
} else {
const uri = new URL(githubUrl);
if (uri.hostname !== "github.com" && uri.hostname !== "www.github.com") {
const uri = new URL(repositoryUrl);
if (!validHostnames.includes(uri.hostname)) {
return;
}
paths = uri.pathname.split("/").filter((segment: string) => segment);
Expand Down
29 changes: 15 additions & 14 deletions extensions/ql-vscode/src/common/vscode/authentication.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import { authentication } from "vscode";
import type { Octokit } from "@octokit/rest";
import type { Credentials } from "../authentication";
import { AppOctokit } from "../octokit";

export const GITHUB_AUTH_PROVIDER_ID = "github";
import { hasGhecDrUri } from "../../config";
import { getOctokitBaseUrl } from "./octokit";

// We need 'repo' scope for triggering workflows, 'gist' scope for exporting results to Gist,
// and 'read:packages' for reading private CodeQL packages.
Expand All @@ -16,30 +16,24 @@ const SCOPES = ["repo", "gist", "read:packages"];
*/
export class VSCodeCredentials implements Credentials {
/**
* A specific octokit to return, otherwise a new authenticated octokit will be created when needed.
*/
private octokit: Octokit | undefined;

/**
* Creates or returns an instance of Octokit.
* Creates or returns an instance of Octokit. The returned instance should
* not be stored and reused, as it may become out-of-date with the current
* authentication session.
*
* @returns An instance of Octokit.
*/
async getOctokit(): Promise<Octokit> {
if (this.octokit) {
return this.octokit;
}

const accessToken = await this.getAccessToken();

return new AppOctokit({
auth: accessToken,
baseUrl: getOctokitBaseUrl(),
});
}

async getAccessToken(): Promise<string> {
const session = await authentication.getSession(
GITHUB_AUTH_PROVIDER_ID,
this.authProviderId,
SCOPES,
{ createIfNone: true },
);
Expand All @@ -49,11 +43,18 @@ export class VSCodeCredentials implements Credentials {

async getExistingAccessToken(): Promise<string | undefined> {
const session = await authentication.getSession(
GITHUB_AUTH_PROVIDER_ID,
this.authProviderId,
SCOPES,
{ createIfNone: false },
);

return session?.accessToken;
}

public get authProviderId(): string {
if (hasGhecDrUri()) {
return "github-enterprise";
}
return "github";
}
}
15 changes: 15 additions & 0 deletions extensions/ql-vscode/src/common/vscode/octokit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { getGitHubInstanceApiUrl } from "../../config";

/**
* Returns the Octokit base URL to use based on the GitHub instance URL.
*
* This is necessary because the Octokit base URL should not have a trailing
* slash, but this is included by default in a URL.
*/
export function getOctokitBaseUrl(): string {
let apiUrl = getGitHubInstanceApiUrl().toString();
if (apiUrl.endsWith("/")) {
apiUrl = apiUrl.slice(0, -1);
}
return apiUrl;
}
54 changes: 53 additions & 1 deletion extensions/ql-vscode/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,12 +108,55 @@ export function hasEnterpriseUri(): boolean {
return getEnterpriseUri() !== undefined;
}

/**
* Does the uri look like GHEC-DR?
*/
function isGhecDrUri(uri: Uri | undefined): boolean {
return uri !== undefined && uri.authority.toLowerCase().endsWith(".ghe.com");
}

/**
* Is the GitHub Enterprise URI set to something that looks like GHEC-DR?
*/
export function hasGhecDrUri(): boolean {
const uri = getEnterpriseUri();
return uri !== undefined && uri.authority.toLowerCase().endsWith(".ghe.com");
return isGhecDrUri(uri);
}

/**
* The URI for GitHub.com.
*/
export const GITHUB_URL = new URL("https://github.com");
export const GITHUB_API_URL = new URL("https://api.github.com");

/**
* If the GitHub Enterprise URI is set to something that looks like GHEC-DR, return it.
*/
export function getGhecDrUri(): Uri | undefined {
const uri = getEnterpriseUri();
if (isGhecDrUri(uri)) {
return uri;
} else {
return undefined;
}
}

export function getGitHubInstanceUrl(): URL {
const ghecDrUri = getGhecDrUri();
if (ghecDrUri) {
return new URL(ghecDrUri.toString());
}
return GITHUB_URL;
}

export function getGitHubInstanceApiUrl(): URL {
const ghecDrUri = getGhecDrUri();
if (ghecDrUri) {
const url = new URL(ghecDrUri.toString());
url.hostname = `api.${url.hostname}`;
return url;
}
return GITHUB_API_URL;
}

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

Expand All @@ -591,6 +639,10 @@ export class VariantAnalysisConfigListener
public get showSystemDefinedRepositoryLists(): boolean {
return !hasEnterpriseUri();
}

public get githubUrl(): URL {
return getGitHubInstanceUrl();
}
}

const VARIANT_ANALYSIS_FILTER_RESULTS = new Setting(
Expand Down
2 changes: 2 additions & 0 deletions extensions/ql-vscode/src/databases/code-search-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { AppOctokit } from "../common/octokit";
import type { ProgressCallback } from "../common/vscode/progress";
import { UserCancellationException } from "../common/vscode/progress";
import type { EndpointDefaults } from "@octokit/types";
import { getOctokitBaseUrl } from "../common/vscode/octokit";

export async function getCodeSearchRepositories(
query: string,
Expand Down Expand Up @@ -54,6 +55,7 @@ async function provideOctokitWithThrottling(

const octokit = new MyOctokit({
auth,
baseUrl: getOctokitBaseUrl(),
throttle: {
onRateLimit: (retryAfter: number, options: EndpointDefaults): boolean => {
void logger.log(
Expand Down
15 changes: 10 additions & 5 deletions extensions/ql-vscode/src/databases/database-fetcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ import {
addDatabaseSourceToWorkspace,
allowHttp,
downloadTimeout,
getGitHubInstanceUrl,
hasGhecDrUri,
isCanary,
} from "../config";
import { showAndLogInformationMessage } from "../common/logging";
Expand Down Expand Up @@ -150,10 +152,11 @@ export class DatabaseFetcher {
maxStep: 2,
});

const instanceUrl = getGitHubInstanceUrl();

const options: InputBoxOptions = {
title:
'Enter a GitHub repository URL or "name with owner" (e.g. https://github.com/github/codeql or github/codeql)',
placeHolder: "https://github.com/<owner>/<repo> or <owner>/<repo>",
title: `Enter a GitHub repository URL or "name with owner" (e.g. ${new URL("/github/codeql", instanceUrl).toString()} or github/codeql)`,
placeHolder: `${new URL("/", instanceUrl).toString()}<owner>/<repo> or <owner>/<repo>`,
ignoreFocusOut: true,
};

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

const credentials = isCanary() ? this.app.credentials : undefined;
const credentials =
isCanary() || hasGhecDrUri() ? this.app.credentials : undefined;

const octokit = credentials
? await credentials.getOctokit()
Expand Down
6 changes: 5 additions & 1 deletion extensions/ql-vscode/src/databases/github-databases/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { Octokit } from "@octokit/rest";
import type { RestEndpointMethodTypes } from "@octokit/plugin-rest-endpoint-methods";
import { showNeverAskAgainDialog } from "../../common/vscode/dialog";
import type { GitHubDatabaseConfig } from "../../config";
import { hasGhecDrUri } from "../../config";
import type { Credentials } from "../../common/authentication";
import { AppOctokit } from "../../common/octokit";
import type { ProgressCallback } from "../../common/vscode/progress";
Expand Down Expand Up @@ -67,7 +68,10 @@ export async function listDatabases(
credentials: Credentials,
config: GitHubDatabaseConfig,
): Promise<ListDatabasesResult | undefined> {
const hasAccessToken = !!(await credentials.getExistingAccessToken());
// On GHEC-DR, unauthenticated requests will never work, so we should always ask
// for authentication.
const hasAccessToken =
!!(await credentials.getExistingAccessToken()) || hasGhecDrUri();

let octokit = hasAccessToken
? await credentials.getOctokit()
Expand Down
17 changes: 12 additions & 5 deletions extensions/ql-vscode/src/databases/ui/db-panel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import type { App } from "../../common/app";
import { QueryLanguage } from "../../common/query-language";
import { getCodeSearchRepositories } from "../code-search-api";
import { showAndLogErrorMessage } from "../../common/logging";
import { getGitHubInstanceUrl } from "../../config";

export interface RemoteDatabaseQuickPickItem extends QuickPickItem {
remoteDatabaseKind: string;
Expand Down Expand Up @@ -146,16 +147,19 @@ export class DbPanel extends DisposableObject {
}

private async addNewRemoteRepo(parentList?: string): Promise<void> {
const instanceUrl = getGitHubInstanceUrl();

const repoName = await window.showInputBox({
title: "Add a repository",
prompt: "Insert a GitHub repository URL or name with owner",
placeHolder: "<owner>/<repo> or https://github.com/<owner>/<repo>",
placeHolder: `<owner>/<repo> or ${new URL("/", instanceUrl).toString()}<owner>/<repo>`,
});
if (!repoName) {
return;
}

const nwo = getNwoFromGitHubUrl(repoName) || repoName;
const nwo =
getNwoFromGitHubUrl(repoName, getGitHubInstanceUrl()) || repoName;
if (!isValidGitHubNwo(nwo)) {
void showAndLogErrorMessage(
this.app.logger,
Expand All @@ -176,17 +180,20 @@ export class DbPanel extends DisposableObject {
}

private async addNewRemoteOwner(): Promise<void> {
const instanceUrl = getGitHubInstanceUrl();

const ownerName = await window.showInputBox({
title: "Add all repositories of a GitHub org or owner",
prompt: "Insert a GitHub organization or owner name",
placeHolder: "<owner> or https://github.com/<owner>",
placeHolder: `<owner> or ${new URL("/", instanceUrl).toString()}<owner>`,
});

if (!ownerName) {
return;
}

const owner = getOwnerFromGitHubUrl(ownerName) || ownerName;
const owner =
getOwnerFromGitHubUrl(ownerName, getGitHubInstanceUrl()) || ownerName;
if (!isValidGitHubOwner(owner)) {
void showAndLogErrorMessage(
this.app.logger,
Expand Down Expand Up @@ -411,7 +418,7 @@ export class DbPanel extends DisposableObject {
if (treeViewItem.dbItem === undefined) {
throw new Error("Unable to open on GitHub. Please select a valid item.");
}
const githubUrl = getGitHubUrl(treeViewItem.dbItem);
const githubUrl = getGitHubUrl(treeViewItem.dbItem, getGitHubInstanceUrl());
if (!githubUrl) {
throw new Error(
"Unable to open on GitHub. Please select a variant analysis repository or owner.",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,12 +62,15 @@ function canImportCodeSearch(dbItem: DbItem): boolean {
return DbItemKind.RemoteUserDefinedList === dbItem.kind;
}

export function getGitHubUrl(dbItem: DbItem): string | undefined {
export function getGitHubUrl(
dbItem: DbItem,
githubUrl: URL,
): string | undefined {
switch (dbItem.kind) {
case DbItemKind.RemoteOwner:
return `https://github.com/${dbItem.ownerName}`;
return new URL(`/${dbItem.ownerName}`, githubUrl).toString();
case DbItemKind.RemoteRepo:
return `https://github.com/${dbItem.repoFullName}`;
return new URL(`/${dbItem.repoFullName}`, githubUrl).toString();
default:
return undefined;
}
Expand Down
Loading

0 comments on commit 247df2e

Please sign in to comment.