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

add workspace trust required editor #123181

Merged
merged 6 commits into from May 10, 2021
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
50 changes: 43 additions & 7 deletions src/vs/workbench/browser/parts/editor/editorControl.ts
Expand Up @@ -15,6 +15,8 @@ import { IEditorProgressService, LongRunningOperation } from 'vs/platform/progre
import { IEditorGroupView, DEFAULT_EDITOR_MIN_DIMENSIONS, DEFAULT_EDITOR_MAX_DIMENSIONS } from 'vs/workbench/browser/parts/editor/editor';
import { Emitter } from 'vs/base/common/event';
import { assertIsDefined } from 'vs/base/common/types';
import { IWorkspaceTrustManagementService } from 'vs/platform/workspace/common/workspaceTrust';
import { WorkspaceTrustRequiredEditor } from 'vs/workbench/browser/parts/editor/workspaceTrustRequiredEditor';

export interface IOpenEditorResult {
readonly editorPane: EditorPane;
Expand Down Expand Up @@ -42,31 +44,65 @@ export class EditorControl extends Disposable {
private readonly activeEditorPaneDisposables = this._register(new DisposableStore());
private dimension: Dimension | undefined;
private readonly editorOperation = this._register(new LongRunningOperation(this.editorProgressService));
private readonly editorsRegistry = Registry.as<IEditorRegistry>(EditorExtensions.Editors);

constructor(
private parent: HTMLElement,
private groupView: IEditorGroupView,
@IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService,
@IInstantiationService private readonly instantiationService: IInstantiationService,
@IEditorProgressService private readonly editorProgressService: IEditorProgressService
@IEditorProgressService private readonly editorProgressService: IEditorProgressService,
@IWorkspaceTrustManagementService private readonly workspaceTrustService: IWorkspaceTrustManagementService,
) {
super();

this.registerListeners();
}

async openEditor(editor: EditorInput, options: EditorOptions | undefined, context: IEditorOpenContext): Promise<IOpenEditorResult> {
private registerListeners(): void {
this._register(this.workspaceTrustService.onDidChangeTrust(() => this.onDidChangeWorkspaceTrust()));
}

// Editor pane
const descriptor = Registry.as<IEditorRegistry>(EditorExtensions.Editors).getEditor(editor);
if (!descriptor) {
throw new Error(`No editor descriptor found for input id ${editor.typeId}`);
private async onDidChangeWorkspaceTrust(): Promise<void> {

// If the active editor pane requires workspace trust
// we need to re-open it anytime trust changes to
// account for it.
// For that we explicitly call into the group-view
// to handle errors properly.
const editor = this._activeEditorPane?.input;
const options = this._activeEditorPane?.options;
if (editor && await editor.requiresWorkspaceTrust()) {
this.groupView.openEditor(editor, options);
}
}

async openEditor(editor: EditorInput, options: EditorOptions | undefined, context: IEditorOpenContext = Object.create(null)): Promise<IOpenEditorResult> {

// Editor descriptor
const descriptor = await this.resolveEditorDescriptor(editor);

// Editor pane
const editorPane = this.doShowEditorPane(descriptor);

// Set input
// Apply input to pane
const editorChanged = await this.doSetInput(editorPane, editor, options, context);
return { editorPane, editorChanged };
}

private async resolveEditorDescriptor(editor: EditorInput): Promise<IEditorDescriptor> {
const editorRequiresTrust = await editor.requiresWorkspaceTrust();
const editorBlockedByTrust = editorRequiresTrust && !this.workspaceTrustService.isWorkpaceTrusted();

// Workspace trust: if an editor signals it needs workspace trust
// but the current workspace is untrusted, we fallback to a generic
// editor descriptor to indicate this an do NOT load the registered
// editor.
const descriptor = editorBlockedByTrust ? WorkspaceTrustRequiredEditor.DESCRIPTOR : this.editorsRegistry.getEditor(editor);

return assertIsDefined(descriptor);
}

private doShowEditorPane(descriptor: IEditorDescriptor): EditorPane {

// Return early if the currently active editor pane can handle the input
Expand Down
@@ -0,0 +1,20 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

.monaco-workspace-trust-required-editor:focus {
outline: none !important;
}

.monaco-workspace-trust-required-editor {
padding: 5px 0 0 10px;
box-sizing: border-box;
}

.monaco-workspace-trust-required-editor .embedded-link,
.monaco-workspace-trust-required-editor .embedded-link:hover {
cursor: pointer;
text-decoration: underline;
margin-left: 5px;
}
127 changes: 127 additions & 0 deletions src/vs/workbench/browser/parts/editor/workspaceTrustRequiredEditor.ts
@@ -0,0 +1,127 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import 'vs/css!./media/workspacetrusteditor';
import { localize } from 'vs/nls';
import { EditorInput, EditorOptions, IEditorOpenContext } from 'vs/workbench/common/editor';
import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { DomScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement';
import { ScrollbarVisibility } from 'vs/base/common/scrollable';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { Dimension, size, clearNode, append, addDisposableListener, EventType, $ } from 'vs/base/browser/dom';
import { CancellationToken } from 'vs/base/common/cancellation';
import { DisposableStore, IDisposable, MutableDisposable } from 'vs/base/common/lifecycle';
import { IStorageService } from 'vs/platform/storage/common/storage';
import { assertIsDefined, assertAllDefined } from 'vs/base/common/types';
import { ICommandService } from 'vs/platform/commands/common/commands';
import { isSingleFolderWorkspaceIdentifier, toWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { EditorDescriptor } from 'vs/workbench/browser/editor';

export class WorkspaceTrustRequiredEditor extends EditorPane {

static readonly ID = 'workbench.editors.workspaceTrustRequiredEditor';
static readonly LABEL = localize('trustRequiredEditor', "Workspace Trust Required");
static readonly DESCRIPTOR = EditorDescriptor.create(WorkspaceTrustRequiredEditor, WorkspaceTrustRequiredEditor.ID, WorkspaceTrustRequiredEditor.LABEL);

private container: HTMLElement | undefined;
private scrollbar: DomScrollableElement | undefined;
private inputDisposable = this._register(new MutableDisposable());

constructor(
@ITelemetryService telemetryService: ITelemetryService,
@IThemeService themeService: IThemeService,
@ICommandService private readonly commandService: ICommandService,
@IWorkspaceContextService private readonly workspaceService: IWorkspaceContextService,
@IStorageService storageService: IStorageService
) {
super(WorkspaceTrustRequiredEditor.ID, telemetryService, themeService, storageService);
}

override getTitle(): string {
return WorkspaceTrustRequiredEditor.LABEL;
}

protected createEditor(parent: HTMLElement): void {

// Container
this.container = document.createElement('div');
this.container.className = 'monaco-workspace-trust-required-editor';
this.container.style.outline = 'none';
this.container.tabIndex = 0; // enable focus support from the editor part (do not remove)

// Custom Scrollbars
this.scrollbar = this._register(new DomScrollableElement(this.container, { horizontal: ScrollbarVisibility.Auto, vertical: ScrollbarVisibility.Auto }));
parent.appendChild(this.scrollbar.getDomNode());
}

override async setInput(input: EditorInput, options: EditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise<void> {
await super.setInput(input, options, context, token);

// Check for cancellation
if (token.isCancellationRequested) {
return;
}

// Render Input
this.inputDisposable.value = this.renderInput();
}

private renderInput(): IDisposable {
const [container, scrollbar] = assertAllDefined(this.container, this.scrollbar);

clearNode(container);

const disposables = new DisposableStore();

const label = container.appendChild(document.createElement('p'));
label.textContent = isSingleFolderWorkspaceIdentifier(toWorkspaceIdentifier(this.workspaceService.getWorkspace())) ?
localize('requiresFolderTrustText', "The file is not displayed in the editor because trust has not been granted to the folder.") :
localize('requiresWorkspaceTrustText', "The file is not displayed in the editor because trust has not been granted to the workspace.");

const link = append(label, $('a.embedded-link'));
link.setAttribute('role', 'button');
link.textContent = localize('manageTrust', "Manage Workspace Trust");

disposables.add(addDisposableListener(link, EventType.CLICK, async () => {
await this.commandService.executeCommand('workbench.trust.manage');
}));

scrollbar.scanDomNode();

return disposables;
}

override clearInput(): void {
if (this.container) {
clearNode(this.container);
}

this.inputDisposable.clear();

super.clearInput();
}

layout(dimension: Dimension): void {

// Pass on to Container
const [container, scrollbar] = assertAllDefined(this.container, this.scrollbar);
size(container, dimension.width, dimension.height);
scrollbar.scanDomNode();
}

override focus(): void {
const container = assertIsDefined(this.container);

container.focus();
}

override dispose(): void {
this.container?.remove();

super.dispose();
}
}
15 changes: 15 additions & 0 deletions src/vs/workbench/common/editor.ts
Expand Up @@ -432,6 +432,11 @@ export interface IEditorInput extends IDisposable {
*/
resolve(): Promise<IEditorModel | null>;

/**
* Returns if the input requires workspace trust or not.
*/
requiresWorkspaceTrust(): Promise<boolean>;

/**
* Returns if this input is readonly or not.
*/
Expand Down Expand Up @@ -572,6 +577,10 @@ export abstract class EditorInput extends Disposable implements IEditorInput {
return { typeId: this.typeId };
}

async requiresWorkspaceTrust(): Promise<boolean> {
return false;
}

isReadonly(): boolean {
return true;
}
Expand Down Expand Up @@ -790,6 +799,12 @@ export class SideBySideEditorInput extends EditorInput {
return this.description;
}

override async requiresWorkspaceTrust(): Promise<boolean> {
const requiresTrust = await Promise.all([this.primary.requiresWorkspaceTrust(), this.secondary.requiresWorkspaceTrust()]);

return requiresTrust.some(value => value === true);
}

override isReadonly(): boolean {
return this.primary.isReadonly();
}
Expand Down
76 changes: 75 additions & 1 deletion src/vs/workbench/test/browser/parts/editor/editorPane.test.ts
Expand Up @@ -5,13 +5,14 @@

import * as assert from 'assert';
import { EditorPane, EditorMemento } from 'vs/workbench/browser/parts/editor/editorPane';
import { WorkspaceTrustRequiredEditor } from 'vs/workbench/browser/parts/editor/workspaceTrustRequiredEditor';
import { EditorInput, EditorOptions, IEditorInputSerializer, IEditorInputFactoryRegistry, EditorExtensions } from 'vs/workbench/common/editor';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { Registry } from 'vs/platform/registry/common/platform';
import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils';
import { workbenchInstantiationService, TestEditorGroupView, TestEditorGroupsService, registerTestResourceEditor, TestEditorInput } from 'vs/workbench/test/browser/workbenchTestServices';
import { workbenchInstantiationService, TestEditorGroupView, TestEditorGroupsService, registerTestResourceEditor, TestEditorInput, createEditorPart } from 'vs/workbench/test/browser/workbenchTestServices';
import { TextResourceEditorInput } from 'vs/workbench/common/editor/textResourceEditorInput';
import { TestThemeService } from 'vs/platform/theme/test/common/testThemeService';
import { URI } from 'vs/base/common/uri';
Expand All @@ -21,6 +22,11 @@ import { IEditorModel } from 'vs/platform/editor/common/editor';
import { DisposableStore, dispose } from 'vs/base/common/lifecycle';
import { TestStorageService } from 'vs/workbench/test/common/workbenchTestServices';
import { extUri } from 'vs/base/common/resources';
import { EditorService } from 'vs/workbench/services/editor/browser/editorService';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService';
import { TestWorkspaceTrustManagementService } from 'vs/workbench/services/workspaces/test/common/testWorkspaceTrustService';
import { IWorkspaceTrustManagementService } from 'vs/platform/workspace/common/workspaceTrust';

const NullThemeService = new TestThemeService();

Expand Down Expand Up @@ -393,4 +399,72 @@ suite('Workbench EditorPane', () => {
res = memento.loadEditorState(testGroup0, testInputB);
assert.ok(!res);
});

test('WorkspaceTrustRequiredEditor', async function () {
class TrustRequiredTestEditor extends EditorPane {
constructor(@ITelemetryService telemetryService: ITelemetryService) {
super('TestEditor', NullTelemetryService, NullThemeService, new TestStorageService());
}

override getId(): string { return 'trustRequiredTestEditor'; }
layout(): void { }
createEditor(): any { }
}

class TrustRequiredTestInput extends EditorInput {

readonly resource = undefined;

override get typeId(): string {
return 'trustRequiredTestInput';
}

override async requiresWorkspaceTrust(): Promise<boolean> {
return true;
}

override resolve(): any {
return null;
}
}


const disposables = new DisposableStore();

const instantiationService = workbenchInstantiationService();
const workspaceTrustService = instantiationService.createInstance(TestWorkspaceTrustManagementService);
instantiationService.stub(IWorkspaceTrustManagementService, workspaceTrustService);
workspaceTrustService.setWorkspaceTrust(false);

const editorPart = await createEditorPart(instantiationService, disposables);
instantiationService.stub(IEditorGroupsService, editorPart);

const editorService = instantiationService.createInstance(EditorService);
instantiationService.stub(IEditorService, editorService);

const group = editorPart.activeGroup;

let editorDescriptor = EditorDescriptor.create(TrustRequiredTestEditor, 'id1', 'name');
disposables.add(EditorRegistry.registerEditor(editorDescriptor, [new SyncDescriptor(TrustRequiredTestInput)]));

const testInput = new TrustRequiredTestInput();

await group.openEditor(testInput);
assert.strictEqual(group.activeEditorPane?.getId(), WorkspaceTrustRequiredEditor.ID);

const getEditorPaneIdAsync = () => new Promise(resolve => {
disposables.add(editorService.onDidActiveEditorChange(event => {
resolve(group.activeEditorPane?.getId());
}));
});

workspaceTrustService.setWorkspaceTrust(true);

assert.strictEqual(await getEditorPaneIdAsync(), 'trustRequiredTestEditor');

workspaceTrustService.setWorkspaceTrust(false);
assert.strictEqual(await getEditorPaneIdAsync(), WorkspaceTrustRequiredEditor.ID);

dispose(disposables);
});
});