Skip to content

Commit

Permalink
Converts GitHub integration authentication to use gk.dev GLVSC-554 (#…
Browse files Browse the repository at this point in the history
…3356)

Starts using gk.dev’s auth flow first, otherwise check for a local authentication to GitHub and use it if we have access.

* Moves Jira authentication to the superclass
* Moves getSession and createSession from auth service to auth providers
* Wraps supporting of built-in VSCode providers in BuiltInAuthenticationProvider class
* Introduces a provider for GitHub integration that uses GK.dev flow and if no success there it tries to check for existing GitHub session
* Splits base auth-provider to local and cloud subclasses that implement createSession differently but share the common logic, which is implemented in the base class, of managing the created session
* Stops refreshing GitHub tokens because they never expire: sets the expiration period to 1 year from now.
* Ensures that manageCloudIntegrations is always called before attempting integration.connect for GitHub
* Skips manage integrations page if GitHub is already connected
* Uses different keys for cloud and local tokens saved to the secret-storage. Renames keys of cloud tokens saved under local keys.
* Deletes only cloud ones on `syncCloudIntegrations`
  • Loading branch information
sergeibbb authored Jul 3, 2024
1 parent 063ae58 commit 2f9750e
Show file tree
Hide file tree
Showing 16 changed files with 511 additions and 234 deletions.
6 changes: 3 additions & 3 deletions src/commands/cloudIntegrations.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import type { Source } from '../constants';
import { Commands } from '../constants';
import type { Container } from '../container';
import type { IssueIntegrationId } from '../plus/integrations/providers/models';
import type { SupportedCloudIntegrationIds } from '../plus/integrations/authentication/models';
import { command } from '../system/command';
import { Command } from './base';

export interface ManageCloudIntegrationsCommandArgs extends Source {
integrationId?: IssueIntegrationId.Jira;
integrationId?: SupportedCloudIntegrationIds;
}

@command()
Expand All @@ -17,7 +17,7 @@ export class ManageCloudIntegrationsCommand extends Command {

async execute(args?: ManageCloudIntegrationsCommandArgs) {
await this.container.integrations.manageCloudIntegrations(
args?.integrationId,
args?.integrationId ? { integrationId: args.integrationId } : undefined,
args?.source ? { source: args.source, detail: args?.detail } : undefined,
);
}
Expand Down
24 changes: 23 additions & 1 deletion src/commands/remoteProviders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type { GitRemote } from '../git/models/remote';
import { isRemote } from '../git/models/remote';
import type { Repository } from '../git/models/repository';
import type { RemoteProvider } from '../git/remotes/remoteProvider';
import { isSupportedCloudIntegrationId } from '../plus/integrations/authentication/models';
import { showRepositoryPicker } from '../quickpicks/repositoryPicker';
import { command } from '../system/command';
import { first } from '../system/iterable';
Expand Down Expand Up @@ -92,7 +93,28 @@ export class ConnectRemoteProviderCommand extends Command {
const integration = await this.container.integrations.getByRemote(remote);
if (integration == null) return false;

const connected = await integration.connect();
// Some integrations does not require managmement of Cloud Integrations (e.g. GitHub that can take a built-in VS Code session),
// therefore we try to connect them right away.
// Only if our attempt fails, we fall to manageCloudIntegrations flow.
let connected = await integration.connect();

if (!connected) {
if (isSupportedCloudIntegrationId(integration.id)) {
await this.container.integrations.manageCloudIntegrations(
{ integrationId: integration.id, skipIfConnected: true },
{
source: 'remoteProvider',
detail: {
action: 'connect',
integration: integration.id,
},
},
);
}

connected = await integration.connect();
}

if (
connected &&
!(remotes ?? (await this.container.git.getRemotesWithProviders(repoPath))).some(r => r.default)
Expand Down
7 changes: 5 additions & 2 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ import type { FileAnnotationType, ViewShowBranchComparison } from './config';
import type { Environment } from './container';
import type { StoredSearchQuery } from './git/search';
import type { Subscription, SubscriptionPlanId, SubscriptionState } from './plus/gk/account/subscription';
import type { SupportedCloudIntegrationIds } from './plus/integrations/authentication/models';
import type { Integration } from './plus/integrations/integration';
import type { IntegrationId, IssueIntegrationId } from './plus/integrations/providers/models';
import type { IntegrationId } from './plus/integrations/providers/models';
import type { TelemetryEventData } from './telemetry/telemetry';
import type { TrackedUsage, TrackedUsageKeys } from './telemetry/usageTracker';

Expand Down Expand Up @@ -846,6 +847,7 @@ export type Sources =
| 'notification'
| 'patchDetails'
| 'prompt'
| 'remoteProvider'
| 'settings'
| 'timeline'
| 'trial-indicator'
Expand Down Expand Up @@ -877,6 +879,7 @@ export type SupportedAIModels =

export type SecretKeys =
| `gitlens.integration.auth:${IntegrationId}|${string}`
| `gitlens.integration.auth.cloud:${IntegrationId}|${string}`
| `gitlens.${AIProviders}.key`
| `gitlens.plus.auth:${Environment}`;

Expand Down Expand Up @@ -1224,7 +1227,7 @@ export type TelemetryEvents = {
};
/** Sent when a user chooses to manage the cloud integrations */
'cloudIntegrations/settingsOpened': {
'integration.id': IssueIntegrationId | undefined;
'integration.id': SupportedCloudIntegrationIds | undefined;
};

/** Sent when a code suggestion is archived */
Expand Down
2 changes: 1 addition & 1 deletion src/container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -636,7 +636,7 @@ export class Container {
private _integrations: IntegrationService | undefined;
get integrations(): IntegrationService {
if (this._integrations == null) {
const authenticationService = new IntegrationAuthenticationService(this, this._connection);
const authenticationService = new IntegrationAuthenticationService(this);
this._disposables.push(
authenticationService,
(this._integrations = new IntegrationService(this, authenticationService)),
Expand Down
13 changes: 13 additions & 0 deletions src/plus/focus/focus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import { getScopedCounter } from '../../system/counter';
import { fromNow } from '../../system/date';
import { interpolate, pluralize } from '../../system/string';
import { openUrl } from '../../system/utils';
import { isSupportedCloudIntegrationId } from '../integrations/authentication/models';
import type { IntegrationId } from '../integrations/providers/models';
import {
HostingIntegrationId,
Expand Down Expand Up @@ -160,6 +161,18 @@ export class FocusCommand extends QuickCommand<State> {
const integration = await this.container.integrations.get(id);
let connected = integration.maybeConnected ?? (await integration.isConnected());
if (!connected) {
if (isSupportedCloudIntegrationId(integration.id)) {
await this.container.integrations.manageCloudIntegrations(
{ integrationId: integration.id },
{
source: 'launchpad',
detail: {
action: 'connect',
integration: integration.id,
},
},
);
}
connected = await integration.connect();
}

Expand Down
10 changes: 10 additions & 0 deletions src/plus/focus/focusIndicator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -567,6 +567,16 @@ export class FocusIndicator implements Disposable {
const github = await this.container.integrations?.get(HostingIntegrationId.GitHub);
if (github == null) break;
if (!(github.maybeConnected ?? (await github.isConnected()))) {
await this.container.integrations.manageCloudIntegrations(
{ integrationId: HostingIntegrationId.GitHub },
{
source: 'launchpad-indicator',
detail: {
action: 'connect',
integration: HostingIntegrationId.GitHub,
},
},
);
void github.connect();
}
break;
Expand Down
15 changes: 7 additions & 8 deletions src/plus/integrations/authentication/azureDevOps.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
import type { AuthenticationSession, Disposable, QuickInputButton } from 'vscode';
import { env, ThemeIcon, Uri, window } from 'vscode';
import { base64 } from '../../../system/string';
import type {
IntegrationAuthenticationProvider,
IntegrationAuthenticationSessionDescriptor,
} from './integrationAuthentication';
import { HostingIntegrationId } from '../providers/models';
import type { IntegrationAuthenticationSessionDescriptor } from './integrationAuthentication';
import { LocalIntegrationAuthenticationProvider } from './integrationAuthentication';

export class AzureDevOpsAuthenticationProvider implements IntegrationAuthenticationProvider {
getSessionId(descriptor?: IntegrationAuthenticationSessionDescriptor): string {
return descriptor?.domain ?? '';
export class AzureDevOpsAuthenticationProvider extends LocalIntegrationAuthenticationProvider<HostingIntegrationId.AzureDevOps> {
protected override get authProviderId(): HostingIntegrationId.AzureDevOps {
return HostingIntegrationId.AzureDevOps;
}

async createSession(
override async createSession(
descriptor?: IntegrationAuthenticationSessionDescriptor,
): Promise<AuthenticationSession | undefined> {
let azureOrganization: string | undefined = descriptor?.organization as string | undefined;
Expand Down
15 changes: 7 additions & 8 deletions src/plus/integrations/authentication/bitbucket.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
import type { AuthenticationSession, Disposable, QuickInputButton } from 'vscode';
import { env, ThemeIcon, Uri, window } from 'vscode';
import { base64 } from '../../../system/string';
import type {
IntegrationAuthenticationProvider,
IntegrationAuthenticationSessionDescriptor,
} from './integrationAuthentication';
import { HostingIntegrationId } from '../providers/models';
import type { IntegrationAuthenticationSessionDescriptor } from './integrationAuthentication';
import { LocalIntegrationAuthenticationProvider } from './integrationAuthentication';

export class BitbucketAuthenticationProvider implements IntegrationAuthenticationProvider {
getSessionId(descriptor?: IntegrationAuthenticationSessionDescriptor): string {
return descriptor?.domain ?? '';
export class BitbucketAuthenticationProvider extends LocalIntegrationAuthenticationProvider<HostingIntegrationId.Bitbucket> {
protected override get authProviderId(): HostingIntegrationId.Bitbucket {
return HostingIntegrationId.Bitbucket;
}

async createSession(
override async createSession(
descriptor?: IntegrationAuthenticationSessionDescriptor,
): Promise<AuthenticationSession | undefined> {
let bitbucketUsername: string | undefined = descriptor?.username as string | undefined;
Expand Down
43 changes: 35 additions & 8 deletions src/plus/integrations/authentication/github.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,43 @@
import type { AuthenticationSession, Disposable, QuickInputButton } from 'vscode';
import { env, ThemeIcon, Uri, window } from 'vscode';
import type {
IntegrationAuthenticationProvider,
IntegrationAuthenticationSessionDescriptor,
import { authentication, env, ThemeIcon, Uri, window } from 'vscode';
import { wrapForForcedInsecureSSL } from '@env/fetch';
import { HostingIntegrationId, SelfHostedIntegrationId } from '../providers/models';
import type { IntegrationAuthenticationSessionDescriptor } from './integrationAuthentication';
import {
CloudIntegrationAuthenticationProvider,
LocalIntegrationAuthenticationProvider,
} from './integrationAuthentication';

export class GitHubEnterpriseAuthenticationProvider implements IntegrationAuthenticationProvider {
getSessionId(descriptor?: IntegrationAuthenticationSessionDescriptor): string {
return descriptor?.domain ?? '';
export class GitHubAuthenticationProvider extends CloudIntegrationAuthenticationProvider<HostingIntegrationId.GitHub> {
protected override get authProviderId(): HostingIntegrationId.GitHub {
return HostingIntegrationId.GitHub;
}

async createSession(
override async getBuiltInExistingSession(
descriptor?: IntegrationAuthenticationSessionDescriptor,
): Promise<AuthenticationSession | undefined> {
if (descriptor == null) return undefined;

return wrapForForcedInsecureSSL(
this.container.integrations.ignoreSSLErrors({ id: this.authProviderId, domain: descriptor?.domain }),
() =>
authentication.getSession(this.authProviderId, descriptor.scopes, {
silent: true,
}),
);
}

protected override getCompletionInputTitle(): string {
return 'Connect to GitHub';
}
}

export class GitHubEnterpriseAuthenticationProvider extends LocalIntegrationAuthenticationProvider<SelfHostedIntegrationId.GitHubEnterprise> {
protected override get authProviderId(): SelfHostedIntegrationId.GitHubEnterprise {
return SelfHostedIntegrationId.GitHubEnterprise;
}

override async createSession(
descriptor?: IntegrationAuthenticationSessionDescriptor,
): Promise<AuthenticationSession | undefined> {
const input = window.createInputBox();
Expand Down
21 changes: 13 additions & 8 deletions src/plus/integrations/authentication/gitlab.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
import type { AuthenticationSession, Disposable, QuickInputButton } from 'vscode';
import { env, ThemeIcon, Uri, window } from 'vscode';
import type {
IntegrationAuthenticationProvider,
IntegrationAuthenticationSessionDescriptor,
} from './integrationAuthentication';
import type { Container } from '../../../container';
import type { HostingIntegrationId, SelfHostedIntegrationId } from '../providers/models';
import type { IntegrationAuthenticationSessionDescriptor } from './integrationAuthentication';
import { LocalIntegrationAuthenticationProvider } from './integrationAuthentication';

export class GitLabAuthenticationProvider implements IntegrationAuthenticationProvider {
getSessionId(descriptor?: IntegrationAuthenticationSessionDescriptor): string {
return descriptor?.domain ?? '';
type GitLabId = HostingIntegrationId.GitLab | SelfHostedIntegrationId.GitLabSelfHosted;

export class GitLabAuthenticationProvider extends LocalIntegrationAuthenticationProvider<GitLabId> {
constructor(
container: Container,
protected readonly authProviderId: GitLabId,
) {
super(container);
}

async createSession(
override async createSession(
descriptor?: IntegrationAuthenticationSessionDescriptor,
): Promise<AuthenticationSession | undefined> {
const input = window.createInputBox();
Expand Down
Loading

0 comments on commit 2f9750e

Please sign in to comment.