domains: a domain's DMARC reports are deleted with it (audit A5/W2)
Security audit 2026-10-17, finding A5/W2 (low), stage 1; the mockup is
7effc98. dmarc_reports belong to a domain by name, outlive the domain (no
FK), and a domain deleted and added again handed its old reports to
whoever it was assigned to then - the logs were moved to ids in 2.5.0 for
exactly this case, reports were not.
The cheap form of the plan: store.DeleteDomain deletes the domain's
reports (their records by the table's cascade) in the same transaction as
the domain. The confirmation page says so when the domain has reports
("delete the 12 DMARC reports received for it;"), counted by the new
store.CountDMARCReports; NewOutDomainDelete takes the count.
Tests: TestDeletingADomainDeletesItsReports (the domain added again has
none, another domain keeps its report and record, a second delete is
ErrDomainNotFound); TestOutDomainDeleteNamesWhatGoesWithIt with 12, 1 and
0 reports.
Docs: security.md § Roles; CHANGELOG; plan.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -136,7 +136,13 @@ func (h *Handlers) HandleDeleteConfirm(w http.ResponseWriter, r *http.Request) {
|
||||
for i, a := range apps {
|
||||
logins[i] = a.Login
|
||||
}
|
||||
h.view.Render(w, http.StatusOK, "out-domain-delete", view.NewOutDomainDelete(h.shellMeta(r), d.ID, d.Name, logins))
|
||||
reports, err := h.store.CountDMARCReports(d.Name)
|
||||
if err != nil {
|
||||
logf("panel: delete domain %d: %v", d.ID, err)
|
||||
http.Error(w, "internal error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
h.view.Render(w, http.StatusOK, "out-domain-delete", view.NewOutDomainDelete(h.shellMeta(r), d.ID, d.Name, logins, reports))
|
||||
}
|
||||
|
||||
// HandleDeleteDomain performs the deletion and returns to the domain list.
|
||||
|
||||
@@ -260,7 +260,7 @@ func outAppCreatedFixture() *OutAppCreated {
|
||||
// outDomainDeleteFixture is the mockup's confirmation: the domain has the two
|
||||
// applications that go with it.
|
||||
func outDomainDeleteFixture() *OutDomainDelete {
|
||||
return NewOutDomainDelete(admin(), 1, "example.org", []string{"prod-server", "alerts"})
|
||||
return NewOutDomainDelete(admin(), 1, "example.org", []string{"prod-server", "alerts"}, 12)
|
||||
}
|
||||
|
||||
// outLogFixture is the mockup's Outbound log: six messages of three domains on
|
||||
|
||||
@@ -768,8 +768,9 @@ type OutDomainDelete struct {
|
||||
}
|
||||
|
||||
// NewOutDomainDelete builds the page. logins are the applications the domain
|
||||
// has: they are named, because they go with it.
|
||||
func NewOutDomainDelete(m Meta, id int64, name string, logins []string) *OutDomainDelete {
|
||||
// has: they are named, because they go with it; reports is how many DMARC
|
||||
// reports go with it (security audit 2026-10-17, A5/W2), not said when none.
|
||||
func NewOutDomainDelete(m Meta, id int64, name string, logins []string, reports int) *OutDomainDelete {
|
||||
m.Title, m.Section, m.Page = "Delete "+name, "outbound", "domains"
|
||||
href := DomainHref(id)
|
||||
p := &OutDomainDelete{
|
||||
@@ -797,6 +798,12 @@ func NewOutDomainDelete(m Meta, id int64, name string, logins []string) *OutDoma
|
||||
item = append(item, Inline{Text: ") with their credentials and sender bindings;"})
|
||||
p.Consequences = append(p.Consequences, item)
|
||||
}
|
||||
switch {
|
||||
case reports == 1:
|
||||
p.Consequences = append(p.Consequences, Rich("delete the ", Strong("DMARC report"), " received for it;"))
|
||||
case reports > 1:
|
||||
p.Consequences = append(p.Consequences, Rich("delete the ", Strong(strconv.Itoa(reports)+" DMARC reports"), " received for it;"))
|
||||
}
|
||||
p.Consequences = append(p.Consequences, Rich("stop signing mail as the domain."))
|
||||
return p
|
||||
}
|
||||
|
||||
@@ -435,18 +435,20 @@ func TestOutDomainDeleteNamesWhatGoesWithIt(t *testing.T) {
|
||||
`<h1 class="title is-3">Delete example.org</h1>`, `<div class="box sp-danger-zone">`, `<h2>This cannot be undone</h2>`,
|
||||
`Deleting <strong>example.org</strong> will:`, `permanently delete its DKIM signing key;`,
|
||||
`delete <strong>both applications</strong> (<span class="sp-mono">prod-server</span>, <span class="sp-mono">alerts</span>) with their credentials and sender bindings;`,
|
||||
`delete the <strong>12 DMARC reports</strong> received for it;`,
|
||||
`stop signing mail as the domain.`, `Inbound domains are not touched.`,
|
||||
`<form method="post" action="/outbound/domains/1/delete">`, `class="button is-danger"`, `Delete example.org</button>`,
|
||||
`href="/outbound/domains/1">Keep it</a>`,
|
||||
`<h2>Moving it instead?</h2>`, `<a href="/outbound/domains/1/settings">Export the domain</a>`,
|
||||
)
|
||||
|
||||
// No applications: nothing to say about them.
|
||||
out = renderSignedIn(t, "out-domain-delete", NewOutDomainDelete(admin(), 1, "example.org", nil))
|
||||
pageLacks(t, "Delete", out, `applications`, `credentials`)
|
||||
// No applications and no reports: nothing to say about them.
|
||||
out = renderSignedIn(t, "out-domain-delete", NewOutDomainDelete(admin(), 1, "example.org", nil, 0))
|
||||
pageLacks(t, "Delete", out, `applications`, `credentials`, `received for it`)
|
||||
pageHas(t, "Delete", out, `permanently delete its DKIM signing key;`, `stop signing mail as the domain.`)
|
||||
out = renderSignedIn(t, "out-domain-delete", NewOutDomainDelete(admin(), 1, "example.org", []string{"only"}))
|
||||
pageHas(t, "Delete", out, `<strong>its application</strong> (<span class="sp-mono">only</span>)`)
|
||||
out = renderSignedIn(t, "out-domain-delete", NewOutDomainDelete(admin(), 1, "example.org", []string{"only"}, 1))
|
||||
pageHas(t, "Delete", out, `<strong>its application</strong> (<span class="sp-mono">only</span>)`,
|
||||
`delete the <strong>DMARC report</strong> received for it;`)
|
||||
}
|
||||
|
||||
// ---- Helpers
|
||||
|
||||
Reference in New Issue
Block a user