package console import ( "context" "encoding/json" "errors" "testing" "time" "github.com/carlosframework/platform/internal/hostops" "github.com/carlosframework/platform/internal/mail" "github.com/carlosframework/platform/internal/release" ) // mailGuardPermits seeds an open admission for each app and returns a // context holding a live exclusive app-mutation permit for every one of // them: the shape a mail admission's context has. func mailGuardPermits(t *testing.T, raw release.Store, apps ...hostops.AppScope) context.Context { t.Helper() ctx := t.Context() w := claimTestWriter(t, raw) for i, a := range apps { appID := "id-" + a.Account + "-" + a.App seedClaimAdmission(t, raw, a.Account, a.App, hostops.AppAdmission{V: 1, AppID: appID, Revision: 1, Epoch: 1, State: "open", Registrations: map[string]hostops.Registration{}, Effects: map[string]hostops.Effect{}}) op := "mail-op-" + string(rune('a'+i)) h, err := w.BeginExclusive(ctx, a.Account, a.App, appID, hostops.AppOperation{ID: op, Kind: hostops.OpAppMutation, AppID: appID, Phase: "issued", Intent: map[string]string{"action": "mail"}}, claimTestOwner) if err != nil { t.Fatal(err) } next, _, err := h.Runner(time.Now).Issue(ctx, hostops.Effect{ID: op, Resource: "mail-gate:test", Action: "mail"}) if err != nil { t.Fatal(err) } ctx = next } return ctx } func putMailJSON(t *testing.T, s release.Store, key string, v any) { t.Helper() body, err := json.Marshal(v) if err != nil { t.Fatal(err) } _, tag, _ := s.Get(context.Background(), key) if _, err = s.Put(context.Background(), key, body, tag); err != nil { t.Fatal(err) } } func putMailClaim(t *testing.T, s release.Store, c mail.DomainClaim) { t.Helper() if c.Schema == 0 { c.Schema = 1 } if c.CreatedAt.IsZero() { c.CreatedAt = time.Date(2026, 10, 7, 12, 0, 0, 0, time.UTC) } putMailJSON(t, s, mail.DomainClaimKey(c.Domain), c) } func scopesEqual(got []hostops.AppScope, want ...hostops.AppScope) bool { if len(got) != len(want) { return false } set := map[hostops.AppScope]bool{} for _, g := range got { set[g] = true } for _, w := range want { if !set[w] { return false } } return true } // The invariant failure this closes: EnsureDomain calls EnsureIdentity after // claimDomain and BEFORE the account domain record exists. The identity is // bound to the account's create-only global claim, resolved to the claim's // app, and is never accepted on a record alone or on a foreign claim. func TestMailGuardBindsNewIdentityToDomainClaim(t *testing.T) { raw := release.NewDirStore(t.TempDir()) titogo := hostops.AppScope{Account: "bab", App: "titogo"} ctx := mailGuardPermits(t, raw, titogo) g := backendGuard{data: raw} const domain = "titogo.bab.oncarlos.com" // No claim and no record: nothing proves the domain. if _, err := g.domainMembers(ctx, domain, false, domainCreate, "eu-west-1"); !errors.Is(err, errBackendUnresolved) { t.Fatalf("unclaimed domain: %v", err) } // A record alone (account data) never proves a NEW identity. putMailJSON(t, raw, mail.DomainKey("bab", "loose.example.com"), mail.DomainRecord{Domain: "loose.example.com", Apps: []string{"titogo"}}) if _, err := g.domainMembers(ctx, "loose.example.com", false, domainCreate, "eu-west-1"); !errors.Is(err, errBackendUnproven) { t.Fatalf("record without claim admitted a new identity: %v", err) } // ...but still resolves the effects written after the record. if scopes, err := g.domainMembers(ctx, "loose.example.com", false, domainTouch, ""); err != nil || !scopesEqual(scopes, titogo) { t.Fatalf("record members for a bind: %v %v", scopes, err) } // The claim exists, the record does not yet: EnsureIdentity resolves. putMailClaim(t, raw, mail.DomainClaim{Domain: domain, Account: "bab", App: "titogo"}) scopes, err := g.domainMembers(ctx, domain, false, domainCreate, "eu-west-1") if err != nil || !scopesEqual(scopes, titogo) { t.Fatalf("claimed domain before its record: %v %v", scopes, err) } // Its DKIM, MAIL FROM and DMARC names resolve to the same owner. for _, name := range []string{"tok1._domainkey." + domain, mail.MailFromDomain(domain, "eu-west-1"), "_dmarc." + domain} { if scopes, err := g.domainMembers(ctx, name, true, domainCreate, ""); err != nil || !scopesEqual(scopes, titogo) { t.Fatalf("publish %s: %v %v", name, scopes, err) } } // DeleteIdentity needs the claim's own created-in-region stamp. if _, err := g.domainMembers(ctx, domain, false, domainDestroy, "eu-west-1"); !errors.Is(err, errBackendUnproven) { t.Fatalf("delete of an identity the claim never created: %v", err) } putMailClaim(t, raw, mail.DomainClaim{Domain: domain, Account: "bab", App: "titogo", IdentityRegions: []string{"eu-west-1"}}) if _, err := g.domainMembers(ctx, domain, false, domainDestroy, "eu-west-1"); err != nil { t.Fatalf("delete of the claim's own identity: %v", err) } if _, err := g.domainMembers(ctx, domain, false, domainDestroy, "us-east-1"); !errors.Is(err, errBackendUnproven) { t.Fatalf("delete in a region the claim never created: %v", err) } } // A claim held by an account with no permit here owns the name, whatever a // permitted account's record or a shorter suffix says. func TestMailGuardRefusesForeignClaim(t *testing.T) { raw := release.NewDirStore(t.TempDir()) ctx := mailGuardPermits(t, raw, hostops.AppScope{Account: "bab", App: "shop"}) g := backendGuard{data: raw} // bab holds a (legacy) record for victim.com, but zed holds the claim. putMailJSON(t, raw, mail.DomainKey("bab", "victim.com"), mail.DomainRecord{Domain: "victim.com", Apps: []string{"shop"}}) putMailClaim(t, raw, mail.DomainClaim{Domain: "victim.com", Account: "zed", App: "web", IdentityRegions: []string{"eu-west-1"}}) for _, use := range []domainUse{domainTouch, domainCreate, domainDestroy} { if _, err := g.domainMembers(ctx, "victim.com", false, use, "eu-west-1"); !errors.Is(err, errBackendUnproven) { t.Fatalf("use %d on a foreign claim: %v", use, err) } } // bab owns example.com; zed claimed sub.example.com. A DNS name under // sub.example.com is zed's, not bab's parent domain's. putMailClaim(t, raw, mail.DomainClaim{Domain: "example.com", Account: "bab", App: "shop"}) putMailJSON(t, raw, mail.DomainKey("bab", "example.com"), mail.DomainRecord{Domain: "example.com", Apps: []string{"shop"}}) putMailClaim(t, raw, mail.DomainClaim{Domain: "sub.example.com", Account: "zed", App: "web"}) if _, err := g.domainMembers(ctx, "tok._domainkey.sub.example.com", true, domainCreate, ""); !errors.Is(err, errBackendUnproven) { t.Fatalf("DNS under another account's claimed subdomain: %v", err) } if scopes, err := g.domainMembers(ctx, "tok._domainkey.example.com", true, domainCreate, ""); err != nil || !scopesEqual(scopes, hostops.AppScope{Account: "bab", App: "shop"}) { t.Fatalf("own domain DNS: %v %v", scopes, err) } // No permit at all: nothing resolves. if _, err := g.domainMembers(t.Context(), "example.com", false, domainTouch, ""); !errors.Is(err, errBackendUnresolved) { t.Fatalf("no permits: %v", err) } } // The persisted members (record and claim app) and the proposed member all // resolve, so a sibling's re-run needs the first claimer's permit too. func TestMailGuardDomainScopeIsComplete(t *testing.T) { raw := release.NewDirStore(t.TempDir()) first := hostops.AppScope{Account: "bab", App: "first"} second := hostops.AppScope{Account: "bab", App: "second"} ctx := mailGuardPermits(t, raw, first, second) g := backendGuard{data: raw} putMailClaim(t, raw, mail.DomainClaim{Domain: "shared.com", Account: "bab", App: "first"}) putMailJSON(t, raw, mail.DomainKey("bab", "shared.com"), mail.DomainRecord{Domain: "shared.com", Apps: []string{"second"}}) scopes, err := g.domainMembers(ctx, "shared.com", false, domainCreate, "eu-west-1") if err != nil || !scopesEqual(scopes, first, second) { t.Fatalf("complete scope: %v %v", scopes, err) } } // Rev 5 P2: main passes "" to SetIdentityConfigSet to unbind an identity when // SNS is disabled. The wrapper authorizes the domain and lets the empty // destination through; a named set still needs the account's own set name, // and an unproven domain still refuses an unbind. Fails on the original // wrapper, which resolved "" through ConfigSetName and always refused. func TestClaimMailProvisionerSetIdentityConfigSetUnbinds(t *testing.T) { raw := release.NewDirStore(t.TempDir()) titogo := hostops.AppScope{Account: "bab", App: "titogo"} ctx := mailGuardPermits(t, raw, titogo) w := claimTestWriter(t, raw) lease, err := w.AcquireMailGate(ctx, hostops.MailGateOwner{Console: "c", Enrollment: "e", Controller: "k"}, hostops.MailGateScope{}) if err != nil { t.Fatal(err) } ctx = hostops.WithMailGateLease(ctx, lease) const domain = "titogo.bab.oncarlos.com" putMailClaim(t, raw, mail.DomainClaim{Domain: domain, Account: "bab", App: "titogo"}) putMailJSON(t, raw, mail.DomainKey("bab", domain), mail.DomainRecord{Domain: domain, Apps: []string{"titogo"}}) mem := mail.NewMemProvisioner("1") if _, err = mem.EnsureIdentity(ctx, "eu-west-1", domain, "", false); err != nil { t.Fatal(err) } p := newClaimMailProvisioner(mem, backendGuard{data: raw, control: w}) set := func() string { return mem.Identities["eu-west-1|"+domain].ConfigSet } if err = p.SetIdentityConfigSet(ctx, "eu-west-1", domain, mail.ConfigSetName("bab")); err != nil || set() != mail.ConfigSetName("bab") { t.Fatalf("bind to the account's own set: err=%v set=%q", err, set()) } if err = p.SetIdentityConfigSet(ctx, "eu-west-1", domain, "carlos-zed"); err == nil || set() != mail.ConfigSetName("bab") { t.Fatalf("bind to another account's set: err=%v set=%q", err, set()) } if err = p.SetIdentityConfigSet(ctx, "eu-west-1", domain, ""); err != nil { t.Fatalf("unbind: %v", err) } if set() != "" { t.Fatalf("unbind left set %q", set()) } // The same call through the generated-wrapper check path. if _, err = mem.EnsureIdentity(ctx, "eu-west-1", "other.example.com", "", false); err != nil { t.Fatal(err) } check := claimBackendCheck(backendGuard{data: raw, control: w}) after := func(domain, set string) json.RawMessage { b, _ := json.Marshal(map[string]string{"region": "eu-west-1", "domain": domain, "configSet": set}) return b } target := func(domain, set string) hostops.MutationTarget { return hostops.MutationTarget{Surface: "mail.Provisioner", Method: "SetIdentityConfigSet", Key: "eu-west-1", After: after(domain, set)} } if err = check(ctx, target(domain, "")); err != nil { t.Fatalf("check of an unbind: %v", err) } if err = check(ctx, target("other.example.com", "")); err == nil { t.Fatal("an unbind on a domain nothing proves was authorized") } if err = p.SetIdentityConfigSet(ctx, "eu-west-1", "other.example.com", ""); err == nil { t.Fatal("wrapper unbound a domain nothing proves") } } // Standalone and sender principals resolve by exact forward rendering only. func TestMailGuardUserNamesForwardOnly(t *testing.T) { raw := release.NewDirStore(t.TempDir()) shop := hostops.AppScope{Account: "bab", App: "shop"} ctx := mailGuardPermits(t, raw, shop) g := backendGuard{data: raw} // CreateStandalone's EnsureUser runs before its index is written. user := mail.StandaloneUserName("bab", "shop", "laptop") if scopes, err := g.resolveMailUser(ctx, user); err != nil || !scopesEqual(scopes, shop) { t.Fatalf("standalone of a permitted app: %v %v", scopes, err) } if scopes, err := g.resolveMailUser(ctx, mail.UserName("bab", "shop")); err != nil || !scopesEqual(scopes, shop) { t.Fatalf("delivered user: %v %v", scopes, err) } // Hyphen collision: app shop-eu's names are never shop's. for _, u := range []string{ mail.UserName("bab", "shop-eu"), mail.StandaloneUserName("bab", "shop-eu", "laptop"), "carlos-mail-bab-shop+s+", "carlos-mail-bab-shop+s+Bad", "carlos-mail-bab-shop+s+a+s+b", mail.StandaloneUserName("zed", "shop", "laptop"), } { if _, err := g.resolveMailUser(ctx, u); !errors.Is(err, errBackendUnproven) { t.Fatalf("%s resolved: %v", u, err) } } // An index naming bab/shop for a name outside bab/shop's namespace is // refused, so an index cannot move another app's principal. victim := mail.UserName("bab", "shop-eu") putMailJSON(t, raw, mail.CallerKey(victim), mail.CallerIndex{Schema: 1, IAMUser: victim, Account: "bab", App: "shop", Sender: mail.SenderAppID}) if _, err := g.resolveMailUser(ctx, victim); !errors.Is(err, errBackendUnproven) { t.Fatalf("forged caller index: %v", err) } // A correct index resolves without any permit in ctx. own := mail.StandaloneUserName("bab", "shop", "ci") putMailJSON(t, raw, mail.CallerKey(own), mail.CallerIndex{Schema: 1, IAMUser: own, Account: "bab", App: "shop", Sender: mail.SenderAppID}) if scopes, err := g.resolveMailUser(t.Context(), own); err != nil || !scopesEqual(scopes, shop) { t.Fatalf("indexed standalone: %v %v", scopes, err) } }