diff --git a/extensions/git/package.json b/extensions/git/package.json index 36a8bcf872463..fd5b557f6ddbc 100644 --- a/extensions/git/package.json +++ b/extensions/git/package.json @@ -39,7 +39,8 @@ "tabInputMultiDiff", "tabInputTextMerge", "textEditorDiffInformation", - "timeline" + "timeline", + "workspaceTrust" ], "categories": [ "Other" diff --git a/extensions/git/src/model.ts b/extensions/git/src/model.ts index b40de9b49d6a3..07db4607a3dab 100644 --- a/extensions/git/src/model.ts +++ b/extensions/git/src/model.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { workspace, WorkspaceFoldersChangeEvent, Uri, window, Event, EventEmitter, QuickPickItem, Disposable, SourceControl, SourceControlResourceGroup, TextEditor, Memento, commands, LogOutputChannel, l10n, ProgressLocation, WorkspaceFolder, ThemeIcon } from 'vscode'; +import { workspace, WorkspaceFoldersChangeEvent, Uri, window, Event, EventEmitter, QuickPickItem, Disposable, SourceControl, SourceControlResourceGroup, TextEditor, Memento, commands, LogOutputChannel, l10n, ProgressLocation, WorkspaceFolder, ThemeIcon, ResourceTrustRequestOptions } from 'vscode'; import TelemetryReporter from '@vscode/extension-telemetry'; import { IRepositoryResolver, Repository, RepositoryState } from './repository'; import { memoize, sequentialize, debounce } from './decorators'; @@ -588,18 +588,15 @@ export class Model implements IRepositoryResolver, IBranchProtectionProviderRegi return; } - if (!workspace.isTrusted) { - // Check if the folder is a bare repo: if it has a file named HEAD && `rev-parse --show -cdup` is empty - try { - fs.accessSync(path.join(repoPath, 'HEAD'), fs.constants.F_OK); - const result = await this.git.exec(repoPath, ['-C', repoPath, 'rev-parse', '--show-cdup']); - if (result.stderr.trim() === '' && result.stdout.trim() === '') { - this.logger.trace(`[Model][openRepository] Bare repository: ${repoPath}`); - return; - } - } catch { - // If this throw, we should be good to open the repo (e.g. HEAD doesn't exist) - } + // Repository trust check + const result = await workspace.requestResourceTrust({ + message: l10n.t('You are opening a repository from a location that is not trusted. Do you trust the authors of the files in the repository you are opening?'), + uri: Uri.file(repoPath), + } satisfies ResourceTrustRequestOptions); + + if (!result) { + this.logger.trace(`[Model][openRepository] Repository folder is not trusted: ${repoPath}`); + return; } try { diff --git a/extensions/git/tsconfig.json b/extensions/git/tsconfig.json index e0586d16816db..db4f26a864e03 100644 --- a/extensions/git/tsconfig.json +++ b/extensions/git/tsconfig.json @@ -28,6 +28,7 @@ "../../src/vscode-dts/vscode.proposed.tabInputTextMerge.d.ts", "../../src/vscode-dts/vscode.proposed.textEditorDiffInformation.d.ts", "../../src/vscode-dts/vscode.proposed.timeline.d.ts", + "../../src/vscode-dts/vscode.proposed.workspaceTrust.d.ts", "../types/lib.textEncoder.d.ts" ] } diff --git a/src/vs/platform/workspace/common/workspaceTrust.ts b/src/vs/platform/workspace/common/workspaceTrust.ts index 36ebbb9523c10..ef67642bb0948 100644 --- a/src/vs/platform/workspace/common/workspaceTrust.ts +++ b/src/vs/platform/workspace/common/workspaceTrust.ts @@ -18,6 +18,11 @@ export interface WorkspaceTrustRequestButton { readonly type: 'ContinueWithTrust' | 'ContinueWithoutTrust' | 'Manage' | 'Cancel'; } +export interface ResourceTrustRequestOptions { + readonly uri: URI; + readonly message?: string; +} + export interface WorkspaceTrustRequestOptions { readonly buttons?: WorkspaceTrustRequestButton[]; readonly message?: string; @@ -75,10 +80,14 @@ export interface IWorkspaceTrustRequestService { readonly onDidInitiateOpenFilesTrustRequest: Event; readonly onDidInitiateWorkspaceTrustRequest: Event; readonly onDidInitiateWorkspaceTrustRequestOnStartup: Event; + readonly onDidInitiateResourcesTrustRequest: Event; completeOpenFilesTrustRequest(result: WorkspaceTrustUriResponse, saveResponse?: boolean): Promise; requestOpenFilesTrust(openFiles: URI[]): Promise; + completeResourcesTrustRequest(uri: URI, result: WorkspaceTrustUriResponse): Promise; + requestResourcesTrust(options: ResourceTrustRequestOptions): Promise; + cancelWorkspaceTrustRequest(): void; completeWorkspaceTrustRequest(trusted?: boolean): Promise; requestWorkspaceTrust(options?: WorkspaceTrustRequestOptions): Promise; diff --git a/src/vs/workbench/api/browser/mainThreadWorkspace.ts b/src/vs/workbench/api/browser/mainThreadWorkspace.ts index 7496427f1aa4b..2befe296032dd 100644 --- a/src/vs/workbench/api/browser/mainThreadWorkspace.ts +++ b/src/vs/workbench/api/browser/mainThreadWorkspace.ts @@ -15,7 +15,7 @@ import { IInstantiationService } from '../../../platform/instantiation/common/in import { ILabelService } from '../../../platform/label/common/label.js'; import { INotificationService } from '../../../platform/notification/common/notification.js'; import { AuthInfo, Credentials, IRequestService } from '../../../platform/request/common/request.js'; -import { WorkspaceTrustRequestOptions, IWorkspaceTrustManagementService, IWorkspaceTrustRequestService } from '../../../platform/workspace/common/workspaceTrust.js'; +import { WorkspaceTrustRequestOptions, IWorkspaceTrustManagementService, IWorkspaceTrustRequestService, ResourceTrustRequestOptions } from '../../../platform/workspace/common/workspaceTrust.js'; import { IWorkspace, IWorkspaceContextService, WorkbenchState, isUntitledWorkspace, WorkspaceFolder } from '../../../platform/workspace/common/workspace.js'; import { extHostNamedCustomer, IExtHostContext } from '../../services/extensions/common/extHostCustomers.js'; import { checkGlobFileExists } from '../../services/extensions/common/workspaceContains.js'; @@ -23,7 +23,7 @@ import { IFileQueryBuilderOptions, ITextQueryBuilderOptions, QueryBuilder } from import { IEditorService, ISaveEditorsResult } from '../../services/editor/common/editorService.js'; import { IFileMatch, IPatternInfo, ISearchProgressItem, ISearchService } from '../../services/search/common/search.js'; import { IWorkspaceEditingService } from '../../services/workspaces/common/workspaceEditing.js'; -import { ExtHostContext, ExtHostWorkspaceShape, ITextSearchComplete, IWorkspaceData, MainContext, MainThreadWorkspaceShape } from '../common/extHost.protocol.js'; +import { ExtHostContext, ExtHostWorkspaceShape, ITextSearchComplete, IWorkspaceData, MainContext, MainThreadWorkspaceShape, ResourceTrustRequestOptionsDto } from '../common/extHost.protocol.js'; import { IEditSessionIdentityService } from '../../../platform/workspace/common/editSessions.js'; import { EditorResourceAccessor, SaveReason, SideBySideEditor } from '../../common/editor.js'; import { coalesce } from '../../../base/common/arrays.js'; @@ -241,6 +241,11 @@ export class MainThreadWorkspace implements MainThreadWorkspaceShape { // --- trust --- + $requestResourceTrust(optionsDto: ResourceTrustRequestOptionsDto): Promise { + const options = { ...optionsDto, uri: URI.revive(optionsDto.uri) } satisfies ResourceTrustRequestOptions; + return this._workspaceTrustRequestService.requestResourcesTrust(options); + } + $requestWorkspaceTrust(options?: WorkspaceTrustRequestOptions): Promise { return this._workspaceTrustRequestService.requestWorkspaceTrust(options); } diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 779c6c822ea1a..55b0cdf30da7e 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -1259,6 +1259,10 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I get isTrusted() { return extHostWorkspace.trusted; }, + requestResourceTrust: (options: vscode.ResourceTrustRequestOptions) => { + checkProposedApiEnabled(extension, 'workspaceTrust'); + return extHostWorkspace.requestResourceTrust(options); + }, requestWorkspaceTrust: (options?: vscode.WorkspaceTrustRequestOptions) => { checkProposedApiEnabled(extension, 'workspaceTrust'); return extHostWorkspace.requestWorkspaceTrust(options); diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index a61232dde5e64..9aee5348fa8b4 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -1595,6 +1595,11 @@ export interface ITextSearchComplete { message?: TextSearchCompleteMessage | TextSearchCompleteMessage[]; } +export interface ResourceTrustRequestOptionsDto { + readonly uri: UriComponents; + readonly message?: string; +} + export interface MainThreadWorkspaceShape extends IDisposable { $startFileSearch(includeFolder: UriComponents | null, options: IFileQueryBuilderOptions, token: CancellationToken): Promise; $startTextSearch(query: search.IPatternInfo, folder: UriComponents | null, options: ITextQueryBuilderOptions, requestId: number, token: CancellationToken): Promise; @@ -1606,6 +1611,7 @@ export interface MainThreadWorkspaceShape extends IDisposable { $lookupAuthorization(authInfo: AuthInfo): Promise; $lookupKerberosAuthorization(url: string): Promise; $loadCertificates(): Promise; + $requestResourceTrust(options: ResourceTrustRequestOptionsDto): Promise; $requestWorkspaceTrust(options?: WorkspaceTrustRequestOptions): Promise; $registerEditSessionIdentityProvider(handle: number, scheme: string): void; $unregisterEditSessionIdentityProvider(handle: number): void; diff --git a/src/vs/workbench/api/common/extHostWorkspace.ts b/src/vs/workbench/api/common/extHostWorkspace.ts index bcc691f7fa4ed..00f258e8df2eb 100644 --- a/src/vs/workbench/api/common/extHostWorkspace.ts +++ b/src/vs/workbench/api/common/extHostWorkspace.ts @@ -802,6 +802,10 @@ export class ExtHostWorkspace implements ExtHostWorkspaceShape, IExtHostWorkspac return this._trusted; } + requestResourceTrust(options: vscode.ResourceTrustRequestOptions): Promise { + return this._proxy.$requestResourceTrust(options); + } + requestWorkspaceTrust(options?: vscode.WorkspaceTrustRequestOptions): Promise { return this._proxy.$requestWorkspaceTrust(options); } diff --git a/src/vs/workbench/contrib/workspace/browser/workspace.contribution.ts b/src/vs/workbench/contrib/workspace/browser/workspace.contribution.ts index c512b648529b5..aa9493a1567ac 100644 --- a/src/vs/workbench/contrib/workspace/browser/workspace.contribution.ts +++ b/src/vs/workbench/contrib/workspace/browser/workspace.contribution.ts @@ -147,6 +147,36 @@ export class WorkspaceTrustRequestHandler extends Disposable implements IWorkben }); })); + // Resources trust request + this._register(this.workspaceTrustRequestService.onDidInitiateResourcesTrustRequest(async (options) => { + await this.workspaceTrustManagementService.workspaceResolved; + + // Details + const markdownDetails = [ + options?.message ?? localize('resourcesTrustDetails', "You are trying to open an untrusted folder. Do you trust the authors of this content?"), + localize('resourcesTrustLearnMore', "If you don't trust the authors of these files, we recommend not continuing as the files may be malicious. See [our docs](https://aka.ms/vscode-workspace-trust) to learn more.") + ]; + + // Dialog + await this.dialogService.prompt({ + type: Severity.Info, + message: localize('resourcesTrustMessage', "Do you trust the authors of the files in this folder?"), + buttons: [ + { + label: localize({ key: 'trustResources', comment: ['&& denotes a mnemonic'] }, "&&Trust Folder & Continue"), + run: () => this.workspaceTrustRequestService.completeResourcesTrustRequest(options.uri, WorkspaceTrustUriResponse.Open) + } + ], + cancelButton: { + run: () => this.workspaceTrustRequestService.completeResourcesTrustRequest(options.uri, WorkspaceTrustUriResponse.Cancel) + }, + custom: { + icon: Codicon.shield, + markdownDetails: markdownDetails.map(md => { return { markdown: new MarkdownString(md) }; }) + } + }); + })); + // Workspace trust request this._register(this.workspaceTrustRequestService.onDidInitiateWorkspaceTrustRequest(async requestOptions => { await this.workspaceTrustManagementService.workspaceResolved; diff --git a/src/vs/workbench/services/workspaces/common/workspaceTrust.ts b/src/vs/workbench/services/workspaces/common/workspaceTrust.ts index 81e080c885d72..9a5010337100b 100644 --- a/src/vs/workbench/services/workspaces/common/workspaceTrust.ts +++ b/src/vs/workbench/services/workspaces/common/workspaceTrust.ts @@ -16,7 +16,7 @@ import { getRemoteAuthority } from '../../../../platform/remote/common/remoteHos import { isVirtualResource } from '../../../../platform/workspace/common/virtualWorkspace.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; import { ISingleFolderWorkspaceIdentifier, isSavedWorkspace, isSingleFolderWorkspaceIdentifier, isTemporaryWorkspace, IWorkspace, IWorkspaceContextService, IWorkspaceFolder, toWorkspaceIdentifier, WorkbenchState } from '../../../../platform/workspace/common/workspace.js'; -import { WorkspaceTrustRequestOptions, IWorkspaceTrustManagementService, IWorkspaceTrustInfo, IWorkspaceTrustUriInfo, IWorkspaceTrustRequestService, IWorkspaceTrustTransitionParticipant, WorkspaceTrustUriResponse, IWorkspaceTrustEnablementService } from '../../../../platform/workspace/common/workspaceTrust.js'; +import { WorkspaceTrustRequestOptions, IWorkspaceTrustManagementService, IWorkspaceTrustInfo, IWorkspaceTrustUriInfo, IWorkspaceTrustRequestService, IWorkspaceTrustTransitionParticipant, WorkspaceTrustUriResponse, IWorkspaceTrustEnablementService, ResourceTrustRequestOptions } from '../../../../platform/workspace/common/workspaceTrust.js'; import { Memento } from '../../../common/memento.js'; import { IWorkbenchEnvironmentService } from '../../environment/common/environmentService.js'; import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js'; @@ -24,6 +24,7 @@ import { isEqualAuthority } from '../../../../base/common/resources.js'; import { isWeb } from '../../../../base/common/platform.js'; import { IFileService } from '../../../../platform/files/common/files.js'; import { promiseWithResolvers } from '../../../../base/common/async.js'; +import { ResourceMap } from '../../../../base/common/map.js'; export const WORKSPACE_TRUST_ENABLED = 'security.workspace.trust.enabled'; export const WORKSPACE_TRUST_STARTUP_PROMPT = 'security.workspace.trust.startupPrompt'; @@ -660,12 +661,18 @@ export class WorkspaceTrustRequestService extends Disposable implements IWorkspa private _openFilesTrustRequestPromise?: Promise; private _openFilesTrustRequestResolver?: (response: WorkspaceTrustUriResponse) => void; + private readonly _resourcesTrustRequestPromises = new ResourceMap>(); + private readonly _resourcesTrustRequestResolvers = new ResourceMap<(trusted: boolean | undefined) => void>(); + private _workspaceTrustRequestPromise?: Promise; private _workspaceTrustRequestResolver?: (trusted: boolean | undefined) => void; private readonly _onDidInitiateOpenFilesTrustRequest = this._register(new Emitter()); readonly onDidInitiateOpenFilesTrustRequest = this._onDidInitiateOpenFilesTrustRequest.event; + private readonly _onDidInitiateResourcesTrustRequest = this._register(new Emitter()); + readonly onDidInitiateResourcesTrustRequest = this._onDidInitiateResourcesTrustRequest.event; + private readonly _onDidInitiateWorkspaceTrustRequest = this._register(new Emitter()); readonly onDidInitiateWorkspaceTrustRequest = this._onDidInitiateWorkspaceTrustRequest.event; @@ -761,6 +768,48 @@ export class WorkspaceTrustRequestService extends Disposable implements IWorkspa //#endregion + //#region Resource(s) trust request + + async completeResourcesTrustRequest(uri: URI, result: WorkspaceTrustUriResponse): Promise { + const resolver = this._resourcesTrustRequestResolvers.get(uri); + if (!resolver) { + return; + } + + const trusted = result === WorkspaceTrustUriResponse.Open; + await this.workspaceTrustManagementService.setUrisTrust([uri], trusted); + + resolver(trusted); + + this._resourcesTrustRequestResolvers.delete(uri); + this._resourcesTrustRequestPromises.delete(uri); + } + + async requestResourcesTrust(options: ResourceTrustRequestOptions): Promise { + // Check if all resources are already trusted + const resourcesTrustInfo = await this.workspaceTrustManagementService.getUriTrustInfo(options.uri); + if (resourcesTrustInfo.trusted) { + return true; + } + + // Return existing promise for this URI + const existingPromise = this._resourcesTrustRequestPromises.get(options.uri); + if (existingPromise) { + return existingPromise; + } + + // Create a new promise for this URI + const promise = new Promise(resolve => { + this._resourcesTrustRequestResolvers.set(options.uri, resolve); + }); + this._resourcesTrustRequestPromises.set(options.uri, promise); + this._onDidInitiateResourcesTrustRequest.fire(options); + + return promise; + } + + //#endregion + //#region Workspace trust request private resolveWorkspaceTrustRequest(trusted?: boolean): void { diff --git a/src/vs/workbench/test/common/workbenchTestServices.ts b/src/vs/workbench/test/common/workbenchTestServices.ts index e967c83d2fd4c..fb2eb6d0ac3d9 100644 --- a/src/vs/workbench/test/common/workbenchTestServices.ts +++ b/src/vs/workbench/test/common/workbenchTestServices.ts @@ -27,7 +27,7 @@ import { IProgress, IProgressStep } from '../../../platform/progress/common/prog import { InMemoryStorageService, WillSaveStateReason } from '../../../platform/storage/common/storage.js'; import { toUserDataProfile } from '../../../platform/userDataProfile/common/userDataProfile.js'; import { ISingleFolderWorkspaceIdentifier, IWorkspace, IWorkspaceContextService, IWorkspaceFolder, IWorkspaceFoldersChangeEvent, IWorkspaceFoldersWillChangeEvent, IWorkspaceIdentifier, WorkbenchState, Workspace } from '../../../platform/workspace/common/workspace.js'; -import { IWorkspaceTrustEnablementService, IWorkspaceTrustManagementService, IWorkspaceTrustRequestService, IWorkspaceTrustTransitionParticipant, IWorkspaceTrustUriInfo, WorkspaceTrustRequestOptions, WorkspaceTrustUriResponse } from '../../../platform/workspace/common/workspaceTrust.js'; +import { IWorkspaceTrustEnablementService, IWorkspaceTrustManagementService, IWorkspaceTrustRequestService, IWorkspaceTrustTransitionParticipant, IWorkspaceTrustUriInfo, ResourceTrustRequestOptions, WorkspaceTrustRequestOptions, WorkspaceTrustUriResponse } from '../../../platform/workspace/common/workspaceTrust.js'; import { TestWorkspace } from '../../../platform/workspace/test/common/testWorkspace.js'; import { GroupIdentifier, IRevertOptions, ISaveOptions, SaveReason } from '../../common/editor.js'; import { EditorInput } from '../../common/editor/editorInput.js'; @@ -451,6 +451,9 @@ export class TestWorkspaceTrustRequestService extends Disposable implements IWor private readonly _onDidInitiateOpenFilesTrustRequest = this._register(new Emitter()); readonly onDidInitiateOpenFilesTrustRequest = this._onDidInitiateOpenFilesTrustRequest.event; + private readonly _onDidInitiateResourcesTrustRequest = this._register(new Emitter()); + readonly onDidInitiateResourcesTrustRequest = this._onDidInitiateResourcesTrustRequest.event; + private readonly _onDidInitiateWorkspaceTrustRequest = this._register(new Emitter()); readonly onDidInitiateWorkspaceTrustRequest = this._onDidInitiateWorkspaceTrustRequest.event; @@ -473,6 +476,14 @@ export class TestWorkspaceTrustRequestService extends Disposable implements IWor throw new Error('Method not implemented.'); } + async completeResourcesTrustRequest(uri: URI, result: WorkspaceTrustUriResponse): Promise { + throw new Error('Method not implemented.'); + } + + async requestResourcesTrust(options: ResourceTrustRequestOptions): Promise { + return this._trusted; + } + cancelWorkspaceTrustRequest(): void { throw new Error('Method not implemented.'); } diff --git a/src/vscode-dts/vscode.proposed.workspaceTrust.d.ts b/src/vscode-dts/vscode.proposed.workspaceTrust.d.ts index d48071af2ccb4..2c8edbd9d182f 100644 --- a/src/vscode-dts/vscode.proposed.workspaceTrust.d.ts +++ b/src/vscode-dts/vscode.proposed.workspaceTrust.d.ts @@ -7,6 +7,20 @@ declare module 'vscode' { // https://github.com/microsoft/vscode/issues/120173 + export interface ResourceTrustRequestOptions { + /** + * An resource related to the trust request. + */ + readonly uri: Uri; + + /** + * Custom message describing the user action that requires resource + * trust. If omitted, a generic message will be displayed in the resource + * trust request dialog. + */ + readonly message?: string; + } + /** * The object describing the properties of the workspace trust request */ @@ -20,6 +34,12 @@ declare module 'vscode' { } export namespace workspace { + /** + * Prompt the user to chose whether to trust the specified resource (ex: folder) + * @param options Object describing the properties of the resource trust request. + */ + export function requestResourceTrust(options: ResourceTrustRequestOptions): Thenable; + /** * Prompt the user to chose whether to trust the current workspace * @param options Optional object describing the properties of the