carlos / platform Public

console: audit line for instance PATCH names environment changes too

Fix round 1 for Task 5. handleAPIInstanceUpdate's changed list, used to
build the audit detail, was built from req.* nil checks (addrs, probe,
steering, gate) but omitted environment even though the mutation block
applies req.Environment. An environment-only PATCH audited as
"updated instance <host> () (cli)" — an empty parenthetical on a
security-relevant log line.

Add the missing req.Environment != nil check between steering and gate,
matching the request struct's declaration order. Extend
TestAPIInstanceUpdateAuditNamesChangedFields with an environment-only
PATCH and a combined steering+environment+gate PATCH.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Paul Campbell pushed by paul@keymail.dev b4dee57a6c597700812d03d3d17f7d90d986ab29 parent 6c38f71
2 files changed, +24 −1
  • internal/console/apiinstances.go +3 −0
  • internal/console/apiinstances_test.go +21 −1
diff --git a/internal/console/apiinstances.go b/internal/console/apiinstances.go
index 92a74efc4..de186a440 100644
--- a/internal/console/apiinstances.go
+++ b/internal/console/apiinstances.go
@@ -1163,6 +1163,9 @@ func (s *Server) handleAPIInstanceUpdate(w http.ResponseWriter, r *http.Request)
if req.Steering != nil {
changed = append(changed, "steering")
}
+ if req.Environment != nil {
+ changed = append(changed, "environment")
+ }
// "gate" names that the host's password was set or cleared, never which
// it was: the audit log answers "did anyone change the gate on this host
// last week?" without becoming a place a password can be read.
diff --git a/internal/console/apiinstances_test.go b/internal/console/apiinstances_test.go
index dad6e8472..0f9a38a78 100644
--- a/internal/console/apiinstances_test.go
+++ b/internal/console/apiinstances_test.go
@@ -1460,7 +1460,8 @@ func TestAPIInstanceUpdateAuditNamesChangedFields(t *testing.T) {
// Multi-field case: a patch touching addrs AND steering together names
// both, in the order the request struct declares them (addrs, probe,
- // steering, gate) — not just one of the two, and not a generic "updated".
+ // steering, environment, gate) — not just one of the two, and not a
+ // generic "updated".
if status, _, out := rawReq(t, http.MethodPatch, pu, token,
`{"addrs":["127.0.0.1:7002"],"steering":""}`); status != http.StatusOK {
t.Fatalf("patch addrs+steering = %d %s, want 200", status, out)
@@ -1468,6 +1469,25 @@ func TestAPIInstanceUpdateAuditNamesChangedFields(t *testing.T) {
if detail := lastEventDetail(t, fx, "titogo"); detail != "updated instance audit.go.tito.io (addrs, steering) (cli)" {
t.Fatalf("audit detail = %q, want it to name both addrs and steering", detail)
}
+
+ // environment-only case: an environment patch names ONLY environment —
+ // the mutation applies req.Environment, so the audit line must too.
+ if status, _, out := rawReq(t, http.MethodPatch, pu, token, `{"environment":"demo"}`); status != http.StatusOK {
+ t.Fatalf("patch environment = %d %s, want 200", status, out)
+ }
+ if detail := lastEventDetail(t, fx, "titogo"); detail != "updated instance audit.go.tito.io (environment) (cli)" {
+ t.Fatalf("audit detail = %q, want it to name exactly environment", detail)
+ }
+
+ // Combined case: steering, environment and gate together name all
+ // three, in request-struct order.
+ if status, _, out := rawReq(t, http.MethodPatch, pu, token,
+ `{"steering":"","environment":"","gate":""}`); status != http.StatusOK {
+ t.Fatalf("patch steering+environment+gate = %d %s, want 200", status, out)
+ }
+ if detail := lastEventDetail(t, fx, "titogo"); detail != "updated instance audit.go.tito.io (steering, environment, gate) (cli)" {
+ t.Fatalf("audit detail = %q, want it to name steering, environment and gate", detail)
+ }
}
// TestAPIInstanceUpdateSteeringInvalidValue: the door's own vocabulary rail