From afd102cbd2e17305a510701d7fd963ec2528e4ea Mon Sep 17 00:00:00 2001 From: SteVen Batten <6561887+sbatten@users.noreply.github.com> Date: Sat, 6 Feb 2021 00:38:32 -0800 Subject: [PATCH] Trusted Workspaces Feature Branch Merge (#115961) * draft trusted workspace service / model * renaming * add request model and action * err fix * add request handlers with mock actions * some quick fixes * adding badge icon to activity bar gear * Add Statusbar item to indicate trust * Cleanup code * Add background color * Use theme color for the status background color * adding basic editing experience * observe trust with startup tasks * Extension enablement * Add capability to provide a custom message * Remove old actions * explorer: if you can not undo, pass undo to editor fixes #111630 * Remove plug icon from ports view Part of https://github.com/microsoft/vscode-internalbacklog/issues/1689 * Fixed compilation error * Handle extension uninstall * Handle extension install * Ability to prompt when state is untrusted * Do not change state is the modal dialog is dismissed or the Cancel button is pressed * Refactored enablement code * Prompt when installing from VSIX * Prompt when installing from the Gallery * Move file into the browser folder * fixes and polish * restructure workspace contributions * restructure actions and use confirmations * Initial draft of the proposed APIs * Added stubs for the proposed api * Trusted Workspace proposed API * Fix a regression introduced by merge * status bar indicator improvements * remove helper command as we now have hooks * verbose messaging for the immediate request * add indication to global activity icon of pending request * try personal title * Add configuration setting * Add additional extension actions * Fix contributions * Removed context key that is not needed * Fixed issue with the dialog * Reduce arbitrary event limiter from 16ms down to 4.16666 (support for monitors up-to 240hz) #107016 * Fixes #115221: update emoji tests * Give a higher priority to language configuration set via API call (#114684) * debug console menu action polish * Avoid the CSS general sibling combinator ~ for perf reasons * more notebook todos * Use label as tooltip fallback properly Part of #115337 * Fixes microsoft/monaco-editor#2329: Move `registerThemingParticipant` call to `/editor/` * Fix port label not always getting set Part of microsoft/vscode-remote-release#4364 * simplify map creation, fyi @bpasero * Fix #114432: Multiple save dialogs appearing on Windows if Ctrl+S is pressed multiple times (#114450) * fix multiple save dialogs appearing on Windows when spamming Ctrl+S * remove old fix and instead keep track of windows with open dialogs in the dialogMainService * keep initialisation of activeWindowDialogs in constructor * remove unused variable * some changes * queue dialogs based on hash of options * simplify structure, fix comment typo * Apply suggestions from code review Co-authored-by: Benjamin Pasero * remove unnecessary async/await for aquireFileDialogLock method * don't acquire file dialog lock for message boxes * use MessageBoxReturnValue | SaveDialogReturnValue | OpenDialogReturnValue instead of any type for getWindowDialogQueue * Apply suggestions from code review Co-authored-by: Benjamin Pasero Co-authored-by: Benjamin Pasero Co-authored-by: Benjamin Pasero * :lipstick: dialog main service locks * debt - adopt some ? operator * Better hiding of custom hover in icon label * Limit to 8ms (120fps) * more API todos for notebooks * :lipstick: * Update grammars * chore - group notebook specific api proposals together * added unreleased fixes to endgame notebook * Add changes back to the modal dialog * Add back the workspace trust proposed APIs * Adjust dialog buttons * Standardize on WorkspaceTrust name across interfaces, classes, variables * Renamed some of the missing keys * Add TestWorkspaceTrust stub and fix failing tests * Add requiresWorkspaceTrust property to fix test failure * remove notebook change Co-authored-by: Ladislau Szomoru Co-authored-by: isidor Co-authored-by: Alex Ross Co-authored-by: TacticalDan Co-authored-by: Alexandru Dima Co-authored-by: Johannes Rieken Co-authored-by: Cameron Co-authored-by: Benjamin Pasero Co-authored-by: Benjamin Pasero --- build/lib/i18n.resources.json | 4 + extensions/vscode-api-tests/package.json | 1 + .../platform/extensions/common/extensions.ts | 3 + .../workspace/common/workspaceTrust.ts | 375 ++++++++++++++++++ .../test/common/testWorkspaceTrust.ts | 31 ++ src/vs/vscode.proposed.d.ts | 56 +++ .../api/browser/mainThreadWorkspace.ts | 23 +- .../workbench/api/common/extHost.api.impl.ts | 20 +- .../workbench/api/common/extHost.protocol.ts | 7 +- src/vs/workbench/api/common/extHostTypes.ts | 6 + .../workbench/api/common/extHostWorkspace.ts | 26 +- .../browser/actions/workspaceActions.ts | 23 +- .../activitybar/media/activityaction.css | 13 + .../browser/parts/compositeBarActions.ts | 8 +- .../experimentService.test.ts | 3 + .../extensions/browser/extensionsActions.ts | 10 +- .../extensions/browser/extensionsIcons.ts | 1 + .../browser/extensionsWorkbenchService.ts | 67 +++- .../extensionsActions.test.ts | 3 + .../tasks/browser/abstractTaskService.ts | 4 +- .../tasks/browser/runAutomaticTasks.ts | 14 +- .../tasks/electron-browser/taskService.ts | 3 + .../browser/workspace.contribution.ts | 335 ++++++++++++++++ .../workspaceTrustFileSystemProvider.ts | 86 ++++ .../services/activity/common/activity.ts | 4 +- .../browser/extensionEnablementService.ts | 100 ++++- .../common/extensionManagement.ts | 1 + .../extensionEnablementService.test.ts | 18 +- .../services/extensions/common/extensions.ts | 10 + .../browser/api/extHostConfiguration.test.ts | 9 +- .../test/browser/api/extHostWorkspace.test.ts | 3 +- .../test/browser/workbenchTestServices.ts | 3 + src/vs/workbench/workbench.common.main.ts | 4 + 33 files changed, 1220 insertions(+), 54 deletions(-) create mode 100644 src/vs/platform/workspace/common/workspaceTrust.ts create mode 100644 src/vs/platform/workspace/test/common/testWorkspaceTrust.ts create mode 100644 src/vs/workbench/contrib/workspace/browser/workspace.contribution.ts create mode 100644 src/vs/workbench/contrib/workspace/common/workspaceTrustFileSystemProvider.ts diff --git a/build/lib/i18n.resources.json b/build/lib/i18n.resources.json index bb2ed8a7e41..9dc574614ff 100644 --- a/build/lib/i18n.resources.json +++ b/build/lib/i18n.resources.json @@ -218,6 +218,10 @@ "name": "vs/workbench/contrib/webviewPanel", "project": "vscode-workbench" }, + { + "name": "vs/workbench/contrib/workspace", + "project": "vscode-workbench" + }, { "name": "vs/workbench/contrib/workspaces", "project": "vscode-workbench" diff --git a/extensions/vscode-api-tests/package.json b/extensions/vscode-api-tests/package.json index 162b2dce4e3..6974a720072 100644 --- a/extensions/vscode-api-tests/package.json +++ b/extensions/vscode-api-tests/package.json @@ -5,6 +5,7 @@ "publisher": "vscode", "license": "MIT", "enableProposedApi": true, + "requiresWorkspaceTrust": "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 a1b15a23ae7..8f0a3b204c3 100644 --- a/src/vs/platform/extensions/common/extensions.ts +++ b/src/vs/platform/extensions/common/extensions.ts @@ -136,6 +136,8 @@ export interface IExtensionContributions { export type ExtensionKind = 'ui' | 'workspace' | 'web'; +export type ExtensionWorkspaceTrustRequirement = false | 'onStart' | 'onDemand'; + export function isIExtensionIdentifier(thing: any): thing is IExtensionIdentifier { return thing && typeof thing === 'object' @@ -190,6 +192,7 @@ export interface IExtensionManifest { readonly enableProposedApi?: boolean; readonly api?: string; readonly scripts?: { [key: string]: string; }; + readonly requiresWorkspaceTrust?: ExtensionWorkspaceTrustRequirement; } export const enum ExtensionType { diff --git a/src/vs/platform/workspace/common/workspaceTrust.ts b/src/vs/platform/workspace/common/workspaceTrust.ts new file mode 100644 index 00000000000..1990e310180 --- /dev/null +++ b/src/vs/platform/workspace/common/workspaceTrust.ts @@ -0,0 +1,375 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Emitter, Event } from 'vs/base/common/event'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { URI } from 'vs/base/common/uri'; +import { localize } from 'vs/nls'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; +import { IWorkspace, IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; + +export const WORKSPACE_TRUST_ENABLED = 'workspace.trustEnabled'; +export const WORKSPACE_TRUST_URI = URI.parse('workspaceTrust:/Trusted Workspaces'); + +export enum WorkspaceTrustScope { + Local = 0, + Remote = 1 +} + +export enum WorkspaceTrustState { + Untrusted = 0, + Trusted = 1, + Unknown = 2 +} + +export function workspaceTrustStateToString(trustState: WorkspaceTrustState) { + switch (trustState) { + case WorkspaceTrustState.Trusted: + return localize('trusted', "Trusted"); + case WorkspaceTrustState.Untrusted: + return localize('untrusted', "Untrusted"); + case WorkspaceTrustState.Unknown: + default: + return localize('unknown', "Unknown"); + } +} + +export const WorkspaceTrustContext = { + PendingRequest: new RawContextKey('workspaceTrustPendingRequest', false), + TrustState: new RawContextKey('workspaceTrustState', WorkspaceTrustState.Unknown) +}; + +export interface IWorkspaceTrustModel { + + readonly onDidChangeTrustState: Event; + + setFolderTrustState(folder: URI, trustState: WorkspaceTrustState): void; + getFolderTrustState(folder: URI): WorkspaceTrustState; +} + +export interface IWorkspaceTrustRequest { + immediate: boolean; + message?: string; +} + +export interface IWorkspaceTrustRequestModel { + readonly trustRequest: IWorkspaceTrustRequest | undefined; + + readonly onDidInitiateRequest: Event; + readonly onDidCompleteRequest: Event; + + initiateRequest(request?: IWorkspaceTrustRequest): void; + completeRequest(trustState?: WorkspaceTrustState): void; +} + +export interface WorkspaceTrustStateChangeEvent { + previousTrustState: WorkspaceTrustState; + currentTrustState: WorkspaceTrustState; +} + +export type WorkspaceTrustChangeEvent = Event; + +export const IWorkspaceTrustService = createDecorator('workspaceTrustService'); + +export interface IWorkspaceTrustService { + readonly _serviceBrand: undefined; + + readonly requestModel: IWorkspaceTrustRequestModel; + + onDidChangeTrustState: WorkspaceTrustChangeEvent; + getWorkspaceTrustState(): WorkspaceTrustState; + isWorkspaceTrustEnabled(): boolean; + requireWorkspaceTrust(request: IWorkspaceTrustRequest): Promise; + resetWorkspaceTrust(): Promise; +} + +interface IWorkspaceTrustStateInfo { + localFolders: { uri: string, trustState: WorkspaceTrustState }[] + + // Removing complexity of remote items + //trustedRemoteItems: { uri: string }[] +} + +export const WORKSPACE_TRUST_STORAGE_KEY = 'content.trust.model.key'; + +export class WorkspaceTrustModel extends Disposable implements IWorkspaceTrustModel { + + private storageKey = WORKSPACE_TRUST_STORAGE_KEY; + private trustStateInfo: IWorkspaceTrustStateInfo; + + private readonly _onDidChangeTrustState = this._register(new Emitter()); + readonly onDidChangeTrustState = this._onDidChangeTrustState.event; + + constructor( + private readonly storageService: IStorageService + ) { + super(); + + this.trustStateInfo = this.loadTrustInfo(); + this._register(this.storageService.onDidChangeValue(changeEvent => { + if (changeEvent.key === this.storageKey) { + this.onDidStorageChange(); + } + })); + } + + private loadTrustInfo(): IWorkspaceTrustStateInfo { + const infoAsString = this.storageService.get(this.storageKey, StorageScope.GLOBAL); + + let result: IWorkspaceTrustStateInfo | undefined; + try { + if (infoAsString) { + result = JSON.parse(infoAsString); + } + } catch { } + + if (!result) { + result = { + localFolders: [], + //trustedRemoteItems: [] + }; + } + + if (!result.localFolders) { + result.localFolders = []; + } + + // if (!result.trustedRemoteItems) { + // result.trustedRemoteItems = []; + // } + + return result; + } + + private saveTrustInfo(): void { + this.storageService.store(this.storageKey, JSON.stringify(this.trustStateInfo), StorageScope.GLOBAL, StorageTarget.MACHINE); + } + + private onDidStorageChange(): void { + this.trustStateInfo = this.loadTrustInfo(); + + this._onDidChangeTrustState.fire(); + } + + setFolderTrustState(folder: URI, trustState: WorkspaceTrustState): void { + let changed = false; + + if (trustState === WorkspaceTrustState.Unknown) { + const before = this.trustStateInfo.localFolders.length; + this.trustStateInfo.localFolders = this.trustStateInfo.localFolders.filter(info => info.uri !== folder.toString()); + + if (this.trustStateInfo.localFolders.length !== before) { + changed = true; + } + } else { + let found = false; + for (const trustInfo of this.trustStateInfo.localFolders) { + if (trustInfo.uri === folder.toString()) { + found = true; + if (trustInfo.trustState !== trustState) { + trustInfo.trustState = trustState; + changed = true; + } + } + } + + if (!found) { + this.trustStateInfo.localFolders.push({ uri: folder.toString(), trustState }); + changed = true; + } + } + + if (changed) { + this.saveTrustInfo(); + } + } + + getFolderTrustState(folder: URI): WorkspaceTrustState { + for (const trustInfo of this.trustStateInfo.localFolders) { + if (trustInfo.uri === folder.toString()) { + return trustInfo.trustState; + } + } + + return WorkspaceTrustState.Unknown; + } +} + +export class WorkspaceTrustRequestModel extends Disposable implements IWorkspaceTrustRequestModel { + trustRequest: IWorkspaceTrustRequest | undefined; + + _onDidInitiateRequest = this._register(new Emitter()); + onDidInitiateRequest: Event = this._onDidInitiateRequest.event; + + _onDidCompleteRequest = this._register(new Emitter()); + onDidCompleteRequest = this._onDidCompleteRequest.event; + + initiateRequest(request: IWorkspaceTrustRequest): void { + if (this.trustRequest && (!request.immediate || this.trustRequest.immediate)) { + return; + } + + this.trustRequest = request; + this._onDidInitiateRequest.fire(); + } + + completeRequest(trustState?: WorkspaceTrustState): void { + this.trustRequest = undefined; + this._onDidCompleteRequest.fire(trustState); + } +} + +export class WorkspaceTrustService extends Disposable implements IWorkspaceTrustService { + + _serviceBrand: undefined; + private readonly dataModel: IWorkspaceTrustModel; + readonly requestModel: IWorkspaceTrustRequestModel; + + private readonly _onDidChangeTrustState = this._register(new Emitter()); + readonly onDidChangeTrustState = this._onDidChangeTrustState.event; + + private _currentTrustState: WorkspaceTrustState = WorkspaceTrustState.Unknown; + private _inFlightResolver?: (trustState: WorkspaceTrustState) => void; + private _trustRequestPromise?: Promise; + private _workspace: IWorkspace; + + private readonly _ctxWorkspaceTrustState: IContextKey; + private readonly _ctxWorkspaceTrustPendingRequest: IContextKey; + + constructor( + @IStorageService private readonly storageService: IStorageService, + @IWorkspaceContextService private readonly workspaceService: IWorkspaceContextService, + @IConfigurationService readonly configurationService: IConfigurationService, + @IContextKeyService readonly contextKeyService: IContextKeyService + ) { + super(); + + this.dataModel = this._register(new WorkspaceTrustModel(this.storageService)); + this.requestModel = this._register(new WorkspaceTrustRequestModel()); + + this._workspace = this.workspaceService.getWorkspace(); + this._currentTrustState = this.calculateWorkspaceTrustState(); + + this._register(this.dataModel.onDidChangeTrustState(() => this.currentTrustState = this.calculateWorkspaceTrustState())); + this._register(this.requestModel.onDidCompleteRequest((trustState) => this.onTrustRequestCompleted(trustState))); + + this._ctxWorkspaceTrustState = WorkspaceTrustContext.TrustState.bindTo(contextKeyService); + this._ctxWorkspaceTrustPendingRequest = WorkspaceTrustContext.PendingRequest.bindTo(contextKeyService); + this._ctxWorkspaceTrustState.set(this.currentTrustState); + } + + private get currentTrustState(): WorkspaceTrustState { + return this._currentTrustState; + } + + private set currentTrustState(trustState: WorkspaceTrustState) { + if (this._currentTrustState === trustState) { return; } + const previousState = this._currentTrustState; + this._currentTrustState = trustState; + + this._onDidChangeTrustState.fire({ previousTrustState: previousState, currentTrustState: this._currentTrustState }); + } + + private calculateWorkspaceTrustState(): WorkspaceTrustState { + if (!this.isWorkspaceTrustEnabled()) { + return WorkspaceTrustState.Trusted; + } + + if (this.workspaceService.getWorkbenchState() === WorkbenchState.EMPTY) { + return WorkspaceTrustState.Trusted; + } + + let state = undefined; + for (const folder of this._workspace.folders) { + const folderTrust = this.dataModel.getFolderTrustState(folder.uri); + + switch (folderTrust) { + case WorkspaceTrustState.Untrusted: + return WorkspaceTrustState.Untrusted; + case WorkspaceTrustState.Unknown: + state = folderTrust; + break; + case WorkspaceTrustState.Trusted: + if (state === undefined) { + state = folderTrust; + } + break; + } + } + + return state ?? WorkspaceTrustState.Unknown; + } + + private onTrustRequestCompleted(trustState?: WorkspaceTrustState): void { + if (this._inFlightResolver) { + this._inFlightResolver(trustState === undefined ? this.currentTrustState : trustState); + } + + this._inFlightResolver = undefined; + this._trustRequestPromise = undefined; + + if (trustState === undefined) { + return; + } + + this._workspace.folders.forEach(folder => { + this.dataModel.setFolderTrustState(folder.uri, trustState); + }); + + this._ctxWorkspaceTrustPendingRequest.set(false); + this._ctxWorkspaceTrustState.set(trustState); + } + + getWorkspaceTrustState(): WorkspaceTrustState { + return this.currentTrustState; + } + + isWorkspaceTrustEnabled(): boolean { + return this.configurationService.getValue(WORKSPACE_TRUST_ENABLED) ?? false; + } + + async requireWorkspaceTrust(request?: IWorkspaceTrustRequest): Promise { + if (this.currentTrustState === WorkspaceTrustState.Trusted) { + return this.currentTrustState; + } + if (this.currentTrustState === WorkspaceTrustState.Untrusted && !request?.immediate) { + return this.currentTrustState; + } + + if (this._trustRequestPromise) { + if (request?.immediate && + this.requestModel.trustRequest && + !this.requestModel.trustRequest.immediate) { + this.requestModel.initiateRequest(request); + } + + return this._trustRequestPromise; + } + + this._trustRequestPromise = new Promise(resolve => { + this._inFlightResolver = resolve; + }); + + this.requestModel.initiateRequest(request); + this._ctxWorkspaceTrustPendingRequest.set(true); + + return this._trustRequestPromise; + } + + async resetWorkspaceTrust(): Promise { + if (this.currentTrustState !== WorkspaceTrustState.Unknown) { + this._workspace.folders.forEach(folder => { + this.dataModel.setFolderTrustState(folder.uri, WorkspaceTrustState.Unknown); + }); + } + return Promise.resolve(WorkspaceTrustState.Unknown); + } +} + +registerSingleton(IWorkspaceTrustService, WorkspaceTrustService); diff --git a/src/vs/platform/workspace/test/common/testWorkspaceTrust.ts b/src/vs/platform/workspace/test/common/testWorkspaceTrust.ts new file mode 100644 index 00000000000..f7a189af2b6 --- /dev/null +++ b/src/vs/platform/workspace/test/common/testWorkspaceTrust.ts @@ -0,0 +1,31 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Event } from 'vs/base/common/event'; +import { IWorkspaceTrustRequest, IWorkspaceTrustRequestModel, IWorkspaceTrustService, WorkspaceTrustChangeEvent, WorkspaceTrustRequestModel, WorkspaceTrustState } from 'vs/platform/workspace/common/workspaceTrust'; + +export class TestWorkspaceTrustService implements IWorkspaceTrustService { + _serviceBrand: undefined; + + requestModel: IWorkspaceTrustRequestModel = new WorkspaceTrustRequestModel(); + + onDidChangeTrustState: WorkspaceTrustChangeEvent = Event.None; + + getWorkspaceTrustState(): WorkspaceTrustState { + return WorkspaceTrustState.Trusted; + } + + isWorkspaceTrustEnabled(): boolean { + return true; + } + + requireWorkspaceTrust(request: IWorkspaceTrustRequest): Promise { + return Promise.resolve(WorkspaceTrustState.Trusted); + } + + resetWorkspaceTrust(): Promise { + return Promise.resolve(WorkspaceTrustState.Unknown); + } +} diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index 03779a0324e..837425917f5 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -2615,4 +2615,60 @@ declare module 'vscode' { } //#endregion + + //#region https://github.com/microsoft/vscode/issues/106488 + + export enum WorkspaceTrustState { + /** + * The workspace is untrusted, and it will have limited functionality. + */ + Untrusted = 0, + + /** + * The workspace is trusted, and all functionality will be available. + */ + Trusted = 1, + + /** + * The initial state of the workspace. + * + * If trust will be required, users will be prompted to make a choice. + */ + Unknown = 2 + } + + /** + * The event data that is fired when the trust state of the workspace changes + */ + export interface WorkspaceTrustStateChangeEvent { + /** + * Previous trust state of the workspace + */ + previousTrustState: WorkspaceTrustState; + + /** + * Current trust state of the workspace + */ + currentTrustState: WorkspaceTrustState; + } + + export namespace workspace { + /** + * The trust state of the current workspace + */ + export const trustState: WorkspaceTrustState; + + /** + * Prompt the user to chose whether to trust the current workspace + * @param message Optional message which would be displayed in the prompt + */ + export function requireWorkspaceTrust(message?: string): Thenable; + + /** + * Event that fires when the trust state of the current workspace changes + */ + export const onDidChangeWorkspaceTrustState: Event; + } + + //#endregion } diff --git a/src/vs/workbench/api/browser/mainThreadWorkspace.ts b/src/vs/workbench/api/browser/mainThreadWorkspace.ts index 946c7bd3d65..bd8725fd299 100644 --- a/src/vs/workbench/api/browser/mainThreadWorkspace.ts +++ b/src/vs/workbench/api/browser/mainThreadWorkspace.ts @@ -16,6 +16,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { ILabelService } from 'vs/platform/label/common/label'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { IRequestService } from 'vs/platform/request/common/request'; +import { WorkspaceTrustStateChangeEvent, IWorkspaceTrustService, WorkspaceTrustState } from 'vs/platform/workspace/common/workspaceTrust'; import { IWorkspace, IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { isUntitledWorkspace } from 'vs/platform/workspaces/common/workspaces'; import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers'; @@ -45,19 +46,21 @@ export class MainThreadWorkspace implements MainThreadWorkspaceShape { @IInstantiationService private readonly _instantiationService: IInstantiationService, @ILabelService private readonly _labelService: ILabelService, @IEnvironmentService private readonly _environmentService: IEnvironmentService, - @IFileService fileService: IFileService + @IFileService fileService: IFileService, + @IWorkspaceTrustService private readonly _workspaceTrustService: IWorkspaceTrustService ) { this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostWorkspace); const workspace = this._contextService.getWorkspace(); // The workspace file is provided be a unknown file system provider. It might come // from the extension host. So initialize now knowing that `rootPath` is undefined. if (workspace.configuration && !isNative && !fileService.canHandleResource(workspace.configuration)) { - this._proxy.$initializeWorkspace(this.getWorkspaceData(workspace)); + this._proxy.$initializeWorkspace(this.getWorkspaceData(workspace), this.getWorkspaceTrustState()); } else { - this._contextService.getCompleteWorkspace().then(workspace => this._proxy.$initializeWorkspace(this.getWorkspaceData(workspace))); + this._contextService.getCompleteWorkspace().then(workspace => this._proxy.$initializeWorkspace(this.getWorkspaceData(workspace), this.getWorkspaceTrustState())); } this._contextService.onDidChangeWorkspaceFolders(this._onDidChangeWorkspace, this, this._toDispose); this._contextService.onDidChangeWorkbenchState(this._onDidChangeWorkspace, this, this._toDispose); + this._workspaceTrustService.onDidChangeTrustState(this._onDidChangeWorkspaceTrustState, this, this._toDispose); } dispose(): void { @@ -202,4 +205,18 @@ export class MainThreadWorkspace implements MainThreadWorkspaceShape { $resolveProxy(url: string): Promise { return this._requestService.resolveProxy(url); } + + // --- trust --- + + $requireWorkspaceTrust(message?: string): Promise { + return this._workspaceTrustService.requireWorkspaceTrust({ immediate: true, message }); + } + + private getWorkspaceTrustState(): WorkspaceTrustState { + return this._workspaceTrustService.getWorkspaceTrustState(); + } + + private _onDidChangeWorkspaceTrustState(state: WorkspaceTrustStateChangeEvent): void { + this._proxy.$onDidChangeWorkspaceTrustState(state); + } } diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index ba227f1074f..e13a14baa6b 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -48,7 +48,7 @@ import { ExtHostUrls } from 'vs/workbench/api/common/extHostUrls'; import { ExtHostWebviews } from 'vs/workbench/api/common/extHostWebview'; import { IExtHostWindow } from 'vs/workbench/api/common/extHostWindow'; import { IExtHostWorkspace } from 'vs/workbench/api/common/extHostWorkspace'; -import { throwProposedApiError, checkProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions'; +import { throwProposedApiError, checkProposedApiEnabled, checkRequiresWorkspaceTrust } from 'vs/workbench/services/extensions/common/extensions'; import { ProxyIdentifier } from 'vs/workbench/services/extensions/common/proxyIdentifier'; import { ExtensionDescriptionRegistry } from 'vs/workbench/services/extensions/common/extensionDescriptionRegistry'; import type * as vscode from 'vscode'; @@ -881,11 +881,23 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I onDidChangeTunnels: (listener, thisArg?, disposables?) => { checkProposedApiEnabled(extension); return extHostTunnelService.onDidChangeTunnels(listener, thisArg, disposables); - }, registerTimelineProvider: (scheme: string | string[], provider: vscode.TimelineProvider) => { checkProposedApiEnabled(extension); return extHostTimeline.registerTimelineProvider(scheme, provider, extension.identifier, extHostCommands.converter); + }, + get trustState() { + checkProposedApiEnabled(extension); + checkRequiresWorkspaceTrust(extension); + return extHostWorkspace.trustState; + }, + requireWorkspaceTrust: (message?: string) => { + checkProposedApiEnabled(extension); + checkRequiresWorkspaceTrust(extension); + return extHostWorkspace.requireWorkspaceTrust(message); + }, + onDidChangeWorkspaceTrustState: (listener, thisArgs?, disposables?) => { + return extHostWorkspace.onDidChangeWorkspaceTrustState(listener, thisArgs, disposables); } }; @@ -1284,6 +1296,10 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I // checkProposedApiEnabled(extension); return extHostTypes.TestState; }, + get WorkspaceTrustState() { + // checkProposedApiEnabled(extension); + return extHostTypes.WorkspaceTrustState; + } }; }; } diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 52aa0b773e7..b4e3779cc4d 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -55,11 +55,12 @@ import { IProcessedOutput, INotebookDisplayOrder, NotebookCellMetadata, Notebook import { CallHierarchyItem } from 'vs/workbench/contrib/callHierarchy/common/callHierarchy'; import { Dto } from 'vs/base/common/types'; import { ISerializableEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariable'; -import { DebugConfigurationProviderTriggerKind } from 'vs/workbench/api/common/extHostTypes'; +import { DebugConfigurationProviderTriggerKind, WorkspaceTrustState } from 'vs/workbench/api/common/extHostTypes'; import { IAccessibilityInformation } from 'vs/platform/accessibility/common/accessibility'; import { IExtensionIdWithVersion } from 'vs/platform/userDataSync/common/extensionsStorageSync'; import { InternalTestItem, InternalTestResults, RunTestForProviderRequest, RunTestsRequest, RunTestsResult, TestIdWithProvider, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection'; import { CandidatePort } from 'vs/workbench/services/remote/common/remoteExplorerService'; +import { WorkspaceTrustStateChangeEvent } from 'vs/platform/workspace/common/workspaceTrust'; export interface IEnvironment { isExtensionDevelopmentDebug: boolean; @@ -831,6 +832,7 @@ export interface MainThreadWorkspaceShape extends IDisposable { $saveAll(includeUntitled?: boolean): Promise; $updateWorkspaceFolders(extensionName: string, index: number, deleteCount: number, workspaceFoldersToAdd: { uri: UriComponents, name?: string; }[]): Promise; $resolveProxy(url: string): Promise; + $requireWorkspaceTrust(message?: string): Promise } export interface IFileChangeDto { @@ -1093,9 +1095,10 @@ export interface ExtHostTreeViewsShape { } export interface ExtHostWorkspaceShape { - $initializeWorkspace(workspace: IWorkspaceData | null): void; + $initializeWorkspace(workspace: IWorkspaceData | null, trustState: WorkspaceTrustState): void; $acceptWorkspaceData(workspace: IWorkspaceData | null): void; $handleTextSearchResult(result: search.IRawFileMatch2, requestId: number): void; + $onDidChangeWorkspaceTrustState(state: WorkspaceTrustStateChangeEvent): void; } export interface ExtHostFileSystemInfoShape { diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index facd14e0b4f..a4199cad15e 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -3001,3 +3001,9 @@ export enum ExternalUriOpenerPriority { Default = 2, Preferred = 3, } + +export enum WorkspaceTrustState { + Untrusted = 0, + Trusted = 1, + Unknown = 2 +} diff --git a/src/vs/workbench/api/common/extHostWorkspace.ts b/src/vs/workbench/api/common/extHostWorkspace.ts index c03c6be54e5..f73edbc8445 100644 --- a/src/vs/workbench/api/common/extHostWorkspace.ts +++ b/src/vs/workbench/api/common/extHostWorkspace.ts @@ -20,11 +20,12 @@ import { FileSystemProviderCapabilities } from 'vs/platform/files/common/files'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { ILogService } from 'vs/platform/log/common/log'; import { Severity } from 'vs/platform/notification/common/notification'; +import { WorkspaceTrustStateChangeEvent } from 'vs/platform/workspace/common/workspaceTrust'; import { Workspace, WorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { IExtHostFileSystemInfo } from 'vs/workbench/api/common/extHostFileSystemInfo'; import { IExtHostInitDataService } from 'vs/workbench/api/common/extHostInitDataService'; import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; -import { Range, RelativePattern } from 'vs/workbench/api/common/extHostTypes'; +import { Range, RelativePattern, WorkspaceTrustState } from 'vs/workbench/api/common/extHostTypes'; import { ITextQueryBuilderOptions } from 'vs/workbench/contrib/search/common/queryBuilder'; import { IRawFileMatch2, resultIsMatch } from 'vs/workbench/services/search/common/search'; import * as vscode from 'vscode'; @@ -168,6 +169,9 @@ export class ExtHostWorkspace implements ExtHostWorkspaceShape, IExtHostWorkspac private readonly _onDidChangeWorkspace = new Emitter(); readonly onDidChangeWorkspace: Event = this._onDidChangeWorkspace.event; + private readonly _onDidChangeWorkspaceTrustState = new Emitter(); + readonly onDidChangeWorkspaceTrustState: Event = this._onDidChangeWorkspaceTrustState.event; + private readonly _logService: ILogService; private readonly _requestIdProvider: Counter; private readonly _barrier: Barrier; @@ -181,6 +185,8 @@ export class ExtHostWorkspace implements ExtHostWorkspaceShape, IExtHostWorkspac private readonly _activeSearchCallbacks: ((match: IRawFileMatch2) => any)[] = []; + private _workspaceTrustState: WorkspaceTrustState = WorkspaceTrustState.Unknown; + constructor( @IExtHostRpcService extHostRpc: IExtHostRpcService, @IExtHostInitDataService initData: IExtHostInitDataService, @@ -198,7 +204,8 @@ export class ExtHostWorkspace implements ExtHostWorkspaceShape, IExtHostWorkspac this._confirmedWorkspace = data ? new ExtHostWorkspaceImpl(data.id, data.name, [], data.configuration ? URI.revive(data.configuration) : null, !!data.isUntitled, uri => ignorePathCasing(uri, extHostFileSystemInfo)) : undefined; } - $initializeWorkspace(data: IWorkspaceData | null): void { + $initializeWorkspace(data: IWorkspaceData | null, trustState: WorkspaceTrustState): void { + this._workspaceTrustState = trustState; this.$acceptWorkspaceData(data); this._barrier.open(); } @@ -549,6 +556,21 @@ export class ExtHostWorkspace implements ExtHostWorkspaceShape, IExtHostWorkspac resolveProxy(url: string): Promise { return this._proxy.$resolveProxy(url); } + + // --- trust --- + + get trustState(): WorkspaceTrustState { + return this._workspaceTrustState; + } + + requireWorkspaceTrust(message?: string): Promise { + return this._proxy.$requireWorkspaceTrust(message); + } + + $onDidChangeWorkspaceTrustState(state: WorkspaceTrustStateChangeEvent): void { + this._workspaceTrustState = state.currentTrustState; + this._onDidChangeWorkspaceTrustState.fire(Object.freeze(state)); + } } export const IExtHostWorkspace = createDecorator('IExtHostWorkspace'); diff --git a/src/vs/workbench/browser/actions/workspaceActions.ts b/src/vs/workbench/browser/actions/workspaceActions.ts index ea67ce4c511..2f1ffb9250b 100644 --- a/src/vs/workbench/browser/actions/workspaceActions.ts +++ b/src/vs/workbench/browser/actions/workspaceActions.ts @@ -12,9 +12,9 @@ import { IEditorService } from 'vs/workbench/services/editor/common/editorServic import { ICommandService, CommandsRegistry } from 'vs/platform/commands/common/commands'; import { ADD_ROOT_FOLDER_COMMAND_ID, ADD_ROOT_FOLDER_LABEL, PICK_WORKSPACE_FOLDER_COMMAND_ID } from 'vs/workbench/browser/actions/workspaceCommands'; import { IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; -import { MenuRegistry, MenuId, SyncActionDescriptor } from 'vs/platform/actions/common/actions'; +import { MenuRegistry, MenuId, SyncActionDescriptor, Action2, registerAction2 } from 'vs/platform/actions/common/actions'; import { EmptyWorkspaceSupportContext, WorkbenchStateContext, WorkspaceFolderCountContext } from 'vs/workbench/browser/contextkeys'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { Registry } from 'vs/platform/registry/common/platform'; import { IWorkbenchActionRegistry, Extensions } from 'vs/workbench/common/actions'; import { INotificationService } from 'vs/platform/notification/common/notification'; @@ -23,6 +23,7 @@ import { KeyChord, KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { IWorkspacesService, hasWorkspaceFileExtension } from 'vs/platform/workspaces/common/workspaces'; +import { WORKSPACE_TRUST_ENABLED, WORKSPACE_TRUST_URI } from 'vs/platform/workspace/common/workspaceTrust'; export class OpenFileAction extends Action { @@ -250,6 +251,24 @@ export class DuplicateWorkspaceInNewWindowAction extends Action { } } +class WorkspaceTrustManageAction extends Action2 { + constructor() { + super({ + id: 'workbench.action.manageTrust', + title: { value: nls.localize('resetTrustAction', "Manage Trusted Workspaces"), original: 'Manage Trusted Workspaces' }, + precondition: ContextKeyExpr.equals(`config.${WORKSPACE_TRUST_ENABLED}`, true), + f1: true, + }); + } + + run(accessor: ServicesAccessor) { + const editorService = accessor.get(IEditorService); + editorService.openEditor({ resource: WORKSPACE_TRUST_URI, mode: 'jsonc', options: { pinned: true } }); + } +} + +registerAction2(WorkspaceTrustManageAction); + // --- Actions Registration const registry = Registry.as(Extensions.WorkbenchActions); diff --git a/src/vs/workbench/browser/parts/activitybar/media/activityaction.css b/src/vs/workbench/browser/parts/activitybar/media/activityaction.css index 57d6f2a8588..e72afbea624 100644 --- a/src/vs/workbench/browser/parts/activitybar/media/activityaction.css +++ b/src/vs/workbench/browser/parts/activitybar/media/activityaction.css @@ -156,6 +156,19 @@ text-align: center; } + +.monaco-workbench .activitybar > .content :not(.monaco-menu) > .monaco-action-bar .badge .codicon.badge-content { + font-size: 12px; + font-weight: unset; + padding: 0 2px; +} + +.monaco-workbench .activitybar > .content :not(.monaco-menu) > .monaco-action-bar .badge .codicon.badge-content::before { + text-align: center; + vertical-align: baseline; +} + + /* Right aligned */ .monaco-workbench .activitybar.right > .content :not(.monaco-menu) > .monaco-action-bar .action-label:not(.codicon) { diff --git a/src/vs/workbench/browser/parts/compositeBarActions.ts b/src/vs/workbench/browser/parts/compositeBarActions.ts index 779b309c588..411766b2ea2 100644 --- a/src/vs/workbench/browser/parts/compositeBarActions.ts +++ b/src/vs/workbench/browser/parts/compositeBarActions.ts @@ -9,7 +9,7 @@ import * as dom from 'vs/base/browser/dom'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { dispose, toDisposable, MutableDisposable } from 'vs/base/common/lifecycle'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; -import { IThemeService, IColorTheme } from 'vs/platform/theme/common/themeService'; +import { IThemeService, IColorTheme, ThemeIcon } from 'vs/platform/theme/common/themeService'; import { TextBadge, NumberBadge, IBadge, IconBadge, ProgressBadge } from 'vs/workbench/services/activity/common/activity'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { contrastBorder } from 'vs/platform/theme/common/colorRegistry'; @@ -19,8 +19,8 @@ import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { Emitter, Event } from 'vs/base/common/event'; import { CompositeDragAndDropObserver, ICompositeDragAndDrop, Before2D, toggleDropEffect } from 'vs/workbench/browser/dnd'; import { Color } from 'vs/base/common/color'; -import { Codicon } from 'vs/base/common/codicons'; import { IBaseActionViewItemOptions, BaseActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems'; +import { Codicon } from 'vs/base/common/codicons'; export interface ICompositeActivity { badge: IBadge; @@ -288,8 +288,10 @@ export class ActivityActionViewItem extends BaseActionViewItem { dom.show(this.badge); } - // Text + // Icon else if (badge instanceof IconBadge) { + const clazzList = ThemeIcon.asClassNameArray(badge.icon); + this.badgeContent.classList.add(...clazzList); dom.show(this.badge); } diff --git a/src/vs/workbench/contrib/experiments/test/electron-browser/experimentService.test.ts b/src/vs/workbench/contrib/experiments/test/electron-browser/experimentService.test.ts index 12454df7f2a..ae6e2d75052 100644 --- a/src/vs/workbench/contrib/experiments/test/electron-browser/experimentService.test.ts +++ b/src/vs/workbench/contrib/experiments/test/electron-browser/experimentService.test.ts @@ -31,6 +31,8 @@ import { IWillActivateEvent, IExtensionService } from 'vs/workbench/services/ext import { timeout } from 'vs/base/common/async'; import { TestExtensionService } from 'vs/workbench/test/common/workbenchTestServices'; import { OS } from 'vs/base/common/platform'; +import { IWorkspaceTrustService } from 'vs/platform/workspace/common/workspaceTrust'; +import { TestWorkspaceTrustService } from 'vs/platform/workspace/test/common/testWorkspaceTrust'; interface ExperimentSettings { enabled?: boolean; @@ -94,6 +96,7 @@ suite('Experiment Service', () => { instantiationService.stub(IConfigurationService, testConfigurationService); instantiationService.stub(ILifecycleService, new TestLifecycleService()); instantiationService.stub(IStorageService, >{ get: (a: string, b: StorageScope, c?: string) => c, getBoolean: (a: string, b: StorageScope, c?: boolean) => c, store: () => { }, remove: () => { } }); + instantiationService.stub(IWorkspaceTrustService, new TestWorkspaceTrustService()); setup(() => { instantiationService.stub(IProductService, {}); diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts index 3401152e5ca..5e3d9d5ebba 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts @@ -58,7 +58,8 @@ import { ActionWithDropdownActionViewItem, IActionWithDropdownActionViewItemOpti import { IContextMenuProvider } from 'vs/base/browser/contextmenu'; import { ILogService } from 'vs/platform/log/common/log'; import * as Constants from 'vs/workbench/contrib/logs/common/logConstants'; -import { infoIcon, manageExtensionIcon, syncEnabledIcon, syncIgnoredIcon, warningIcon } from 'vs/workbench/contrib/extensions/browser/extensionsIcons'; +import { infoIcon, manageExtensionIcon, syncEnabledIcon, syncIgnoredIcon, trustIcon, warningIcon } from 'vs/workbench/contrib/extensions/browser/extensionsIcons'; +import { IWorkspaceTrustService } from 'vs/platform/workspace/common/workspaceTrust'; function getRelativeDateLabel(date: Date): string { const delta = new Date().getTime() - date.getTime(); @@ -2112,6 +2113,7 @@ export class SystemDisabledWarningAction extends ExtensionAction { private static readonly CLASS = `${ExtensionAction.ICON_ACTION_CLASS} system-disable`; private static readonly WARNING_CLASS = `${SystemDisabledWarningAction.CLASS} ${ThemeIcon.asClassName(warningIcon)}`; private static readonly INFO_CLASS = `${SystemDisabledWarningAction.CLASS} ${ThemeIcon.asClassName(infoIcon)}`; + private static readonly TRUST_CLASS = `${SystemDisabledWarningAction.CLASS} ${ThemeIcon.asClassName(trustIcon)}`; updateWhenCounterExtensionChanges: boolean = true; private _runningExtensions: IExtensionDescription[] | null = null; @@ -2123,6 +2125,7 @@ export class SystemDisabledWarningAction extends ExtensionAction { @IExtensionService private readonly extensionService: IExtensionService, @IProductService private readonly productService: IProductService, @IConfigurationService private readonly configurationService: IConfigurationService, + @IWorkspaceTrustService private readonly workspaceTrustService: IWorkspaceTrustService ) { super('extensions.install', '', `${SystemDisabledWarningAction.CLASS} hide`, false); this._register(this.labelService.onDidChangeFormatters(() => this.update(), this)); @@ -2188,6 +2191,11 @@ export class SystemDisabledWarningAction extends ExtensionAction { return; } } + if (this.workspaceTrustService.isWorkspaceTrustEnabled() && this.extension.enablementState === EnablementState.DisabledByTrustRequirement) { + this.class = `${SystemDisabledWarningAction.TRUST_CLASS}`; + this.tooltip = localize('extension disabled because of trust requirement', "This extension has been disabled as it requires a trusted workspace"); + return; + } } run(): Promise { diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsIcons.ts b/src/vs/workbench/contrib/extensions/browser/extensionsIcons.ts index 55fea5ddf9f..f096881c6a4 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsIcons.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsIcons.ts @@ -31,3 +31,4 @@ export const starEmptyIcon = registerIcon('extensions-star-empty', Codicon.starE export const warningIcon = registerIcon('extensions-warning-message', Codicon.warning, localize('warningIcon', 'Icon shown with a warning message in the extensions editor.')); export const infoIcon = registerIcon('extensions-info-message', Codicon.info, localize('infoIcon', 'Icon shown with an info message in the extensions editor.')); +export const trustIcon = registerIcon('extension-trust-message', Codicon.shield, localize('trustIcon', 'Icon shown with a message in the extension editor.')); diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts index 7097869562e..6c093794e37 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts @@ -41,6 +41,7 @@ import { FileAccess } from 'vs/base/common/network'; import { IIgnoredExtensionsManagementService } from 'vs/platform/userDataSync/common/ignoredExtensions'; import { IUserDataAutoSyncService } from 'vs/platform/userDataSync/common/userDataSync'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IWorkspaceTrustService, WorkspaceTrustState } from 'vs/platform/workspace/common/workspaceTrust'; interface IExtensionStateProvider { (extension: Extension): T; @@ -524,7 +525,8 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension @IIgnoredExtensionsManagementService private readonly extensionsSyncManagementService: IIgnoredExtensionsManagementService, @IUserDataAutoSyncService private readonly userDataAutoSyncService: IUserDataAutoSyncService, @IProductService private readonly productService: IProductService, - @IContextKeyService contextKeyService: IContextKeyService + @IContextKeyService contextKeyService: IContextKeyService, + @IWorkspaceTrustService private readonly workspaceTrustService: IWorkspaceTrustService ) { super(); this.hasOutdatedExtensionsContextKey = HasOutdatedExtensionsContext.bindTo(contextKeyService); @@ -1031,33 +1033,54 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension private async installFromVSIX(vsix: URI): Promise { const manifest = await this.extensionManagementService.getManifest(vsix); - const existingExtension = this.local.find(local => areSameExtensions(local.identifier, { id: getGalleryExtensionId(manifest.publisher, manifest.name) })); - const { identifier } = await this.extensionManagementService.install(vsix); + return this.promptForTrustIfNeededAndInstall(manifest, async () => { + const existingExtension = this.local.find(local => areSameExtensions(local.identifier, { id: getGalleryExtensionId(manifest.publisher, manifest.name) })); + const { identifier } = await this.extensionManagementService.install(vsix); - if (existingExtension && existingExtension.latestVersion !== manifest.version) { - this.ignoreAutoUpdate(new ExtensionIdentifierWithVersion(identifier, manifest.version)); - } + if (existingExtension && existingExtension.latestVersion !== manifest.version) { + this.ignoreAutoUpdate(new ExtensionIdentifierWithVersion(identifier, manifest.version)); + } - return this.local.filter(local => areSameExtensions(local.identifier, identifier))[0]; + return this.local.filter(local => areSameExtensions(local.identifier, identifier))[0]; + }); } private async installFromGallery(extension: IExtension, gallery: IGalleryExtension, installOptions?: InstallOptions): Promise { - this.installing.push(extension); - this._onChange.fire(extension); - try { - if (extension.state === ExtensionState.Installed && extension.local) { - await this.extensionManagementService.updateFromGallery(gallery, extension.local); - } else { - await this.extensionManagementService.installFromGallery(gallery, installOptions); - } - const ids: string[] | undefined = extension.identifier.uuid ? [extension.identifier.uuid] : undefined; - const names: string[] | undefined = extension.identifier.uuid ? undefined : [extension.identifier.id]; - this.queryGallery({ names, ids, pageSize: 1 }, CancellationToken.None); - return this.local.filter(local => areSameExtensions(local.identifier, gallery.identifier))[0]; - } finally { - this.installing = this.installing.filter(e => e !== extension); - this._onChange.fire(this.local.filter(e => areSameExtensions(e.identifier, extension.identifier))[0]); + const manifest = await extension.getManifest(CancellationToken.None); + + if (manifest) { + this.promptForTrustIfNeededAndInstall(manifest, async () => { + this.installing.push(extension); + this._onChange.fire(extension); + try { + if (extension.state === ExtensionState.Installed && extension.local) { + await this.extensionManagementService.updateFromGallery(gallery, extension.local); + } else { + await this.extensionManagementService.installFromGallery(gallery, installOptions); + } + const ids: string[] | undefined = extension.identifier.uuid ? [extension.identifier.uuid] : undefined; + const names: string[] | undefined = extension.identifier.uuid ? undefined : [extension.identifier.id]; + this.queryGallery({ names, ids, pageSize: 1 }, CancellationToken.None); + return this.local.filter(local => areSameExtensions(local.identifier, gallery.identifier))[0]; + } finally { + this.installing = this.installing.filter(e => e !== extension); + this._onChange.fire(this.local.filter(e => areSameExtensions(e.identifier, extension.identifier))[0]); + } + }); } + return Promise.reject(); + } + + private async promptForTrustIfNeededAndInstall(manifest: IExtensionManifest, installTask: () => Promise): Promise { + if (manifest.requiresWorkspaceTrust === 'onStart') { + const trustState = await this.workspaceTrustService.requireWorkspaceTrust( + { + immediate: true, + message: 'Installing this extension requires you to trust the contents of this workspace.' + }); + return trustState === WorkspaceTrustState.Trusted ? installTask() : Promise.reject(); + } + return installTask(); } private promptAndSetEnablement(extensions: IExtension[], enablementState: EnablementState): Promise { diff --git a/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsActions.test.ts b/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsActions.test.ts index bcef66bbdc0..d5f066d539f 100644 --- a/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsActions.test.ts +++ b/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsActions.test.ts @@ -55,6 +55,8 @@ import { IUserDataAutoSyncEnablementService, IUserDataSyncResourceEnablementServ import { UserDataSyncResourceEnablementService } from 'vs/platform/userDataSync/common/userDataSyncResourceEnablementService'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { MockContextKeyService } from 'vs/platform/keybinding/test/common/mockKeybindingService'; +import { IWorkspaceTrustService } from 'vs/platform/workspace/common/workspaceTrust'; +import { TestWorkspaceTrustService } from 'vs/platform/workspace/test/common/testWorkspaceTrust'; let instantiationService: TestInstantiationService; let installEvent: Emitter, @@ -131,6 +133,7 @@ async function setupTest() { instantiationService.stub(IUserDataSyncResourceEnablementService, instantiationService.createInstance(UserDataSyncResourceEnablementService)); instantiationService.set(IExtensionsWorkbenchService, disposables.add(instantiationService.createInstance(ExtensionsWorkbenchService))); + instantiationService.stub(IWorkspaceTrustService, new TestWorkspaceTrustService()); } diff --git a/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts b/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts index 5890d50f739..e3ab792c05e 100644 --- a/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts +++ b/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts @@ -80,6 +80,7 @@ import { isWorkspaceFolder, TaskQuickPickEntry, QUICKOPEN_DETAIL_CONFIG, TaskQui import { ILogService } from 'vs/platform/log/common/log'; import { once } from 'vs/base/common/functional'; import { ThemeIcon } from 'vs/platform/theme/common/themeService'; +import { IWorkspaceTrustService } from 'vs/platform/workspace/common/workspaceTrust'; const QUICKOPEN_HISTORY_LIMIT_CONFIG = 'task.quickOpen.history'; const PROBLEM_MATCHER_NEVER_CONFIG = 'task.problemMatchers.neverPrompt'; @@ -255,6 +256,7 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer @ITextModelService private readonly textModelResolverService: ITextModelService, @IPreferencesService private readonly preferencesService: IPreferencesService, @IViewDescriptorService private readonly viewDescriptorService: IViewDescriptorService, + @IWorkspaceTrustService private readonly workspaceTrustService: IWorkspaceTrustService, @ILogService private readonly logService: ILogService ) { super(); @@ -911,7 +913,7 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer }).then((value) => { if (runSource === TaskRunSource.User) { this.getWorkspaceTasks().then(workspaceTasks => { - RunAutomaticTasks.promptForPermission(this, this.storageService, this.notificationService, workspaceTasks); + RunAutomaticTasks.promptForPermission(this, this.storageService, this.notificationService, this.workspaceTrustService, workspaceTasks); }); } return value; diff --git a/src/vs/workbench/contrib/tasks/browser/runAutomaticTasks.ts b/src/vs/workbench/contrib/tasks/browser/runAutomaticTasks.ts index 40899ecaf61..743b1c75374 100644 --- a/src/vs/workbench/contrib/tasks/browser/runAutomaticTasks.ts +++ b/src/vs/workbench/contrib/tasks/browser/runAutomaticTasks.ts @@ -14,16 +14,19 @@ import { INotificationService, Severity } from 'vs/platform/notification/common/ import { IQuickPickItem, IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; import { Action2 } from 'vs/platform/actions/common/actions'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { IWorkspaceTrustService, WorkspaceTrustState } from 'vs/platform/workspace/common/workspaceTrust'; const ARE_AUTOMATIC_TASKS_ALLOWED_IN_WORKSPACE = 'tasks.run.allowAutomatic'; export class RunAutomaticTasks extends Disposable implements IWorkbenchContribution { constructor( @ITaskService private readonly taskService: ITaskService, - @IStorageService storageService: IStorageService) { + @IStorageService storageService: IStorageService, + @IWorkspaceTrustService workspaceTrustService: IWorkspaceTrustService) { super(); const isFolderAutomaticAllowed = storageService.getBoolean(ARE_AUTOMATIC_TASKS_ALLOWED_IN_WORKSPACE, StorageScope.WORKSPACE, undefined); - this.tryRunTasks(isFolderAutomaticAllowed); + const isWorkspaceTrusted = workspaceTrustService.getWorkspaceTrustState() === WorkspaceTrustState.Trusted; + this.tryRunTasks(isFolderAutomaticAllowed && isWorkspaceTrusted); } private tryRunTasks(isAllowed: boolean | undefined) { @@ -84,8 +87,13 @@ export class RunAutomaticTasks extends Disposable implements IWorkbenchContribut return { tasks, taskNames }; } - public static promptForPermission(taskService: ITaskService, storageService: IStorageService, notificationService: INotificationService, + public static async promptForPermission(taskService: ITaskService, storageService: IStorageService, notificationService: INotificationService, workspaceTrustService: IWorkspaceTrustService, workspaceTaskResult: Map) { + const isWorkspaceTrusted = await workspaceTrustService.requireWorkspaceTrust({ immediate: false }) === WorkspaceTrustState.Trusted; + if (!isWorkspaceTrusted) { + return; + } + const isFolderAutomaticAllowed = storageService.getBoolean(ARE_AUTOMATIC_TASKS_ALLOWED_IN_WORKSPACE, StorageScope.WORKSPACE, undefined); if (isFolderAutomaticAllowed !== undefined) { return; diff --git a/src/vs/workbench/contrib/tasks/electron-browser/taskService.ts b/src/vs/workbench/contrib/tasks/electron-browser/taskService.ts index 3df8b849992..7ba96460b75 100644 --- a/src/vs/workbench/contrib/tasks/electron-browser/taskService.ts +++ b/src/vs/workbench/contrib/tasks/electron-browser/taskService.ts @@ -46,6 +46,7 @@ import { IPanelService } from 'vs/workbench/services/panel/common/panelService'; import { IPathService } from 'vs/workbench/services/path/common/pathService'; import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; +import { IWorkspaceTrustService } from 'vs/platform/workspace/common/workspaceTrust'; interface WorkspaceFolderConfigurationResult { workspaceFolder: IWorkspaceFolder; @@ -85,6 +86,7 @@ export class TaskService extends AbstractTaskService { @ITextModelService textModelResolverService: ITextModelService, @IPreferencesService preferencesService: IPreferencesService, @IViewDescriptorService viewDescriptorService: IViewDescriptorService, + @IWorkspaceTrustService workspaceTrustService: IWorkspaceTrustService, @ILogService logService: ILogService) { super(configurationService, markerService, @@ -115,6 +117,7 @@ export class TaskService extends AbstractTaskService { textModelResolverService, preferencesService, viewDescriptorService, + workspaceTrustService, logService); this._register(lifecycleService.onBeforeShutdown(event => event.veto(this.beforeShutdown(), 'veto.tasks'))); } diff --git a/src/vs/workbench/contrib/workspace/browser/workspace.contribution.ts b/src/vs/workbench/contrib/workspace/browser/workspace.contribution.ts new file mode 100644 index 00000000000..20bba461f0a --- /dev/null +++ b/src/vs/workbench/contrib/workspace/browser/workspace.contribution.ts @@ -0,0 +1,335 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable, MutableDisposable } from 'vs/base/common/lifecycle'; +import { localize } from 'vs/nls'; +import { Action2, MenuId, MenuRegistry, registerAction2 } from 'vs/platform/actions/common/actions'; +import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { Severity } from 'vs/platform/notification/common/notification'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { IWorkspaceTrustService, WorkspaceTrustContext, WORKSPACE_TRUST_ENABLED, WORKSPACE_TRUST_URI, WorkspaceTrustState, WorkspaceTrustStateChangeEvent, workspaceTrustStateToString } from 'vs/platform/workspace/common/workspaceTrust'; +import { Extensions as WorkbenchExtensions, IWorkbenchContribution, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions'; +import { IActivityService, IconBadge } from 'vs/workbench/services/activity/common/activity'; +import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; +import { registerIcon } from 'vs/platform/theme/common/iconRegistry'; +import { Codicon } from 'vs/base/common/codicons'; +import { ThemeColor } from 'vs/workbench/api/common/extHostTypes'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { WorkspaceTrustFileSystemProvider } from 'vs/workbench/contrib/workspace/common/workspaceTrustFileSystemProvider'; +import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; +import { WorkbenchStateContext } from 'vs/workbench/browser/contextkeys'; +import { ICommandService } from 'vs/platform/commands/common/commands'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IHostService } from 'vs/workbench/services/host/browser/host'; +import { IStatusbarEntry, IStatusbarEntryAccessor, IStatusbarService, StatusbarAlignment } from 'vs/workbench/services/statusbar/common/statusbar'; + +const workspaceTrustIcon = registerIcon('workspace-trust-icon', Codicon.shield, localize('workspaceTrustIcon', "Icon for workspace trust badge.")); + +/* + * Trust Request UX Handler + */ +export class WorkspaceTrustRequestHandler extends Disposable implements IWorkbenchContribution { + private readonly requestModel = this.workspaceTrustService.requestModel; + private readonly badgeDisposable = this._register(new MutableDisposable()); + + constructor( + @IHostService private readonly hostService: IHostService, + @IDialogService private readonly dialogService: IDialogService, + @IActivityService private readonly activityService: IActivityService, + @ICommandService private readonly commandService: ICommandService, + @IConfigurationService private readonly configurationService: IConfigurationService, + @IWorkspaceTrustService private readonly workspaceTrustService: IWorkspaceTrustService + ) { + super(); + + this.registerListeners(); + } + + private toggleRequestBadge(visible: boolean): void { + this.badgeDisposable.clear(); + + if (visible) { + this.badgeDisposable.value = this.activityService.showGlobalActivity({ + badge: new IconBadge(workspaceTrustIcon, () => localize('requestTrustIconText', "Some features require workspace trust.")), + priority: 0 + }); + } + } + + private registerListeners(): void { + this._register(this.requestModel.onDidInitiateRequest(async () => { + if (this.requestModel.trustRequest) { + this.toggleRequestBadge(true); + + if (this.requestModel.trustRequest.immediate) { + 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"), + ], + { + 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."), + } + ); + + switch (result.choice) { + case 0: // Trust + this.requestModel.completeRequest(WorkspaceTrustState.Trusted); + break; + case 1: // Don't Trust + this.requestModel.completeRequest(WorkspaceTrustState.Untrusted); + break; + case 2: // Manage + this.requestModel.completeRequest(undefined); + await this.commandService.executeCommand('workbench.trust.manage'); + break; + default: // Cancel + this.requestModel.completeRequest(undefined); + break; + } + } + } + })); + + this._register(this.requestModel.onDidCompleteRequest(trustState => { + if (trustState !== undefined && trustState !== WorkspaceTrustState.Unknown) { + this.toggleRequestBadge(false); + } + })); + + this._register(this.configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration(WORKSPACE_TRUST_ENABLED)) { + const isEnabled = this.configurationService.getValue(WORKSPACE_TRUST_ENABLED); + if (!isEnabled || typeof isEnabled === 'boolean') { + this.dialogService.confirm({ + message: localize('trustConfigurationChangeMessage', "In order for this change to take effect, the window needs to be reloaded. Do you want to reload the window now?") + }).then(result => { + if (result.confirmed) { + this.hostService.reload(); + } + }); + } + } + })); + } +} + +Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(WorkspaceTrustRequestHandler, LifecyclePhase.Ready); + +/* + * Status Bar Entry + */ +class WorkspaceTrustStatusbarItem extends Disposable implements IWorkbenchContribution { + private static readonly ID = 'status.workspaceTrust'; + private readonly statusBarEntryAccessor: MutableDisposable; + + constructor( + @IStatusbarService private readonly statusbarService: IStatusbarService, + @IWorkspaceTrustService private readonly workspaceTrustService: IWorkspaceTrustService + ) { + super(); + + this.statusBarEntryAccessor = this._register(new MutableDisposable()); + + if (this.workspaceTrustService.isWorkspaceTrustEnabled()) { + const entry = this.getStatusbarEntry(this.workspaceTrustService.getWorkspaceTrustState()); + this.statusBarEntryAccessor.value = this.statusbarService.addEntry(entry, WorkspaceTrustStatusbarItem.ID, localize('status.WorkspaceTrust', "Workspace Trust"), StatusbarAlignment.LEFT, 0.99 * Number.MAX_VALUE /* Right of remote indicator */); + this._register(this.workspaceTrustService.onDidChangeTrustState(trustState => this.updateStatusbarEntry(trustState))); + } + } + + private getStatusbarEntry(state: WorkspaceTrustState): IStatusbarEntry { + const text = workspaceTrustStateToString(state); + const backgroundColor = state === WorkspaceTrustState.Trusted ? + 'transparent' : new ThemeColor('statusBarItem.prominentBackground'); + const color = state === WorkspaceTrustState.Trusted ? '#00dd3b' : '#ff5462'; + + return { + text: state === WorkspaceTrustState.Trusted ? `$(shield)` : `$(shield) ${text}`, + ariaLabel: localize('status.WorkspaceTrust', "Workspace Trust"), + tooltip: localize('status.WorkspaceTrust', "Workspace Trust"), + command: 'workbench.trust.manage', + backgroundColor, + color + }; + } + + private updateStatusbarEntry(trustState: WorkspaceTrustStateChangeEvent): void { + this.statusBarEntryAccessor.value?.update(this.getStatusbarEntry(trustState.currentTrustState)); + this.statusbarService.updateEntryVisibility(WorkspaceTrustStatusbarItem.ID, trustState.currentTrustState !== WorkspaceTrustState.Unknown); + } +} + +Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution( + WorkspaceTrustStatusbarItem, + LifecyclePhase.Starting +); + +/* + * Trusted Workspace JSON Editor + */ +Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution( + WorkspaceTrustFileSystemProvider, + LifecyclePhase.Ready +); + +/* + * Actions + */ + +// Grant Workspace Trust +registerAction2(class extends Action2 { + constructor() { + super({ + id: 'workbench.trust.grant', + title: { + original: 'Grant Workspace Trust', + value: localize('grantWorkspaceTrust', "Grant Workspace Trust") + }, + category: localize('workspacesCategory', "Workspaces"), + f1: true, + precondition: WorkspaceTrustContext.TrustState.isEqualTo(WorkspaceTrustState.Trusted).negate(), + menu: { + id: MenuId.GlobalActivity, + when: WorkspaceTrustContext.PendingRequest, + group: '7_trust', + order: 10 + }, + }); + } + + async run(accessor: ServicesAccessor) { + const dialogService = accessor.get(IDialogService); + const workspaceTrustService = accessor.get(IWorkspaceTrustService); + + const result = await dialogService.confirm({ + message: localize('grantWorkspaceTrust', "Grant Workspace Trust"), + detail: localize('confirmGrantWorkspaceTrust', "Granting trust to the workspace will enable features that may pose a security risk if the contents of the workspace cannot be trusted. Are you sure you want to trust this workspace?"), + primaryButton: localize('yes', 'Yes'), + secondaryButton: localize('no', 'No') + }); + + if (result.confirmed) { + workspaceTrustService.requestModel.completeRequest(WorkspaceTrustState.Trusted); + } + + return; + } +}); + +// Deny Workspace Trust +registerAction2(class extends Action2 { + constructor() { + super({ + id: 'workbench.trust.deny', + title: { + original: 'Deny Workspace Trust', + value: localize('denyWorkspaceTrust', "Deny Workspace Trust") + }, + category: localize('workspacesCategory', "Workspaces"), + f1: true, + precondition: WorkspaceTrustContext.TrustState.isEqualTo(WorkspaceTrustState.Untrusted).negate(), + menu: { + id: MenuId.GlobalActivity, + when: WorkspaceTrustContext.PendingRequest, + group: '7_trust', + order: 20 + }, + }); + } + + async run(accessor: ServicesAccessor) { + const dialogService = accessor.get(IDialogService); + const workspaceTrustService = accessor.get(IWorkspaceTrustService); + + const result = await dialogService.confirm({ + message: localize('denyWorkspaceTrust', "Deny Workspace Trust"), + detail: localize('confirmDenyWorkspaceTrust', "Denying trust to the workspace will disable features that may pose a security risk if the contents of the workspace cannot be trusted. Are you sure you want to deny trust to this workspace?"), + primaryButton: localize('yes', 'Yes'), + secondaryButton: localize('no', 'No') + }); + + if (result.confirmed) { + workspaceTrustService.requestModel.completeRequest(WorkspaceTrustState.Untrusted); + } + return; + } +}); + +// Reset Workspace Trust +registerAction2(class extends Action2 { + constructor() { + super({ + id: 'workbench.trust.reset', + title: { + original: 'Reset Workspace Trust', + value: localize('reset', "Reset Workspace Trust") + }, + category: localize('workspacesCategory', "Workspaces"), + f1: true, + precondition: ContextKeyExpr.and(WorkbenchStateContext.isEqualTo('empty').negate(), WorkspaceTrustContext.TrustState.isEqualTo(WorkspaceTrustState.Unknown).negate()) + }); + } + + async run(accessor: ServicesAccessor) { + const dialogService = accessor.get(IDialogService); + const workspaceTrustService = accessor.get(IWorkspaceTrustService); + + const result = await dialogService.confirm({ + message: localize('reset', "Reset Workspace Trust"), + detail: localize('confirmResetWorkspaceTrust', "Resetting workspace trust to the workspace will disable features that may pose a security risk if the contents of the workspace cannot be trusted. Are you sure you want to reset trust this workspace?"), + primaryButton: localize('yesGrant', 'Yes'), + secondaryButton: localize('noGrant', 'No') + }); + + if (result.confirmed) { + workspaceTrustService.resetWorkspaceTrust(); + } + + return; + } +}); + +// Manage Workspace Trust +registerAction2(class extends Action2 { + constructor() { + super({ + id: 'workbench.trust.manage', + title: { + original: 'Manage Trusted Workspaces', + value: localize('manageWorkspaceTrust', "Manage Trusted Workspaces") + }, + category: localize('workspacesCategory', "Workspaces"), + menu: { + id: MenuId.GlobalActivity, + group: '7_trust', + order: 40, + when: ContextKeyExpr.and(ContextKeyExpr.equals(`config.${WORKSPACE_TRUST_ENABLED}`, true), WorkspaceTrustContext.PendingRequest.negate()) + }, + }); + } + + run(accessor: ServicesAccessor) { + const editorService = accessor.get(IEditorService); + editorService.openEditor({ resource: WORKSPACE_TRUST_URI, mode: 'jsonc', options: { pinned: true } }); + return; + } +}); + +MenuRegistry.appendMenuItem(MenuId.GlobalActivity, { + command: { + id: 'workbench.trust.manage', + title: localize('manageWorkspaceTrustPending', "Manage Trusted Workspaces (1)"), + }, + group: '7_trust', + order: 40, + when: ContextKeyExpr.and(ContextKeyExpr.equals(`config.${WORKSPACE_TRUST_ENABLED}`, true), WorkspaceTrustContext.PendingRequest) +}); diff --git a/src/vs/workbench/contrib/workspace/common/workspaceTrustFileSystemProvider.ts b/src/vs/workbench/contrib/workspace/common/workspaceTrustFileSystemProvider.ts new file mode 100644 index 00000000000..8964edd181b --- /dev/null +++ b/src/vs/workbench/contrib/workspace/common/workspaceTrustFileSystemProvider.ts @@ -0,0 +1,86 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Event } from 'vs/base/common/event'; +import { IDisposable } from 'vs/base/common/lifecycle'; +import { URI } from 'vs/base/common/uri'; +import { FileDeleteOptions, FileOverwriteOptions, FileSystemProviderCapabilities, FileType, FileWriteOptions, IFileService, IStat, IWatchOptions, IFileSystemProviderWithFileReadWriteCapability } from 'vs/platform/files/common/files'; +import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; +import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; +import { VSBuffer } from 'vs/base/common/buffer'; +import { WORKSPACE_TRUST_STORAGE_KEY } from 'vs/platform/workspace/common/workspaceTrust'; + +const WORKSPACE_TRUST_SCHEMA = 'workspaceTrust'; + +const TRUSTED_WORKSPACES_STAT: IStat = { + type: FileType.File, + ctime: Date.now(), + mtime: Date.now(), + size: 0 +}; + +const PREPENDED_TEXT = `// The following file is a placeholder UX for managing trusted workspaces. It will be replaced by a rich editor and provide +// additonal information about what trust means. e.g. enabling trust will unblock automatic tasks on startup and list the tasks. It will enable certain extensions +// and list the extensions with associated functionality. +`; + +export class WorkspaceTrustFileSystemProvider implements IFileSystemProviderWithFileReadWriteCapability, IWorkbenchContribution { + readonly capabilities = FileSystemProviderCapabilities.FileReadWrite; + + readonly onDidChangeCapabilities = Event.None; + readonly onDidChangeFile = Event.None; + + constructor( + @IFileService private readonly fileService: IFileService, + @IStorageService private readonly storageService: IStorageService, + ) { + this.fileService.registerProvider(WORKSPACE_TRUST_SCHEMA, this); + } + + stat(resource: URI): Promise { + return Promise.resolve(TRUSTED_WORKSPACES_STAT); + } + + async readFile(resource: URI): Promise { + let workspacesTrustContent = this.storageService.get(WORKSPACE_TRUST_STORAGE_KEY, StorageScope.GLOBAL); + + let objectForm = {}; + try { + objectForm = JSON.parse(workspacesTrustContent || '{}'); + } catch { } + + const buffer = VSBuffer.fromString(PREPENDED_TEXT + JSON.stringify(objectForm, undefined, 2)).buffer; + return buffer; + } + + writeFile(resource: URI, content: Uint8Array, opts: FileWriteOptions): Promise { + try { + const workspacesTrustContent = VSBuffer.wrap(content).toString().replace(PREPENDED_TEXT, ''); + this.storageService.store(WORKSPACE_TRUST_STORAGE_KEY, workspacesTrustContent, StorageScope.GLOBAL, StorageTarget.MACHINE); + } catch (err) { } + + return Promise.resolve(); + } + + watch(resource: URI, opts: IWatchOptions): IDisposable { + return { + dispose() { + return; + } + }; + } + mkdir(resource: URI): Promise { + return Promise.resolve(undefined!); + } + readdir(resource: URI): Promise<[string, FileType][]> { + return Promise.resolve(undefined!); + } + delete(resource: URI, opts: FileDeleteOptions): Promise { + return Promise.resolve(undefined!); + } + rename(from: URI, to: URI, opts: FileOverwriteOptions): Promise { + return Promise.resolve(undefined!); + } +} diff --git a/src/vs/workbench/services/activity/common/activity.ts b/src/vs/workbench/services/activity/common/activity.ts index 3ca5c6a34db..b7665e85069 100644 --- a/src/vs/workbench/services/activity/common/activity.ts +++ b/src/vs/workbench/services/activity/common/activity.ts @@ -5,6 +5,7 @@ import { IDisposable } from 'vs/base/common/lifecycle'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { ThemeIcon } from 'vs/platform/theme/common/themeService'; export interface IActivity { readonly badge: IBadge; @@ -75,8 +76,7 @@ export class TextBadge extends BaseBadge { } export class IconBadge extends BaseBadge { - - constructor(descriptorFn: () => string) { + constructor(public readonly icon: ThemeIcon, descriptorFn: () => string) { super(descriptorFn); } } diff --git a/src/vs/workbench/services/extensionManagement/browser/extensionEnablementService.ts b/src/vs/workbench/services/extensionManagement/browser/extensionEnablementService.ts index 42c9bd4fae8..d5e0e57ec7b 100644 --- a/src/vs/workbench/services/extensionManagement/browser/extensionEnablementService.ts +++ b/src/vs/workbench/services/extensionManagement/browser/extensionEnablementService.ts @@ -6,7 +6,7 @@ import { localize } from 'vs/nls'; import { Event, Emitter } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; -import { IExtensionManagementService, DidUninstallExtensionEvent, IExtensionIdentifier, IGlobalExtensionEnablementService, ENABLED_EXTENSIONS_STORAGE_PATH, DISABLED_EXTENSIONS_STORAGE_PATH } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { IExtensionManagementService, DidUninstallExtensionEvent, IExtensionIdentifier, IGlobalExtensionEnablementService, ENABLED_EXTENSIONS_STORAGE_PATH, DISABLED_EXTENSIONS_STORAGE_PATH, DidInstallExtensionEvent } from 'vs/platform/extensionManagement/common/extensionManagement'; import { IWorkbenchExtensionEnablementService, EnablementState, IExtensionManagementServerService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; @@ -25,6 +25,7 @@ import { ILifecycleService, LifecyclePhase } from 'vs/workbench/services/lifecyc import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; import { IHostService } from 'vs/workbench/services/host/browser/host'; import { IExtensionBisectService } from 'vs/workbench/services/extensionManagement/browser/extensionBisect'; +import { WorkspaceTrustStateChangeEvent, IWorkspaceTrustService, WorkspaceTrustState } from 'vs/platform/workspace/common/workspaceTrust'; import { Promises } from 'vs/base/common/async'; const SOURCE = 'IWorkbenchExtensionEnablementService'; @@ -37,6 +38,7 @@ export class ExtensionEnablementService extends Disposable implements IWorkbench public readonly onEnablementChanged: Event = this._onEnablementChanged.event; private readonly storageManger: StorageManager; + private extensionsDisabledByTrustRequirement: IExtension[] = []; constructor( @IStorageService storageService: IStorageService, @@ -51,13 +53,24 @@ export class ExtensionEnablementService extends Disposable implements IWorkbench @IUserDataSyncAccountService private readonly userDataSyncAccountService: IUserDataSyncAccountService, @ILifecycleService private readonly lifecycleService: ILifecycleService, @INotificationService private readonly notificationService: INotificationService, - @IHostService hostService: IHostService, + @IHostService private readonly hostService: IHostService, @IExtensionBisectService private readonly extensionBisectService: IExtensionBisectService, + @IWorkspaceTrustService private readonly workspaceTrustService: IWorkspaceTrustService ) { super(); this.storageManger = this._register(new StorageManager(storageService)); this._register(this.globalExtensionEnablementService.onDidChangeEnablement(({ extensions, source }) => this.onDidChangeExtensions(extensions, source))); + this._register(extensionManagementService.onDidInstallExtension(this._onDidInstallExtension, this)); this._register(extensionManagementService.onDidUninstallExtension(this._onDidUninstallExtension, this)); + this._register(this.workspaceTrustService.onDidChangeTrustState(this._onDidChangeTrustState, this)); + + // Trusted extensions notification + // TODO: Confirm that this is the right lifecycle phase + this.lifecycleService.when(LifecyclePhase.Eventually).then(() => { + if (this.extensionsDisabledByTrustRequirement.length > 0) { + this.workspaceTrustService.requireWorkspaceTrust({ immediate: false }); + } + }); // delay notification for extensions disabled until workbench restored if (this.allUserExtensionsDisabled) { @@ -88,6 +101,9 @@ export class ExtensionEnablementService extends Disposable implements IWorkbench if (this._isDisabledByExtensionKind(extension)) { return EnablementState.DisabledByExtensionKind; } + if (this._isEnabled(extension) && this._isDisabledByTrustRequirement(extension)) { + return EnablementState.DisabledByTrustRequirement; + } return this._getEnablementState(extension.identifier); } @@ -147,7 +163,23 @@ export class ExtensionEnablementService extends Disposable implements IWorkbench } } - const result = await Promises.settled(extensions.map(e => this._setEnablement(e, newState))); + const result = await Promises.settled(extensions.map(e => { + if (this._isDisabledByTrustRequirement(e)) { + return this.workspaceTrustService.requireWorkspaceTrust({ + immediate: true, + message: 'Enabling this extension requires you to trust the contents of this workspace.' + }).then(trustState => { + if (trustState === WorkspaceTrustState.Trusted) { + return this._setEnablement(e, newState); + } else { + return Promise.resolve(false); + } + }); + } else { + return this._setEnablement(e, newState); + } + })); + const changedExtensions = extensions.filter((e, index) => result[index]); if (changedExtensions.length) { this._onEnablementChanged.fire(changedExtensions); @@ -186,6 +218,11 @@ export class ExtensionEnablementService extends Disposable implements IWorkbench return enablementState === EnablementState.EnabledWorkspace || enablementState === EnablementState.EnabledGlobally; } + private _isEnabled(extension: IExtension): boolean { + const enablementState = this._getEnablementState(extension.identifier); + return enablementState === EnablementState.EnabledWorkspace || enablementState === EnablementState.EnabledGlobally; + } + isDisabledGlobally(extension: IExtension): boolean { return this._isDisabledGlobally(extension.identifier); } @@ -234,6 +271,18 @@ export class ExtensionEnablementService extends Disposable implements IWorkbench return false; } + private _isDisabledByTrustRequirement(extension: IExtension): boolean { + const workspaceTrustState = this.workspaceTrustService.getWorkspaceTrustState(); + + if (extension.manifest.requiresWorkspaceTrust === 'onStart') { + if (workspaceTrustState !== WorkspaceTrustState.Trusted) { + this._addToWorkspaceDisabledExtensionsByTrustRequirement(extension); + } + return workspaceTrustState !== WorkspaceTrustState.Trusted; + } + return false; + } + private _getEnablementState(identifier: IExtensionIdentifier): EnablementState { if (this.hasWorkspace) { if (this._getWorkspaceEnabledExtensions().filter(e => areSameExtensions(e, identifier))[0]) { @@ -334,6 +383,26 @@ export class ExtensionEnablementService extends Disposable implements IWorkbench return false; } + private _addToWorkspaceDisabledExtensionsByTrustRequirement(extension: IExtension): void { + if (this.extensionsDisabledByTrustRequirement.every(e => !areSameExtensions(extension.identifier, e.identifier))) { + this.extensionsDisabledByTrustRequirement.push(extension); + } + } + + private _removeFromWorkspaceDisabledExtensionsByTrustRequirement(identifier: IExtensionIdentifier): void { + let index = -1; + for (let i = 0; i < this.extensionsDisabledByTrustRequirement.length; i++) { + const disabledExtension = this.extensionsDisabledByTrustRequirement[i]; + if (areSameExtensions(disabledExtension.identifier, identifier)) { + index = i; + break; + } + } + if (index !== -1) { + this.extensionsDisabledByTrustRequirement.splice(index, 1); + } + } + protected _getWorkspaceEnabledExtensions(): IExtensionIdentifier[] { return this._getExtensions(ENABLED_EXTENSIONS_STORAGE_PATH); } @@ -369,6 +438,30 @@ export class ExtensionEnablementService extends Disposable implements IWorkbench } } + private async _onDidChangeTrustState(state: WorkspaceTrustStateChangeEvent): Promise { + if (state.previousTrustState === WorkspaceTrustState.Trusted && ( + state.currentTrustState === WorkspaceTrustState.Untrusted || + state.currentTrustState === WorkspaceTrustState.Unknown)) { + // Reload window + this.hostService.reload(); + return; + } + if (state.currentTrustState === WorkspaceTrustState.Trusted) { + // Enable extensions + this._onEnablementChanged.fire(this.extensionsDisabledByTrustRequirement); + this.extensionsDisabledByTrustRequirement = []; + } + } + + private _onDidInstallExtension({ local, error }: DidInstallExtensionEvent): void { + if (local && !error && this._isDisabledByTrustRequirement(local)) { + this.workspaceTrustService.requireWorkspaceTrust({ + immediate: true, + message: 'Enabling this extension requires you to trust the contents of this workspace.' + }); + } + } + private _onDidUninstallExtension({ identifier, error }: DidUninstallExtensionEvent): void { if (!error) { this._reset(identifier); @@ -378,6 +471,7 @@ export class ExtensionEnablementService extends Disposable implements IWorkbench private _reset(extension: IExtensionIdentifier) { this._removeFromWorkspaceDisabledExtensions(extension); this._removeFromWorkspaceEnabledExtensions(extension); + this._removeFromWorkspaceDisabledExtensionsByTrustRequirement(extension); this.globalExtensionEnablementService.enableExtension(extension); } } diff --git a/src/vs/workbench/services/extensionManagement/common/extensionManagement.ts b/src/vs/workbench/services/extensionManagement/common/extensionManagement.ts index 31b04b85480..d8d20d10229 100644 --- a/src/vs/workbench/services/extensionManagement/common/extensionManagement.ts +++ b/src/vs/workbench/services/extensionManagement/common/extensionManagement.ts @@ -33,6 +33,7 @@ export interface IWorkbenchExtensioManagementService extends IExtensionManagemen } export const enum EnablementState { + DisabledByTrustRequirement, DisabledByExtensionKind, DisabledByEnvironment, DisabledGlobally, diff --git a/src/vs/workbench/services/extensionManagement/test/browser/extensionEnablementService.test.ts b/src/vs/workbench/services/extensionManagement/test/browser/extensionEnablementService.test.ts index 24826debc55..c0ea3e9bf35 100644 --- a/src/vs/workbench/services/extensionManagement/test/browser/extensionEnablementService.test.ts +++ b/src/vs/workbench/services/extensionManagement/test/browser/extensionEnablementService.test.ts @@ -30,6 +30,8 @@ import { TestNotificationService } from 'vs/platform/notification/test/common/te import { IHostService } from 'vs/workbench/services/host/browser/host'; import { mock } from 'vs/base/test/common/mock'; import { IExtensionBisectService } from 'vs/workbench/services/extensionManagement/browser/extensionBisect'; +import { IWorkspaceTrustService } from 'vs/platform/workspace/common/workspaceTrust'; +import { TestWorkspaceTrustService } from 'vs/platform/workspace/test/common/testWorkspaceTrust'; function createStorageService(instantiationService: TestInstantiationService): IStorageService { let service = instantiationService.get(IStorageService); @@ -64,7 +66,8 @@ export class TestExtensionEnablementService extends ExtensionEnablementService { instantiationService.get(ILifecycleService) || instantiationService.stub(ILifecycleService, new TestLifecycleService()), instantiationService.get(INotificationService) || instantiationService.stub(INotificationService, new TestNotificationService()), instantiationService.get(IHostService), - new class extends mock() { isDisabledByBisect() { return false; } } + new class extends mock() { isDisabledByBisect() { return false; } }, + instantiationService.get(IWorkspaceTrustService) || instantiationService.stub(IWorkspaceTrustService, new TestWorkspaceTrustService()) ); } @@ -88,12 +91,17 @@ suite('ExtensionEnablementService Test', () => { let instantiationService: TestInstantiationService; let testObject: IWorkbenchExtensionEnablementService; + const didInstallEvent = new Emitter(); const didUninstallEvent = new Emitter(); setup(() => { instantiationService = new TestInstantiationService(); instantiationService.stub(IConfigurationService, new TestConfigurationService()); - instantiationService.stub(IExtensionManagementService, { onDidUninstallExtension: didUninstallEvent.event, getInstalled: () => Promise.resolve([] as ILocalExtension[]) } as IExtensionManagementService); + instantiationService.stub(IExtensionManagementService, >{ + onDidInstallExtension: didInstallEvent.event, + onDidUninstallExtension: didUninstallEvent.event, + getInstalled: () => Promise.resolve([] as ILocalExtension[]) + }); instantiationService.stub(IExtensionManagementServerService, { localExtensionManagementServer: { extensionManagementService: instantiationService.get(IExtensionManagementService) @@ -455,7 +463,11 @@ suite('ExtensionEnablementService Test', () => { test('test extension is disabled when disabled in environment', async () => { const extension = aLocalExtension('pub.a'); instantiationService.stub(IWorkbenchEnvironmentService, { disableExtensions: ['pub.a'] } as IWorkbenchEnvironmentService); - instantiationService.stub(IExtensionManagementService, { onDidUninstallExtension: didUninstallEvent.event, getInstalled: () => Promise.resolve([extension, aLocalExtension('pub.b')]) } as IExtensionManagementService); + instantiationService.stub(IExtensionManagementService, >{ + onDidInstallExtension: didInstallEvent.event, + onDidUninstallExtension: didUninstallEvent.event, + getInstalled: () => Promise.resolve([extension, aLocalExtension('pub.b')]) + }); testObject = new TestExtensionEnablementService(instantiationService); assert.ok(!testObject.isEnabled(extension)); assert.deepEqual(testObject.getEnablementState(extension), EnablementState.DisabledByEnvironment); diff --git a/src/vs/workbench/services/extensions/common/extensions.ts b/src/vs/workbench/services/extensions/common/extensions.ts index eb04f95a35f..33883bedb25 100644 --- a/src/vs/workbench/services/extensions/common/extensions.ts +++ b/src/vs/workbench/services/extensions/common/extensions.ts @@ -270,6 +270,16 @@ export function throwProposedApiError(extension: IExtensionDescription): never { throw new Error(`[${extension.identifier.value}]: Proposed API is only available when running out of dev or with the following command line switch: --enable-proposed-api ${extension.identifier.value}`); } +export function checkRequiresWorkspaceTrust(extension: IExtensionDescription): void { + if (!extension.requiresWorkspaceTrust) { + 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.`); +} + export function toExtension(extensionDescription: IExtensionDescription): IExtension { return { type: extensionDescription.isBuiltin ? ExtensionType.System : ExtensionType.User, diff --git a/src/vs/workbench/test/browser/api/extHostConfiguration.test.ts b/src/vs/workbench/test/browser/api/extHostConfiguration.test.ts index 430eaf3e935..6c27e95c9e0 100644 --- a/src/vs/workbench/test/browser/api/extHostConfiguration.test.ts +++ b/src/vs/workbench/test/browser/api/extHostConfiguration.test.ts @@ -18,6 +18,7 @@ import { IExtHostInitDataService } from 'vs/workbench/api/common/extHostInitData import { IExtHostFileSystemInfo } from 'vs/workbench/api/common/extHostFileSystemInfo'; import { FileSystemProviderCapabilities } from 'vs/platform/files/common/files'; import { isLinux } from 'vs/base/common/platform'; +import { WorkspaceTrustState } from 'vs/platform/workspace/common/workspaceTrust'; suite('ExtHostConfiguration', function () { @@ -318,7 +319,7 @@ suite('ExtHostConfiguration', function () { 'id': 'foo', 'folders': [aWorkspaceFolder(URI.file('foo'), 0)], 'name': 'foo' - }); + }, WorkspaceTrustState.Trusted); const testObject = new ExtHostConfigProvider( new class extends mock() { }, extHostWorkspace, @@ -394,7 +395,7 @@ suite('ExtHostConfiguration', function () { 'id': 'foo', 'folders': [aWorkspaceFolder(firstRoot, 0), aWorkspaceFolder(secondRoot, 1)], 'name': 'foo' - }); + }, WorkspaceTrustState.Trusted); const testObject = new ExtHostConfigProvider( new class extends mock() { }, extHostWorkspace, @@ -497,7 +498,7 @@ suite('ExtHostConfiguration', function () { 'id': 'foo', 'folders': [aWorkspaceFolder(firstRoot, 0), aWorkspaceFolder(secondRoot, 1)], 'name': 'foo' - }); + }, WorkspaceTrustState.Trusted); const testObject = new ExtHostConfigProvider( new class extends mock() { }, extHostWorkspace, @@ -675,7 +676,7 @@ suite('ExtHostConfiguration', function () { 'id': 'foo', 'folders': [workspaceFolder], 'name': 'foo' - }); + }, WorkspaceTrustState.Trusted); const testObject = new ExtHostConfigProvider( new class extends mock() { }, extHostWorkspace, diff --git a/src/vs/workbench/test/browser/api/extHostWorkspace.test.ts b/src/vs/workbench/test/browser/api/extHostWorkspace.test.ts index 894d43726d4..709acc159ba 100644 --- a/src/vs/workbench/test/browser/api/extHostWorkspace.test.ts +++ b/src/vs/workbench/test/browser/api/extHostWorkspace.test.ts @@ -23,6 +23,7 @@ import { IPatternInfo } from 'vs/workbench/services/search/common/search'; import { isLinux, isWindows } from 'vs/base/common/platform'; import { IExtHostFileSystemInfo } from 'vs/workbench/api/common/extHostFileSystemInfo'; import { FileSystemProviderCapabilities } from 'vs/platform/files/common/files'; +import { WorkspaceTrustState } from 'vs/platform/workspace/common/workspaceTrust'; function createExtHostWorkspace(mainContext: IMainContext, data: IWorkspaceData, logService: ILogService): ExtHostWorkspace { const result = new ExtHostWorkspace( @@ -31,7 +32,7 @@ function createExtHostWorkspace(mainContext: IMainContext, data: IWorkspaceData, new class extends mock() { getCapabilities() { return isLinux ? FileSystemProviderCapabilities.PathCaseSensitive : undefined; } }, logService, ); - result.$initializeWorkspace(data); + result.$initializeWorkspace(data, WorkspaceTrustState.Trusted); return result; } diff --git a/src/vs/workbench/test/browser/workbenchTestServices.ts b/src/vs/workbench/test/browser/workbenchTestServices.ts index 4af20b81780..ab712e8834c 100644 --- a/src/vs/workbench/test/browser/workbenchTestServices.ts +++ b/src/vs/workbench/test/browser/workbenchTestServices.ts @@ -124,6 +124,8 @@ import { ResourceEditorInput } from 'vs/workbench/common/editor/resourceEditorIn import { UntitledTextEditorInput } from 'vs/workbench/services/untitled/common/untitledTextEditorInput'; import { SideBySideEditor } from 'vs/workbench/browser/parts/editor/sideBySideEditor'; import { IEnterWorkspaceResult, IRecent, IRecentlyOpened, IWorkspaceFolderCreationData, IWorkspaceIdentifier, IWorkspacesService } from 'vs/platform/workspaces/common/workspaces'; +import { IWorkspaceTrustService } from 'vs/platform/workspace/common/workspaceTrust'; +import { TestWorkspaceTrustService } from 'vs/platform/workspace/test/common/testWorkspaceTrust'; export function createFileEditorInput(instantiationService: IInstantiationService, resource: URI): FileEditorInput { return instantiationService.createInstance(FileEditorInput, resource, undefined, undefined, undefined, undefined, undefined); @@ -223,6 +225,7 @@ export function workbenchInstantiationService( instantiationService.stub(IListService, new TestListService()); instantiationService.stub(IQuickInputService, disposables.add(new QuickInputService(configService, instantiationService, keybindingService, contextKeyService, themeService, accessibilityService, layoutService))); instantiationService.stub(IWorkspacesService, new TestWorkspacesService()); + instantiationService.stub(IWorkspaceTrustService, new TestWorkspaceTrustService()); return instantiationService; } diff --git a/src/vs/workbench/workbench.common.main.ts b/src/vs/workbench/workbench.common.main.ts index 5b2c3ae159d..aa57432c433 100644 --- a/src/vs/workbench/workbench.common.main.ts +++ b/src/vs/workbench/workbench.common.main.ts @@ -53,6 +53,7 @@ import 'vs/workbench/browser/parts/views/viewsService'; //#region --- workbench services +import 'vs/platform/workspace/common/workspaceTrust'; import 'vs/platform/undoRedo/common/undoRedoService'; import 'vs/workbench/services/extensions/browser/extensionUrlHandler'; import 'vs/workbench/services/keybinding/common/keybindingEditing'; @@ -309,6 +310,9 @@ import 'vs/workbench/contrib/welcome/common/viewsWelcome.contribution'; // Timeline import 'vs/workbench/contrib/timeline/browser/timeline.contribution'; +// Workspace +import 'vs/workbench/contrib/workspace/browser/workspace.contribution'; + // Workspaces import 'vs/workbench/contrib/workspaces/browser/workspaces.contribution';