- Two inbound domains on one upstream could never change its TLS mode,
each save refused for the other (medium): SetTransportAs changes the
mode for every domain on the upstream when the one saving may change
them all; a domain user who may not is refused with the mode, not the
other tenant's domain name (security F1: the first version named it).
- A deletion's leftover DKIM key is removed by Resync (Reload
configuration, a restore's first start) for names no domain has.
- The backup's write deadline moves before Check.
- hasUnsavedEdit skips password and file fields and compares a multiple
select's options; htmx.config.timeout 30 s, so a refresh never answered
ends and is tried again; a failed refresh's promise is handled.
- An empty filtered Outbound log says no message matches the filters.
- The dummy bcrypt hash is made at start; a DMARC part over 8 MiB is
refused for its size like the other two limits.
- Health's Configuration box says what Reload writes (design first,
32dfe4a).
- Docs: CHANGELOG, guide § Inbound, security.md (the shared upstream's
mode, the profile form's "username taken"), architecture.md (owed to an
address), the plan's notes corrected.
Tests: the shared upstream (service, handler), the leftover key, all eight
Health boxes ordered, the subject from before migration 0011, the new JS
cases.
Tested on the test server: go vet + go test of 25 packages, make e2e (all
pass, 636 s) on this tree.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+53
-26
@@ -5,6 +5,7 @@
|
||||
package inbound
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/mixeme/selfpost/internal/postfix"
|
||||
@@ -54,51 +55,77 @@ func (s *Service) Add(name string) (store.InboundDomain, error) {
|
||||
return d, nil
|
||||
}
|
||||
|
||||
// SetTransport validates and saves the upstream, then rebuilds the maps.
|
||||
// SetTransport validates and saves the upstream, then rebuilds the maps; the
|
||||
// other domains forwarded to the same upstream take its TLS mode with it
|
||||
// (SetTransportAs, with every domain changeable).
|
||||
func (s *Service) SetTransport(id int64, host, portRaw, tlsMode string) error {
|
||||
_, err := s.SetTransportAs(id, host, portRaw, tlsMode, func(store.InboundDomain) bool { return true })
|
||||
return err
|
||||
}
|
||||
|
||||
// UpstreamShared refuses an upstream another inbound domain forwards to with
|
||||
// another TLS mode, when the one saving may not change that domain. Its text
|
||||
// is for the log; the panel tells a domain user the mode, not the domain,
|
||||
// which is another tenant's (security review of audit stage 4).
|
||||
type UpstreamShared struct {
|
||||
Upstream string
|
||||
Mode string
|
||||
Domain string
|
||||
}
|
||||
|
||||
func (e *UpstreamShared) Error() string {
|
||||
return fmt.Sprintf("%s is the upstream of %s too, with TLS mode %s", e.Upstream, e.Domain, e.Mode)
|
||||
}
|
||||
|
||||
// SetTransportAs validates and saves the upstream of domain id, then rebuilds
|
||||
// the maps. An upstream has one TLS mode: Postfix's TLS policy is keyed by the
|
||||
// next hop, not by the domain (smtp_tls_policy_maps; Postfix 3.7.11,
|
||||
// README_FILES/TLS_README: the key is the verbatim next-hop), so two domains
|
||||
// to one [host]:port share one mode - the map held both keys and the first by
|
||||
// domain name won with a warning (audit 2026-10-12, L5). The other domains
|
||||
// forwarded to the upstream with another mode take the new one, when
|
||||
// mayChange allows each; one it does not is an *UpstreamShared and nothing is
|
||||
// saved. Before, the save was refused, and two domains sharing an upstream
|
||||
// could never change its mode (review of audit stage 4). changed is how many
|
||||
// other domains took the mode.
|
||||
func (s *Service) SetTransportAs(id int64, host, portRaw, tlsMode string, mayChange func(store.InboundDomain) bool) (changed int, err error) {
|
||||
if _, err := s.store.GetInboundDomain(id); err != nil {
|
||||
return err
|
||||
return 0, err
|
||||
}
|
||||
host = normalizeHost(host)
|
||||
if err := checkHost(host); err != nil {
|
||||
return err
|
||||
return 0, err
|
||||
}
|
||||
port, err := parsePort(portRaw)
|
||||
if err != nil {
|
||||
return err
|
||||
return 0, err
|
||||
}
|
||||
if err := checkTLSMode(tlsMode); err != nil {
|
||||
return err
|
||||
return 0, err
|
||||
}
|
||||
if err := s.checkOneTLSModePerUpstream(id, host, port, tlsMode); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.store.UpdateInboundTransport(id, host, port, tlsMode); err != nil {
|
||||
return err
|
||||
}
|
||||
return s.Resync()
|
||||
}
|
||||
|
||||
// checkOneTLSModePerUpstream refuses an upstream another inbound domain
|
||||
// already forwards to with another TLS mode. Postfix's TLS policy is keyed by
|
||||
// the next hop, not by the domain (smtp_tls_policy_maps; Postfix 3.7.11,
|
||||
// README_FILES/TLS_README: the key is the verbatim next-hop), so two domains
|
||||
// to one [host]:port share one mode: the map held both keys, and the first by
|
||||
// domain name won with a warning - the form's choice for the second domain
|
||||
// was not what Postfix did (audit 2026-10-12, L5).
|
||||
func (s *Service) checkOneTLSModePerUpstream(id int64, host string, port int, tlsMode string) error {
|
||||
domains, err := s.store.ListInboundDomains()
|
||||
if err != nil {
|
||||
return err
|
||||
return 0, err
|
||||
}
|
||||
var others []store.InboundDomain
|
||||
for _, d := range domains {
|
||||
if d.ID == id || d.Port != port || normalizeHost(d.Host) != host || d.TLSMode == tlsMode {
|
||||
continue
|
||||
}
|
||||
return invalid("[%s]:%d is already the upstream of %s, with TLS mode %s: an upstream has one TLS mode - choose %s here, or change it on %s first",
|
||||
host, port, d.Name, d.TLSMode, d.TLSMode, d.Name)
|
||||
if !mayChange(d) {
|
||||
return 0, &UpstreamShared{Upstream: fmt.Sprintf("[%s]:%d", host, port), Mode: d.TLSMode, Domain: d.Name}
|
||||
}
|
||||
others = append(others, d)
|
||||
}
|
||||
return nil
|
||||
if err := s.store.UpdateInboundTransport(id, host, port, tlsMode); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
for _, d := range others {
|
||||
if err := s.store.UpdateInboundTransport(d.ID, d.Host, d.Port, tlsMode); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
return len(others), s.Resync()
|
||||
}
|
||||
|
||||
// SetRecipients validates the mode and, in list mode, every address, then
|
||||
|
||||
Reference in New Issue
Block a user