carlos / platform Public

console: audit every db door GET, ranged ones included

The door wrote its db-read row only for a read from offset 0, so listing
the coordinates and fetching each file with Range: bytes=1- returned
nearly every byte with no row. Every successful GET now writes one; a
ranged read names the byte it started from.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev e3f26602eeae3f5aa00b40568405d2f16dd7760b parent 6786a58
2 files changed, +43 −8
  • internal/console/apidb.go +11 −8
  • internal/console/apidb_test.go +32 −0
diff --git a/internal/console/apidb.go b/internal/console/apidb.go
index 4f4cd8542..5013973c7 100644
--- a/internal/console/apidb.go
+++ b/internal/console/apidb.go
@@ -304,14 +304,17 @@ func (s *Server) handleAPIDBGet(w http.ResponseWriter, r *http.Request) {
return
}
defer rc.Close()
- // One row per file opened from its start; a resumed read of the same
- // file is the same open continuing, not a second one.
- if offset == 0 {
- s.auditActivity(g.acct, g.app, g.actor, Activity{
- Action: dbAuditAction, TargetType: "instance", Target: g.host,
- Detail: fmt.Sprintf("read database replica file level %d, transactions %d to %d (cli)", level, minTXID, maxTXID),
- })
- }
+ // One row per successful GET, ranged or not. A ranged read is still a
+ // read of the file: skipping it would let a caller list the coordinates
+ // and fetch every file from byte 1 with no row written. A resumed read
+ // says where it started.
+ detail := fmt.Sprintf("read database replica file level %d, transactions %d to %d (cli)", level, minTXID, maxTXID)
+ if offset > 0 {
+ detail = fmt.Sprintf("read database replica file level %d, transactions %d to %d, from byte %d (cli)", level, minTXID, maxTXID, offset)
+ }
+ s.auditActivity(g.acct, g.app, g.actor, Activity{
+ Action: dbAuditAction, TargetType: "instance", Target: g.host, Detail: detail,
+ })
w.Header().Set("Content-Type", "application/octet-stream")
w.Header().Set("Cache-Control", "no-store")
status := http.StatusOK
diff --git a/internal/console/apidb_test.go b/internal/console/apidb_test.go
index 7131e4704..4aec2ddd3 100644
--- a/internal/console/apidb_test.go
+++ b/internal/console/apidb_test.go
@@ -579,3 +579,35 @@ func TestAPIDBRestoresThroughTheDoor(t *testing.T) {
t.Fatalf("restored %d rows, txid %d at %v; want %d rows and an as-of", n, got.TXID, got.At, 3*dbdoortest.Rows)
}
}
+
+// A ranged GET is still a read of the file. Listing the coordinates and
+// fetching each one with Range: bytes=1- returns all but one byte of every
+// file, so every successful GET writes a row, and a resumed one says where
+// it started.
+func TestAPIDBRangedGetIsAudited(t *testing.T) {
+ fx := dbFixture(t)
+ fx.replicate(t, dbTestHost, 1)
+ f := listFiles(t, lsfile.NewReplicaClient(filepath.Join(fx.root, "instances", dbTestHost)), 0)[0]
+ req, _ := http.NewRequest(http.MethodGet, fx.url(dbdoor.GetPath("bab", "titogo", dbTestHost, 0, uint64(f.MinTXID), uint64(f.MaxTXID))), nil)
+ req.Header.Set("Authorization", "Bearer "+fx.member)
+ req.Header.Set("Range", "bytes=1-")
+ res, err := http.DefaultClient.Do(req)
+ if err != nil {
+ t.Fatal(err)
+ }
+ io.Copy(io.Discard, res.Body)
+ res.Body.Close()
+ if res.StatusCode != http.StatusPartialContent {
+ t.Fatalf("ranged get = %d", res.StatusCode)
+ }
+ evs, err := fx.st.Events(OpsAccountID, "titogo", 10)
+ if err != nil {
+ t.Fatal(err)
+ }
+ for _, e := range evs {
+ if e.Action == "db-read" && e.Target == dbTestHost && strings.Contains(e.Detail, "byte 1") {
+ return
+ }
+ }
+ t.Fatalf("a ranged read wrote no db-read row naming its start: %+v", evs)
+}