Skip to content

Commit

Permalink
Disables Git access in Restricted Mode (untrusted)
Browse files Browse the repository at this point in the history
  • Loading branch information
eamodio committed May 17, 2023
1 parent 2ff1890 commit ee2a0c4
Show file tree
Hide file tree
Showing 8 changed files with 105 additions and 35 deletions.
8 changes: 7 additions & 1 deletion src/env/node/git/git.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import type { CoreConfiguration } from '../../../constants';
import { GlyphChars } from '../../../constants';
import type { GitCommandOptions, GitSpawnOptions } 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 { isUncommitted, isUncommittedStaged, shortenRevision } from '../../../git/models/reference';
import type { GitUser } from '../../../git/models/user';
Expand Down Expand Up @@ -123,6 +123,8 @@ export class Git {
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> {
if (!workspace.isTrusted) throw new WorkspaceUntrustedError();

const start = hrtime();

const { configs, correlationKey, errors: errorHandling, encoding, ...opts } = options;
Expand Down Expand Up @@ -224,6 +226,8 @@ export class Git {
}

async gitSpawn(options: GitSpawnOptions, ...args: any[]): Promise<ChildProcess> {
if (!workspace.isTrusted) throw new WorkspaceUntrustedError();

const start = hrtime();

const { cancellation, configs, stdin, stdinEncoding, ...opts } = options;
Expand Down Expand Up @@ -1645,6 +1649,8 @@ export class Git {
? (emptyArray as [])
: [true, normalizePath(data.trimStart().replace(/[\r|\n]+$/, ''))];
} catch (ex) {
if (ex instanceof WorkspaceUntrustedError) return emptyArray as [];

const unsafeMatch =
/^fatal: detected dubious ownership in repository at '([^']+)'[\s\S]*git config --global --add safe\.directory '?([^'\n]+)'?$/m.exec(
ex.stderr,
Expand Down
2 changes: 1 addition & 1 deletion src/env/node/git/localGitProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1088,7 +1088,7 @@ export class LocalGitProvider implements GitProvider, Disposable {
[safe, repoPath] = await this.git.rev_parse__show_toplevel(uri.fsPath);
if (safe) {
this.unsafePaths.delete(uri.fsPath);
} else {
} else if (safe === false) {
this.unsafePaths.add(uri.fsPath);
}
if (!repoPath) return undefined;
Expand Down
9 changes: 9 additions & 0 deletions src/git/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,15 @@ export class StashPushError extends Error {
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 {
AlreadyCheckedOut = 1,
AlreadyExists = 2,
Expand Down
13 changes: 13 additions & 0 deletions src/git/gitProviderService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,12 @@ import type { RichRemoteProvider } from './remotes/richRemoteProvider';
import type { GitSearch, SearchQuery } from './search';

const emptyArray = Object.freeze([]) as unknown as any[];
const emptyDisposable = Object.freeze({
dispose: () => {
/* noop */
},
});

const maxDefaultBranchWeight = 100;
const weightedDefaultBranches = new Map<string, number>([
['master', maxDefaultBranchWeight],
Expand Down Expand Up @@ -216,6 +222,13 @@ export class GitProviderService implements Disposable {
this.resetCaches('providers');
this.updateContext();
}),
!workspace.isTrusted
? workspace.onDidGrantWorkspaceTrust(() => {
if (workspace.isTrusted && workspace.workspaceFolders?.length) {
void this.discoverRepositories(workspace.workspaceFolders, { force: true });
}
})
: emptyDisposable,
...this.registerCommands(),
);

Expand Down
11 changes: 11 additions & 0 deletions src/webviews/apps/home/home.html
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,17 @@ <h1 class="alert__title">Unsafe repository</h1>
</p>
</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>
<main class="home__main scrollable" id="main" tabindex="-1">
Expand Down
64 changes: 36 additions & 28 deletions src/webviews/apps/home/home.ts
Original file line number Diff line number Diff line change
Expand Up @@ -247,36 +247,26 @@ export class HomeApp extends App<State> {
}

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() {
Expand Down Expand Up @@ -371,6 +361,24 @@ export class HomeApp extends App<State> {
}
}

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) {
const hasStep = list.includes(item);
if (!hasStep && add) {
Expand Down
32 changes: 27 additions & 5 deletions src/webviews/home/homeWebview.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { ConfigurationChangeEvent } from 'vscode';
import { Disposable, window } from 'vscode';
import { Disposable, window, workspace } from 'vscode';
import { getAvatarUriFromGravatarEmail } from '../../avatars';
import { ViewsLayout } from '../../commands/setViewsLayout';
import type { Container } from '../../container';
Expand All @@ -10,11 +10,18 @@ import { executeCoreCommand, registerCommand } from '../../system/command';
import { configuration } from '../../system/configuration';
import type { Deferrable } from '../../system/function';
import { debounce } from '../../system/function';
import { getSettledValue } from '../../system/promise';
import type { StorageChangeEvent } from '../../system/storage';
import type { IpcMessage } from '../protocol';
import { onIpc } from '../protocol';
import type { WebviewController, WebviewProvider } from '../webviewController';
import type { CompleteStepParams, DismissBannerParams, DismissSectionParams, State } from './protocol';
import type {
CompleteStepParams,
DidChangeRepositoriesParams,
DismissBannerParams,
DismissSectionParams,
State,
} from './protocol';
import {
CompletedActions,
CompleteStepCommandType,
Expand All @@ -27,6 +34,12 @@ import {
DismissStatusCommandType,
} from './protocol';

const emptyDisposable = Object.freeze({
dispose: () => {
/* noop */
},
});

export class HomeWebviewProvider implements WebviewProvider<State> {
private readonly _disposable: Disposable;

Expand All @@ -36,6 +49,9 @@ export class HomeWebviewProvider implements WebviewProvider<State> {
this.container.git.onDidChangeRepositories(this.onRepositoriesChanged, this),
configuration.onDidChange(this.onConfigurationChanged, this),
this.container.storage.onDidChange(this.onStorageChanged, this),
!workspace.isTrusted
? workspace.onDidGrantWorkspaceTrust(this.notifyDidChangeRepositories, this)
: emptyDisposable,
);
}

Expand Down Expand Up @@ -221,7 +237,12 @@ export class HomeWebviewProvider implements WebviewProvider<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 sections = this.container.storage.get('home:sections:dismissed', []);
const dismissedBanners = this.container.storage.get('home:banners:dismissed', []);
Expand All @@ -233,7 +254,7 @@ export class HomeWebviewProvider implements WebviewProvider<State> {
subscription: sub.subscription,
completedActions: sub.completedActions,
plusEnabled: this.getPlusEnabled(),
visibility: await this.getRepoVisibility(),
visibility: getSettledValue(visibilityResult)!,
completedSteps: steps,
dismissedSections: sections,
avatar: sub.avatar,
Expand All @@ -255,11 +276,12 @@ export class HomeWebviewProvider implements WebviewProvider<State> {
});
}

private getRepositoriesState() {
private getRepositoriesState(): DidChangeRepositoriesParams {
return {
count: this.container.git.repositoryCount,
openCount: this.container.git.openRepositoryCount,
hasUnsafe: this.container.git.hasUnsafeRepositories(),
trusted: workspace.isTrusted,
};
}

Expand Down
1 change: 1 addition & 0 deletions src/webviews/home/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ export interface DidChangeRepositoriesParams {
count: number;
openCount: number;
hasUnsafe: boolean;
trusted: boolean;
}
export const DidChangeRepositoriesType = new IpcNotificationType<DidChangeRepositoriesParams>('repositories/didChange');

Expand Down

0 comments on commit ee2a0c4

Please sign in to comment.