Skip to content

Commit

Permalink
Workspace trust changes (#119017)
Browse files Browse the repository at this point in the history
* Add dialog button customisation and reject promise if cancelled
* Use different promises to modal/soft requests
  • Loading branch information
lszomoru authored and lramos15 committed Mar 16, 2021
1 parent 34659d6 commit 5f2cc2a
Show file tree
Hide file tree
Showing 10 changed files with 104 additions and 39 deletions.
4 changes: 3 additions & 1 deletion extensions/typescript-language-features/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down
4 changes: 3 additions & 1 deletion extensions/vscode-api-tests/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
"publisher": "vscode",
"license": "MIT",
"enableProposedApi": true,
"requiresWorkspaceTrust": "onDemand",
"workspaceTrust": {
"required": "onDemand"
},
"private": true,
"activationEvents": [],
"main": "./out/extension",
Expand Down
4 changes: 2 additions & 2 deletions src/vs/platform/extensions/common/extensions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down
9 changes: 9 additions & 0 deletions src/vs/platform/workspace/common/workspaceTrust.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand All @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}
Expand Down
4 changes: 2 additions & 2 deletions src/vs/workbench/services/extensions/common/extensions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
67 changes: 54 additions & 13 deletions src/vs/workbench/services/workspaces/common/workspaceTrust.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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;
Expand All @@ -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 {
Expand All @@ -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>;
Expand All @@ -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);
Expand Down Expand Up @@ -358,13 +371,20 @@ 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);
}

this._inFlightResolver = undefined;
this._trustRequestPromise = undefined;

this._modalTrustRequestResolver = undefined;
this._modalTrustRequestRejecter = undefined;
this._modalTrustRequestPromise = undefined;

if (trustState === undefined) {
return;
}
Expand All @@ -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;
}
Expand All @@ -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!;
}
}

Expand Down

0 comments on commit 5f2cc2a

Please sign in to comment.