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 <benjamin.pasero@gmail.com>

* 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 <benjamin.pasero@gmail.com>

Co-authored-by: Benjamin Pasero <benjpas@microsoft.com>
Co-authored-by: Benjamin Pasero <benjamin.pasero@gmail.com>

* 💄 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

* 💄

* 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 <lszomoru@microsoft.com>
Co-authored-by: isidor <inikolic@microsoft.com>
Co-authored-by: Alex Ross <alros@microsoft.com>
Co-authored-by: TacticalDan <gorksorf@gmail.com>
Co-authored-by: Alexandru Dima <alexdima@microsoft.com>
Co-authored-by: Johannes Rieken <johannes.rieken@gmail.com>
Co-authored-by: Cameron <cameron532@gmail.com>
Co-authored-by: Benjamin Pasero <benjpas@microsoft.com>
Co-authored-by: Benjamin Pasero <benjamin.pasero@gmail.com>
This commit is contained in:
SteVen Batten 2021-02-06 00:38:32 -08:00 committed by GitHub
parent 01a3787cca
commit afd102cbd2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
33 changed files with 1220 additions and 54 deletions

View file

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

View file

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

View file

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

View file

@ -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<boolean>('workspaceTrustPendingRequest', false),
TrustState: new RawContextKey<WorkspaceTrustState>('workspaceTrustState', WorkspaceTrustState.Unknown)
};
export interface IWorkspaceTrustModel {
readonly onDidChangeTrustState: Event<void>;
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<void>;
readonly onDidCompleteRequest: Event<WorkspaceTrustState | undefined>;
initiateRequest(request?: IWorkspaceTrustRequest): void;
completeRequest(trustState?: WorkspaceTrustState): void;
}
export interface WorkspaceTrustStateChangeEvent {
previousTrustState: WorkspaceTrustState;
currentTrustState: WorkspaceTrustState;
}
export type WorkspaceTrustChangeEvent = Event<WorkspaceTrustStateChangeEvent>;
export const IWorkspaceTrustService = createDecorator<IWorkspaceTrustService>('workspaceTrustService');
export interface IWorkspaceTrustService {
readonly _serviceBrand: undefined;
readonly requestModel: IWorkspaceTrustRequestModel;
onDidChangeTrustState: WorkspaceTrustChangeEvent;
getWorkspaceTrustState(): WorkspaceTrustState;
isWorkspaceTrustEnabled(): boolean;
requireWorkspaceTrust(request: IWorkspaceTrustRequest): Promise<WorkspaceTrustState>;
resetWorkspaceTrust(): Promise<WorkspaceTrustState>;
}
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<void>());
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<void>());
onDidInitiateRequest: Event<void> = this._onDidInitiateRequest.event;
_onDidCompleteRequest = this._register(new Emitter<WorkspaceTrustState | undefined>());
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<WorkspaceTrustStateChangeEvent>());
readonly onDidChangeTrustState = this._onDidChangeTrustState.event;
private _currentTrustState: WorkspaceTrustState = WorkspaceTrustState.Unknown;
private _inFlightResolver?: (trustState: WorkspaceTrustState) => void;
private _trustRequestPromise?: Promise<WorkspaceTrustState>;
private _workspace: IWorkspace;
private readonly _ctxWorkspaceTrustState: IContextKey<WorkspaceTrustState>;
private readonly _ctxWorkspaceTrustPendingRequest: IContextKey<boolean>;
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<boolean>(WORKSPACE_TRUST_ENABLED) ?? false;
}
async requireWorkspaceTrust(request?: IWorkspaceTrustRequest): Promise<WorkspaceTrustState> {
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<WorkspaceTrustState> {
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);

View file

@ -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<WorkspaceTrustState> {
return Promise.resolve(WorkspaceTrustState.Trusted);
}
resetWorkspaceTrust(): Promise<WorkspaceTrustState> {
return Promise.resolve(WorkspaceTrustState.Unknown);
}
}

View file

@ -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<WorkspaceTrustState>;
/**
* Event that fires when the trust state of the current workspace changes
*/
export const onDidChangeWorkspaceTrustState: Event<WorkspaceTrustStateChangeEvent>;
}
//#endregion
}

View file

@ -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<string | undefined> {
return this._requestService.resolveProxy(url);
}
// --- trust ---
$requireWorkspaceTrust(message?: string): Promise<WorkspaceTrustState> {
return this._workspaceTrustService.requireWorkspaceTrust({ immediate: true, message });
}
private getWorkspaceTrustState(): WorkspaceTrustState {
return this._workspaceTrustService.getWorkspaceTrustState();
}
private _onDidChangeWorkspaceTrustState(state: WorkspaceTrustStateChangeEvent): void {
this._proxy.$onDidChangeWorkspaceTrustState(state);
}
}

View file

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

View file

@ -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<boolean>;
$updateWorkspaceFolders(extensionName: string, index: number, deleteCount: number, workspaceFoldersToAdd: { uri: UriComponents, name?: string; }[]): Promise<void>;
$resolveProxy(url: string): Promise<string | undefined>;
$requireWorkspaceTrust(message?: string): Promise<WorkspaceTrustState>
}
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 {

View file

@ -3001,3 +3001,9 @@ export enum ExternalUriOpenerPriority {
Default = 2,
Preferred = 3,
}
export enum WorkspaceTrustState {
Untrusted = 0,
Trusted = 1,
Unknown = 2
}

View file

@ -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<vscode.WorkspaceFoldersChangeEvent>();
readonly onDidChangeWorkspace: Event<vscode.WorkspaceFoldersChangeEvent> = this._onDidChangeWorkspace.event;
private readonly _onDidChangeWorkspaceTrustState = new Emitter<vscode.WorkspaceTrustStateChangeEvent>();
readonly onDidChangeWorkspaceTrustState: Event<vscode.WorkspaceTrustStateChangeEvent> = 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<string | undefined> {
return this._proxy.$resolveProxy(url);
}
// --- trust ---
get trustState(): WorkspaceTrustState {
return this._workspaceTrustState;
}
requireWorkspaceTrust(message?: string): Promise<WorkspaceTrustState> {
return this._proxy.$requireWorkspaceTrust(message);
}
$onDidChangeWorkspaceTrustState(state: WorkspaceTrustStateChangeEvent): void {
this._workspaceTrustState = state.currentTrustState;
this._onDidChangeWorkspaceTrustState.fire(Object.freeze(state));
}
}
export const IExtHostWorkspace = createDecorator<IExtHostWorkspace>('IExtHostWorkspace');

View file

@ -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<IWorkbenchActionRegistry>(Extensions.WorkbenchActions);

View file

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

View file

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

View file

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

View file

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

View file

@ -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.'));

View file

@ -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<T> {
(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<IExtension> {
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<IExtension> {
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<T>(manifest: IExtensionManifest, installTask: () => Promise<T>): Promise<T> {
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<any> {

View file

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

View file

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

View file

@ -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<string, WorkspaceFolderTaskResult>) {
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;

View file

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

View file

@ -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<boolean>(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<IWorkbenchContributionsRegistry>(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<IStatusbarEntryAccessor>;
constructor(
@IStatusbarService private readonly statusbarService: IStatusbarService,
@IWorkspaceTrustService private readonly workspaceTrustService: IWorkspaceTrustService
) {
super();
this.statusBarEntryAccessor = this._register(new MutableDisposable<IStatusbarEntryAccessor>());
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<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench).registerWorkbenchContribution(
WorkspaceTrustStatusbarItem,
LifecyclePhase.Starting
);
/*
* Trusted Workspace JSON Editor
*/
Registry.as<IWorkbenchContributionsRegistry>(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)
});

View file

@ -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<IStat> {
return Promise.resolve(TRUSTED_WORKSPACES_STAT);
}
async readFile(resource: URI): Promise<Uint8Array> {
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<void> {
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<void> {
return Promise.resolve(undefined!);
}
readdir(resource: URI): Promise<[string, FileType][]> {
return Promise.resolve(undefined!);
}
delete(resource: URI, opts: FileDeleteOptions): Promise<void> {
return Promise.resolve(undefined!);
}
rename(from: URI, to: URI, opts: FileOverwriteOptions): Promise<void> {
return Promise.resolve(undefined!);
}
}

View file

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

View file

@ -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<readonly IExtension[]> = 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<void> {
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);
}
}

View file

@ -33,6 +33,7 @@ export interface IWorkbenchExtensioManagementService extends IExtensionManagemen
}
export const enum EnablementState {
DisabledByTrustRequirement,
DisabledByExtensionKind,
DisabledByEnvironment,
DisabledGlobally,

View file

@ -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<IExtensionBisectService>() { isDisabledByBisect() { return false; } }
new class extends mock<IExtensionBisectService>() { 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<DidInstallExtensionEvent>();
const didUninstallEvent = new Emitter<DidUninstallExtensionEvent>();
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, <Partial<IExtensionManagementService>>{
onDidInstallExtension: didInstallEvent.event,
onDidUninstallExtension: didUninstallEvent.event,
getInstalled: () => Promise.resolve([] as ILocalExtension[])
});
instantiationService.stub(IExtensionManagementServerService, <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, <Partial<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);

View file

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

View file

@ -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<MainThreadConfigurationShape>() { },
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<MainThreadConfigurationShape>() { },
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<MainThreadConfigurationShape>() { },
extHostWorkspace,
@ -675,7 +676,7 @@ suite('ExtHostConfiguration', function () {
'id': 'foo',
'folders': [workspaceFolder],
'name': 'foo'
});
}, WorkspaceTrustState.Trusted);
const testObject = new ExtHostConfigProvider(
new class extends mock<MainThreadConfigurationShape>() { },
extHostWorkspace,

View file

@ -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<IExtHostFileSystemInfo>() { getCapabilities() { return isLinux ? FileSystemProviderCapabilities.PathCaseSensitive : undefined; } },
logService,
);
result.$initializeWorkspace(data);
result.$initializeWorkspace(data, WorkspaceTrustState.Trusted);
return result;
}

View file

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

View file

@ -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';