Workspace trust changes (#119017)

* Add dialog button customisation and reject promise if cancelled
* Use different promises to modal/soft requests
This commit is contained in:
Ladislau Szomoru 2021-03-16 11:18:42 +01:00 committed by GitHub
parent e787d6e384
commit 149a8b71c5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 104 additions and 39 deletions

View file

@ -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"
},

View file

@ -5,7 +5,9 @@
"publisher": "vscode",
"license": "MIT",
"enableProposedApi": true,
"requiresWorkspaceTrust": "onDemand",
"workspaceTrust": {
"required": "onDemand"
},
"private": true,
"activationEvents": [],
"main": "./out/extension",

View file

@ -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 {

View file

@ -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<void>;
readonly onDidCompleteRequest: Event<WorkspaceTrustState | undefined>;
readonly onDidCancelRequest: Event<void>;
initiateRequest(request?: WorkspaceTrustRequest): void;
completeRequest(trustState?: WorkspaceTrustState): void;
cancelRequest(): void;
}
export interface WorkspaceTrustStateChangeEvent {

View file

@ -90,38 +90,49 @@ export class WorkspaceTrustRequestHandler extends Disposable implements IWorkben
this.telemetryService.publicLog2<WorkspaceTrustRequestedEvent, WorkspaceTrustRequestedEventClassification>('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;
}
}

View file

@ -248,7 +248,7 @@ export class WorkspaceTrustEditor extends EditorPane {
}
private async getExtensionsByTrustRequirement(extensions: IExtensionStatus[], trustRequirement: ExtensionWorkspaceTrustRequirement): Promise<IExtension[]> {
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);

View file

@ -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);
}

View file

@ -361,7 +361,7 @@ export class ExtensionManagementService extends Disposable implements IWorkbench
}
protected async checkForWorkspaceTrust(manifest: IExtensionManifest): Promise<void> {
if (manifest.requiresWorkspaceTrust === 'onStart') {
if (manifest.workspaceTrust?.required === 'onStart') {
const trustState = await this.workspaceTrustService.requireWorkspaceTrust();
return trustState === WorkspaceTrustState.Trusted ? Promise.resolve() : Promise.reject(canceled());
}

View file

@ -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 {

View file

@ -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<WorkspaceTrustState | undefined>());
readonly onDidCompleteRequest = this._onDidCompleteRequest.event;
private readonly _onDidCancelRequest = this._register(new Emitter<void>());
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<WorkspaceTrustState>;
private _inFlightResolver?: (trustState: WorkspaceTrustState) => void;
private _modalTrustRequestPromise?: Promise<WorkspaceTrustState>;
private _modalTrustRequestResolver?: (trustState: WorkspaceTrustState) => void;
private _modalTrustRequestRejecter?: (error: Error) => void;
private _workspace: IWorkspace;
private readonly _ctxWorkspaceTrustState: IContextKey<WorkspaceTrustState>;
@ -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<WorkspaceTrustState> {
// 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!;
}
}