162 lines
4.8 KiB
Go
162 lines
4.8 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, "", dirsOf(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, dirs map[string]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, dirs, found)
|
|
}
|
|
}
|
|
case []any:
|
|
for _, e := range x {
|
|
walk(e, key, dirs, found)
|
|
}
|
|
case string:
|
|
// A path under a directory the mesh places for the module (${dir:<id>} of a directory with a place and no path,
|
|
// 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). Any other ${dir:<id>} is resolved to the path its resource declares and judged.
|
|
if resolved, placed := resolveDir(x, dirs); !placed && underSSH.MatchString(resolved) {
|
|
found(key, x)
|
|
}
|
|
}
|
|
}
|
|
|
|
var ofDir = regexp.MustCompile(`^\$\{dir:([a-z0-9][a-z0-9_-]*)\}`)
|
|
|
|
// placedDir marks a directory the mesh places for the module: it has no path of its own.
|
|
const placedDir = "\x00placed"
|
|
|
|
// dirsOf are the module's directories by id: the path each declares, or placedDir for one with a place and no path.
|
|
func dirsOf(m any) map[string]string {
|
|
dirs := map[string]string{}
|
|
obj, _ := m.(map[string]any)
|
|
list, _ := obj["resources"].([]any)
|
|
for _, r := range list {
|
|
res, _ := r.(map[string]any)
|
|
id, _ := res["id"].(string)
|
|
path, _ := res["path"].(string)
|
|
switch {
|
|
case id == "":
|
|
case path != "":
|
|
dirs[id] = path
|
|
case res["type"] == "directory":
|
|
dirs[id] = placedDir
|
|
}
|
|
}
|
|
return dirs
|
|
}
|
|
|
|
// resolveDir follows a leading ${dir:<id>} through the module's directories. It answers the path with the references
|
|
// resolved, and whether it ends under a placed directory.
|
|
func resolveDir(x string, dirs map[string]string) (string, bool) {
|
|
for i := 0; i < 8; i++ {
|
|
m := ofDir.FindStringSubmatch(x)
|
|
if m == nil {
|
|
return x, false
|
|
}
|
|
to, ok := dirs[m[1]]
|
|
if !ok {
|
|
return x, false
|
|
}
|
|
if to == placedDir {
|
|
return x, true
|
|
}
|
|
x = to + strings.TrimPrefix(x, m[0])
|
|
}
|
|
return x, false
|
|
}
|
|
|
|
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")
|
|
}
|