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 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';
@ -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;
@ -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;
@ -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,

+ 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);
if (safe) {
this.unsafePaths.delete(uri.fsPath);
} else {
} else if (safe === false) {
this.unsafePaths.add(uri.fsPath);
}
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);
}
}
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,

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

@ -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],
@ -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(),
);

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

@ -96,6 +96,17 @@
</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">

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

@ -247,36 +247,26 @@ export class HomeApp extends App {
}
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() {
@ -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) {
const hasStep = list.includes(item);
if (!hasStep && add) {

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

@ -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';
@ -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,
@ -27,6 +34,12 @@ import {
DismissStatusCommandType,
} from './protocol';
const emptyDisposable = Object.freeze({
dispose: () => {
/* noop */
},
});
export class HomeWebviewProvider implements WebviewProvider<State> {
private readonly _disposable: Disposable;
@ -36,6 +49,9 @@ export class HomeWebviewProvider implements WebviewProvider {
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,
);
}
@ -221,7 +237,12 @@ export class HomeWebviewProvider implements WebviewProvider {
}
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', []);
@ -233,7 +254,7 @@ export class HomeWebviewProvider implements WebviewProvider {
subscription: sub.subscription,
completedActions: sub.completedActions,
plusEnabled: this.getPlusEnabled(),
visibility: await this.getRepoVisibility(),
visibility: getSettledValue(visibilityResult)!,
completedSteps: steps,
dismissedSections: sections,
avatar: sub.avatar,
@ -255,11 +276,12 @@ export class HomeWebviewProvider implements WebviewProvider {
});
}
private getRepositoriesState() {
private getRepositoriesState() class="o">: DidChangeRepositoriesParams {
return {
count: this.container.git.repositoryCount,
openCount: this.container.git.openRepositoryCount,
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;
openCount: number;
hasUnsafe: boolean;
trusted: boolean;
}
export const DidChangeRepositoriesType = new IpcNotificationType<DidChangeRepositoriesParams>('repositories/didChange');

Loading…
Cancel
Save