Skip to content

Commit

Permalink
Implement auth service switching for GitHub Enterprise (#2468)
Browse files Browse the repository at this point in the history
  • Loading branch information
kabel authored May 7, 2021
1 parent 851dd6d commit 239e6c3
Show file tree
Hide file tree
Showing 15 changed files with 253 additions and 65 deletions.
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -1441,6 +1441,8 @@
"@types/temp": "0.8.34",
"@typescript-eslint/eslint-plugin": "4.18.0",
"@typescript-eslint/parser": "4.18.0",
"buffer": "^6.0.3",
"crypto-browserify": "3.12.0",
"css-loader": "5.1.3",
"esbuild-loader": "2.10.0",
"eslint": "7.22.0",
Expand All @@ -1466,6 +1468,7 @@
"react-testing-library": "7.0.1",
"sinon": "9.0.0",
"source-map-support": "0.5.19",
"stream-browserify": "^3.0.0",
"style-loader": "2.0.0",
"svg-inline-loader": "^0.8.2",
"temp": "0.9.4",
Expand Down
2 changes: 1 addition & 1 deletion src/authentication/githubServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export class GitHubManager {
let isGitHub = false;
try {
const response = await fetch(uri.toString(), options);
isGitHub = response.headers['x-github-request-id'] !== undefined;
isGitHub = response.headers.get('x-github-request-id') !== undefined;
return isGitHub;
} catch (ex) {
Logger.appendLine(`No response from host ${host}: ${ex.message}`, 'GitHubServer');
Expand Down
6 changes: 6 additions & 0 deletions src/common/remote.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
*--------------------------------------------------------------------------------------------*/

import { Repository } from '../api/api';
import { AuthProvider } from '../github/credentials';
import { getEnterpriseUri } from '../github/utils';
import { Protocol } from './protocol';

export class Remote {
Expand All @@ -22,6 +24,10 @@ export class Remote {
return `${normalizedUri!.scheme}://${normalizedUri!.authority}`;
}

public get authProviderId(): AuthProvider {
return this.host === getEnterpriseUri()?.authority ? AuthProvider['github-enterprise'] : AuthProvider.github;
}

constructor(
public readonly remoteName: string,
public readonly url: string,
Expand Down
8 changes: 6 additions & 2 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,10 @@ import { handler as uriHandler } from './common/uri';
import { onceEvent } from './common/utils';
import { EXTENSION_ID, FOCUS_REVIEW_MODE } from './constants';
import { createExperimentationService, ExperimentationTelemetry } from './experimentationService';
import { CredentialStore } from './github/credentials';
import { AuthProvider, CredentialStore } from './github/credentials';
import { FolderRepositoryManager } from './github/folderRepositoryManager';
import { RepositoriesManager } from './github/repositoriesManager';
import { hasEnterpriseUri } from './github/utils';
import { registerBuiltinGitProvider, registerLiveShareGitProvider } from './gitProviders/api';
import { GitHubContactServiceProvider } from './gitProviders/GitHubContactServiceProvider';
import { GitLensIntegration } from './integrations/gitlens/gitlensImpl';
Expand Down Expand Up @@ -228,7 +229,10 @@ async function deferredActivate(context: vscode.ExtensionContext, apiImpl: GitAp
PersistentState.init(context);
const credentialStore = new CredentialStore(telemetry);
context.subscriptions.push(credentialStore);
await credentialStore.initialize();
await credentialStore.initialize(AuthProvider.github);
if (hasEnterpriseUri()) {
await credentialStore.initialize(AuthProvider['github-enterprise']);
}

const builtInGitProvider = registerBuiltinGitProvider(credentialStore, apiImpl);
if (builtInGitProvider) {
Expand Down
12 changes: 8 additions & 4 deletions src/gitExtensionIntegration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

import { RemoteSource, RemoteSourceProvider } from './@types/git';
import { OctokitCommon } from './github/common';
import { CredentialStore, GitHub } from './github/credentials';
import { AuthProvider, CredentialStore, GitHub } from './github/credentials';

interface Repository {
readonly full_name: string;
Expand All @@ -31,16 +31,20 @@ function asRemoteSource(raw: Repository): RemoteSource {
}

export class GithubRemoteSourceProvider implements RemoteSourceProvider {
readonly name = 'GitHub';
readonly name: string = 'GitHub';
readonly icon = 'github';
readonly supportsQuery = true;

private userReposCache: RemoteSource[] = [];

constructor(private readonly credentialStore: CredentialStore) {}
constructor(private readonly credentialStore: CredentialStore, private readonly authProviderId: AuthProvider = AuthProvider.github) {
if (authProviderId === AuthProvider['github-enterprise']) {
this.name = 'GitHub Enterprise';
}
}

async getRemoteSources(query?: string): Promise<RemoteSource[]> {
const hub = await this.credentialStore.getHubOrLogin();
const hub = await this.credentialStore.getHubOrLogin(this.authProviderId);

if (!hub) {
throw new Error('Could not fetch repositories from GitHub.');
Expand Down
138 changes: 99 additions & 39 deletions src/github/credentials.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import * as PersistentState from '../common/persistentState';
import { ITelemetry } from '../common/telemetry';
import { agent } from '../env/node/net';
import { OctokitCommon } from './common';
import { getEnterpriseUri, hasEnterpriseUri } from './utils';

const TRY_AGAIN = 'Try again?';
const CANCEL = 'Cancel';
Expand All @@ -23,9 +24,13 @@ const IGNORE_COMMAND = "Don't show again";
const PROMPT_FOR_SIGN_IN_SCOPE = 'prompt for sign in';
const PROMPT_FOR_SIGN_IN_STORAGE_KEY = 'login';

const AUTH_PROVIDER_ID = 'github';
const SCOPES = ['read:user', 'user:email', 'repo'];

export enum AuthProvider {
github = 'github',
'github-enterprise' = 'github-enterprise'
}

export interface GitHub {
octokit: Octokit;
graphql: ApolloClient<NormalizedCacheObject> | null;
Expand All @@ -35,6 +40,8 @@ export interface GitHub {
export class CredentialStore implements vscode.Disposable {
private _githubAPI: GitHub | undefined;
private _sessionId: string | undefined;
private _githubEnterpriseAPI: GitHub | undefined;
private _enterpriseSessionId: string | undefined;
private _disposables: vscode.Disposable[];
private _onDidInitialize: vscode.EventEmitter<void> = new vscode.EventEmitter();
public readonly onDidInitialize: vscode.Event<void> = this._onDidInitialize.event;
Expand All @@ -43,58 +50,89 @@ export class CredentialStore implements vscode.Disposable {
this._disposables = [];
this._disposables.push(
vscode.authentication.onDidChangeSessions(() => {
if (!this.isAuthenticated()) {
return this.initialize();
if (!this.isAuthenticated(AuthProvider.github)) {
this.initialize(AuthProvider.github);
}

if (!this.isAuthenticated(AuthProvider['github-enterprise']) && hasEnterpriseUri()) {
this.initialize(AuthProvider['github-enterprise']);
}
}),
);
}

public async initialize(): Promise<void> {
const session = await vscode.authentication.getSession(AUTH_PROVIDER_ID, SCOPES, { createIfNone: false });
public async initialize(authProviderId: AuthProvider): Promise<void> {
if (authProviderId === AuthProvider['github-enterprise']) {
if (!hasEnterpriseUri()) {
Logger.debug(`GitHub Enterprise provider selected without URI.`, 'Authentication');
return;
}
}

const session = await vscode.authentication.getSession(authProviderId, SCOPES, { createIfNone: false });

if (session) {
const token = session.accessToken;
this._sessionId = session.id;
const octokit = await this.createHub(token);
this._githubAPI = octokit;
await this.setCurrentUser(octokit);
if (authProviderId === AuthProvider.github) {
this._sessionId = session.id;
} else {
this._enterpriseSessionId = session.id;
}
const github = await this.createHub(session.accessToken, authProviderId);
if (authProviderId === AuthProvider.github) {
this._githubAPI = github;
} else {
this._githubEnterpriseAPI = github;
}
await this.setCurrentUser(github);
this._onDidInitialize.fire();
} else {
Logger.debug(`No token found.`, 'Authentication');
Logger.debug(`No GitHub${getGitHubSuffix(authProviderId)} token found.`, 'Authentication');
}
}

public async reset() {
this._githubAPI = undefined;
await this.initialize();
this._githubEnterpriseAPI = undefined;
await this.initialize(AuthProvider.github);
if (hasEnterpriseUri()) {
await this.initialize(AuthProvider['github-enterprise']);
}
}

public isAuthenticated(): boolean {
return !!this._githubAPI;
public isAuthenticated(authProviderId: AuthProvider): boolean {
if (authProviderId === AuthProvider.github) {
return !!this._githubAPI;
}
return !!this._githubEnterpriseAPI;
}

public getHub(): GitHub | undefined {
return this._githubAPI;
public getHub(authProviderId: AuthProvider): GitHub | undefined {
if (authProviderId === AuthProvider.github) {
return this._githubAPI;
}
return this._githubEnterpriseAPI;
}

public async getHubOrLogin(): Promise<GitHub | undefined> {
return this._githubAPI ?? (await this.login());
public async getHubOrLogin(authProviderId: AuthProvider): Promise<GitHub | undefined> {
if (authProviderId === AuthProvider.github) {
return this._githubAPI ?? (await this.login(authProviderId));
}
return this._githubEnterpriseAPI ?? (await this.login(authProviderId));
}

public async showSignInNotification(): Promise<GitHub | undefined> {
public async showSignInNotification(authProviderId: AuthProvider): Promise<GitHub | undefined> {
if (PersistentState.fetch(PROMPT_FOR_SIGN_IN_SCOPE, PROMPT_FOR_SIGN_IN_STORAGE_KEY) === false) {
return;
}

const result = await vscode.window.showInformationMessage(
`In order to use the Pull Requests functionality, you must sign in to GitHub`,
`In order to use the Pull Requests functionality, you must sign in to GitHub${getGitHubSuffix(authProviderId)}`,
SIGNIN_COMMAND,
IGNORE_COMMAND,
);

if (result === SIGNIN_COMMAND) {
return await this.login();
return await this.login(authProviderId);
} else {
// user cancelled sign in, remember that and don't ask again
PersistentState.store(PROMPT_FOR_SIGN_IN_SCOPE, PROMPT_FOR_SIGN_IN_STORAGE_KEY, false);
Expand All @@ -108,25 +146,30 @@ export class CredentialStore implements vscode.Disposable {

public async logout(): Promise<void> {
if (this._sessionId) {
vscode.authentication.logout('github', this._sessionId);
vscode.authentication.logout(AuthProvider.github, this._sessionId);
}
if (this._enterpriseSessionId) {
vscode.authentication.logout(AuthProvider['github-enterprise'], this._enterpriseSessionId);
}
}

public async login(): Promise<GitHub | undefined> {
public async login(authProviderId: AuthProvider): Promise<GitHub | undefined> {
/* __GDPR__
"auth.start" : {}
*/
this._telemetry.sendTelemetryEvent('auth.start');

const errorPrefix = `Error signing in to GitHub${getGitHubSuffix(authProviderId)}`;
let retry: boolean = true;
let octokit: GitHub | undefined = undefined;


while (retry) {
try {
const token = await this.getSessionOrLogin();
octokit = await this.createHub(token);
const token = await this.getSessionOrLogin(authProviderId);
octokit = await this.createHub(token, authProviderId);
} catch (e) {
Logger.appendLine(`Error signing in to GitHub: ${e}`);
Logger.appendLine(`${errorPrefix}: ${e}`);
if (e instanceof Error && e.stack) {
Logger.appendLine(e.stack);
}
Expand All @@ -135,9 +178,7 @@ export class CredentialStore implements vscode.Disposable {
if (octokit) {
retry = false;
} else {
retry =
(await vscode.window.showErrorMessage(`Error signing in to GitHub`, TRY_AGAIN, CANCEL)) ===
TRY_AGAIN;
retry = (await vscode.window.showErrorMessage(errorPrefix, TRY_AGAIN, CANCEL)) === TRY_AGAIN;
}
}

Expand All @@ -160,37 +201,52 @@ export class CredentialStore implements vscode.Disposable {
}

public isCurrentUser(username: string): boolean {
return this._githubAPI?.currentUser?.login === username;
return this._githubAPI?.currentUser?.login === username || this._githubEnterpriseAPI?.currentUser?.login == username;
}

public getCurrentUser(): OctokitCommon.PullsGetResponseUser {
const octokit = this._githubAPI?.octokit;
// TODO remove cast
return octokit && (this._githubAPI as any).currentUser;
public getCurrentUser(authProviderId: AuthProvider): OctokitCommon.PullsGetResponseUser {
const github = this.getHub(authProviderId);
const octokit = github?.octokit;
return octokit && github.currentUser;
}

private async setCurrentUser(github: GitHub): Promise<void> {
const user = await github.octokit.users.getAuthenticated({});
github.currentUser = user.data;
}

private async getSessionOrLogin(): Promise<string> {
const session = await vscode.authentication.getSession(AUTH_PROVIDER_ID, SCOPES, { createIfNone: true });
this._sessionId = session.id;
private async getSessionOrLogin(authProviderId: AuthProvider): Promise<string> {
const session = await vscode.authentication.getSession(authProviderId, SCOPES, { createIfNone: true });
if (authProviderId === AuthProvider.github) {
this._sessionId = session.id;
} else {
this._enterpriseSessionId = session.id;
}
return session.accessToken;
}

private async createHub(token: string): Promise<GitHub> {
private async createHub(token: string, authProviderId: AuthProvider): Promise<GitHub> {
let baseUrl = 'https://api.github.com';
if (authProviderId === AuthProvider['github-enterprise']) {
const serverUri = getEnterpriseUri();
baseUrl = `${serverUri.scheme}://${serverUri.authority}/api/v3`;
}

const octokit = new Octokit({
request: { agent },
userAgent: 'GitHub VSCode Pull Requests',
// `shadow-cat-preview` is required for Draft PR API access -- https://developer.github.com/v3/previews/#draft-pull-requests
previews: ['shadow-cat-preview'],
auth: `${token || ''}`,
baseUrl: baseUrl,
});

if (authProviderId === AuthProvider['github-enterprise']) {
const serverUri = getEnterpriseUri();
baseUrl = `${serverUri.scheme}://${serverUri.authority}/api`;
}
const graphql = new ApolloClient({
link: link('https://api.github.com', token || ''),
link: link(baseUrl, token || ''),
cache: new InMemoryCache(),
defaultOptions: {
query: {
Expand Down Expand Up @@ -226,3 +282,7 @@ const link = (url: string, token: string) =>
fetch: fetch as any,
}),
);

function getGitHubSuffix(authProviderId: AuthProvider) {
return authProviderId === AuthProvider.github ? '' : ' Enterprise';
}
8 changes: 4 additions & 4 deletions src/github/folderRepositoryManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { formatError, Predicate } from '../common/utils';
import { EXTENSION_ID } from '../constants';
import { UserCompletion, userMarkdown } from '../issues/util';
import { OctokitCommon } from './common';
import { CredentialStore } from './credentials';
import { AuthProvider, CredentialStore } from './credentials';
import { GitHubRepository, ItemsData, PullRequestData, ViewerPermission } from './githubRepository';
import { PullRequestState, UserResponse } from './graphql';
import { IAccount, ILabel, IPullRequestsPagingOptions, PRType, RepoAccessAndMergeMethods, User } from './interface';
Expand Down Expand Up @@ -468,13 +468,13 @@ export class FolderRepositoryManager implements vscode.Disposable {
}

const activeRemotes = await this.getActiveRemotes();
const isAuthenticated = this._credentialStore.isAuthenticated();
const isAuthenticated = this._credentialStore.isAuthenticated(AuthProvider.github) || this._credentialStore.isAuthenticated(AuthProvider['github-enterprise']);
vscode.commands.executeCommand('setContext', 'github:authenticated', isAuthenticated);

const repositories: GitHubRepository[] = [];
const resolveRemotePromises: Promise<void>[] = [];

const authenticatedRemotes = isAuthenticated ? activeRemotes : [];
const authenticatedRemotes = activeRemotes.filter(remote => this._credentialStore.isAuthenticated(remote.authProviderId));
authenticatedRemotes.forEach(remote => {
const repository = this.createGitHubRepository(remote, this._credentialStore);
resolveRemotePromises.push(repository.resolveRemote());
Expand Down Expand Up @@ -1233,7 +1233,7 @@ export class FolderRepositoryManager implements vscode.Disposable {
}

getCurrentUser(issueModel: IssueModel): IAccount {
return convertRESTUserToAccount(this._credentialStore.getCurrentUser(), issueModel.githubRepository);
return convertRESTUserToAccount(this._credentialStore.getCurrentUser(issueModel.githubRepository.remote.authProviderId), issueModel.githubRepository);
}

async mergePullRequest(
Expand Down
Loading

0 comments on commit 239e6c3

Please sign in to comment.