From ee8f570fd71d70b386a00f8272dbfb17337409cf Mon Sep 17 00:00:00 2001 From: Dai Ha Date: Fri, 28 Aug 2026 06:06:12 +0700 Subject: [PATCH] #157: isolate worktree credential helpers --- .../dev/ltms/fleet/session/GitWorktrees.java | 7 +++- .../ltms/fleet/session/GitWorktreesTest.java | 38 ++++++++++++++++++- 2 files changed, 41 insertions(+), 4 deletions(-) diff --git a/fleetd/src/main/java/dev/ltms/fleet/session/GitWorktrees.java b/fleetd/src/main/java/dev/ltms/fleet/session/GitWorktrees.java index bb3e6fa..1a43c2f 100644 --- a/fleetd/src/main/java/dev/ltms/fleet/session/GitWorktrees.java +++ b/fleetd/src/main/java/dev/ltms/fleet/session/GitWorktrees.java @@ -69,7 +69,7 @@ public final class GitWorktrees implements Worktrees { /** A credential helper command which reads only an environment variable at Git call time. */ private static final String ENVIRONMENT_CREDENTIAL_HELPER = "!f() { if [ \"$1\" = get ]; then " - + "printf 'username=%s\\n\\n' \"${GITEA_TOKEN:-$WORKER_GITEA_TOKEN}\"; fi; }; f"; + + "printf 'username=%s\\npassword=%s\\n\\n' git \"$WORKER_GITEA_TOKEN\"; fi; }; f"; /** * A tracked project config that is hostile in a provisioned worktree, and what to replace it @@ -179,7 +179,10 @@ public final class GitWorktrees implements Worktrees { /** Configure a per-worktree helper that supplies a token from the member environment at call time. */ private void configureEnvironmentCredentialHelper(String repoRoot, String worktreePath) { exec("git", "-C", repoRoot, "config", "extensions.worktreeConfig", "true"); - exec("git", "-C", worktreePath, "config", "--worktree", "credential.helper", + // An empty helper resets values inherited from the system or global config. Without it Git + // asks the next helper after this one, which can expose an operator-level credential. + exec("git", "-C", worktreePath, "config", "--worktree", "--replace-all", "credential.helper", ""); + exec("git", "-C", worktreePath, "config", "--worktree", "--add", "credential.helper", ENVIRONMENT_CREDENTIAL_HELPER); } diff --git a/fleetd/src/test/java/dev/ltms/fleet/session/GitWorktreesTest.java b/fleetd/src/test/java/dev/ltms/fleet/session/GitWorktreesTest.java index 2ced046..abb8069 100644 --- a/fleetd/src/test/java/dev/ltms/fleet/session/GitWorktreesTest.java +++ b/fleetd/src/test/java/dev/ltms/fleet/session/GitWorktreesTest.java @@ -61,7 +61,11 @@ class GitWorktreesTest { private static String gitOutput(Path cwd, String... args) throws Exception { List cmd = new java.util.ArrayList<>(List.of("git")); cmd.addAll(List.of(args)); - Process p = new ProcessBuilder(cmd).directory(cwd.toFile()).redirectErrorStream(true).start(); + ProcessBuilder pb = new ProcessBuilder(cmd).directory(cwd.toFile()).redirectErrorStream(true); + pb.environment().put("GIT_CONFIG_GLOBAL", "/dev/null"); + pb.environment().put("GIT_CONFIG_SYSTEM", "/dev/null"); + pb.environment().put("GIT_TERMINAL_PROMPT", "0"); + Process p = pb.start(); String out = new String(p.getInputStream().readAllBytes()); assertTrue(p.waitFor(30, TimeUnit.SECONDS), "git timed out: " + String.join(" ", cmd)); assertEquals(0, p.exitValue(), "git " + String.join(" ", args) + " failed:\n" + out); @@ -231,10 +235,40 @@ class GitWorktreesTest { assertFalse(gitOutput(worktree, "config", "--list").contains("synthetic-test-token"), "git config --list exposed user info"); String helper = gitOutput(worktree, "config", "--worktree", "--get", "credential.helper"); - assertTrue(helper.contains("GITEA_TOKEN"), "credential helper does not read the member environment"); + assertTrue(helper.contains("WORKER_GITEA_TOKEN"), "credential helper does not read the member environment"); assertFalse(helper.contains("synthetic-test-token"), "credential helper stored user info"); } + @Test + void worktreeCredentialHelperCompletesWithoutUsingAnInheritedHelper(@TempDir Path tmp) throws Exception { + Path repo = initRepo(tmp.resolve("repo")); + git(repo, "remote", "add", "origin", "https://git.ltms.dev/akb/kb.git"); + String wt = new GitWorktrees(tmp.resolve("wts").toString()) + .add(repo.toString(), "fleetd-157-helper", "HEAD"); + Path globalConfig = tmp.resolve("global.gitconfig"); + Files.writeString(globalConfig, """ + [credential] + helper = !f() { printf 'username=%s\\npassword=%s\\n\\n' operator operator-secret; }; f + """); + + ProcessBuilder pb = new ProcessBuilder("git", "credential", "fill") + .directory(Path.of(wt).toFile()).redirectErrorStream(true); + pb.environment().put("GIT_CONFIG_GLOBAL", globalConfig.toString()); + pb.environment().put("GIT_CONFIG_SYSTEM", "/dev/null"); + pb.environment().put("GIT_TERMINAL_PROMPT", "0"); + pb.environment().put("WORKER_GITEA_TOKEN", "synthetic-worker-value"); + Process p = pb.start(); + p.getOutputStream().write("protocol=https\nhost=git.ltms.dev\n\n".getBytes(StandardCharsets.UTF_8)); + p.getOutputStream().close(); + String credential = new String(p.getInputStream().readAllBytes(), StandardCharsets.UTF_8); + assertTrue(p.waitFor(30, TimeUnit.SECONDS), "git credential fill timed out"); + assertEquals(0, p.exitValue(), "git credential fill failed"); + assertTrue(credential.contains("username=git"), "helper did not return its fixed username"); + assertTrue(credential.contains("password=synthetic-worker-value"), + "helper did not return the worker token as the password"); + assertFalse(credential.contains("operator-secret"), "Git used the inherited global helper"); + } + @Test void provisioningRefusesAWorktreeWhoseOriginStillHasHttpsUserInfo(@TempDir Path tmp) throws Exception { Path repo = initRepo(tmp.resolve("repo"));