Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement light-weight Workspace Trust API for plugins #10473

Merged
merged 1 commit into from
Feb 4, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/plugin-ext/src/common/plugin-api-rpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -621,13 +621,15 @@ export interface WorkspaceMain {
$onTextDocumentContentChange(uri: string, content: string): void;
$updateWorkspaceFolders(start: number, deleteCount?: number, ...rootsToAdd: string[]): Promise<void>;
$getWorkspace(): Promise<files.FileStat | undefined>;
$requestWorkspaceTrust(options?: theia.WorkspaceTrustRequestOptions): Promise<boolean | undefined>;
}

export interface WorkspaceExt {
$onWorkspaceFoldersChanged(event: WorkspaceRootsChangeEvent): void;
$onWorkspaceLocationChanged(event: files.FileStat | undefined): void;
$provideTextDocumentContent(uri: string): Promise<string | undefined>;
$onTextSearchResult(searchRequestId: number, done: boolean, result?: SearchInWorkspaceResult): void;
$onWorkspaceTrustChanged(trust: boolean | undefined): void;
}

export interface TimelineExt {
Expand Down
10 changes: 9 additions & 1 deletion packages/plugin-ext/src/main/browser/workspace-main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import { URI as Uri } from '@theia/core/shared/vscode-uri';
import { UriComponents } from '../../common/uri-components';
import { FileSearchService } from '@theia/file-search/lib/common/file-search-service';
import URI from '@theia/core/lib/common/uri';
import { WorkspaceService } from '@theia/workspace/lib/browser';
import { WorkspaceService, WorkspaceTrustService } from '@theia/workspace/lib/browser';
import { Resource } from '@theia/core/lib/common/resource';
import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable';
import { Emitter, Event, ResourceResolver, CancellationToken } from '@theia/core';
Expand Down Expand Up @@ -52,6 +52,8 @@ export class WorkspaceMainImpl implements WorkspaceMain, Disposable {

private workspaceService: WorkspaceService;

private workspaceTrustService: WorkspaceTrustService;

private fsPreferences: FileSystemPreferences;

protected readonly toDispose = new DisposableCollection();
Expand All @@ -67,6 +69,7 @@ export class WorkspaceMainImpl implements WorkspaceMain, Disposable {
this.resourceResolver = container.get(TextContentResourceResolver);
this.pluginServer = container.get(PluginServer);
this.workspaceService = container.get(WorkspaceService);
this.workspaceTrustService = container.get(WorkspaceTrustService);
this.fsPreferences = container.get(FileSystemPreferences);

this.processWorkspaceFoldersChanged(this.workspaceService.tryGetRoots().map(root => root.resource.toString()));
Expand All @@ -76,6 +79,8 @@ export class WorkspaceMainImpl implements WorkspaceMain, Disposable {
this.toDispose.push(this.workspaceService.onWorkspaceLocationChanged(stat => {
this.proxy.$onWorkspaceLocationChanged(stat);
}));

this.workspaceTrustService.getWorkspaceTrust().then(trust => this.proxy.$onWorkspaceTrustChanged(trust));
}

dispose(): void {
Expand Down Expand Up @@ -269,6 +274,9 @@ export class WorkspaceMainImpl implements WorkspaceMain, Disposable {
await this.workspaceService.spliceRoots(start, deleteCount, ...rootsToAdd.map(root => new URI(root)));
}

async $requestWorkspaceTrust(_options?: theia.WorkspaceTrustRequestOptions): Promise<boolean | undefined> {
return this.workspaceTrustService.requestWorkspaceTrust();
}
}

/**
Expand Down
9 changes: 9 additions & 0 deletions packages/plugin-ext/src/plugin/plugin-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -571,6 +571,15 @@ export function createAPIFactory(
},
registerTimelineProvider(scheme: string | string[], provider: theia.TimelineProvider): theia.Disposable {
return timelineExt.registerTimelineProvider(plugin, scheme, provider);
},
get isTrusted(): boolean {
return workspaceExt.trusted;
},
async requestWorkspaceTrust(options?: theia.WorkspaceTrustRequestOptions): Promise<boolean | undefined> {
return workspaceExt.requestWorkspaceTrust(options);
},
get onDidGrantWorkspaceTrust(): theia.Event<void> {
return workspaceExt.onDidGrantWorkspaceTrust;
}
};

Expand Down
23 changes: 23 additions & 0 deletions packages/plugin-ext/src/plugin/workspace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@ export class WorkspaceExtImpl implements WorkspaceExt {
private searchInWorkspaceEmitter: Emitter<{ result?: theia.TextSearchResult, searchId: number }> = new Emitter<{ result?: theia.TextSearchResult, searchId: number }>();
protected workspaceSearchSequence: number = 0;

private _trusted?: boolean = undefined;
private didGrantWorkspaceTrustEmitter = new Emitter<void>();
public readonly onDidGrantWorkspaceTrust: Event<void> = this.didGrantWorkspaceTrustEmitter.event;

constructor(rpc: RPCProtocol,
private editorsAndDocuments: EditorsAndDocumentsExtImpl,
private messageService: MessageRegistryExt) {
Expand Down Expand Up @@ -418,4 +422,23 @@ export class WorkspaceExtImpl implements WorkspaceExt {
this.workspaceFileUri = URI.parse(workspace.resource.toString());
}
}

get trusted(): boolean {
if (this._trusted === undefined) {
this.requestWorkspaceTrust();
}
return !!this._trusted;
}

requestWorkspaceTrust(options?: theia.WorkspaceTrustRequestOptions): Promise<boolean | undefined> {
return this.proxy.$requestWorkspaceTrust(options);
}

$onWorkspaceTrustChanged(trust: boolean | undefined): void {
if (!this._trusted && trust) {
this._trusted = trust;
this.didGrantWorkspaceTrustEmitter.fire();
}
}

}
22 changes: 22 additions & 0 deletions packages/plugin/src/theia.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6049,6 +6049,28 @@ export module '@theia/plugin' {
* @return A [disposable](#Disposable) that unregisters this provider when being disposed.
*/
export function registerTaskProvider(type: string, provider: TaskProvider): Disposable;

/**
* When true, the user has explicitly trusted the contents of the workspace.
*/
export const isTrusted: boolean;

export function requestWorkspaceTrust(options?: WorkspaceTrustRequestOptions): Promise<boolean | undefined>;

/**
* Event that fires when the current workspace has been trusted.
*/
export const onDidGrantWorkspaceTrust: Event<void>;
}

export interface WorkspaceTrustRequestButton {
readonly label: string;
readonly type: 'ContinueWithTrust' | 'ContinueWithoutTrust' | 'Manage' | 'Cancel'
}

export interface WorkspaceTrustRequestOptions {
readonly buttons?: WorkspaceTrustRequestButton[];
readonly message?: string;
}

export namespace env {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export class PreferenceTreeGenerator {
['window', 'Window'],
['features', 'Features'],
['application', 'Application'],
['security', 'Security'],
['extensions', 'Extensions']
]);
protected readonly sectionAssignments = new Map([
Expand Down
1 change: 1 addition & 0 deletions packages/workspace/src/browser/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,4 @@ export * from './workspace-service';
export * from './workspace-frontend-contribution';
export * from './workspace-frontend-module';
export * from './workspace-preferences';
export * from './workspace-trust-service';
5 changes: 5 additions & 0 deletions packages/workspace/src/browser/workspace-frontend-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,12 @@ import { JsonSchemaContribution } from '@theia/core/lib/browser/json-schema-stor
import { WorkspaceSchemaUpdater } from './workspace-schema-updater';
import { WorkspaceBreadcrumbsContribution } from './workspace-breadcrumbs-contribution';
import { FilepathBreadcrumbsContribution } from '@theia/filesystem/lib/browser/breadcrumbs/filepath-breadcrumbs-contribution';
import { WorkspaceTrustService } from './workspace-trust-service';
import { bindWorkspaceTrustPreferences } from './workspace-trust-preferences';

export default new ContainerModule((bind: interfaces.Bind, unbind: interfaces.Unbind, isBound: interfaces.IsBound, rebind: interfaces.Rebind) => {
bindWorkspacePreferences(bind);
bindWorkspaceTrustPreferences(bind);

bind(WorkspaceService).toSelf().inSingletonScope();
bind(FrontendApplicationContribution).toService(WorkspaceService);
Expand Down Expand Up @@ -100,4 +103,6 @@ export default new ContainerModule((bind: interfaces.Bind, unbind: interfaces.Un
bind(WorkspaceSchemaUpdater).toSelf().inSingletonScope();
bind(JsonSchemaContribution).toService(WorkspaceSchemaUpdater);
rebind(FilepathBreadcrumbsContribution).to(WorkspaceBreadcrumbsContribution).inSingletonScope();

bind(WorkspaceTrustService).toSelf().inSingletonScope();
});
76 changes: 76 additions & 0 deletions packages/workspace/src/browser/workspace-trust-preferences.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/********************************************************************************
* Copyright (C) 2021 EclipseSource and others.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the Eclipse
* Public License v. 2.0 are satisfied: GNU General Public License, version 2
* with the GNU Classpath Exception which is available at
* https://www.gnu.org/software/classpath/license.html.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/

import {
createPreferenceProxy, PreferenceContribution, PreferenceProxy, PreferenceSchema, PreferenceService
} from '@theia/core/lib/browser/preferences';
import { nls } from '@theia/core/lib/common/nls';
import { interfaces } from '@theia/core/shared/inversify';

export const WORKSPACE_TRUST_ENABLED = 'security.workspace.trust.enabled';
export const WORKSPACE_TRUST_STARTUP_PROMPT = 'security.workspace.trust.startupPrompt';
export const WORKSPACE_TRUST_EMPTY_WINDOW = 'security.workspace.trust.emptyWindow';

export enum WorkspaceTrustPrompt {
ALWAYS = 'always',
ONCE = 'once',
NEVER = 'never'
}

export const workspaceTrustPreferenceSchema: PreferenceSchema = {
type: 'object',
properties: {
[WORKSPACE_TRUST_ENABLED]: {
description: nls.localize('theia/workspace/trustEnabled', 'Controls whether or not workspace trust is enabled. If disabled, all workspaces are trusted.'),
type: 'boolean',
defaultValue: true
},
[WORKSPACE_TRUST_STARTUP_PROMPT]: {
description: nls.localizeByDefault('Controls when the startup prompt to trust a workspace is shown.'),
enum: Object.values(WorkspaceTrustPrompt),
defaultValue: WorkspaceTrustPrompt.ALWAYS
},
[WORKSPACE_TRUST_EMPTY_WINDOW]: {
description: nls.localize('theia/workspace/trustEmptyWindow', 'Controls whether or not the empty workspace is trusted by default.'),
type: 'boolean',
defaultValue: true
}
}
};

export interface WorkspaceTrustConfiguration {
[WORKSPACE_TRUST_ENABLED]: boolean,
[WORKSPACE_TRUST_STARTUP_PROMPT]: WorkspaceTrustPrompt;
[WORKSPACE_TRUST_EMPTY_WINDOW]: boolean;
}

export const WorkspaceTrustPreferenceContribution = Symbol('WorkspaceTrustPreferenceContribution');
export const WorkspaceTrustPreferences = Symbol('WorkspaceTrustPreferences');
export type WorkspaceTrustPreferences = PreferenceProxy<WorkspaceTrustConfiguration>;

export function createWorkspaceTrustPreferences(preferences: PreferenceService, schema: PreferenceSchema = workspaceTrustPreferenceSchema): WorkspaceTrustPreferences {
return createPreferenceProxy(preferences, schema);
}

export function bindWorkspaceTrustPreferences(bind: interfaces.Bind): void {
bind(WorkspaceTrustPreferences).toDynamicValue(ctx => {
const preferences = ctx.container.get<PreferenceService>(PreferenceService);
const contribution = ctx.container.get<PreferenceContribution>(WorkspaceTrustPreferenceContribution);
return createWorkspaceTrustPreferences(preferences, contribution.schema);
}).inSingletonScope();
bind(WorkspaceTrustPreferenceContribution).toConstantValue({ schema: workspaceTrustPreferenceSchema });
bind(PreferenceContribution).toService(WorkspaceTrustPreferenceContribution);
}
143 changes: 143 additions & 0 deletions packages/workspace/src/browser/workspace-trust-service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
/********************************************************************************
* Copyright (C) 2021 EclipseSource and others.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the Eclipse
* Public License v. 2.0 are satisfied: GNU General Public License, version 2
* with the GNU Classpath Exception which is available at
* https://www.gnu.org/software/classpath/license.html.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/

import { ConfirmDialog, Dialog, PreferenceChange, StorageService } from '@theia/core/lib/browser';
import { PreferenceService } from '@theia/core/lib/browser/preferences/preference-service';
import { MessageService } from '@theia/core/lib/common/message-service';
import { nls } from '@theia/core/lib/common/nls';
import { Deferred } from '@theia/core/lib/common/promise-util';
import { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
import { WindowService } from '@theia/core/lib/browser/window/window-service';
import { WorkspaceService } from '.';
import {
WorkspaceTrustPreferences, WORKSPACE_TRUST_EMPTY_WINDOW, WORKSPACE_TRUST_ENABLED, WORKSPACE_TRUST_STARTUP_PROMPT, WorkspaceTrustPrompt
} from './workspace-trust-preferences';
import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/frontend-application-config-provider';

const STORAGE_TRUSTED = 'trusted';

@injectable()
export class WorkspaceTrustService {
@inject(WorkspaceService)
protected readonly workspaceService: WorkspaceService;

@inject(PreferenceService)
protected readonly preferences: PreferenceService;

@inject(StorageService)
protected readonly storage: StorageService;

@inject(MessageService)
protected readonly messageService: MessageService;

@inject(WorkspaceTrustPreferences)
protected readonly workspaceTrustPref: WorkspaceTrustPreferences;

@inject(WindowService)
protected readonly windowService: WindowService;

protected workspaceTrust = new Deferred<boolean>();

@postConstruct()
protected async init(): Promise<void> {
await this.workspaceService.ready;
await this.resolveWorkspaceTrust();
this.preferences.onPreferenceChanged(change => this.handlePreferenceChange(change));
}

getWorkspaceTrust(): Promise<boolean> {
return this.workspaceTrust.promise;
}

protected async resolveWorkspaceTrust(givenTrust?: boolean): Promise<void> {
if (!this.isWorkspaceTrustResolved()) {
const trust = givenTrust ?? await this.calculateWorkspaceTrust();
if (trust !== undefined) {
await this.storeWorkspaceTrust(trust);
this.workspaceTrust.resolve(trust);
}
}
}

protected isWorkspaceTrustResolved(): boolean {
return this.workspaceTrust.state !== 'unresolved';
}

protected async calculateWorkspaceTrust(): Promise<boolean | undefined> {
colin-grant-work marked this conversation as resolved.
Show resolved Hide resolved
if (!this.workspaceTrustPref[WORKSPACE_TRUST_ENABLED]) {
colin-grant-work marked this conversation as resolved.
Show resolved Hide resolved
// in VS Code if workspace trust is disabled, we implicitly trust the workspace
return true;
}

if (this.workspaceTrustPref[WORKSPACE_TRUST_EMPTY_WINDOW] && !this.workspaceService.workspace) {
return true;
}

if (this.workspaceTrustPref[WORKSPACE_TRUST_STARTUP_PROMPT] === WorkspaceTrustPrompt.NEVER) {
return false;
}

return this.loadWorkspaceTrust();
}

protected async loadWorkspaceTrust(): Promise<boolean | undefined> {
if (this.workspaceTrustPref[WORKSPACE_TRUST_STARTUP_PROMPT] === WorkspaceTrustPrompt.ONCE) {
return this.storage.getData<boolean>(STORAGE_TRUSTED);
}
}

protected async storeWorkspaceTrust(trust: boolean): Promise<void> {
if (this.workspaceTrustPref[WORKSPACE_TRUST_STARTUP_PROMPT] === WorkspaceTrustPrompt.ONCE) {
return this.storage.setData(STORAGE_TRUSTED, trust);
}
}

protected async handlePreferenceChange(change: PreferenceChange): Promise<void> {
if (change.preferenceName === WORKSPACE_TRUST_STARTUP_PROMPT && change.newValue !== WorkspaceTrustPrompt.ONCE) {
this.storage.setData(STORAGE_TRUSTED, undefined);
}

if (change.preferenceName === WORKSPACE_TRUST_ENABLED && this.isWorkspaceTrustResolved() && await this.confirmRestart()) {
this.windowService.setSafeToShutDown();
this.windowService.reload();
}

if (change.preferenceName === WORKSPACE_TRUST_ENABLED || change.preferenceName === WORKSPACE_TRUST_EMPTY_WINDOW) {
this.resolveWorkspaceTrust();
}
}

protected async confirmRestart(): Promise<boolean> {
const shouldRestart = await new ConfirmDialog({
title: nls.localizeByDefault('A setting has changed that requires a restart to take effect.'),
msg: nls.localizeByDefault('Press the restart button to restart {0} and enable the setting.', FrontendApplicationConfigProvider.get().applicationName),
ok: nls.localizeByDefault('Restart'),
cancel: Dialog.CANCEL,
}).open();
return shouldRestart === true;
}

async requestWorkspaceTrust(): Promise<boolean | undefined> {
if (!this.isWorkspaceTrustResolved()) {
const isTrusted = await this.messageService.info(nls.localize('theia/workspace/trustRequest',
'An extension requests workspace trust but the corresponding API is not yet fully supported. Do you want to trust this workspace?'),
Dialog.YES, Dialog.NO);
const trusted = isTrusted === Dialog.YES;
this.resolveWorkspaceTrust(trusted);
}
return this.workspaceTrust.promise;
}
}