Browse Source

Disables Git access in Restricted Mode (untrusted)

main
Eric Amodio 1 year ago
parent
commit
ee2a0c42a9
8 changed files with 105 additions and 35 deletions
  1. +7
    -1
      src/env/node/git/git.ts
  2. +1
    -1
      src/env/node/git/localGitProvider.ts
  3. +9
    -0
      src/git/errors.ts
  4. +13
    -0
      src/git/gitProviderService.ts
  5. +11
    -0
      src/webviews/apps/home/home.html
  6. +36
    -28
      src/webviews/apps/home/home.ts
  7. +27
    -5
      src/webviews/home/homeWebview.ts
  8. +1
    -0
      src/webviews/home/protocol.ts

+ 7
- 1
src/env/node/git/git.ts View File

@ -9,7 +9,7 @@ import type { CoreConfiguration } from '../../../constants';
import { GlyphChars } from '../../../constants'; import { GlyphChars } from '../../../constants';
import type { GitCommandOptions, GitSpawnOptions } from '../../../git/commandOptions'; import type { GitCommandOptions, GitSpawnOptions } from '../../../git/commandOptions';
import { GitErrorHandling } from '../../../git/commandOptions'; import { GitErrorHandling } from '../../../git/commandOptions';
import { StashPushError, StashPushErrorReason } from '../../../git/errors';
import { StashPushError, StashPushErrorReason, WorkspaceUntrustedError } from '../../../git/errors';
import type { GitDiffFilter } from '../../../git/models/diff'; import type { GitDiffFilter } from '../../../git/models/diff';
import { isUncommitted, isUncommittedStaged, shortenRevision } from '../../../git/models/reference'; import { isUncommitted, isUncommittedStaged, shortenRevision } from '../../../git/models/reference';
import type { GitUser } from '../../../git/models/user'; import type { GitUser } from '../../../git/models/user';
@ -123,6 +123,8 @@ export class Git {
async git(options: ExitCodeOnlyGitCommandOptions, ...args: any[]): Promise<number>; async git(options: ExitCodeOnlyGitCommandOptions, ...args: any[]): Promise<number>;
async git<T extends string | Buffer>(options: GitCommandOptions, ...args: any[]): Promise<T>; async git<T extends string | Buffer>(options: GitCommandOptions, ...args: any[]): Promise<T>;
async git<T extends string | Buffer>(options: GitCommandOptions, ...args: any[]): Promise<T> { async git<T extends string | Buffer>(options: GitCommandOptions, ...args: any[]): Promise<T> {
if (!workspace.isTrusted) throw new WorkspaceUntrustedError();
const start = hrtime(); const start = hrtime();
const { configs, correlationKey, errors: errorHandling, encoding, ...opts } = options; const { configs, correlationKey, errors: errorHandling, encoding, ...opts } = options;
@ -224,6 +226,8 @@ export class Git {
} }
async gitSpawn(options: GitSpawnOptions, ...args: any[]): Promise<ChildProcess> { async gitSpawn(options: GitSpawnOptions, ...args: any[]): Promise<ChildProcess> {
if (!workspace.isTrusted) throw new WorkspaceUntrustedError();
const start = hrtime(); const start = hrtime();
const { cancellation, configs, stdin, stdinEncoding, ...opts } = options; const { cancellation, configs, stdin, stdinEncoding, ...opts } = options;
@ -1645,6 +1649,8 @@ export class Git {
? (emptyArray as []) ? (emptyArray as [])
: [true, normalizePath(data.trimStart().replace(/[\r|\n]+$/, ''))]; : [true, normalizePath(data.trimStart().replace(/[\r|\n]+$/, ''))];
} catch (ex) { } catch (ex) {
if (ex instanceof WorkspaceUntrustedError) return emptyArray as [];
const unsafeMatch = const unsafeMatch =
/^fatal: detected dubious ownership in repository at '([^']+)'[\s\S]*git config --global --add safe\.directory '?([^'\n]+)'?$/m.exec( /^fatal: detected dubious ownership in repository at '([^']+)'[\s\S]*git config --global --add safe\.directory '?([^'\n]+)'?$/m.exec(
ex.stderr, ex.stderr,

+ 1
- 1
src/env/node/git/localGitProvider.ts View File

@ -1088,7 +1088,7 @@ export class LocalGitProvider implements GitProvider, Disposable {
[safe, repoPath] = await this.git.rev_parse__show_toplevel(uri.fsPath); [safe, repoPath] = await this.git.rev_parse__show_toplevel(uri.fsPath);
if (safe) { if (safe) {
this.unsafePaths.delete(uri.fsPath); this.unsafePaths.delete(uri.fsPath);
} else {
} else if (safe === false) {
this.unsafePaths.add(uri.fsPath); this.unsafePaths.add(uri.fsPath);
} }
if (!repoPath) return undefined; if (!repoPath) return undefined;

+ 9
- 0
src/git/errors.ts View File

@ -81,6 +81,15 @@ export class StashPushError extends Error {
Error.captureStackTrace?.(this, StashApplyError); Error.captureStackTrace?.(this, StashApplyError);
} }
} }
export class WorkspaceUntrustedError extends Error {
constructor() {
super('Unable to perform Git operations because the current workspace is untrusted');
Error.captureStackTrace?.(this, WorkspaceUntrustedError);
}
}
export const enum WorktreeCreateErrorReason { export const enum WorktreeCreateErrorReason {
AlreadyCheckedOut = 1, AlreadyCheckedOut = 1,
AlreadyExists = 2, AlreadyExists = 2,

+ 13
- 0
src/git/gitProviderService.ts View File

@ -82,6 +82,12 @@ import type { RichRemoteProvider } from './remotes/richRemoteProvider';
import type { GitSearch, SearchQuery } from './search'; import type { GitSearch, SearchQuery } from './search';
const emptyArray = Object.freeze([]) as unknown as any[]; const emptyArray = Object.freeze([]) as unknown as any[];
const emptyDisposable = Object.freeze({
dispose: () => {
/* noop */
},
});
const maxDefaultBranchWeight = 100; const maxDefaultBranchWeight = 100;
const weightedDefaultBranches = new Map<string, number>([ const weightedDefaultBranches = new Map<string, number>([
['master', maxDefaultBranchWeight], ['master', maxDefaultBranchWeight],
@ -216,6 +222,13 @@ export class GitProviderService implements Disposable {
this.resetCaches('providers'); this.resetCaches('providers');
this.updateContext(); this.updateContext();
}), }),
!workspace.isTrusted
? workspace.onDidGrantWorkspaceTrust(() => {
if (workspace.isTrusted && workspace.workspaceFolders?.length) {
void this.discoverRepositories(workspace.workspaceFolders, { force: true });
}
})
: emptyDisposable,
...this.registerCommands(), ...this.registerCommands(),
); );

+ 11
- 0
src/webviews/apps/home/home.html View File

@ -96,6 +96,17 @@
</p> </p>
</div> </div>
</div> </div>
<div id="untrusted-alert" class="alert alert--info mb-0" aria-hidden="true" hidden>
<h1 class="alert__title">Untrusted workspace</h1>
<div class="alert__description">
<p>Unable to open repositories in Restricted Mode.</p>
<p class="centered">
<vscode-button data-action="command:workbench.trust.manage"
>Manage Workspace Trust</vscode-button
>
</p>
</div>
</div>
<header-card id="header-card" image="#{webroot}/media/gitlens-logo.webp"></header-card> <header-card id="header-card" image="#{webroot}/media/gitlens-logo.webp"></header-card>
</header> </header>
<main class="home__main scrollable" id="main" tabindex="-1"> <main class="home__main scrollable" id="main" tabindex="-1">

+ 36
- 28
src/webviews/apps/home/home.ts View File

@ -247,36 +247,26 @@ export class HomeApp extends App {
} }
private updateNoRepo() { private updateNoRepo() {
const { repositories } = this.state;
const hasRepos = repositories.openCount > 0;
const value = hasRepos ? 'true' : 'false';
let $el = document.getElementById('no-repo');
$el?.setAttribute('aria-hidden', value);
if (hasRepos) {
$el?.setAttribute('hidden', value);
} else {
$el?.removeAttribute('hidden');
}
const {
repositories: { openCount, hasUnsafe, trusted },
} = this.state;
$el = document.getElementById('no-repo-alert');
const showUnsafe = repositories.hasUnsafe && !hasRepos;
const $unsafeEl = document.getElementById('unsafe-repo-alert');
if (showUnsafe) {
$el?.setAttribute('aria-hidden', 'true');
$el?.setAttribute('hidden', 'true');
$unsafeEl?.setAttribute('aria-hidden', 'false');
$unsafeEl?.removeAttribute('hidden');
} else {
$unsafeEl?.setAttribute('aria-hidden', 'true');
$unsafeEl?.setAttribute('hidden', 'true');
$el?.setAttribute('aria-hidden', value);
if (hasRepos) {
$el?.setAttribute('hidden', value);
} else {
$el?.removeAttribute('hidden');
}
if (!trusted) {
setElementVisibility('untrusted-alert', true);
setElementVisibility('no-repo', false);
setElementVisibility('no-repo-alert', false);
setElementVisibility('unsafe-repo-alert', false);
return;
} }
setElementVisibility('untrusted-alert', false);
const noRepos = openCount === 0;
setElementVisibility('no-repo', noRepos);
setElementVisibility('no-repo-alert', noRepos && !hasUnsafe);
setElementVisibility('unsafe-repo-alert', hasUnsafe);
} }
private updateLayout() { private updateLayout() {
@ -371,6 +361,24 @@ export class HomeApp extends App {
} }
} }
function setElementVisibility(elementOrId: string | HTMLElement | null | undefined, visible: boolean) {
let el;
if (typeof elementOrId === 'string') {
el = document.getElementById(elementOrId);
} else {
el = elementOrId;
}
if (el == null) return;
if (visible) {
el.setAttribute('aria-hidden', 'false');
el.removeAttribute('hidden');
} else {
el.setAttribute('aria-hidden', 'true');
el?.setAttribute('hidden', 'true');
}
}
function toggleArrayItem(list: string[] = [], item: string, add = true) { function toggleArrayItem(list: string[] = [], item: string, add = true) {
const hasStep = list.includes(item); const hasStep = list.includes(item);
if (!hasStep && add) { if (!hasStep && add) {

+ 27
- 5
src/webviews/home/homeWebview.ts View File

@ -1,5 +1,5 @@
import type { ConfigurationChangeEvent } from 'vscode'; import type { ConfigurationChangeEvent } from 'vscode';
import { Disposable, window } from 'vscode';
import { Disposable, window, workspace } from 'vscode';
import { getAvatarUriFromGravatarEmail } from '../../avatars'; import { getAvatarUriFromGravatarEmail } from '../../avatars';
import { ViewsLayout } from '../../commands/setViewsLayout'; import { ViewsLayout } from '../../commands/setViewsLayout';
import type { Container } from '../../container'; import type { Container } from '../../container';
@ -10,11 +10,18 @@ import { executeCoreCommand, registerCommand } from '../../system/command';
import { configuration } from '../../system/configuration'; import { configuration } from '../../system/configuration';
import type { Deferrable } from '../../system/function'; import type { Deferrable } from '../../system/function';
import { debounce } from '../../system/function'; import { debounce } from '../../system/function';
import { getSettledValue } from '../../system/promise';
import type { StorageChangeEvent } from '../../system/storage'; import type { StorageChangeEvent } from '../../system/storage';
import type { IpcMessage } from '../protocol'; import type { IpcMessage } from '../protocol';
import { onIpc } from '../protocol'; import { onIpc } from '../protocol';
import type { WebviewController, WebviewProvider } from '../webviewController'; import type { WebviewController, WebviewProvider } from '../webviewController';
import type { CompleteStepParams, DismissBannerParams, DismissSectionParams, State } from './protocol';
import type {
CompleteStepParams,
DidChangeRepositoriesParams,
DismissBannerParams,
DismissSectionParams,
State,
} from './protocol';
import { import {
CompletedActions, CompletedActions,
CompleteStepCommandType, CompleteStepCommandType,
@ -27,6 +34,12 @@ import {
DismissStatusCommandType, DismissStatusCommandType,
} from './protocol'; } from './protocol';
const emptyDisposable = Object.freeze({
dispose: () => {
/* noop */
},
});
export class HomeWebviewProvider implements WebviewProvider<State> { export class HomeWebviewProvider implements WebviewProvider<State> {
private readonly _disposable: Disposable; private readonly _disposable: Disposable;
@ -36,6 +49,9 @@ export class HomeWebviewProvider implements WebviewProvider {
this.container.git.onDidChangeRepositories(this.onRepositoriesChanged, this), this.container.git.onDidChangeRepositories(this.onRepositoriesChanged, this),
configuration.onDidChange(this.onConfigurationChanged, this), configuration.onDidChange(this.onConfigurationChanged, this),
this.container.storage.onDidChange(this.onStorageChanged, this), this.container.storage.onDidChange(this.onStorageChanged, this),
!workspace.isTrusted
? workspace.onDidGrantWorkspaceTrust(this.notifyDidChangeRepositories, this)
: emptyDisposable,
); );
} }
@ -221,7 +237,12 @@ export class HomeWebviewProvider implements WebviewProvider {
} }
private async getState(subscription?: Subscription): Promise<State> { private async getState(subscription?: Subscription): Promise<State> {
const sub = await this.getSubscription(subscription);
const [visibilityResult, subscriptionResult] = await Promise.allSettled([
this.getRepoVisibility(),
this.getSubscription(subscription),
]);
const sub = getSettledValue(subscriptionResult)!;
const steps = this.container.storage.get('home:steps:completed', []); const steps = this.container.storage.get('home:steps:completed', []);
const sections = this.container.storage.get('home:sections:dismissed', []); const sections = this.container.storage.get('home:sections:dismissed', []);
const dismissedBanners = this.container.storage.get('home:banners:dismissed', []); const dismissedBanners = this.container.storage.get('home:banners:dismissed', []);
@ -233,7 +254,7 @@ export class HomeWebviewProvider implements WebviewProvider {
subscription: sub.subscription, subscription: sub.subscription,
completedActions: sub.completedActions, completedActions: sub.completedActions,
plusEnabled: this.getPlusEnabled(), plusEnabled: this.getPlusEnabled(),
visibility: await this.getRepoVisibility(),
visibility: getSettledValue(visibilityResult)!,
completedSteps: steps, completedSteps: steps,
dismissedSections: sections, dismissedSections: sections,
avatar: sub.avatar, avatar: sub.avatar,
@ -255,11 +276,12 @@ export class HomeWebviewProvider implements WebviewProvider {
}); });
} }
private getRepositoriesState() {
private getRepositoriesState() class="o">: DidChangeRepositoriesParams {
return { return {
count: this.container.git.repositoryCount, count: this.container.git.repositoryCount,
openCount: this.container.git.openRepositoryCount, openCount: this.container.git.openRepositoryCount,
hasUnsafe: this.container.git.hasUnsafeRepositories(), hasUnsafe: this.container.git.hasUnsafeRepositories(),
trusted: workspace.isTrusted,
}; };
} }

+ 1
- 0
src/webviews/home/protocol.ts View File

@ -57,6 +57,7 @@ export interface DidChangeRepositoriesParams {
count: number; count: number;
openCount: number; openCount: number;
hasUnsafe: boolean; hasUnsafe: boolean;
trusted: boolean;
} }
export const DidChangeRepositoriesType = new IpcNotificationType<DidChangeRepositoriesParams>('repositories/didChange'); export const DidChangeRepositoriesType = new IpcNotificationType<DidChangeRepositoriesParams>('repositories/didChange');

Loading…
Cancel
Save