@theia/workspace 1.68.0-next.7 → 1.68.0-next.79
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/browser/quick-open-workspace.js +3 -4
- package/lib/browser/quick-open-workspace.js.map +1 -1
- package/lib/browser/workspace-breadcrumbs-contribution.js +3 -3
- package/lib/browser/workspace-breadcrumbs-contribution.js.map +1 -1
- package/lib/browser/workspace-commands.d.ts +1 -0
- package/lib/browser/workspace-commands.d.ts.map +1 -1
- package/lib/browser/workspace-commands.js +6 -2
- package/lib/browser/workspace-commands.js.map +1 -1
- package/lib/browser/workspace-frontend-contribution.d.ts +5 -1
- package/lib/browser/workspace-frontend-contribution.d.ts.map +1 -1
- package/lib/browser/workspace-frontend-contribution.js +48 -7
- package/lib/browser/workspace-frontend-contribution.js.map +1 -1
- package/lib/browser/workspace-frontend-module.d.ts +1 -0
- package/lib/browser/workspace-frontend-module.d.ts.map +1 -1
- package/lib/browser/workspace-frontend-module.js +2 -0
- package/lib/browser/workspace-frontend-module.js.map +1 -1
- package/lib/browser/workspace-schema-updater.d.ts.map +1 -1
- package/lib/browser/workspace-service.js +3 -5
- package/lib/browser/workspace-service.js.map +1 -1
- package/lib/browser/workspace-trust-dialog.d.ts +13 -0
- package/lib/browser/workspace-trust-dialog.d.ts.map +1 -0
- package/lib/browser/workspace-trust-dialog.js +66 -0
- package/lib/browser/workspace-trust-dialog.js.map +1 -0
- package/lib/browser/workspace-trust-service.d.ts +77 -1
- package/lib/browser/workspace-trust-service.d.ts.map +1 -1
- package/lib/browser/workspace-trust-service.js +322 -12
- package/lib/browser/workspace-trust-service.js.map +1 -1
- package/lib/browser/workspace-trust-service.spec.d.ts +2 -0
- package/lib/browser/workspace-trust-service.spec.d.ts.map +1 -0
- package/lib/browser/workspace-trust-service.spec.js +357 -0
- package/lib/browser/workspace-trust-service.spec.js.map +1 -0
- package/lib/browser/workspace-uri-contribution.js +1 -2
- package/lib/browser/workspace-uri-contribution.js.map +1 -1
- package/lib/browser/workspace-user-working-directory-provider.js +6 -5
- package/lib/browser/workspace-user-working-directory-provider.js.map +1 -1
- package/lib/common/untitled-workspace-service.d.ts +8 -1
- package/lib/common/untitled-workspace-service.d.ts.map +1 -1
- package/lib/common/untitled-workspace-service.js +22 -3
- package/lib/common/untitled-workspace-service.js.map +1 -1
- package/lib/common/workspace-preferences.js +3 -3
- package/lib/common/workspace-preferences.js.map +1 -1
- package/lib/common/workspace-trust-preferences.d.ts +2 -0
- package/lib/common/workspace-trust-preferences.d.ts.map +1 -1
- package/lib/common/workspace-trust-preferences.js +13 -3
- package/lib/common/workspace-trust-preferences.js.map +1 -1
- package/lib/node/default-workspace-server.d.ts +0 -1
- package/lib/node/default-workspace-server.d.ts.map +1 -1
- package/lib/node/default-workspace-server.js +2 -3
- package/lib/node/default-workspace-server.js.map +1 -1
- package/package.json +5 -5
- package/src/browser/style/index.css +75 -0
- package/src/browser/workspace-commands.ts +5 -0
- package/src/browser/workspace-frontend-contribution.ts +45 -2
- package/src/browser/workspace-frontend-module.ts +4 -1
- package/src/browser/workspace-trust-dialog.tsx +90 -0
- package/src/browser/workspace-trust-service.spec.ts +462 -0
- package/src/browser/workspace-trust-service.ts +381 -15
- package/src/common/untitled-workspace-service.ts +21 -2
- package/src/common/workspace-trust-preferences.ts +11 -0
|
@@ -15,20 +15,50 @@
|
|
|
15
15
|
// *****************************************************************************
|
|
16
16
|
|
|
17
17
|
import { ConfirmDialog, Dialog, StorageService } from '@theia/core/lib/browser';
|
|
18
|
-
import {
|
|
18
|
+
import { MarkdownString, MarkdownStringImpl } from '@theia/core/lib/common/markdown-rendering/markdown-string';
|
|
19
|
+
import { StatusBar, StatusBarAlignment } from '@theia/core/lib/browser/status-bar/status-bar';
|
|
20
|
+
import { OS, ContributionProvider, DisposableCollection } from '@theia/core';
|
|
21
|
+
import { Emitter, Event } from '@theia/core/lib/common';
|
|
22
|
+
import URI from '@theia/core/lib/common/uri';
|
|
23
|
+
import { PreferenceChange, PreferenceSchemaService, PreferenceScope, PreferenceService } from '@theia/core/lib/common/preferences';
|
|
19
24
|
import { MessageService } from '@theia/core/lib/common/message-service';
|
|
20
25
|
import { nls } from '@theia/core/lib/common/nls';
|
|
21
26
|
import { Deferred } from '@theia/core/lib/common/promise-util';
|
|
22
|
-
import { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
|
|
27
|
+
import { inject, injectable, named, postConstruct, preDestroy } from '@theia/core/shared/inversify';
|
|
23
28
|
import { WindowService } from '@theia/core/lib/browser/window/window-service';
|
|
24
29
|
import {
|
|
25
|
-
WorkspaceTrustPreferences, WORKSPACE_TRUST_EMPTY_WINDOW, WORKSPACE_TRUST_ENABLED, WORKSPACE_TRUST_STARTUP_PROMPT, WorkspaceTrustPrompt
|
|
30
|
+
WorkspaceTrustPreferences, WORKSPACE_TRUST_EMPTY_WINDOW, WORKSPACE_TRUST_ENABLED, WORKSPACE_TRUST_STARTUP_PROMPT, WORKSPACE_TRUST_TRUSTED_FOLDERS, WorkspaceTrustPrompt
|
|
26
31
|
} from '../common/workspace-trust-preferences';
|
|
27
32
|
import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/frontend-application-config-provider';
|
|
28
33
|
import { WorkspaceService } from './workspace-service';
|
|
34
|
+
import { WorkspaceCommands } from './workspace-commands';
|
|
29
35
|
import { ContextKeyService } from '@theia/core/lib/browser/context-key-service';
|
|
36
|
+
import { WorkspaceTrustDialog } from './workspace-trust-dialog';
|
|
37
|
+
import { UntitledWorkspaceService } from '../common/untitled-workspace-service';
|
|
38
|
+
import { EnvVariablesServer } from '@theia/core/lib/common/env-variables';
|
|
30
39
|
|
|
31
40
|
const STORAGE_TRUSTED = 'trusted';
|
|
41
|
+
export const WORKSPACE_TRUST_STATUS_BAR_ID = 'workspace-trust-status';
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Contribution interface for features that are restricted in untrusted workspaces.
|
|
45
|
+
* Implementations can provide information about what is being restricted.
|
|
46
|
+
*/
|
|
47
|
+
export const WorkspaceRestrictionContribution = Symbol('WorkspaceRestrictionContribution');
|
|
48
|
+
export interface WorkspaceRestrictionContribution {
|
|
49
|
+
/**
|
|
50
|
+
* Returns the restrictions currently active due to workspace trust.
|
|
51
|
+
* Called when building the restricted mode status bar tooltip.
|
|
52
|
+
*/
|
|
53
|
+
getRestrictions(): WorkspaceRestriction[];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface WorkspaceRestriction {
|
|
57
|
+
/** Display name of the feature being restricted */
|
|
58
|
+
label: string;
|
|
59
|
+
/** Optional details (e.g., list of blocked items) */
|
|
60
|
+
details?: string[];
|
|
61
|
+
}
|
|
32
62
|
|
|
33
63
|
@injectable()
|
|
34
64
|
export class WorkspaceTrustService {
|
|
@@ -47,13 +77,35 @@ export class WorkspaceTrustService {
|
|
|
47
77
|
@inject(WorkspaceTrustPreferences)
|
|
48
78
|
protected readonly workspaceTrustPref: WorkspaceTrustPreferences;
|
|
49
79
|
|
|
80
|
+
@inject(PreferenceSchemaService)
|
|
81
|
+
protected readonly preferenceSchemaService: PreferenceSchemaService;
|
|
82
|
+
|
|
50
83
|
@inject(WindowService)
|
|
51
84
|
protected readonly windowService: WindowService;
|
|
52
85
|
|
|
53
86
|
@inject(ContextKeyService)
|
|
54
87
|
protected readonly contextKeyService: ContextKeyService;
|
|
55
88
|
|
|
89
|
+
@inject(StatusBar)
|
|
90
|
+
protected readonly statusBar: StatusBar;
|
|
91
|
+
|
|
92
|
+
@inject(ContributionProvider) @named(WorkspaceRestrictionContribution)
|
|
93
|
+
protected readonly restrictionContributions: ContributionProvider<WorkspaceRestrictionContribution>;
|
|
94
|
+
|
|
95
|
+
@inject(UntitledWorkspaceService)
|
|
96
|
+
protected readonly untitledWorkspaceService: UntitledWorkspaceService;
|
|
97
|
+
|
|
98
|
+
@inject(EnvVariablesServer)
|
|
99
|
+
protected readonly envVariablesServer: EnvVariablesServer;
|
|
100
|
+
|
|
56
101
|
protected workspaceTrust = new Deferred<boolean>();
|
|
102
|
+
protected currentTrust: boolean | undefined;
|
|
103
|
+
protected pendingTrustDialog: Deferred<boolean> | undefined;
|
|
104
|
+
|
|
105
|
+
protected readonly onDidChangeWorkspaceTrustEmitter = new Emitter<boolean>();
|
|
106
|
+
readonly onDidChangeWorkspaceTrust: Event<boolean> = this.onDidChangeWorkspaceTrustEmitter.event;
|
|
107
|
+
|
|
108
|
+
protected readonly toDispose = new DisposableCollection(this.onDidChangeWorkspaceTrustEmitter);
|
|
57
109
|
|
|
58
110
|
@postConstruct()
|
|
59
111
|
protected init(): void {
|
|
@@ -62,11 +114,38 @@ export class WorkspaceTrustService {
|
|
|
62
114
|
|
|
63
115
|
protected async doInit(): Promise<void> {
|
|
64
116
|
await this.workspaceService.ready;
|
|
117
|
+
await this.workspaceTrustPref.ready;
|
|
118
|
+
await this.preferenceSchemaService.ready;
|
|
65
119
|
await this.resolveWorkspaceTrust();
|
|
66
|
-
this.
|
|
120
|
+
this.toDispose.push(
|
|
121
|
+
this.preferences.onPreferenceChanged(change => this.handlePreferenceChange(change))
|
|
122
|
+
);
|
|
123
|
+
this.toDispose.push(
|
|
124
|
+
this.workspaceService.onWorkspaceChanged(() => this.handleWorkspaceChanged())
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
// Show status bar item if starting in restricted mode
|
|
128
|
+
const initialTrust = await this.getWorkspaceTrust();
|
|
129
|
+
this.updateRestrictedModeIndicator(initialTrust);
|
|
130
|
+
|
|
131
|
+
// React to trust changes
|
|
132
|
+
this.toDispose.push(
|
|
133
|
+
this.onDidChangeWorkspaceTrust(trust => {
|
|
134
|
+
this.updateRestrictedModeIndicator(trust);
|
|
135
|
+
})
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
@preDestroy()
|
|
140
|
+
protected onStop(): void {
|
|
141
|
+
this.toDispose.dispose();
|
|
67
142
|
}
|
|
68
143
|
|
|
69
144
|
getWorkspaceTrust(): Promise<boolean> {
|
|
145
|
+
// Return current trust if already resolved, otherwise wait for initial resolution
|
|
146
|
+
if (this.currentTrust !== undefined) {
|
|
147
|
+
return Promise.resolve(this.currentTrust);
|
|
148
|
+
}
|
|
70
149
|
return this.workspaceTrust.promise;
|
|
71
150
|
}
|
|
72
151
|
|
|
@@ -76,22 +155,44 @@ export class WorkspaceTrustService {
|
|
|
76
155
|
if (trust !== undefined) {
|
|
77
156
|
await this.storeWorkspaceTrust(trust);
|
|
78
157
|
this.contextKeyService.setContext('isWorkspaceTrusted', trust);
|
|
158
|
+
this.currentTrust = trust;
|
|
79
159
|
this.workspaceTrust.resolve(trust);
|
|
160
|
+
this.onDidChangeWorkspaceTrustEmitter.fire(trust);
|
|
161
|
+
if (trust && this.workspaceTrustPref[WORKSPACE_TRUST_ENABLED]) {
|
|
162
|
+
await this.addToTrustedFolders();
|
|
163
|
+
}
|
|
80
164
|
}
|
|
81
165
|
}
|
|
82
166
|
}
|
|
83
167
|
|
|
168
|
+
setWorkspaceTrust(trusted: boolean): void {
|
|
169
|
+
if (this.currentTrust === trusted) {
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
this.currentTrust = trusted;
|
|
173
|
+
this.contextKeyService.setContext('isWorkspaceTrusted', trusted);
|
|
174
|
+
if (this.workspaceTrustPref[WORKSPACE_TRUST_STARTUP_PROMPT] === WorkspaceTrustPrompt.ONCE) {
|
|
175
|
+
this.storeWorkspaceTrust(trusted);
|
|
176
|
+
}
|
|
177
|
+
this.onDidChangeWorkspaceTrustEmitter.fire(trusted);
|
|
178
|
+
}
|
|
179
|
+
|
|
84
180
|
protected isWorkspaceTrustResolved(): boolean {
|
|
85
181
|
return this.workspaceTrust.state !== 'unresolved';
|
|
86
182
|
}
|
|
87
183
|
|
|
88
184
|
protected async calculateWorkspaceTrust(): Promise<boolean | undefined> {
|
|
89
|
-
|
|
90
|
-
|
|
185
|
+
const trustEnabled = this.workspaceTrustPref[WORKSPACE_TRUST_ENABLED];
|
|
186
|
+
if (!trustEnabled) {
|
|
91
187
|
return true;
|
|
92
188
|
}
|
|
93
189
|
|
|
94
|
-
|
|
190
|
+
// Empty workspace - no folders open
|
|
191
|
+
if (await this.isEmptyWorkspace()) {
|
|
192
|
+
return !!this.workspaceTrustPref[WORKSPACE_TRUST_EMPTY_WINDOW];
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (await this.areAllWorkspaceUrisTrusted()) {
|
|
95
196
|
return true;
|
|
96
197
|
}
|
|
97
198
|
|
|
@@ -99,7 +200,167 @@ export class WorkspaceTrustService {
|
|
|
99
200
|
return false;
|
|
100
201
|
}
|
|
101
202
|
|
|
102
|
-
|
|
203
|
+
// For ONCE mode, check stored trust first
|
|
204
|
+
if (this.workspaceTrustPref[WORKSPACE_TRUST_STARTUP_PROMPT] === WorkspaceTrustPrompt.ONCE) {
|
|
205
|
+
const storedTrust = await this.loadWorkspaceTrust();
|
|
206
|
+
if (storedTrust !== undefined) {
|
|
207
|
+
return storedTrust;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// For ALWAYS mode or ONCE mode with no stored decision, show dialog
|
|
212
|
+
return this.showTrustPromptDialog();
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Check if the workspace is empty (no workspace or folder opened, or
|
|
217
|
+
* an untitled workspace with no folders).
|
|
218
|
+
* A saved workspace file with 0 folders is NOT empty - it still needs trust
|
|
219
|
+
* evaluation because it could have tasks defined.
|
|
220
|
+
*/
|
|
221
|
+
protected async isEmptyWorkspace(): Promise<boolean> {
|
|
222
|
+
const workspace = this.workspaceService.workspace;
|
|
223
|
+
if (!workspace) {
|
|
224
|
+
return true;
|
|
225
|
+
}
|
|
226
|
+
const roots = this.workspaceService.tryGetRoots();
|
|
227
|
+
// Only consider it empty if it's an untitled workspace with no folders
|
|
228
|
+
// Use secure check with configDirUri for trust-related decisions
|
|
229
|
+
if (roots.length === 0) {
|
|
230
|
+
const configDirUri = new URI(await this.envVariablesServer.getConfigDirUri());
|
|
231
|
+
if (this.untitledWorkspaceService.isUntitledWorkspace(workspace.resource, configDirUri)) {
|
|
232
|
+
return true;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return false;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Get the URIs that need to be trusted for the current workspace.
|
|
240
|
+
* This includes all workspace folder URIs, plus the workspace file URI
|
|
241
|
+
* for saved workspaces (since workspace files can contain tasks/settings).
|
|
242
|
+
*/
|
|
243
|
+
protected getWorkspaceUris(): URI[] {
|
|
244
|
+
const uris = this.workspaceService.tryGetRoots().map(root => root.resource);
|
|
245
|
+
const workspace = this.workspaceService.workspace;
|
|
246
|
+
// For saved workspaces, include the workspace file itself
|
|
247
|
+
if (workspace && this.workspaceService.saved) {
|
|
248
|
+
uris.push(workspace.resource);
|
|
249
|
+
}
|
|
250
|
+
return uris;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Check if all workspace URIs are trusted.
|
|
255
|
+
* A workspace is trusted only if ALL of its folders (and the workspace
|
|
256
|
+
* file for saved workspaces) are trusted.
|
|
257
|
+
*/
|
|
258
|
+
protected async areAllWorkspaceUrisTrusted(): Promise<boolean> {
|
|
259
|
+
const uris = this.getWorkspaceUris();
|
|
260
|
+
if (uris.length === 0) {
|
|
261
|
+
return false;
|
|
262
|
+
}
|
|
263
|
+
return uris.every(uri => this.isUriTrusted(uri));
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Check if a URI is trusted. A URI is trusted if it or any of its
|
|
268
|
+
* parent folders is in the trusted folders list.
|
|
269
|
+
*/
|
|
270
|
+
protected isUriTrusted(uri: URI): boolean {
|
|
271
|
+
const trustedFolders = this.workspaceTrustPref[WORKSPACE_TRUST_TRUSTED_FOLDERS] || [];
|
|
272
|
+
const caseSensitive = !OS.backend.isWindows;
|
|
273
|
+
const normalizedUri = uri.normalizePath();
|
|
274
|
+
|
|
275
|
+
return trustedFolders.some(folder => {
|
|
276
|
+
try {
|
|
277
|
+
const folderUri = new URI(folder).normalizePath();
|
|
278
|
+
// Check if the trusted folder is equal to or a parent of the URI
|
|
279
|
+
return folderUri.isEqualOrParent(normalizedUri, caseSensitive);
|
|
280
|
+
} catch {
|
|
281
|
+
return false; // Invalid URI in preferences
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
protected async showTrustPromptDialog(): Promise<boolean> {
|
|
287
|
+
// If dialog is already open, wait for its result
|
|
288
|
+
if (this.pendingTrustDialog) {
|
|
289
|
+
return this.pendingTrustDialog.promise;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
this.pendingTrustDialog = new Deferred<boolean>();
|
|
293
|
+
try {
|
|
294
|
+
// Show the workspace folders in the dialog
|
|
295
|
+
const folderUris = this.workspaceService.tryGetRoots().map(root => root.resource);
|
|
296
|
+
|
|
297
|
+
const dialog = new WorkspaceTrustDialog(folderUris);
|
|
298
|
+
|
|
299
|
+
const result = await dialog.open();
|
|
300
|
+
const trusted = result === true;
|
|
301
|
+
this.pendingTrustDialog.resolve(trusted);
|
|
302
|
+
return trusted;
|
|
303
|
+
} catch (e) {
|
|
304
|
+
this.pendingTrustDialog.resolve(false);
|
|
305
|
+
throw e;
|
|
306
|
+
} finally {
|
|
307
|
+
this.pendingTrustDialog = undefined;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
async addToTrustedFolders(): Promise<void> {
|
|
312
|
+
const uris = this.getWorkspaceUris();
|
|
313
|
+
if (uris.length === 0) {
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const currentFolders = this.workspaceTrustPref[WORKSPACE_TRUST_TRUSTED_FOLDERS] || [];
|
|
318
|
+
const newFolders = [...currentFolders];
|
|
319
|
+
let changed = false;
|
|
320
|
+
|
|
321
|
+
for (const uri of uris) {
|
|
322
|
+
if (!this.isUriTrusted(uri)) {
|
|
323
|
+
newFolders.push(uri.toString());
|
|
324
|
+
changed = true;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if (changed) {
|
|
329
|
+
await this.preferences.set(
|
|
330
|
+
WORKSPACE_TRUST_TRUSTED_FOLDERS,
|
|
331
|
+
newFolders,
|
|
332
|
+
PreferenceScope.User
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
async removeFromTrustedFolders(): Promise<void> {
|
|
338
|
+
const uris = this.getWorkspaceUris();
|
|
339
|
+
if (uris.length === 0) {
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const currentFolders = this.workspaceTrustPref[WORKSPACE_TRUST_TRUSTED_FOLDERS] || [];
|
|
344
|
+
const caseSensitive = !OS.backend.isWindows;
|
|
345
|
+
const normalizedUris = uris.map(uri => uri.normalizePath());
|
|
346
|
+
|
|
347
|
+
const updatedFolders = currentFolders.filter(folder => {
|
|
348
|
+
try {
|
|
349
|
+
const folderUri = new URI(folder).normalizePath();
|
|
350
|
+
// Remove folder if it exactly matches any workspace URI
|
|
351
|
+
return !normalizedUris.some(wsUri => wsUri.isEqual(folderUri, caseSensitive));
|
|
352
|
+
} catch {
|
|
353
|
+
return true; // Keep invalid URIs
|
|
354
|
+
}
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
if (updatedFolders.length !== currentFolders.length) {
|
|
358
|
+
await this.preferences.set(
|
|
359
|
+
WORKSPACE_TRUST_TRUSTED_FOLDERS,
|
|
360
|
+
updatedFolders,
|
|
361
|
+
PreferenceScope.User
|
|
362
|
+
);
|
|
363
|
+
}
|
|
103
364
|
}
|
|
104
365
|
|
|
105
366
|
protected async loadWorkspaceTrust(): Promise<boolean | undefined> {
|
|
@@ -115,8 +376,21 @@ export class WorkspaceTrustService {
|
|
|
115
376
|
}
|
|
116
377
|
|
|
117
378
|
protected async handlePreferenceChange(change: PreferenceChange): Promise<void> {
|
|
379
|
+
// Handle trustedFolders changes regardless of scope
|
|
380
|
+
if (change.preferenceName === WORKSPACE_TRUST_TRUSTED_FOLDERS) {
|
|
381
|
+
// For empty windows with emptyWindow setting enabled, trust should remain true
|
|
382
|
+
if (await this.isEmptyWorkspace() && this.workspaceTrustPref[WORKSPACE_TRUST_EMPTY_WINDOW]) {
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
const areAllUrisTrusted = await this.areAllWorkspaceUrisTrusted();
|
|
386
|
+
if (areAllUrisTrusted !== this.currentTrust) {
|
|
387
|
+
this.setWorkspaceTrust(areAllUrisTrusted);
|
|
388
|
+
}
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
|
|
118
392
|
if (change.scope === PreferenceScope.User) {
|
|
119
|
-
if (change.preferenceName === WORKSPACE_TRUST_STARTUP_PROMPT &&
|
|
393
|
+
if (change.preferenceName === WORKSPACE_TRUST_STARTUP_PROMPT && this.workspaceTrustPref[WORKSPACE_TRUST_STARTUP_PROMPT] !== WorkspaceTrustPrompt.ONCE) {
|
|
120
394
|
this.storage.setData(STORAGE_TRUSTED, undefined);
|
|
121
395
|
}
|
|
122
396
|
|
|
@@ -125,12 +399,34 @@ export class WorkspaceTrustService {
|
|
|
125
399
|
this.windowService.reload();
|
|
126
400
|
}
|
|
127
401
|
|
|
128
|
-
if (change.preferenceName === WORKSPACE_TRUST_ENABLED
|
|
402
|
+
if (change.preferenceName === WORKSPACE_TRUST_ENABLED) {
|
|
129
403
|
this.resolveWorkspaceTrust();
|
|
130
404
|
}
|
|
405
|
+
|
|
406
|
+
// Handle emptyWindow setting change for empty windows
|
|
407
|
+
if (change.preferenceName === WORKSPACE_TRUST_EMPTY_WINDOW && await this.isEmptyWorkspace()) {
|
|
408
|
+
// For empty windows, directly update trust based on the new setting value
|
|
409
|
+
const shouldTrust = !!this.workspaceTrustPref[WORKSPACE_TRUST_EMPTY_WINDOW];
|
|
410
|
+
if (this.currentTrust !== shouldTrust) {
|
|
411
|
+
this.setWorkspaceTrust(shouldTrust);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
131
414
|
}
|
|
132
415
|
}
|
|
133
416
|
|
|
417
|
+
protected async handleWorkspaceChanged(): Promise<void> {
|
|
418
|
+
// Reset trust state for the new workspace
|
|
419
|
+
this.workspaceTrust = new Deferred<boolean>();
|
|
420
|
+
this.currentTrust = undefined;
|
|
421
|
+
|
|
422
|
+
// Re-evaluate trust for the new workspace
|
|
423
|
+
await this.resolveWorkspaceTrust();
|
|
424
|
+
|
|
425
|
+
// Update status bar indicator
|
|
426
|
+
const trust = await this.getWorkspaceTrust();
|
|
427
|
+
this.updateRestrictedModeIndicator(trust);
|
|
428
|
+
}
|
|
429
|
+
|
|
134
430
|
protected async confirmRestart(): Promise<boolean> {
|
|
135
431
|
const shouldRestart = await new ConfirmDialog({
|
|
136
432
|
title: nls.localizeByDefault('A setting has changed that requires a restart to take effect.'),
|
|
@@ -141,13 +437,83 @@ export class WorkspaceTrustService {
|
|
|
141
437
|
return shouldRestart === true;
|
|
142
438
|
}
|
|
143
439
|
|
|
440
|
+
protected updateRestrictedModeIndicator(trusted: boolean): void {
|
|
441
|
+
if (trusted) {
|
|
442
|
+
this.hideRestrictedModeStatusBarItem();
|
|
443
|
+
} else {
|
|
444
|
+
this.showRestrictedModeStatusBarItem();
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
protected showRestrictedModeStatusBarItem(): void {
|
|
449
|
+
this.statusBar.setElement(WORKSPACE_TRUST_STATUS_BAR_ID, {
|
|
450
|
+
text: '$(shield) ' + nls.localizeByDefault('Restricted Mode'),
|
|
451
|
+
alignment: StatusBarAlignment.LEFT,
|
|
452
|
+
backgroundColor: 'var(--theia-statusBarItem-prominentBackground)',
|
|
453
|
+
color: 'var(--theia-statusBarItem-prominentForeground)',
|
|
454
|
+
priority: 5000,
|
|
455
|
+
tooltip: this.createRestrictedModeTooltip(),
|
|
456
|
+
command: WorkspaceCommands.MANAGE_WORKSPACE_TRUST.id
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
protected createRestrictedModeTooltip(): MarkdownString {
|
|
461
|
+
const md = new MarkdownStringImpl('', { supportThemeIcons: true });
|
|
462
|
+
|
|
463
|
+
md.appendMarkdown(`**${nls.localizeByDefault('Restricted Mode')}**\n\n`);
|
|
464
|
+
|
|
465
|
+
md.appendMarkdown(nls.localize('theia/workspace/restrictedModeDescription',
|
|
466
|
+
'Some features are disabled because this workspace is not trusted.'));
|
|
467
|
+
md.appendMarkdown('\n\n');
|
|
468
|
+
md.appendMarkdown(nls.localize('theia/workspace/restrictedModeNote',
|
|
469
|
+
'*Please note: The workspace trust feature is currently under development in Theia; not all features are integrated with workspace trust yet*'));
|
|
470
|
+
|
|
471
|
+
const restrictions = this.collectRestrictions();
|
|
472
|
+
if (restrictions.length > 0) {
|
|
473
|
+
md.appendMarkdown('\n\n---\n\n');
|
|
474
|
+
for (const restriction of restrictions) {
|
|
475
|
+
md.appendMarkdown(`**${restriction.label}**\n\n`);
|
|
476
|
+
if (restriction.details && restriction.details.length > 0) {
|
|
477
|
+
for (const detail of restriction.details) {
|
|
478
|
+
md.appendMarkdown(`- ${detail}\n`);
|
|
479
|
+
}
|
|
480
|
+
md.appendMarkdown('\n');
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
md.appendMarkdown('\n\n---\n\n');
|
|
486
|
+
md.appendMarkdown(nls.localize('theia/workspace/clickToManageTrust', 'Click to manage trust settings.'));
|
|
487
|
+
|
|
488
|
+
return md;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
protected collectRestrictions(): WorkspaceRestriction[] {
|
|
492
|
+
const restrictions: WorkspaceRestriction[] = [];
|
|
493
|
+
for (const contribution of this.restrictionContributions.getContributions()) {
|
|
494
|
+
restrictions.push(...contribution.getRestrictions());
|
|
495
|
+
}
|
|
496
|
+
return restrictions;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
protected hideRestrictedModeStatusBarItem(): void {
|
|
500
|
+
this.statusBar.removeElement(WORKSPACE_TRUST_STATUS_BAR_ID);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* Refreshes the restricted mode status bar item.
|
|
505
|
+
* Call this when restriction contributions change.
|
|
506
|
+
*/
|
|
507
|
+
refreshRestrictedModeIndicator(): void {
|
|
508
|
+
if (this.currentTrust === false) {
|
|
509
|
+
this.showRestrictedModeStatusBarItem();
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
144
513
|
async requestWorkspaceTrust(): Promise<boolean | undefined> {
|
|
145
514
|
if (!this.isWorkspaceTrustResolved()) {
|
|
146
|
-
const
|
|
147
|
-
|
|
148
|
-
Dialog.YES, Dialog.NO);
|
|
149
|
-
const trusted = isTrusted === Dialog.YES;
|
|
150
|
-
this.resolveWorkspaceTrust(trusted);
|
|
515
|
+
const trusted = await this.showTrustPromptDialog();
|
|
516
|
+
await this.resolveWorkspaceTrust(trusted);
|
|
151
517
|
}
|
|
152
518
|
return this.workspaceTrust.promise;
|
|
153
519
|
}
|
|
@@ -25,8 +25,27 @@ export class UntitledWorkspaceService {
|
|
|
25
25
|
@inject(WorkspaceFileService)
|
|
26
26
|
protected readonly workspaceFileService: WorkspaceFileService;
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
/**
|
|
29
|
+
* Check if a URI is an untitled workspace.
|
|
30
|
+
* @param candidate The URI to check
|
|
31
|
+
* @param configDirUri Optional config directory URI. If provided, also verifies
|
|
32
|
+
* that the candidate is under the expected workspaces directory.
|
|
33
|
+
* This is the secure check and should be used when possible.
|
|
34
|
+
*/
|
|
35
|
+
isUntitledWorkspace(candidate?: URI, configDirUri?: URI): boolean {
|
|
36
|
+
if (!candidate || !this.workspaceFileService.isWorkspaceFile(candidate)) {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
if (!candidate.path.base.startsWith('Untitled')) {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
// If configDirUri is provided, verify the candidate is in the expected location
|
|
43
|
+
if (configDirUri) {
|
|
44
|
+
const expectedParentDir = configDirUri.resolve('workspaces');
|
|
45
|
+
return expectedParentDir.isEqualOrParent(candidate);
|
|
46
|
+
}
|
|
47
|
+
// Without configDirUri, fall back to name-only check (less secure)
|
|
48
|
+
return true;
|
|
30
49
|
}
|
|
31
50
|
|
|
32
51
|
async getUntitledWorkspaceUri(configDirUri: URI, isAcceptable: (candidate: URI) => MaybePromise<boolean>, warnOnHits?: () => unknown): Promise<URI> {
|
|
@@ -21,6 +21,7 @@ import { interfaces } from '@theia/core/shared/inversify';
|
|
|
21
21
|
export const WORKSPACE_TRUST_ENABLED = 'security.workspace.trust.enabled';
|
|
22
22
|
export const WORKSPACE_TRUST_STARTUP_PROMPT = 'security.workspace.trust.startupPrompt';
|
|
23
23
|
export const WORKSPACE_TRUST_EMPTY_WINDOW = 'security.workspace.trust.emptyWindow';
|
|
24
|
+
export const WORKSPACE_TRUST_TRUSTED_FOLDERS = 'security.workspace.trust.trustedFolders';
|
|
24
25
|
|
|
25
26
|
export enum WorkspaceTrustPrompt {
|
|
26
27
|
ALWAYS = 'always',
|
|
@@ -45,6 +46,15 @@ export const workspaceTrustPreferenceSchema: PreferenceSchema = {
|
|
|
45
46
|
description: nls.localize('theia/workspace/trustEmptyWindow', 'Controls whether or not the empty workspace is trusted by default.'),
|
|
46
47
|
type: 'boolean',
|
|
47
48
|
default: true
|
|
49
|
+
},
|
|
50
|
+
[WORKSPACE_TRUST_TRUSTED_FOLDERS]: {
|
|
51
|
+
description: nls.localize('theia/workspace/trustTrustedFolders', 'List of folder URIs that are trusted without prompting.'),
|
|
52
|
+
type: 'array',
|
|
53
|
+
items: {
|
|
54
|
+
type: 'string'
|
|
55
|
+
},
|
|
56
|
+
default: [],
|
|
57
|
+
scope: PreferenceScope.User
|
|
48
58
|
}
|
|
49
59
|
}
|
|
50
60
|
};
|
|
@@ -53,6 +63,7 @@ export interface WorkspaceTrustConfiguration {
|
|
|
53
63
|
[WORKSPACE_TRUST_ENABLED]: boolean,
|
|
54
64
|
[WORKSPACE_TRUST_STARTUP_PROMPT]: WorkspaceTrustPrompt;
|
|
55
65
|
[WORKSPACE_TRUST_EMPTY_WINDOW]: boolean;
|
|
66
|
+
[WORKSPACE_TRUST_TRUSTED_FOLDERS]: string[];
|
|
56
67
|
}
|
|
57
68
|
|
|
58
69
|
export const WorkspaceTrustPreferenceContribution = Symbol('WorkspaceTrustPreferenceContribution');
|