263 lines
9.8 KiB
Go
263 lines
9.8 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/cgi"
|
|
"net/http/httptest"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
const agentToken = "0123456789abcdef0123456789abcdef01234567"
|
|
|
|
// privateForge is a forge serving one private repository over git's smart http, to agent-anchor with its token
|
|
// alone, as the forge does for an account a team lets read it.
|
|
func privateForge(t *testing.T) (origin, bare string) {
|
|
t.Helper()
|
|
backend := filepath.Join(gitExecPath(t), "git-http-backend")
|
|
if _, err := os.Stat(backend); err != nil {
|
|
t.Skipf("git-http-backend is not installed: %v", err)
|
|
}
|
|
root := t.TempDir()
|
|
bare = filepath.Join(root, "private.git")
|
|
work := t.TempDir()
|
|
runGit(t, work, nil, "git", "init", "-q", "-b", "main", ".")
|
|
commit(t, work, "one")
|
|
runGit(t, root, nil, "git", "clone", "-q", "--bare", work, bare)
|
|
cgiHandler := &cgi.Handler{Path: backend, Env: []string{"GIT_PROJECT_ROOT=" + root, "GIT_HTTP_EXPORT_ALL=1"}}
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
user, pass, ok := r.BasicAuth()
|
|
if !ok || user != "agent-anchor" || pass != agentToken {
|
|
w.Header().Set("WWW-Authenticate", `Basic realm="Gitea"`)
|
|
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
|
return
|
|
}
|
|
if r.Method == http.MethodPost && strings.HasSuffix(r.URL.Path, "git-receive-pack") {
|
|
http.Error(w, "read only", http.StatusForbidden)
|
|
return
|
|
}
|
|
cgiHandler.ServeHTTP(w, r)
|
|
}))
|
|
t.Cleanup(srv.Close)
|
|
return srv.URL, bare
|
|
}
|
|
|
|
func gitExecPath(t *testing.T) string {
|
|
t.Helper()
|
|
out, err := exec.Command("git", "--exec-path").Output()
|
|
if err != nil {
|
|
t.Skipf("git is not installed: %v", err)
|
|
}
|
|
return strings.TrimSpace(string(out))
|
|
}
|
|
|
|
func runGit(t *testing.T, dir string, env []string, name string, args ...string) string {
|
|
t.Helper()
|
|
out, err := runErr(dir, env, name, args...)
|
|
if err != nil {
|
|
t.Fatalf("%s %v: %v\n%s", name, args, err, out)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func runErr(dir string, env []string, name string, args ...string) (string, error) {
|
|
cmd := exec.Command(name, args...)
|
|
cmd.Dir = dir
|
|
cmd.Env = append(os.Environ(), "GIT_AUTHOR_NAME=t", "GIT_AUTHOR_EMAIL=t@t", "GIT_COMMITTER_NAME=t",
|
|
"GIT_COMMITTER_EMAIL=t@t", "GIT_TERMINAL_PROMPT=0", "GIT_ASKPASS=", "SSH_ASKPASS=")
|
|
cmd.Env = append(cmd.Env, env...)
|
|
out, err := cmd.CombinedOutput()
|
|
return string(out), err
|
|
}
|
|
|
|
func commit(t *testing.T, dir, text string) {
|
|
t.Helper()
|
|
if err := os.WriteFile(filepath.Join(dir, "file"), []byte(text+"\n"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
runGit(t, dir, nil, "git", "add", "file")
|
|
runGit(t, dir, nil, "git", "commit", "-q", "-m", text)
|
|
}
|
|
|
|
// sessionEnv is the environment a session starts with: the managed settings' `env`, in a home with no git
|
|
// configuration of its own.
|
|
func sessionEnv(t *testing.T, home, managedSettings string) []string {
|
|
t.Helper()
|
|
var settings struct {
|
|
Env map[string]string `json:"env"`
|
|
}
|
|
if err := json.Unmarshal([]byte(managedSettings), &settings); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
env := []string{"HOME=" + home, "XDG_CONFIG_HOME=" + filepath.Join(home, ".config"), "GIT_CONFIG_NOSYSTEM=1"}
|
|
for k, v := range settings.Env {
|
|
env = append(env, k+"="+v)
|
|
}
|
|
return env
|
|
}
|
|
|
|
func forgePaths(t *testing.T) Paths {
|
|
t.Helper()
|
|
return Paths{State: t.TempDir(), Home: t.TempDir(), Node: "anchor"}
|
|
}
|
|
|
|
func managedFor(p Paths) string {
|
|
facts := Facts{Node: p.Node, Console: "http://console"}
|
|
if ForgeCredentialHeld(p) {
|
|
facts.ForgeCredentials = p.forgeCredentials()
|
|
}
|
|
return Render(facts, Settings{}, nil, "", nil, Config{})["managed-settings.json"]
|
|
}
|
|
|
|
// Issue 737's done-when, as far as a machine of its own proves it: an agent session clones a private repository and
|
|
// fetches it again, and no credential appears in a log, a command line or the session.
|
|
func TestAnAgentClonesAPrivateRepositoryAndFetchesItAgainWithTheCredentialPlaced(t *testing.T) {
|
|
origin, bare := privateForge(t)
|
|
p := forgePaths(t)
|
|
repo := origin + "/private.git"
|
|
|
|
// Before the forge issued anything, the session's git has no credential: the clone is refused, as today.
|
|
before := sessionEnv(t, p.Home, managedFor(p))
|
|
if out, err := runErr(t.TempDir(), before, "git", "clone", "-q", repo, "x"); err == nil {
|
|
t.Fatalf("a private repository was cloned with no credential:\n%s", out)
|
|
}
|
|
|
|
// The vault hands this module what the forge issued for this machine's agent.
|
|
lines := strings.Replace(origin, "http://", "http://agent-anchor:"+agentToken+"@", 1) + "\n" +
|
|
"https://agent-anchor:" + agentToken + "@git.example.test\n"
|
|
if err := os.WriteFile(p.forgeSecret(), []byte(lines), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
said, err := PlaceForgeCredential(p)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
info, err := os.Stat(p.forgeCredentials())
|
|
if err != nil || info.Mode().Perm() != 0o600 {
|
|
t.Fatalf("the credential file: %v %v, want mode 0600", info, err)
|
|
}
|
|
if again, err := PlaceForgeCredential(p); err != nil || again != "" {
|
|
t.Fatalf("placed again with nothing changed: %q %v", again, err)
|
|
}
|
|
managed := managedFor(p)
|
|
env := sessionEnv(t, p.Home, managed)
|
|
|
|
work := t.TempDir()
|
|
cloneOut := runGit(t, work, env, "git", "clone", repo, "clone")
|
|
clone := filepath.Join(work, "clone")
|
|
if got := runGit(t, clone, env, "git", "log", "--format=%s"); strings.TrimSpace(got) != "one" {
|
|
t.Fatalf("cloned %q", got)
|
|
}
|
|
|
|
// The forge's repository moves on; the agent fetches it again.
|
|
other := t.TempDir()
|
|
runGit(t, other, nil, "git", "clone", "-q", bare, "w")
|
|
commit(t, filepath.Join(other, "w"), "two")
|
|
runGit(t, filepath.Join(other, "w"), nil, "git", "push", "-q", "origin", "main")
|
|
fetchOut := runGit(t, clone, env, "git", "fetch", "origin")
|
|
if got := runGit(t, clone, env, "git", "log", "--format=%s", "origin/main"); !strings.HasPrefix(got, "two") {
|
|
t.Fatalf("fetched %q", got)
|
|
}
|
|
|
|
// A push is the forge's to refuse (ADR 0365 §5): the token reads, and nothing writes with it.
|
|
commit(t, clone, "three")
|
|
if out, err := runErr(clone, env, "git", "push", "origin", "main"); err == nil {
|
|
t.Fatalf("a push with the agent's credential went through:\n%s", out)
|
|
}
|
|
|
|
// No credential in what the session saw, its command lines, its environment or the module's words.
|
|
remote := runGit(t, clone, env, "git", "remote", "get-url", "origin")
|
|
for what, text := range map[string]string{"the clone's output": cloneOut, "the fetch's output": fetchOut,
|
|
"the remote's url": remote, "the managed settings": managed, "the session's environment": strings.Join(env, "\n"),
|
|
"what the module said": said, "the clone's configuration": readOrEmpty(filepath.Join(clone, ".git", "config"))} {
|
|
if strings.Contains(text, agentToken) {
|
|
t.Errorf("the token appears in %s", what)
|
|
}
|
|
}
|
|
}
|
|
|
|
func readOrEmpty(path string) string {
|
|
raw, _ := os.ReadFile(path)
|
|
return string(raw)
|
|
}
|
|
|
|
func TestWhatGitChangedInTheFileIsPutBackAndAValueNotIssuedIsNeverPlaced(t *testing.T) {
|
|
p := forgePaths(t)
|
|
// A value the mesh made while the forge had not been asked: not credential lines, never placed.
|
|
if err := os.WriteFile(p.forgeSecret(), []byte("Zm9yZ2VkLWJ5LXRoZS1tZXNo\n"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if said, err := PlaceForgeCredential(p); err != nil || said != "" {
|
|
t.Fatalf("%q %v", said, err)
|
|
}
|
|
if _, err := os.Stat(p.forgeCredentials()); err == nil {
|
|
t.Fatal("a value the forge did not issue was placed")
|
|
}
|
|
if ForgeCredentialHeld(p) || strings.Contains(managedFor(p), "GIT_CONFIG") {
|
|
t.Fatal("the sessions' git is pointed at a credential that is not held")
|
|
}
|
|
|
|
issued := "https://agent-anchor:" + agentToken + "@git.example.test\n"
|
|
if err := os.WriteFile(p.forgeSecret(), []byte(issued), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := PlaceForgeCredential(p); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// git's store helper erased the line after a rejection, and a person's password for another host was stored.
|
|
if err := os.WriteFile(p.forgeCredentials(), []byte("https://me:mine@example.org\n"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if said, err := PlaceForgeCredential(p); err != nil || !strings.Contains(said, "placed") {
|
|
t.Fatalf("%q %v", said, err)
|
|
}
|
|
if got := readOrEmpty(p.forgeCredentials()); got != issued {
|
|
t.Fatalf("the file holds %q", strings.ReplaceAll(got, agentToken, "<token>"))
|
|
}
|
|
if info, _ := os.Stat(p.forgeCredentials()); info.Mode().Perm() != 0o600 {
|
|
t.Fatalf("mode %v", info.Mode().Perm())
|
|
}
|
|
|
|
// The credential withdrawn: taken back, and the sessions' git no longer pointed at it.
|
|
if err := os.Remove(p.forgeSecret()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if said, err := PlaceForgeCredential(p); err != nil || !strings.Contains(said, "taken back") || strings.Contains(said, agentToken) {
|
|
t.Fatalf("%q %v", said, err)
|
|
}
|
|
if _, err := os.Stat(p.forgeCredentials()); err == nil {
|
|
t.Fatal("a withdrawn credential stayed in the agent's home")
|
|
}
|
|
}
|
|
|
|
func TestTheSessionsGitIsPointedAtTheFileUnderTheOperatorsOwnEnvironment(t *testing.T) {
|
|
managed := map[string]any{"env": map[string]any{"FOO": "bar", "GIT_CONFIG_COUNT": "7"}}
|
|
forgeEnv(managed, "/home/agent/.config/mesh/forge.credentials")
|
|
env := managed["env"].(map[string]any)
|
|
if env["FOO"] != "bar" || env["GIT_CONFIG_COUNT"] != "1" || env["GIT_CONFIG_KEY_0"] != "credential.helper" ||
|
|
env["GIT_CONFIG_VALUE_0"] != "store --file='/home/agent/.config/mesh/forge.credentials'" {
|
|
t.Fatalf("%v", env)
|
|
}
|
|
none := map[string]any{}
|
|
forgeEnv(none, "")
|
|
if _, set := none["env"]; set {
|
|
t.Fatal("an environment was set with no credential held")
|
|
}
|
|
}
|
|
|
|
func TestForgeCredentialLinesAreReadStrictly(t *testing.T) {
|
|
for _, bad := range []string{"", "\n", "token", "https://git.example.test", "https://agent-anchor@git.example.test",
|
|
"https://agent-anchor:t@git.example.test/novox/hq.git", "ssh://agent-anchor:t@git.example.test",
|
|
"https://agent-anchor:t@git.example.test\nnot a line"} {
|
|
if _, err := ForgeCredentialLines(bad); err == nil {
|
|
t.Errorf("%q was taken", bad)
|
|
} else if strings.Contains(err.Error(), "t@git") {
|
|
t.Errorf("the refusal said the value: %v", err)
|
|
}
|
|
}
|
|
}
|