package main // The forge's read credential in the agent's home (novox/hq ADR 0365 §4, issue 737). // // The forge's module mints each node's agent a token of its own account, `agent-`, scoped to read // repositories alone, and the vault keeps it for this module on this machine alone (provision `gitea-read`, issue // 749). What arrives in this module's state is git's credential-store lines for it, one per origin the forge is // reached at. This module places them in the agent's home, a file of mode 0600 written as the agent's account (ADR // 0266: no other module writes there), and points every session's git at it through the managed settings' // environment: the file's path, never its content. So an agent clones and fetches what its account may read, and no // token is printed, typed on a command line or put in an environment a process listing shows. // // Git's own `store` helper reads the file; on a token the forge rejects it erases that line, and on a password a // person typed elsewhere it adds one. Both are undone at the next look (every few seconds): the file holds what the // forge issued, and nothing else. A value that is not credential lines — one the mesh made while the forge had not // been asked yet — is not placed, and what was placed before is taken back. import ( "bytes" "errors" "fmt" "io/fs" "net/url" "os" "path/filepath" "strings" ) // ForgeProvision is the provision whose credential this is, and the file this module's manifest has it delivered to. const ( ForgeProvision = "gitea-read" forgeSecret = "gitea-read.secret" ) func (p Paths) forgeSecret() string { return filepath.Join(p.State, forgeSecret) } // forgeCredentials is where the agent's git reads the credential: under the agent's home, in a directory of the // mesh's own. func (p Paths) forgeCredentials() string { return filepath.Join(p.Home, ".config", "mesh", "forge.credentials") } // ForgeCredentialLines are the credential lines held, checked: each an http(s) origin with an account and a token, // and nothing else. Anything else is refused, said without the value. func ForgeCredentialLines(raw string) (string, error) { var lines []string for _, line := range strings.Split(raw, "\n") { line = strings.TrimSpace(line) if line == "" { continue } u, err := url.Parse(line) if err != nil || (u.Scheme != "https" && u.Scheme != "http") || u.Host == "" || u.User == nil || u.User.Username() == "" || u.Path != "" || u.RawQuery != "" || u.Fragment != "" { return "", errors.New("what the mesh holds for the forge's read credential is not git credential lines " + "(the forge has not issued it yet, or something else was given)") } if pw, ok := u.User.Password(); !ok || pw == "" { return "", errors.New("a forge credential line names no token") } lines = append(lines, line) } if len(lines) == 0 { return "", errors.New("the forge's read credential is empty") } return strings.Join(lines, "\n") + "\n", nil } // ForgeCredentialHeld says whether this machine holds a forge read credential to place. func ForgeCredentialHeld(p Paths) bool { raw, err := os.ReadFile(p.forgeSecret()) if err != nil { return false } _, err = ForgeCredentialLines(string(raw)) return err == nil } // PlaceForgeCredential puts the forge's read credential in the agent's home as it is held — written, rewritten when // it differs, taken back when none is held — and says what it did, or "" when nothing changed. Never the value. func PlaceForgeCredential(p Paths) (string, error) { path := p.forgeCredentials() raw, err := os.ReadFile(p.forgeSecret()) var lines string var why error switch { case errors.Is(err, fs.ErrNotExist): why = errors.New("this machine holds no forge read credential") case err != nil: return "", fmt.Errorf("the forge's read credential cannot be read: %w", err) default: lines, why = ForgeCredentialLines(string(raw)) } if why != nil { if _, err := fsys.Stat(path); errors.Is(err, fs.ErrNotExist) { return "", nil } if err := fsys.Remove(path); err != nil && !errors.Is(err, fs.ErrNotExist) { return "", fmt.Errorf("the forge credential at %s could not be taken back: %w", path, err) } return fmt.Sprintf("forge credential: taken back from %s: %v", path, why), nil } if held, err := fsys.ReadFile(path); err == nil && bytes.Equal(held, []byte(lines)) { if info, err := fsys.Stat(path); err == nil && info.Mode().Perm() == 0o600 { return "", nil } } if err := fsys.MkdirAll(filepath.Dir(path), 0o700); err != nil { return "", fmt.Errorf("the directory for the forge credential could not be made: %w", err) } tmp := path + ".mesh-tmp" if err := fsys.WriteFile(tmp, []byte(lines), 0o600); err != nil { return "", fmt.Errorf("the forge credential could not be written: %w", err) } if err := fsys.Chmod(tmp, 0o600); err != nil { _ = fsys.Remove(tmp) return "", fmt.Errorf("the forge credential could not be made 0600: %w", err) } if err := fsys.Rename(tmp, path); err != nil { _ = fsys.Remove(tmp) return "", fmt.Errorf("the forge credential could not be put in place: %w", err) } return fmt.Sprintf("forge credential: placed at %s for %d origin(s)", path, strings.Count(lines, "\n")), nil } // forgeEnv is what every session's environment carries for git to read the credential: the file's path in git's // configuration from the environment, never the credential. Laid over the operator's `env`, the mesh's keys winning. func forgeEnv(managed map[string]any, credentials string) { if credentials == "" { return } env := map[string]any{} if held, ok := managed["env"].(map[string]any); ok { for k, v := range held { env[k] = v } } env["GIT_CONFIG_COUNT"] = "1" env["GIT_CONFIG_KEY_0"] = "credential.helper" env["GIT_CONFIG_VALUE_0"] = "store --file=" + shellQuote(credentials) managed["env"] = env }