284 lines
9.0 KiB
Go
284 lines
9.0 KiB
Go
package main
|
|
|
|
// console_hostops.go — the person at the machine always wins: the console
|
|
// commands that take reach BACK. Every one
|
|
// of them only removes something — an approval, a grant, a hold — so none
|
|
// needs machine authority; the local switch (localdeny.go) draws the same line.
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// cmdStepupDrop revokes one unused approval this box issued for a pairing.
|
|
// From the owner's account beside the machine's bastion it asks that
|
|
// bastion (owneracts.go): dropping only takes reach away, so no code.
|
|
func cmdStepupDrop(host string, machine bool, prefix string) error {
|
|
return printOrAct(agentAskRequest{Act: "approvals-drop", Host: host, Machine: machine, ID: prefix},
|
|
stepupCommand("approvals drop", host, machine, prefix),
|
|
func() (string, error) { return stepupDropText(host, machine, prefix) })
|
|
}
|
|
|
|
func stepupDropText(host string, machine bool, prefix string) (string, error) {
|
|
t, err := resolveStepupTarget(host, machine)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
dir := t.dir
|
|
prefix = strings.TrimSpace(prefix)
|
|
if prefix == "" {
|
|
return "", errors.New("which approval? (the id `approve` printed)")
|
|
}
|
|
stepupMu.Lock()
|
|
defer stepupMu.Unlock()
|
|
m := approvalsLoad(dir)
|
|
var hits []string
|
|
for id, a := range m {
|
|
if !a.Used && strings.HasPrefix(id, prefix) {
|
|
hits = append(hits, id)
|
|
}
|
|
}
|
|
switch len(hits) {
|
|
case 0:
|
|
return "", fmt.Errorf("no unused approval starts with %q %s", prefix, t.forWhom())
|
|
case 1:
|
|
default:
|
|
return "", fmt.Errorf("%d approvals start with %q — give more of the id", len(hits), prefix)
|
|
}
|
|
a := m[hits[0]]
|
|
a.Used = true // burned without ever having run: the same end state a use reaches
|
|
m[hits[0]] = a
|
|
if err := approvalsSave(dir, m); err != nil {
|
|
return "", err
|
|
}
|
|
return fmt.Sprintf("dropped approval %s (%s) — nothing can run on it now", hits[0][:8], a.Purpose), nil
|
|
}
|
|
|
|
// cmdGrantsDrop removes a held store grant from this box (through the
|
|
// owner's lane beside the machine's bastion, as cmdStepupDrop).
|
|
func cmdGrantsDrop(host, store string) error {
|
|
return printOrAct(agentAskRequest{Act: "grants-drop", Host: host, ID: store},
|
|
"grants drop "+store+hostFlagWords(host), func() (string, error) { return grantsDropText(host, store) })
|
|
}
|
|
|
|
func grantsDropText(host, store string) (string, error) {
|
|
cfg, err := stepupPairing(host)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
id, err := strconv.ParseInt(strings.TrimSpace(store), 10, 64)
|
|
if err != nil {
|
|
return "", errors.New("store id must be a number (the id on the host's store card)")
|
|
}
|
|
dir := cfg.dcnDir()
|
|
present, err := dcnDropStore(dir, id, "dropped at this machine's console")
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if !present {
|
|
return fmt.Sprintf("store %d was not held here for %s", id, cfg.HostName), nil
|
|
}
|
|
return fmt.Sprintf("store %d dropped for %s — the host's reads of it stop now and it learns on the next check-in", id, cfg.HostName), nil
|
|
}
|
|
|
|
// cmdQuarantineClear lifts this box's hold on a pairing's write lanes.
|
|
func cmdQuarantineClear(host string) error {
|
|
host = strings.TrimSpace(host)
|
|
if host == "" {
|
|
return errors.New("which host?")
|
|
}
|
|
if until := quarantinedUntil(host, time.Now()); until.IsZero() {
|
|
fmt.Printf("%s is not quarantined here\n", host)
|
|
return nil
|
|
}
|
|
quarantineClear(host)
|
|
fmt.Printf("%s may ask this machine for writes again (with your approval, as before)\n", host)
|
|
return nil
|
|
}
|
|
|
|
// cmdOpsList prints the ops and op grants a pairing holds here (ops.go).
|
|
// opRow is one op a Host holds here, verified against its signature or not.
|
|
type opRow struct {
|
|
ID string `json:"id"`
|
|
Name string `json:"name,omitempty"`
|
|
Version string `json:"version,omitempty"`
|
|
Privilege string `json:"privilege,omitempty"`
|
|
Summary string `json:"summary,omitempty"`
|
|
NotVerified string `json:"not_verified,omitempty"`
|
|
}
|
|
|
|
// opGrantRow is one op grant this machine holds and has not dropped.
|
|
type opGrantRow struct {
|
|
ID string `json:"id"`
|
|
Op string `json:"op,omitempty"`
|
|
Version string `json:"version,omitempty"`
|
|
RunsLeft int `json:"runs_left"`
|
|
Until string `json:"until,omitempty"`
|
|
Within string `json:"within,omitempty"`
|
|
NotVerified string `json:"not_verified,omitempty"`
|
|
}
|
|
|
|
// opsRead is what ops and op grants one pairing holds on this machine.
|
|
type opsRead struct {
|
|
Host string `json:"host"`
|
|
Sandbox bool `json:"sandbox"`
|
|
SandboxUnavailable string `json:"sandbox_unavailable,omitempty"`
|
|
Ops []opRow `json:"ops"`
|
|
Grants []opGrantRow `json:"grants"`
|
|
}
|
|
|
|
func readOps(o readOpts) (opsRead, error) {
|
|
cfg, err := stepupPairing(o.Host)
|
|
if err != nil {
|
|
return opsRead{}, err
|
|
}
|
|
dir := cfg.dcnDir()
|
|
v := opsRead{Host: cfg.HostName, Ops: []opRow{}, Grants: []opGrantRow{}}
|
|
v.Sandbox, v.SandboxUnavailable = opSandboxAvailable()
|
|
if entries, err := os.ReadDir(opsDir(dir)); err == nil {
|
|
for _, e := range entries {
|
|
if !e.IsDir() || e.Name() == opsRunsDirName || strings.HasSuffix(e.Name(), ".tmp") {
|
|
continue
|
|
}
|
|
r := opRow{ID: e.Name()[:12]}
|
|
if m, err := opLoad(dir, e.Name()); err == nil {
|
|
r.Name, r.Version, r.Privilege, r.Summary = m.Name, m.Version, m.Privilege, m.Summary
|
|
} else {
|
|
r.NotVerified = err.Error()
|
|
}
|
|
v.Ops = append(v.Ops, r)
|
|
}
|
|
}
|
|
f := opsLoad(dir)
|
|
nonces := make([]string, 0, len(f.Grants))
|
|
for nonce, rec := range f.Grants {
|
|
if rec.Dropped == "" {
|
|
nonces = append(nonces, nonce)
|
|
}
|
|
}
|
|
sort.Strings(nonces)
|
|
for _, nonce := range nonces {
|
|
rec := f.Grants[nonce]
|
|
r := opGrantRow{ID: nonce[:8], RunsLeft: rec.UsesLeft}
|
|
if t, err := verifyOpTerms(dir, rec.TermsCanonical, rec.Signature, opKindGrant); err != nil {
|
|
r.NotVerified = err.Error()
|
|
} else {
|
|
r.Op, r.Version, r.Until, r.Within = t.Op, t.Version, t.Expires, t.Bounds
|
|
}
|
|
v.Grants = append(v.Grants, r)
|
|
}
|
|
return v, nil
|
|
}
|
|
|
|
func cmdOpsList(host string, asJSON bool) error {
|
|
v, err := readMachine("ops", readOpts{Host: host}, readOps)
|
|
if err != nil {
|
|
return laneRefusalError("ops", err)
|
|
}
|
|
if asJSON {
|
|
return printJSON(os.Stdout, v)
|
|
}
|
|
printOps(os.Stdout, v)
|
|
return nil
|
|
}
|
|
|
|
func printOps(w io.Writer, v opsRead) {
|
|
if v.Sandbox {
|
|
fmt.Fprintln(w, "sandbox: available")
|
|
} else {
|
|
fmt.Fprintln(w, "sandbox: NOT available —", v.SandboxUnavailable)
|
|
}
|
|
for _, r := range v.Ops {
|
|
if r.NotVerified != "" {
|
|
fmt.Fprintf(w, "op [%s] NOT VERIFIED: %s\n", r.ID, r.NotVerified)
|
|
continue
|
|
}
|
|
fmt.Fprintf(w, "op %s %s as %s [%s]\n %s\n", r.Name, r.Version, r.Privilege, r.ID, r.Summary)
|
|
}
|
|
for _, r := range v.Grants {
|
|
if r.NotVerified != "" {
|
|
fmt.Fprintf(w, "grant [%s] NOT VERIFIED: %s\n", r.ID, r.NotVerified)
|
|
continue
|
|
}
|
|
fmt.Fprintf(w, "grant [%s] %s %s %d run(s) left, until %s\n within: %s\n", r.ID, r.Op, r.Version, r.RunsLeft, r.Until, r.Within)
|
|
}
|
|
if len(v.Ops) == 0 && len(v.Grants) == 0 {
|
|
fmt.Fprintln(w, "no ops held for", v.Host)
|
|
}
|
|
}
|
|
|
|
// cmdOpsDrop takes a held op grant back at the console.
|
|
func cmdOpsDrop(host, prefix string) error {
|
|
return printOrAct(agentAskRequest{Act: "ops-drop", Host: host, ID: prefix},
|
|
"ops drop "+prefix+hostFlagWords(host), func() (string, error) { return opsDropText(host, prefix) })
|
|
}
|
|
|
|
func opsDropText(host, prefix string) (string, error) {
|
|
cfg, err := stepupPairing(host)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
dir := cfg.dcnDir()
|
|
prefix = strings.TrimSpace(prefix)
|
|
if prefix == "" {
|
|
// Every nonce starts with "": with one grant held, an empty id
|
|
// would drop it unnamed.
|
|
return "", errors.New("which op grant? (the id `privatae-bastion ops` lists, or --all)")
|
|
}
|
|
if prefix == "--all" {
|
|
dropped, err := opsDropWhere(dir, "dropped at this machine's console", func(string, *opGrantRec) bool { return true })
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return fmt.Sprintf("%d op grant(s) dropped — the host cannot run under any of them again", len(dropped)), nil
|
|
}
|
|
var hits []string
|
|
for nonce, rec := range opsLoad(dir).Grants {
|
|
if rec.Dropped == "" && strings.HasPrefix(nonce, prefix) {
|
|
hits = append(hits, nonce)
|
|
}
|
|
}
|
|
switch len(hits) {
|
|
case 0:
|
|
return "", fmt.Errorf("no held op grant starts with %q", prefix)
|
|
case 1:
|
|
default:
|
|
return "", fmt.Errorf("%d grants start with %q — give more of the id", len(hits), prefix)
|
|
}
|
|
if _, err := opsDropGrant(dir, hits[0], "dropped at this machine's console"); err != nil {
|
|
return "", err
|
|
}
|
|
return fmt.Sprintf("op grant %s dropped — the host cannot run under it again", hits[0][:8]), nil
|
|
}
|
|
|
|
// printOrAct runs a narrowing act: through the owner's lane beside the
|
|
// machine's bastion (owneracts.go), else here, printing what it did either way.
|
|
func printOrAct(req agentAskRequest, adminCmd string, here func() (string, error)) error {
|
|
var done string
|
|
var err error
|
|
if readsGoThroughLane() {
|
|
done, err = actViaSock(req)
|
|
if err != nil {
|
|
return laneRefusalError(adminCmd, err)
|
|
}
|
|
} else if done, err = here(); err != nil {
|
|
return err
|
|
}
|
|
fmt.Println(done)
|
|
return nil
|
|
}
|
|
|
|
// hostFlagWords is " --host <host>" when one was named.
|
|
func hostFlagWords(host string) string {
|
|
if host == "" {
|
|
return ""
|
|
}
|
|
return " --host " + host
|
|
}
|