116 lines
3.4 KiB
Go
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")
|
|
}
|