Skip to content

Commit 5af7198

Browse files
committed
fix(providers/git): observe quarantine remotes read-only
Checkout quarantine only needs ls-remote to prove the head is published, so gating it on the push URL blocked SSH and HTTPS remotes entirely. Validate the fetch URL for observation instead and permit the credential -free https/ssh transports for the read while push stays gated.
1 parent d9b4d08 commit 5af7198

3 files changed

Lines changed: 52 additions & 2 deletions

File tree

‎core/providers/git/handoff.go‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -362,7 +362,10 @@ func (runner *MutationRunner) observeRemoteRef(
362362
) (string, error) {
363363
result, err := runner.runWithEnvironment(
364364
ctx, root, map[int]struct{}{0: {}}, nil,
365-
"-c", "protocol.allow=never", "-c", "protocol.file.allow=always",
365+
"-c", "protocol.allow=never",
366+
"-c", "protocol.file.allow=always",
367+
"-c", "protocol.https.allow=always",
368+
"-c", "protocol.ssh.allow=always",
366369
"ls-remote", "--refs", remoteURL, remoteRef,
367370
)
368371
if err != nil {

‎core/providers/git/quarantine.go‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,11 @@ func (runner *MutationRunner) observeCheckoutForQuarantine(
168168
status.Worktrees[0].Locked || status.Worktrees[0].Prunable {
169169
return CheckoutQuarantineEvidence{}, errors.New("checkout is not clean, current, and exclusive")
170170
}
171-
remoteURL, err := runner.validatedPushURL(ctx, physical, "origin")
171+
// Quarantine only reads the remote (ls-remote below) to prove the checkout's
172+
// commits are published; it never pushes. The fetch-side URL is therefore
173+
// the right one to observe and network transports are allowed — the
174+
// mutation-only gate (validatedPushURL) does not apply to a read.
175+
_, remoteURL, err := runner.validatedRemoteURL(ctx, physical, "origin")
172176
if err != nil {
173177
return CheckoutQuarantineEvidence{}, err
174178
}

‎core/providers/git/quarantine_test.go‎

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package git
33
import (
44
"context"
55
"crypto/sha256"
6+
"errors"
67
"fmt"
78
"os"
89
"path/filepath"
@@ -75,3 +76,45 @@ func TestQuarantineCheckoutPreservesDirtyRepository(t *testing.T) {
7576
t.Fatalf("dirty work changed: %q %v", content, err)
7677
}
7778
}
79+
80+
func TestQuarantineRemoteValidationAdmitsNetworkTransportsForReadOnlyObservation(t *testing.T) {
81+
client, _ := mutationRepository(t)
82+
runFetchGit(t, client, "remote", "add", "origin", "git@github.com:example/repository.git")
83+
runner, err := NewMutationRunner()
84+
if err != nil {
85+
t.Fatal(err)
86+
}
87+
_, url, err := runner.validatedRemoteURL(context.Background(), client, "origin")
88+
if err != nil {
89+
t.Fatalf("read-only observation must admit the fetch URL: %v", err)
90+
}
91+
if url != "git@github.com:example/repository.git" {
92+
t.Fatalf("url=%q", url)
93+
}
94+
if _, err := runner.validatedPushURL(context.Background(), client, "origin"); !errors.Is(err, ErrNetworkMutationDisabled) {
95+
t.Fatalf("push URL must remain mutation-gated: %v", err)
96+
}
97+
}
98+
99+
func TestQuarantineCheckoutNetworkRemoteFailsAtObservationNotAtTheMutationGate(t *testing.T) {
100+
fixture := fastForwardFixture(t)
101+
runFetchGit(t, fixture.client, "remote", "set-url", "origin", "ssh://127.0.0.1:1/repository.git")
102+
head := stringsTrim(runFetchGit(t, fixture.client, "rev-parse", "HEAD"))
103+
workspaceRoot := filepath.Dir(fixture.client)
104+
stateRoot := t.TempDir()
105+
quarantine := filepath.Join(stateRoot, "quarantine", "checkouts", "repo_fixture", head)
106+
runner, err := NewMutationRunner()
107+
if err != nil {
108+
t.Fatal(err)
109+
}
110+
_, err = runner.QuarantineCheckout(
111+
context.Background(), workspaceRoot, fixture.client, stateRoot, quarantine,
112+
head, "refs/heads/main", fmt.Sprintf("sha256:%x", sha256.Sum256([]byte("x"))),
113+
)
114+
if err == nil {
115+
t.Fatal("an unreachable remote must fail the observation")
116+
}
117+
if errors.Is(err, ErrNetworkMutationDisabled) {
118+
t.Fatalf("read-only remote observation must not hit the mutation gate: %v", err)
119+
}
120+
}

0 commit comments

Comments
 (0)