Skip to content

Commit 2f9750e

Browse files
authored
Converts GitHub integration authentication to use gk.dev GLVSC-554 (#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`
1 parent 063ae58 commit 2f9750e

16 files changed

+511
-234
lines changed

src/commands/cloudIntegrations.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import type { Source } from '../constants';
22
import { Commands } from '../constants';
33
import type { Container } from '../container';
4-
import type { IssueIntegrationId } from '../plus/integrations/providers/models';
4+
import type { SupportedCloudIntegrationIds } from '../plus/integrations/authentication/models';
55
import { command } from '../system/command';
66
import { Command } from './base';
77

88
export interface ManageCloudIntegrationsCommandArgs extends Source {
9-
integrationId?: IssueIntegrationId.Jira;
9+
integrationId?: SupportedCloudIntegrationIds;
1010
}
1111

1212
@command()
@@ -17,7 +17,7 @@ export class ManageCloudIntegrationsCommand extends Command {
1717

1818
async execute(args?: ManageCloudIntegrationsCommandArgs) {
1919
await this.container.integrations.manageCloudIntegrations(
20-
args?.integrationId,
20+
args?.integrationId ? { integrationId: args.integrationId } : undefined,
2121
args?.source ? { source: args.source, detail: args?.detail } : undefined,
2222
);
2323
}

src/commands/remoteProviders.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import type { GitRemote } from '../git/models/remote';
55
import { isRemote } from '../git/models/remote';
66
import type { Repository } from '../git/models/repository';
77
import type { RemoteProvider } from '../git/remotes/remoteProvider';
8+
import { isSupportedCloudIntegrationId } from '../plus/integrations/authentication/models';
89
import { showRepositoryPicker } from '../quickpicks/repositoryPicker';
910
import { command } from '../system/command';
1011
import { first } from '../system/iterable';
@@ -92,7 +93,28 @@ export class ConnectRemoteProviderCommand extends Command {
9293
const integration = await this.container.integrations.getByRemote(remote);
9394
if (integration == null) return false;
9495

95-
const connected = await integration.connect();
96+
// Some integrations does not require managmement of Cloud Integrations (e.g. GitHub that can take a built-in VS Code session),
97+
// therefore we try to connect them right away.
98+
// Only if our attempt fails, we fall to manageCloudIntegrations flow.
99+
let connected = await integration.connect();
100+
101+
if (!connected) {
102+
if (isSupportedCloudIntegrationId(integration.id)) {
103+
await this.container.integrations.manageCloudIntegrations(
104+
{ integrationId: integration.id, skipIfConnected: true },
105+
{
106+
source: 'remoteProvider',
107+
detail: {
108+
action: 'connect',
109+
integration: integration.id,
110+
},
111+
},
112+
);
113+
}
114+
115+
connected = await integration.connect();
116+
}
117+
96118
if (
97119
connected &&
98120
!(remotes ?? (await this.container.git.getRemotesWithProviders(repoPath))).some(r => r.default)

src/constants.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@ import type { FileAnnotationType, ViewShowBranchComparison } from './config';
77
import type { Environment } from './container';
88
import type { StoredSearchQuery } from './git/search';
99
import type { Subscription, SubscriptionPlanId, SubscriptionState } from './plus/gk/account/subscription';
10+
import type { SupportedCloudIntegrationIds } from './plus/integrations/authentication/models';
1011
import type { Integration } from './plus/integrations/integration';
11-
import type { IntegrationId, IssueIntegrationId } from './plus/integrations/providers/models';
12+
import type { IntegrationId } from './plus/integrations/providers/models';
1213
import type { TelemetryEventData } from './telemetry/telemetry';
1314
import type { TrackedUsage, TrackedUsageKeys } from './telemetry/usageTracker';
1415

@@ -846,6 +847,7 @@ export type Sources =
846847
| 'notification'
847848
| 'patchDetails'
848849
| 'prompt'
850+
| 'remoteProvider'
849851
| 'settings'
850852
| 'timeline'
851853
| 'trial-indicator'
@@ -877,6 +879,7 @@ export type SupportedAIModels =
877879

878880
export type SecretKeys =
879881
| `gitlens.integration.auth:${IntegrationId}|${string}`
882+
| `gitlens.integration.auth.cloud:${IntegrationId}|${string}`
880883
| `gitlens.${AIProviders}.key`
881884
| `gitlens.plus.auth:${Environment}`;
882885

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

12301233
/** Sent when a code suggestion is archived */

src/container.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -636,7 +636,7 @@ export class Container {
636636
private _integrations: IntegrationService | undefined;
637637
get integrations(): IntegrationService {
638638
if (this._integrations == null) {
639-
const authenticationService = new IntegrationAuthenticationService(this, this._connection);
639+
const authenticationService = new IntegrationAuthenticationService(this);
640640
this._disposables.push(
641641
authenticationService,
642642
(this._integrations = new IntegrationService(this, authenticationService)),

src/plus/focus/focus.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import { getScopedCounter } from '../../system/counter';
3939
import { fromNow } from '../../system/date';
4040
import { interpolate, pluralize } from '../../system/string';
4141
import { openUrl } from '../../system/utils';
42+
import { isSupportedCloudIntegrationId } from '../integrations/authentication/models';
4243
import type { IntegrationId } from '../integrations/providers/models';
4344
import {
4445
HostingIntegrationId,
@@ -160,6 +161,18 @@ export class FocusCommand extends QuickCommand<State> {
160161
const integration = await this.container.integrations.get(id);
161162
let connected = integration.maybeConnected ?? (await integration.isConnected());
162163
if (!connected) {
164+
if (isSupportedCloudIntegrationId(integration.id)) {
165+
await this.container.integrations.manageCloudIntegrations(
166+
{ integrationId: integration.id },
167+
{
168+
source: 'launchpad',
169+
detail: {
170+
action: 'connect',
171+
integration: integration.id,
172+
},
173+
},
174+
);
175+
}
163176
connected = await integration.connect();
164177
}
165178

src/plus/focus/focusIndicator.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -567,6 +567,16 @@ export class FocusIndicator implements Disposable {
567567
const github = await this.container.integrations?.get(HostingIntegrationId.GitHub);
568568
if (github == null) break;
569569
if (!(github.maybeConnected ?? (await github.isConnected()))) {
570+
await this.container.integrations.manageCloudIntegrations(
571+
{ integrationId: HostingIntegrationId.GitHub },
572+
{
573+
source: 'launchpad-indicator',
574+
detail: {
575+
action: 'connect',
576+
integration: HostingIntegrationId.GitHub,
577+
},
578+
},
579+
);
570580
void github.connect();
571581
}
572582
break;

src/plus/integrations/authentication/azureDevOps.ts

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,16 @@
11
import type { AuthenticationSession, Disposable, QuickInputButton } from 'vscode';
22
import { env, ThemeIcon, Uri, window } from 'vscode';
33
import { base64 } from '../../../system/string';
4-
import type {
5-
IntegrationAuthenticationProvider,
6-
IntegrationAuthenticationSessionDescriptor,
7-
} from './integrationAuthentication';
4+
import { HostingIntegrationId } from '../providers/models';
5+
import type { IntegrationAuthenticationSessionDescriptor } from './integrationAuthentication';
6+
import { LocalIntegrationAuthenticationProvider } from './integrationAuthentication';
87

9-
export class AzureDevOpsAuthenticationProvider implements IntegrationAuthenticationProvider {
10-
getSessionId(descriptor?: IntegrationAuthenticationSessionDescriptor): string {
11-
return descriptor?.domain ?? '';
8+
export class AzureDevOpsAuthenticationProvider extends LocalIntegrationAuthenticationProvider<HostingIntegrationId.AzureDevOps> {
9+
protected override get authProviderId(): HostingIntegrationId.AzureDevOps {
10+
return HostingIntegrationId.AzureDevOps;
1211
}
1312

14-
async createSession(
13+
override async createSession(
1514
descriptor?: IntegrationAuthenticationSessionDescriptor,
1615
): Promise<AuthenticationSession | undefined> {
1716
let azureOrganization: string | undefined = descriptor?.organization as string | undefined;

src/plus/integrations/authentication/bitbucket.ts

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,16 @@
11
import type { AuthenticationSession, Disposable, QuickInputButton } from 'vscode';
22
import { env, ThemeIcon, Uri, window } from 'vscode';
33
import { base64 } from '../../../system/string';
4-
import type {
5-
IntegrationAuthenticationProvider,
6-
IntegrationAuthenticationSessionDescriptor,
7-
} from './integrationAuthentication';
4+
import { HostingIntegrationId } from '../providers/models';
5+
import type { IntegrationAuthenticationSessionDescriptor } from './integrationAuthentication';
6+
import { LocalIntegrationAuthenticationProvider } from './integrationAuthentication';
87

9-
export class BitbucketAuthenticationProvider implements IntegrationAuthenticationProvider {
10-
getSessionId(descriptor?: IntegrationAuthenticationSessionDescriptor): string {
11-
return descriptor?.domain ?? '';
8+
export class BitbucketAuthenticationProvider extends LocalIntegrationAuthenticationProvider<HostingIntegrationId.Bitbucket> {
9+
protected override get authProviderId(): HostingIntegrationId.Bitbucket {
10+
return HostingIntegrationId.Bitbucket;
1211
}
1312

14-
async createSession(
13+
override async createSession(
1514
descriptor?: IntegrationAuthenticationSessionDescriptor,
1615
): Promise<AuthenticationSession | undefined> {
1716
let bitbucketUsername: string | undefined = descriptor?.username as string | undefined;

src/plus/integrations/authentication/github.ts

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,43 @@
11
import type { AuthenticationSession, Disposable, QuickInputButton } from 'vscode';
2-
import { env, ThemeIcon, Uri, window } from 'vscode';
3-
import type {
4-
IntegrationAuthenticationProvider,
5-
IntegrationAuthenticationSessionDescriptor,
2+
import { authentication, env, ThemeIcon, Uri, window } from 'vscode';
3+
import { wrapForForcedInsecureSSL } from '@env/fetch';
4+
import { HostingIntegrationId, SelfHostedIntegrationId } from '../providers/models';
5+
import type { IntegrationAuthenticationSessionDescriptor } from './integrationAuthentication';
6+
import {
7+
CloudIntegrationAuthenticationProvider,
8+
LocalIntegrationAuthenticationProvider,
69
} from './integrationAuthentication';
710

8-
export class GitHubEnterpriseAuthenticationProvider implements IntegrationAuthenticationProvider {
9-
getSessionId(descriptor?: IntegrationAuthenticationSessionDescriptor): string {
10-
return descriptor?.domain ?? '';
11+
export class GitHubAuthenticationProvider extends CloudIntegrationAuthenticationProvider<HostingIntegrationId.GitHub> {
12+
protected override get authProviderId(): HostingIntegrationId.GitHub {
13+
return HostingIntegrationId.GitHub;
1114
}
1215

13-
async createSession(
16+
override async getBuiltInExistingSession(
17+
descriptor?: IntegrationAuthenticationSessionDescriptor,
18+
): Promise<AuthenticationSession | undefined> {
19+
if (descriptor == null) return undefined;
20+
21+
return wrapForForcedInsecureSSL(
22+
this.container.integrations.ignoreSSLErrors({ id: this.authProviderId, domain: descriptor?.domain }),
23+
() =>
24+
authentication.getSession(this.authProviderId, descriptor.scopes, {
25+
silent: true,
26+
}),
27+
);
28+
}
29+
30+
protected override getCompletionInputTitle(): string {
31+
return 'Connect to GitHub';
32+
}
33+
}
34+
35+
export class GitHubEnterpriseAuthenticationProvider extends LocalIntegrationAuthenticationProvider<SelfHostedIntegrationId.GitHubEnterprise> {
36+
protected override get authProviderId(): SelfHostedIntegrationId.GitHubEnterprise {
37+
return SelfHostedIntegrationId.GitHubEnterprise;
38+
}
39+
40+
override async createSession(
1441
descriptor?: IntegrationAuthenticationSessionDescriptor,
1542
): Promise<AuthenticationSession | undefined> {
1643
const input = window.createInputBox();

src/plus/integrations/authentication/gitlab.ts

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,21 @@
11
import type { AuthenticationSession, Disposable, QuickInputButton } from 'vscode';
22
import { env, ThemeIcon, Uri, window } from 'vscode';
3-
import type {
4-
IntegrationAuthenticationProvider,
5-
IntegrationAuthenticationSessionDescriptor,
6-
} from './integrationAuthentication';
3+
import type { Container } from '../../../container';
4+
import type { HostingIntegrationId, SelfHostedIntegrationId } from '../providers/models';
5+
import type { IntegrationAuthenticationSessionDescriptor } from './integrationAuthentication';
6+
import { LocalIntegrationAuthenticationProvider } from './integrationAuthentication';
77

8-
export class GitLabAuthenticationProvider implements IntegrationAuthenticationProvider {
9-
getSessionId(descriptor?: IntegrationAuthenticationSessionDescriptor): string {
10-
return descriptor?.domain ?? '';
8+
type GitLabId = HostingIntegrationId.GitLab | SelfHostedIntegrationId.GitLabSelfHosted;
9+
10+
export class GitLabAuthenticationProvider extends LocalIntegrationAuthenticationProvider<GitLabId> {
11+
constructor(
12+
container: Container,
13+
protected readonly authProviderId: GitLabId,
14+
) {
15+
super(container);
1116
}
1217

13-
async createSession(
18+
override async createSession(
1419
descriptor?: IntegrationAuthenticationSessionDescriptor,
1520
): Promise<AuthenticationSession | undefined> {
1621
const input = window.createInputBox();

0 commit comments

Comments
 (0)