diff --git a/extensions/typescript-language-features/package.json b/extensions/typescript-language-features/package.json index d5844e1af8e..c646d670fd9 100644 --- a/extensions/typescript-language-features/package.json +++ b/extensions/typescript-language-features/package.json @@ -9,7 +9,9 @@ "license": "MIT", "aiKey": "AIF-d9b70cd4-b9f9-4d70-929b-a071c400b217", "enableProposedApi": true, - "requiresWorkspaceTrust": "onDemand", + "workspaceTrust": { + "required": "onDemand" + }, "engines": { "vscode": "^1.30.0" }, diff --git a/extensions/vscode-api-tests/package.json b/extensions/vscode-api-tests/package.json index 7e576805d5c..6d39b43c989 100644 --- a/extensions/vscode-api-tests/package.json +++ b/extensions/vscode-api-tests/package.json @@ -5,7 +5,9 @@ "publisher": "vscode", "license": "MIT", "enableProposedApi": true, - "requiresWorkspaceTrust": "onDemand", + "workspaceTrust": { + "required": "onDemand" + }, "private": true, "activationEvents": [], "main": "./out/extension", diff --git a/src/vs/platform/extensions/common/extensions.ts b/src/vs/platform/extensions/common/extensions.ts index ea75dae1406..c3827be8dcc 100644 --- a/src/vs/platform/extensions/common/extensions.ts +++ b/src/vs/platform/extensions/common/extensions.ts @@ -156,8 +156,8 @@ export interface IExtensionContributions { } export type ExtensionKind = 'ui' | 'workspace' | 'web'; - export type ExtensionWorkspaceTrustRequirement = false | 'onStart' | 'onDemand'; +export type ExtensionWorkspaceTrust = { required: ExtensionWorkspaceTrustRequirement, description?: string }; export function isIExtensionIdentifier(thing: any): thing is IExtensionIdentifier { return thing @@ -214,7 +214,7 @@ export interface IExtensionManifest { readonly enableProposedApi?: boolean; readonly api?: string; readonly scripts?: { [key: string]: string; }; - readonly requiresWorkspaceTrust?: ExtensionWorkspaceTrustRequirement; + readonly workspaceTrust?: ExtensionWorkspaceTrust; } export const enum ExtensionType { diff --git a/src/vs/platform/workspace/common/workspaceTrust.ts b/src/vs/platform/workspace/common/workspaceTrust.ts index 47075c714df..74bc4625793 100644 --- a/src/vs/platform/workspace/common/workspaceTrust.ts +++ b/src/vs/platform/workspace/common/workspaceTrust.ts @@ -44,7 +44,14 @@ export interface IWorkspaceTrustModel { getTrustStateInfo(): IWorkspaceTrustStateInfo; } +export interface WorkspaceTrustRequestButton { + label: string; + type: 'ContinueWithTrust' | 'ContinueWithoutTrust' | 'Manage' | 'Cancel' +} + export interface WorkspaceTrustRequest { + buttons?: WorkspaceTrustRequestButton[]; + message?: string; modal: boolean; } @@ -53,9 +60,11 @@ export interface IWorkspaceTrustRequestModel { readonly onDidInitiateRequest: Event; readonly onDidCompleteRequest: Event; + readonly onDidCancelRequest: Event; initiateRequest(request?: WorkspaceTrustRequest): void; completeRequest(trustState?: WorkspaceTrustState): void; + cancelRequest(): void; } export interface WorkspaceTrustStateChangeEvent { diff --git a/src/vs/workbench/contrib/workspace/browser/workspace.contribution.ts b/src/vs/workbench/contrib/workspace/browser/workspace.contribution.ts index 26f0d614f7a..8b285d2e546 100644 --- a/src/vs/workbench/contrib/workspace/browser/workspace.contribution.ts +++ b/src/vs/workbench/contrib/workspace/browser/workspace.contribution.ts @@ -90,38 +90,49 @@ export class WorkspaceTrustRequestHandler extends Disposable implements IWorkben this.telemetryService.publicLog2('workspaceTrustRequested', { modal: this.requestModel.trustRequest.modal, workspaceId: this.workspaceContextService.getWorkspace().id, - extensions: (await this.extensionService.getExtensions()).filter(ext => !!ext.requiresWorkspaceTrust).map(ext => ext.identifier.value) + extensions: (await this.extensionService.getExtensions()).filter(ext => !!ext.workspaceTrust).map(ext => ext.identifier.value) }); if (this.requestModel.trustRequest.modal) { + // Message + const defaultMessage = localize('immediateTrustRequestMessage', "A feature you are trying to use may be a security risk if you do not trust the source of the files or folders you currently have open."); + const message = this.requestModel.trustRequest.message ?? defaultMessage; + + // Buttons + const buttons = this.requestModel.trustRequest.buttons ?? [ + { label: localize('grantWorkspaceTrustButton', "Continue"), type: 'ContinueWithTrust' }, + { label: localize('manageWorkspaceTrustButton', "Learn More"), type: 'Manage' } + ]; + // Add Cancel button if not provided + if (!buttons.some(b => b.type === 'Cancel')) { + buttons.push({ label: localize('cancelWorkspaceTrustButton', "Cancel"), type: 'Cancel' }); + } + + // Dialog const result = await this.dialogService.show( Severity.Warning, localize('immediateTrustRequestTitle', "Do you trust the files in this folder?"), - [ - localize('grantWorkspaceTrustButton', "Trust"), - localize('denyWorkspaceTrustButton', "Don't Trust"), - localize('manageWorkspaceTrustButton', "Manage"), - localize('cancelWorkspaceTrustButton', "Cancel"), - ], + buttons.map(b => b.label), { - cancelId: 3, - detail: localize('immediateTrustRequestDetail', "A feature you are trying to use may be a security risk if you do not trust the source of the files or folders you currently have open.\n\nYou should only trust this workspace if you trust its source. Otherwise, features will be enabled that may compromise your device or personal information."), + cancelId: buttons.findIndex(b => b.type === 'Cancel'), + detail: localize('immediateTrustRequestDetail', "{0}\n\nYou should only trust this workspace if you trust its source. Otherwise, features will be enabled that may compromise your device or personal information.", message), } ); - switch (result.choice) { - case 0: // Trust + // Dialog result + switch (buttons[result.choice].type) { + case 'ContinueWithTrust': this.requestModel.completeRequest(WorkspaceTrustState.Trusted); break; - case 1: // Don't Trust - this.requestModel.completeRequest(WorkspaceTrustState.Untrusted); - break; - case 2: // Manage + case 'ContinueWithoutTrust': this.requestModel.completeRequest(undefined); + break; + case 'Manage': + this.requestModel.cancelRequest(); await this.commandService.executeCommand('workbench.trust.manage'); break; - default: // Cancel - this.requestModel.completeRequest(undefined); + case 'Cancel': + this.requestModel.cancelRequest(); break; } } diff --git a/src/vs/workbench/contrib/workspace/browser/workspaceTrustEditor.ts b/src/vs/workbench/contrib/workspace/browser/workspaceTrustEditor.ts index 8894c6f0630..853d136deb6 100644 --- a/src/vs/workbench/contrib/workspace/browser/workspaceTrustEditor.ts +++ b/src/vs/workbench/contrib/workspace/browser/workspaceTrustEditor.ts @@ -248,7 +248,7 @@ export class WorkspaceTrustEditor extends EditorPane { } private async getExtensionsByTrustRequirement(extensions: IExtensionStatus[], trustRequirement: ExtensionWorkspaceTrustRequirement): Promise { - const filtered = extensions.filter(ext => ext.local.manifest.requiresWorkspaceTrust === trustRequirement); + const filtered = extensions.filter(ext => ext.local.manifest.workspaceTrust?.required === trustRequirement); const ids = filtered.map(ext => ext.identifier.id); return getExtensions(ids, this.extensionWorkbenchService); diff --git a/src/vs/workbench/services/extensionManagement/browser/extensionEnablementService.ts b/src/vs/workbench/services/extensionManagement/browser/extensionEnablementService.ts index f1bf55fbe5e..fc289efd555 100644 --- a/src/vs/workbench/services/extensionManagement/browser/extensionEnablementService.ts +++ b/src/vs/workbench/services/extensionManagement/browser/extensionEnablementService.ts @@ -281,7 +281,7 @@ export class ExtensionEnablementService extends Disposable implements IWorkbench private _isDisabledByTrustRequirement(extension: IExtension): boolean { const workspaceTrustState = this.workspaceTrustService.getWorkspaceTrustState(); - if (extension.manifest.requiresWorkspaceTrust === 'onStart') { + if (extension.manifest.workspaceTrust?.required === 'onStart') { if (workspaceTrustState !== WorkspaceTrustState.Trusted) { this._addToWorkspaceDisabledExtensionsByTrustRequirement(extension); } diff --git a/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts b/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts index c6a2489375b..835a2851834 100644 --- a/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts +++ b/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts @@ -361,7 +361,7 @@ export class ExtensionManagementService extends Disposable implements IWorkbench } protected async checkForWorkspaceTrust(manifest: IExtensionManifest): Promise { - if (manifest.requiresWorkspaceTrust === 'onStart') { + if (manifest.workspaceTrust?.required === 'onStart') { const trustState = await this.workspaceTrustService.requireWorkspaceTrust(); return trustState === WorkspaceTrustState.Trusted ? Promise.resolve() : Promise.reject(canceled()); } diff --git a/src/vs/workbench/services/extensions/common/extensions.ts b/src/vs/workbench/services/extensions/common/extensions.ts index f08798dd813..9689215d3db 100644 --- a/src/vs/workbench/services/extensions/common/extensions.ts +++ b/src/vs/workbench/services/extensions/common/extensions.ts @@ -270,13 +270,13 @@ export function throwProposedApiError(extension: IExtensionDescription): never { } export function checkRequiresWorkspaceTrust(extension: IExtensionDescription): void { - if (!extension.requiresWorkspaceTrust) { + if (!extension.workspaceTrust?.required) { throwRequiresWorkspaceTrustError(extension); } } export function throwRequiresWorkspaceTrustError(extension: IExtensionDescription): void { - throw new Error(`[${extension.identifier.value}]: This API is only available when the "requiresWorkspaceTrust" is set to "onStart" or "onDemand" in the extension's package.json.`); + throw new Error(`[${extension.identifier.value}]: This API is only available when the "workspaceTrust.require" is set to "onStart" or "onDemand" in the extension's package.json.`); } export function toExtension(extensionDescription: IExtensionDescription): IExtension { diff --git a/src/vs/workbench/services/workspaces/common/workspaceTrust.ts b/src/vs/workbench/services/workspaces/common/workspaceTrust.ts index 7d60bff7703..e8005a80565 100644 --- a/src/vs/workbench/services/workspaces/common/workspaceTrust.ts +++ b/src/vs/workbench/services/workspaces/common/workspaceTrust.ts @@ -16,6 +16,7 @@ import { isEqual, isEqualOrParent } from 'vs/base/common/extpath'; import { EditorModel } from 'vs/workbench/common/editor'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { dirname, resolve } from 'vs/base/common/path'; +import { canceled } from 'vs/base/common/errors'; export const WORKSPACE_TRUST_ENABLED = 'workspace.trustEnabled'; export const WORKSPACE_TRUST_STORAGE_KEY = 'content.trust.model.key'; @@ -191,6 +192,9 @@ export class WorkspaceTrustRequestModel extends Disposable implements IWorkspace private readonly _onDidCompleteRequest = this._register(new Emitter()); readonly onDidCompleteRequest = this._onDidCompleteRequest.event; + private readonly _onDidCancelRequest = this._register(new Emitter()); + readonly onDidCancelRequest = this._onDidCancelRequest.event; + initiateRequest(request: WorkspaceTrustRequest): void { if (this.trustRequest && (!request.modal || this.trustRequest.modal)) { return; @@ -204,6 +208,11 @@ export class WorkspaceTrustRequestModel extends Disposable implements IWorkspace this.trustRequest = undefined; this._onDidCompleteRequest.fire(trustState); } + + cancelRequest(): void { + this.trustRequest = undefined; + this._onDidCancelRequest.fire(); + } } export class WorkspaceTrustService extends Disposable implements IWorkspaceTrustService { @@ -217,8 +226,11 @@ export class WorkspaceTrustService extends Disposable implements IWorkspaceTrust readonly onDidChangeTrustState = this._onDidChangeTrustState.event; private _currentTrustState: WorkspaceTrustState = WorkspaceTrustState.Unknown; - private _inFlightResolver?: (trustState: WorkspaceTrustState) => void; private _trustRequestPromise?: Promise; + private _inFlightResolver?: (trustState: WorkspaceTrustState) => void; + private _modalTrustRequestPromise?: Promise; + private _modalTrustRequestResolver?: (trustState: WorkspaceTrustState) => void; + private _modalTrustRequestRejecter?: (error: Error) => void; private _workspace: IWorkspace; private readonly _ctxWorkspaceTrustState: IContextKey; @@ -243,6 +255,7 @@ export class WorkspaceTrustService extends Disposable implements IWorkspaceTrust this._register(this.dataModel.onDidChangeTrustState(() => this.currentTrustState = this.calculateWorkspaceTrustState())); this._register(this.requestModel.onDidCompleteRequest((trustState) => this.onTrustRequestCompleted(trustState))); + this._register(this.requestModel.onDidCancelRequest(() => this.onTrustRequestCancelled())); this._ctxWorkspaceTrustState = WorkspaceTrustContext.TrustState.bindTo(contextKeyService); this._ctxWorkspaceTrustPendingRequest = WorkspaceTrustContext.PendingRequest.bindTo(contextKeyService); @@ -358,6 +371,9 @@ export class WorkspaceTrustService extends Disposable implements IWorkspaceTrust } private onTrustRequestCompleted(trustState?: WorkspaceTrustState): void { + if (this._modalTrustRequestResolver) { + this._modalTrustRequestResolver(trustState === undefined ? this.currentTrustState : trustState); + } if (this._inFlightResolver) { this._inFlightResolver(trustState === undefined ? this.currentTrustState : trustState); } @@ -365,6 +381,10 @@ export class WorkspaceTrustService extends Disposable implements IWorkspaceTrust this._inFlightResolver = undefined; this._trustRequestPromise = undefined; + this._modalTrustRequestResolver = undefined; + this._modalTrustRequestRejecter = undefined; + this._modalTrustRequestPromise = undefined; + if (trustState === undefined) { return; } @@ -377,6 +397,16 @@ export class WorkspaceTrustService extends Disposable implements IWorkspaceTrust this._ctxWorkspaceTrustState.set(trustState); } + private onTrustRequestCancelled(): void { + if (this._modalTrustRequestRejecter) { + this._modalTrustRequestRejecter(canceled()); + } + + this._modalTrustRequestResolver = undefined; + this._modalTrustRequestRejecter = undefined; + this._modalTrustRequestPromise = undefined; + } + getWorkspaceTrustState(): WorkspaceTrustState { return this.currentTrustState; } @@ -386,31 +416,42 @@ export class WorkspaceTrustService extends Disposable implements IWorkspaceTrust } async requireWorkspaceTrust(request: WorkspaceTrustRequest = { modal: true }): Promise { + // Trusted workspace if (this.currentTrustState === WorkspaceTrustState.Trusted) { return this.currentTrustState; } + // Untrusted workspace - soft request if (this.currentTrustState === WorkspaceTrustState.Untrusted && !request.modal) { return this.currentTrustState; } - if (this._trustRequestPromise) { - if (request.modal && - this.requestModel.trustRequest && - !this.requestModel.trustRequest.modal) { - this.requestModel.initiateRequest(request); + if (request.modal) { + // Modal request + if (!this._modalTrustRequestPromise) { + // Create promise + this._modalTrustRequestPromise = new Promise((resolve, reject) => { + this._modalTrustRequestResolver = resolve; + this._modalTrustRequestRejecter = reject; + }); + } else { + // Return existing promises + return this._modalTrustRequestPromise; + } + } else { + // Soft request + if (!this._trustRequestPromise) { + this._trustRequestPromise = new Promise(resolve => { + this._inFlightResolver = resolve; + }); + } else { + return this._trustRequestPromise; } - - return this._trustRequestPromise; } - this._trustRequestPromise = new Promise(resolve => { - this._inFlightResolver = resolve; - }); - this.requestModel.initiateRequest(request); this._ctxWorkspaceTrustPendingRequest.set(true); - return this._trustRequestPromise; + return request.modal ? this._modalTrustRequestPromise! : this._trustRequestPromise!; } }