Skip to content

Commit ab91532

Browse files
author
Rachel Macfarlane
committed
Add 'manage trusted extensions' option to account quickpick, microsoft#90385
1 parent ce90579 commit ab91532

5 files changed

Lines changed: 79 additions & 52 deletions

File tree

extensions/github-authentication/src/extension.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ export async function activate(context: vscode.ExtensionContext) {
2424
try {
2525
const session = await loginService.login(scopeList.join(' '));
2626
Logger.info('Login success!');
27+
onDidChangeSessions.fire({ added: [session.id], removed: [], changed: [] });
2728
return session;
2829
} catch (e) {
2930
vscode.window.showErrorMessage(`Sign in failed: ${e}`);
@@ -32,7 +33,8 @@ export async function activate(context: vscode.ExtensionContext) {
3233
}
3334
},
3435
logout: async (id: string) => {
35-
return loginService.logout(id);
36+
await loginService.logout(id);
37+
onDidChangeSessions.fire({ added: [], removed: [id], changed: [] });
3638
}
3739
});
3840

src/vs/workbench/api/browser/mainThreadAuthentication.ts

Lines changed: 57 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,23 @@ const BUILT_IN_AUTH_DEPENDENTS: AuthDependent[] = [
3232
}
3333
];
3434

35+
interface AllowedExtension {
36+
id: string;
37+
name: string;
38+
}
39+
40+
function readAllowedExtensions(storageService: IStorageService, providerId: string, accountName: string): AllowedExtension[] {
41+
let trustedExtensions: AllowedExtension[] = [];
42+
try {
43+
const trustedExtensionSrc = storageService.get(`${providerId}-${accountName}`, StorageScope.GLOBAL);
44+
if (trustedExtensionSrc) {
45+
trustedExtensions = JSON.parse(trustedExtensionSrc);
46+
}
47+
} catch (err) { }
48+
49+
return trustedExtensions;
50+
}
51+
3552
export class MainThreadAuthenticationProvider extends Disposable {
3653
private _sessionMenuItems = new Map<string, IDisposable[]>();
3754
private _accounts = new Map<string, string[]>(); // Map account name to session ids
@@ -48,30 +65,25 @@ export class MainThreadAuthenticationProvider extends Disposable {
4865
this.registerCommandsAndContextMenuItems();
4966
}
5067

51-
private setPermissionsForAccount(quickInputService: IQuickInputService, doLogin?: boolean) {
52-
const quickPick = quickInputService.createQuickPick();
68+
private manageTrustedExtensions(quickInputService: IQuickInputService, storageService: IStorageService, accountName: string) {
69+
const quickPick = quickInputService.createQuickPick<{ label: string, extension: AllowedExtension }>();
5370
quickPick.canSelectMany = true;
54-
const items = this.dependents.map(dependent => {
71+
const allowedExtensions = readAllowedExtensions(storageService, this.id, accountName);
72+
const items = allowedExtensions.map(extension => {
5573
return {
56-
label: dependent.label,
57-
description: dependent.scopeDescriptions,
58-
picked: true,
59-
scopes: dependent.scopes
74+
label: extension.name,
75+
extension
6076
};
6177
});
6278

6379
quickPick.items = items;
64-
// TODO read from storage and filter is not doLogin
6580
quickPick.selectedItems = items;
66-
quickPick.title = nls.localize('signInTo', "Sign in to {0}", this.displayName);
67-
quickPick.placeholder = nls.localize('accountPermissions', "Choose what features and extensions to authorize to use this account");
81+
quickPick.title = nls.localize('manageTrustedExtensions', "Manage Trusted Extensions");
82+
quickPick.placeholder = nls.localize('manageExensions', "Choose which extensions can access this account");
6883

6984
quickPick.onDidAccept(() => {
70-
const scopes = quickPick.selectedItems.reduce((previous, current) => previous.concat((current as any).scopes), []);
71-
if (scopes.length && doLogin) {
72-
this.login(scopes);
73-
}
74-
85+
const updatedAllowedList = quickPick.selectedItems.map(item => item.extension);
86+
storageService.store(`${this.id}-${accountName}`, JSON.stringify(updatedAllowedList), StorageScope.GLOBAL);
7587
quickPick.dispose();
7688
});
7789

@@ -87,7 +99,7 @@ export class MainThreadAuthenticationProvider extends Disposable {
8799
this._register(CommandsRegistry.registerCommand({
88100
id: `signIn${this.id}`,
89101
handler: (accessor, args) => {
90-
this.setPermissionsForAccount(accessor.get(IQuickInputService), true);
102+
this.login(this.dependents.reduce((previous: string[], current) => previous.concat(current.scopes), []));
91103
},
92104
}));
93105

@@ -130,19 +142,26 @@ export class MainThreadAuthenticationProvider extends Disposable {
130142
id: `configureSessions${session.id}`,
131143
handler: (accessor, args) => {
132144
const quickInputService = accessor.get(IQuickInputService);
145+
const storageService = accessor.get(IStorageService);
133146

134147
const quickPick = quickInputService.createQuickPick();
135-
const items = [{ label: 'Sign Out' }];
148+
const manage = nls.localize('manageTrustedExtensions', "Manage Trusted Extensions");
149+
const signOut = nls.localize('signOut', "Sign Out");
150+
const items = ([{ label: manage }, { label: signOut }]);
136151

137152
quickPick.items = items;
138153

139154
quickPick.onDidAccept(e => {
140155
const selected = quickPick.selectedItems[0];
141-
if (selected.label === 'Sign Out') {
156+
if (selected.label === signOut) {
142157
const sessionsForAccount = this._accounts.get(session.accountName);
143158
sessionsForAccount?.forEach(sessionId => this.logout(sessionId));
144159
}
145160

161+
if (selected.label === manage) {
162+
this.manageTrustedExtensions(quickInputService, storageService, session.accountName);
163+
}
164+
146165
quickPick.dispose();
147166
});
148167

@@ -185,6 +204,7 @@ export class MainThreadAuthenticationProvider extends Disposable {
185204
disposeables.forEach(disposeable => disposeable.dispose());
186205
this._sessionMenuItems.delete(accountName);
187206
}
207+
this._accounts.delete(accountName);
188208
}
189209
}
190210
});
@@ -242,55 +262,45 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu
242262
this.authenticationService.sessionsUpdate(id, event);
243263
}
244264

245-
async $getSessionsPrompt(providerId: string, providerName: string, extensionId: string, extensionName: string): Promise<boolean> {
246-
const alwaysAllow = this.storageService.get(`${extensionId}-${providerId}`, StorageScope.GLOBAL);
247-
if (alwaysAllow) {
248-
return alwaysAllow === 'true';
265+
async $getSessionsPrompt(providerId: string, accountName: string, providerName: string, extensionId: string, extensionName: string): Promise<boolean> {
266+
let allowList = readAllowedExtensions(this.storageService, providerId, accountName);
267+
if (allowList.some(extension => extension.id === extensionId)) {
268+
return true;
249269
}
250270

251-
const { choice, checkboxChecked } = await this.dialogService.show(
271+
const { choice } = await this.dialogService.show(
252272
Severity.Info,
253-
nls.localize('confirmAuthenticationAccess', "The extension '{0}' is trying to access authentication information from {1}.", extensionName, providerName),
273+
nls.localize('confirmAuthenticationAccess', "The extension '{0}' is trying to access authentication information for the {1} account '{2}'.", extensionName, providerName, accountName),
254274
[nls.localize('cancel', "Cancel"), nls.localize('allow', "Allow")],
255275
{
256-
cancelId: 0,
257-
checkbox: {
258-
label: nls.localize('neverAgain', "Don't Show Again")
259-
}
276+
cancelId: 0
260277
}
261278
);
262279

263280
const allow = choice === 1;
264-
if (checkboxChecked) {
265-
this.storageService.store(`${extensionId}-${providerId}`, allow ? 'true' : 'false', StorageScope.GLOBAL);
281+
if (allow) {
282+
allowList = allowList.concat({ id: extensionId, name: extensionName });
283+
this.storageService.store(`${providerId}-${accountName}`, JSON.stringify(allowList), StorageScope.GLOBAL);
266284
}
267285

268286
return allow;
269287
}
270288

271-
async $loginPrompt(providerId: string, providerName: string, extensionId: string, extensionName: string): Promise<boolean> {
272-
const alwaysAllow = this.storageService.get(`${extensionId}-${providerId}`, StorageScope.GLOBAL);
273-
if (alwaysAllow) {
274-
return alwaysAllow === 'true';
275-
}
276-
277-
const { choice, checkboxChecked } = await this.dialogService.show(
289+
async $loginPrompt(providerName: string, extensionName: string): Promise<boolean> {
290+
const { choice } = await this.dialogService.show(
278291
Severity.Info,
279292
nls.localize('confirmLogin', "The extension '{0}' wants to sign in using {1}.", extensionName, providerName),
280-
[nls.localize('cancel', "Cancel"), nls.localize('continue', "Continue")],
293+
[nls.localize('cancel', "Cancel"), nls.localize('allow', "Allow")],
281294
{
282-
cancelId: 0,
283-
checkbox: {
284-
label: nls.localize('neverAgain', "Don't Show Again")
285-
}
295+
cancelId: 0
286296
}
287297
);
288298

289-
const allow = choice === 1;
290-
if (checkboxChecked) {
291-
this.storageService.store(`${extensionId}-${providerId}`, allow ? 'true' : 'false', StorageScope.GLOBAL);
292-
}
299+
return choice === 1;
300+
}
293301

294-
return allow;
302+
async $setTrustedExtension(providerId: string, accountName: string, extensionId: string, extensionName: string): Promise<void> {
303+
const allowList = readAllowedExtensions(this.storageService, providerId, accountName).concat({ id: extensionId, name: extensionName });
304+
this.storageService.store(`${providerId}-${accountName}`, JSON.stringify(allowList), StorageScope.GLOBAL);
295305
}
296306
}

src/vs/workbench/api/common/extHost.protocol.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -157,8 +157,9 @@ export interface MainThreadAuthenticationShape extends IDisposable {
157157
$registerAuthenticationProvider(id: string, displayName: string): void;
158158
$unregisterAuthenticationProvider(id: string): void;
159159
$onDidChangeSessions(providerId: string, event: modes.AuthenticationSessionsChangeEvent): void;
160-
$getSessionsPrompt(providerId: string, providerName: string, extensionId: string, extensionName: string): Promise<boolean>;
161-
$loginPrompt(providerId: string, providerName: string, extensionId: string, extensionName: string): Promise<boolean>;
160+
$getSessionsPrompt(providerId: string, accountName: string, providerName: string, extensionId: string, extensionName: string): Promise<boolean>;
161+
$loginPrompt(providerName: string, extensionName: string): Promise<boolean>;
162+
$setTrustedExtension(providerId: string, accountName: string, extensionId: string, extensionName: string): Promise<void>;
162163
}
163164

164165
export interface MainThreadConfigurationShape extends IDisposable {

src/vs/workbench/api/common/extHostAuthentication.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ export class ExtHostAuthentication implements ExtHostAuthenticationShape {
5050
getAccessToken: async () => {
5151
const isAllowed = await this._proxy.$getSessionsPrompt(
5252
provider.id,
53+
session.accountName,
5354
provider.displayName,
5455
ExtensionIdentifier.toKey(requestingExtension.identifier),
5556
requestingExtension.displayName || requestingExtension.name);
@@ -70,12 +71,15 @@ export class ExtHostAuthentication implements ExtHostAuthenticationShape {
7071
throw new Error(`No authentication provider with id '${providerId}' is currently registered.`);
7172
}
7273

73-
const isAllowed = await this._proxy.$loginPrompt(provider.id, provider.displayName, ExtensionIdentifier.toKey(requestingExtension.identifier), requestingExtension.displayName || requestingExtension.name);
74+
const extensionName = requestingExtension.displayName || requestingExtension.name;
75+
const isAllowed = await this._proxy.$loginPrompt(provider.displayName, extensionName);
7476
if (!isAllowed) {
7577
throw new Error('User did not consent to login.');
7678
}
7779

78-
return provider.login(scopes);
80+
const newSession = await provider.login(scopes);
81+
await this._proxy.$setTrustedExtension(provider.id, newSession.accountName, ExtensionIdentifier.toKey(requestingExtension.identifier), extensionName);
82+
return newSession;
7983
}
8084

8185
registerAuthenticationProvider(provider: vscode.AuthenticationProvider): vscode.Disposable {

src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -604,7 +604,17 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo
604604
case 0: this.openerService.open(URI.parse('https://aka.ms/vscode-settings-sync-help')); return;
605605
case 2: return;
606606
}
607+
} else if (skipAccountPick) {
608+
const result = await this.dialogService.confirm({
609+
type: 'info',
610+
message: localize('turn on sync confirmation', "Do you want to turn on preferences sync?"),
611+
primaryButton: localize('turn on', "Turn On")
612+
});
613+
if (!result.confirmed) {
614+
return;
615+
}
607616
}
617+
608618
return new Promise((c, e) => {
609619
const disposables: DisposableStore = new DisposableStore();
610620
const quickPick = this.quickInputService.createQuickPick<ConfigureSyncQuickPickItem>();

0 commit comments

Comments
 (0)