Files
mesh-catalog/checks/sshdir/main.go
T

116 lines
3.4 KiB
Go

// Command sshdir refuses any manifest but modules/ssh-client naming a path under ~/.ssh (novox/hq ADR 0415 §6,
// issue 1020): ssh-client owns ~/.ssh and sweeps what it does not know, so a path another module placed there
// would be moved out at the next sweep. A module that needs something there provides it to ssh-client instead.
//
// go run . <catalogue root>
//
// Every string of a manifest is read, wherever it stands, except prose and a file's text (Prose): a path, a
// place, a volume; a path inside a directory of the module's own (`${dir:<id>}`) is not ~/.ssh. Exits 1 on a finding.
package main
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"regexp"
"sort"
"strings"
)
// SSHModule is the one module allowed to name a path under ~/.ssh.
const SSHModule = "ssh-client"
// underSSH is a path under ~/.ssh, or ~/.ssh itself, however the home is spelled: ${machine:account-home}/.ssh,
// ~/.ssh, .ssh/config, or a volume's host side (…/.ssh:/root/.ssh).
var underSSH = regexp.MustCompile(`(^|[/~])\.ssh($|[/:])`)
// Prose are the keys whose strings are words or a file's text, not a path: a template may say ~/.ssh in a comment.
var Prose = map[string]bool{"description": true, "why": true, "does": true, "template": true, "content": true, "title": true}
// Finding is one path under ~/.ssh in a manifest other than ssh-client's.
type Finding struct {
Module string
Key string
Value string
}
// Check reads every manifest under root/modules and answers which paths it refuses.
func Check(root string) ([]Finding, error) {
manifests, err := filepath.Glob(filepath.Join(root, "modules", "*", "module.json"))
if err != nil {
return nil, err
}
sort.Strings(manifests)
var out []Finding
for _, path := range manifests {
raw, err := os.ReadFile(path)
if err != nil {
return nil, err
}
var m any
if err := json.Unmarshal(raw, &m); err != nil {
return nil, fmt.Errorf("%s: %w", path, err)
}
module := filepath.Base(filepath.Dir(path))
if obj, ok := m.(map[string]any); ok {
if name, ok := obj["module"].(string); ok && name != "" {
module = name
}
}
if module == SSHModule {
continue
}
walk(m, "", func(key, value string) {
out = append(out, Finding{Module: module, Key: key, Value: value})
})
}
return out, nil
}
// walk calls found for every string under v, outside Prose, that names a path under ~/.ssh.
func walk(v any, key string, found func(key, value string)) {
switch x := v.(type) {
case map[string]any:
keys := make([]string, 0, len(x))
for k := range x {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
if !Prose[k] {
walk(x[k], k, found)
}
}
case []any:
for _, e := range x {
walk(e, key, found)
}
case string:
// A path under a directory of the module's own (${dir:<id>}, as a runs-as module's home) is that account's
// own .ssh, not the operator's ~/.ssh that ssh-client sweeps (issue 1484, ADR 0466 §4).
if underSSH.MatchString(x) && !strings.HasPrefix(x, "${dir:") {
found(key, x)
}
}
}
func main() {
root := "."
if len(os.Args) > 1 {
root = os.Args[1]
}
found, err := Check(root)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
for _, f := range found {
fmt.Printf("%s names %q (%s) under ~/.ssh: only %s may, and the sweep moves anything else out (novox/hq ADR 0415 §6)\n", f.Module, f.Value, f.Key, SSHModule)
}
if len(found) > 0 {
os.Exit(1)
}
fmt.Println("~/.ssh: named only by ssh-client")
}