Files
selfpost/internal/web/handlers/authz.go
T
mixandClaude Opus 5.5 a618ffb0e4 send log: rows belong to a domain by id (audit L12, 0008)
A domain user's log, the message page, a domain's statistics, the level-2
count and the notifier's refusal count picked send-log rows by domain name,
so a domain deleted and re-added under its old name showed its earlier mail
to the next assignee and went on counting it.

Migration 0008 (not run on the live database yet, so extended here):
send_log.domain_id and inbound_spam_log.inbound_domain_id, REFERENCES ...
ON DELETE SET NULL, filled once by name from the registry as it is at
migration time, each with an (id, created_at) index.

- Writes: InsertQueued / InsertRejected set domain_id to the domain of the
  row's login (loginDomainID) - the journal milter and the sender check's
  refusal rows both go through them; the inbound journal sets
  inbound_domain_id from the inbound domain's name at insert.
- Reads: SendLogFilter.DomainIDs (was Domains, by name) and
  InboundSpamFilter.DomainIDs are the scope; the global role still reads
  everything. The message page checks row.DomainID against the user's
  domains. DomainSendStats and CountMessages keep their name argument and
  select domain_id = (SELECT id FROM domains WHERE name = ?). The notifier
  joins send_log.domain_id to the domain and its application.
- DMARC reports stay by name on purpose (a report is about the DNS name).

Tests: store - scope by ids; a re-added domain's log, level-2 count and
statistics hold its own row only, the global log both; inbound scope by
ids. Handlers - the new assignee gets 404 for the old row's page and does
not see it in the log, the global role sees both. Tests that wrote rows for
a login that is no application now register it (a row is a domain's through
its application). Schema at 0008 with the two columns and indexes.

schema-migrations row 8, CHANGELOG [Unreleased], audit plan line ticked,
roadmap 19/62.
Evidence: go vet ./... clean; go test ./... ok locally but for the two
Windows-only tests; the server run follows before the push.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 15:37:26 +03:00

70 lines
2.0 KiB
Go

package handlers
import (
"net/http"
"github.com/mixeme/selfpost/internal/dmarc"
"github.com/mixeme/selfpost/internal/store"
"github.com/mixeme/selfpost/internal/web/auth"
"github.com/mixeme/selfpost/internal/web/view"
)
func (h *Handlers) principal(r *http.Request) (auth.Principal, bool) {
return auth.PrincipalFromRequest(r)
}
func (h *Handlers) requireGlobal(w http.ResponseWriter, r *http.Request) (auth.Principal, bool) {
p, ok := h.principal(r)
if !ok || !p.IsGlobal() {
http.NotFound(w, r)
return auth.Principal{}, false
}
return p, true
}
// shellMeta is what the shell of a page of the component kit needs to know
// about the person looking at it: their name and which groups of the menu they
// reach. The page's constructor adds its title and where it sits in the menu.
func (h *Handlers) shellMeta(r *http.Request) view.Meta {
p, _ := h.principal(r)
user := auth.CurrentUser(r)
if user == "" {
user = p.Username
}
return view.Meta{User: user, IsGlobal: p.IsGlobal(), HasOutbound: p.HasOutbound(), HasInbound: p.HasInbound()}
}
func (h *Handlers) assignedDomains(p auth.Principal) ([]store.Domain, error) {
if p.IsGlobal() {
return h.store.ListDomains()
}
return h.store.ListDomainsForUser(p.ID)
}
// hostedDMARCAddress is the SelfPost-hosted report mailbox for a sending
// domain, "" when report ingest is off on this server.
func (h *Handlers) hostedDMARCAddress(domainName string) string {
if h.dmarc == nil || !h.cfg.DMARCEnabled {
return ""
}
return dmarc.HostedReportAddress(h.cfg.Hostname, domainName)
}
func domainNameSet(domains []store.Domain) map[string]bool {
m := make(map[string]bool, len(domains))
for _, d := range domains {
m[d.Name] = true
}
return m
}
// domainIDSet is the set of the ids of domains; a send-log row's DomainID is
// in it when the row belongs to one of them (0, a row of no domain, never is).
func domainIDSet(domains []store.Domain) map[int64]bool {
m := make(map[int64]bool, len(domains))
for _, d := range domains {
m[d.ID] = true
}
return m
}