Files
zeekay 16e9b4d525 fix(hanzodns): org-scope the store + fail-closed auth (red HIGH/MED/LOW)
Red found the authoritative store had no tenant isolation: only the CF-token
read was org-scoped, so on the shared multi-tenant instance any authenticated
org could list/read/mutate/delete another org's zones and records (DNS hijack /
outage). Fixes:

HIGH — org-scope the store structurally. Zone carries an Org (the validated
`owner` claim, stamped server-side); every Store method takes org and funnels
through zoneForOrg(org,name), so a caller can never reach another org's zone
even if a handler forgets the check. List filters to the caller's org;
get/mutate/delete of a foreign or absent zone is 'not found' (404, no existence
confirmation); zone names are globally unique on the shared nameservers so a
name taken by another org is a conflict. /v1/dns/sync derives the owner from the
ctx principal (never the body — the dead SyncZoneRequest.OrgID is removed) and
refuses to overwrite another org's zone.

MED — auth fails closed. authChain denies (503) when neither OIDC nor a static
key is configured; the anonymous path is reachable only behind an explicit
HANZO_DNS_DEV_INSECURE=1 opt-in (authMiddleware no longer allow-alls on an empty
key). OIDC audience is a comma-separated allowlist (string-or-array aud), and a
token with no exp is rejected.

LOW — Cloudflare updates now PATCH only the caller's changed fields (a stale
local mirror can't revert an out-of-band upstream change); a create/update
mirror failure is logged, not swallowed.

Tests: TestCrossTenantIsolation (org B gets 404 on list/get/create/update/
delete of org A's zone+records, org A's data intact), TestSyncIsOrgScoped,
TestAuthChainFailsClosedByDefault, TestDevInsecureAllows. go test -tags metrics
-race green; go build ./... green.
2026-07-18 10:58:01 -07:00

164 lines
4.1 KiB
Go

package hanzodns
import (
"testing"
)
func TestCreateAndListZones(t *testing.T) {
s := NewStore()
z, err := s.CreateZone(testOrg, "example.com", ProviderAuthoritative)
if err != nil {
t.Fatal(err)
}
if z.Name != "example.com." {
t.Fatalf("expected example.com., got %s", z.Name)
}
if z.Status != "active" {
t.Fatalf("expected active, got %s", z.Status)
}
zones := s.ListZones(testOrg)
if len(zones) != 1 {
t.Fatalf("expected 1 zone, got %d", len(zones))
}
}
func TestDuplicateZone(t *testing.T) {
s := NewStore()
if _, err := s.CreateZone(testOrg, "example.com", ProviderAuthoritative); err != nil {
t.Fatal(err)
}
if _, err := s.CreateZone(testOrg, "example.com.", ProviderAuthoritative); err == nil {
t.Fatal("expected error for duplicate zone")
}
}
func TestDeleteZone(t *testing.T) {
s := NewStore()
if _, err := s.CreateZone(testOrg, "example.com", ProviderAuthoritative); err != nil {
t.Fatal(err)
}
if err := s.DeleteZone(testOrg, "example.com"); err != nil {
t.Fatal(err)
}
if err := s.DeleteZone(testOrg, "example.com"); err == nil {
t.Fatal("expected error deleting nonexistent zone")
}
}
func TestCRUDRecords(t *testing.T) {
s := NewStore()
if _, err := s.CreateZone(testOrg, "example.com", ProviderAuthoritative); err != nil {
t.Fatal(err)
}
// Create
rec, err := s.CreateRecord(testOrg, "example.com", "www", TypeA, 300, "1.2.3.4", 0, false)
if err != nil {
t.Fatal(err)
}
if rec.Name != "www" || rec.Content != "1.2.3.4" {
t.Fatalf("unexpected record: %+v", rec)
}
// List
records, err := s.ListRecords(testOrg, "example.com")
if err != nil {
t.Fatal(err)
}
if len(records) != 1 {
t.Fatalf("expected 1 record, got %d", len(records))
}
// Get
got, err := s.GetRecord(testOrg, "example.com", rec.ID)
if err != nil {
t.Fatal(err)
}
if got.ID != rec.ID {
t.Fatalf("expected ID %s, got %s", rec.ID, got.ID)
}
// Update
newContent := "5.6.7.8"
updated, err := s.UpdateRecord(testOrg, "example.com", rec.ID, RecordPatch{Content: &newContent})
if err != nil {
t.Fatal(err)
}
if updated.Content != "5.6.7.8" {
t.Fatalf("expected 5.6.7.8, got %s", updated.Content)
}
// Delete
if err := s.DeleteRecord(testOrg, "example.com", rec.ID); err != nil {
t.Fatal(err)
}
records, _ = s.ListRecords(testOrg, "example.com")
if len(records) != 0 {
t.Fatalf("expected 0 records after delete, got %d", len(records))
}
}
func TestCreateRecordInvalidType(t *testing.T) {
s := NewStore()
if _, err := s.CreateZone(testOrg, "example.com", ProviderAuthoritative); err != nil {
t.Fatal(err)
}
if _, err := s.CreateRecord(testOrg, "example.com", "www", RecordType("BOGUS"), 300, "1.2.3.4", 0, false); err == nil {
t.Fatal("expected error for invalid record type")
}
}
func TestCreateRecordNoZone(t *testing.T) {
s := NewStore()
if _, err := s.CreateRecord(testOrg, "nope.com", "www", TypeA, 300, "1.2.3.4", 0, false); err == nil {
t.Fatal("expected error for nonexistent zone")
}
}
func TestLookup(t *testing.T) {
s := NewStore()
if _, err := s.CreateZone(testOrg, "example.com", ProviderAuthoritative); err != nil {
t.Fatal(err)
}
if _, err := s.CreateRecord(testOrg, "example.com", "www", TypeA, 300, "1.2.3.4", 0, false); err != nil {
t.Fatal(err)
}
if _, err := s.CreateRecord(testOrg, "example.com", "@", TypeA, 300, "10.0.0.1", 0, false); err != nil {
t.Fatal(err)
}
// Lookup www.example.com.
recs := s.Lookup("www.example.com.", "A")
if len(recs) != 1 {
t.Fatalf("expected 1 record for www, got %d", len(recs))
}
if recs[0].Content != "1.2.3.4" {
t.Fatalf("expected 1.2.3.4, got %s", recs[0].Content)
}
// Lookup apex.
recs = s.Lookup("example.com.", "A")
if len(recs) != 1 {
t.Fatalf("expected 1 record for apex, got %d", len(recs))
}
// Lookup miss.
recs = s.Lookup("nope.example.com.", "A")
if len(recs) != 0 {
t.Fatalf("expected 0 records, got %d", len(recs))
}
}
func TestZoneNames(t *testing.T) {
s := NewStore()
s.CreateZone(testOrg, "a.com", ProviderAuthoritative)
s.CreateZone(testOrg, "b.com", ProviderAuthoritative)
names := s.ZoneNames()
if len(names) != 2 {
t.Fatalf("expected 2 zone names, got %d", len(names))
}
}