package mail import ( "encoding/json" "testing" ) type policyDoc struct { Version string `json:"Version"` Statement []struct { Effect string `json:"Effect"` Action []string `json:"Action"` Resource []string `json:"Resource"` Condition map[string]map[string][]string } `json:"Statement"` } func parsePolicy(t *testing.T, s string) policyDoc { t.Helper() var d policyDoc if err := json.Unmarshal([]byte(s), &d); err != nil { t.Fatalf("policy is not valid JSON: %v\n%s", err, s) } return d } // BOTH rails or neither. Resource alone authorises sending AS the identity, // and a subdomain identity covers From addresses the app must not claim. // The condition alone does not bind the identity used to sign. A change that // drops either must fail a test, not a review (spec §4). func TestPolicyCarriesBothIsolationRails(t *testing.T) { got, err := SendPolicyJSON( []string{"arn:aws:ses:eu-west-1:1:identity/harbourmail.bag.oncarlos.com"}, nil, []string{"harbourmail.bag.oncarlos.com"}) if err != nil { t.Fatalf("SendPolicyJSON: %v", err) } d := parsePolicy(t, got) if len(d.Statement) != 1 { t.Fatalf("want exactly one statement, got %d", len(d.Statement)) } st := d.Statement[0] if st.Effect != "Allow" { t.Fatalf("Effect = %q, want Allow", st.Effect) } if len(st.Action) != 1 || st.Action[0] != "ses:SendRawEmail" { t.Fatalf("Action = %v, want [ses:SendRawEmail] — the contract is SMTP-only", st.Action) } if len(st.Resource) != 1 || st.Resource[0] != "arn:aws:ses:eu-west-1:1:identity/harbourmail.bag.oncarlos.com" { t.Fatalf("Resource = %v — rail one is missing or wrong", st.Resource) } from := st.Condition["StringLike"]["ses:FromAddress"] if len(from) != 1 || from[0] != "*@harbourmail.bag.oncarlos.com" { t.Fatalf("ses:FromAddress = %v — rail two is missing or wrong", from) } } // Multi-region: one credential, one policy, an ARN per (domain, region). func TestPolicyListsEveryVerifiedARN(t *testing.T) { got, err := SendPolicyJSON([]string{ "arn:aws:ses:eu-west-1:1:identity/x.com", "arn:aws:ses:us-east-1:1:identity/x.com", }, nil, []string{"x.com"}) if err != nil { t.Fatalf("SendPolicyJSON: %v", err) } if n := len(parsePolicy(t, got).Statement[0].Resource); n != 2 { t.Fatalf("Resource count = %d, want 2 — a credential spans regions", n) } } // Steady state is multiple domains per app. Untested single-domain case would // allow an implementation that rendered only froms[0] to pass the suite. func TestPolicyWithMultipleDomainsAndARNs(t *testing.T) { got, err := SendPolicyJSON([]string{ "arn:aws:ses:eu-west-1:1:identity/platform.com", "arn:aws:ses:us-east-1:1:identity/custom.app", }, nil, []string{"platform.com", "custom.app"}) if err != nil { t.Fatalf("SendPolicyJSON: %v", err) } d := parsePolicy(t, got) st := d.Statement[0] // Check resources if len(st.Resource) != 2 { t.Fatalf("Resource count = %d, want 2", len(st.Resource)) } resourceSet := make(map[string]bool) for _, r := range st.Resource { resourceSet[r] = true } if !resourceSet["arn:aws:ses:eu-west-1:1:identity/platform.com"] { t.Fatalf("Resource missing arn:aws:ses:eu-west-1:1:identity/platform.com") } if !resourceSet["arn:aws:ses:us-east-1:1:identity/custom.app"] { t.Fatalf("Resource missing arn:aws:ses:us-east-1:1:identity/custom.app") } // Check from addresses froms := st.Condition["StringLike"]["ses:FromAddress"] if len(froms) != 2 { t.Fatalf("ses:FromAddress count = %d, want 2", len(froms)) } fromSet := make(map[string]bool) for _, f := range froms { fromSet[f] = true } if !fromSet["*@platform.com"] { t.Fatalf("ses:FromAddress missing *@platform.com") } if !fromSet["*@custom.app"] { t.Fatalf("ses:FromAddress missing *@custom.app") } } // An empty grant must not render a policy that permits everything. Refusing // is the only safe direction: a wildcard here would let one app send as any // identity in the estate. // TestPolicyGrantsTheConfigSetItSendsThrough is issue #271, and it is the // difference between a primitive that can emit events and one that can send // mail — before this, it could not do both. // // SES authorises a configuration set as a SEPARATE resource. The moment // SetIdentityConfigSet binds one to an identity, every send through that // identity needs ses:SendRawEmail on the config-set ARN too, and a policy // naming only the identity is refused with a 554: // // Access denied: User '...' is not authorized to perform 'ses:SendRawEmail' // on resource 'arn:aws:ses:eu-west-1:...:configuration-set/carlos-bag' // // That is not hypothetical and it is not retrofit-specific: EnsureDomain // binds a config set whenever SNSTopicARN is set, so on any deployment with // event ingest configured a freshly enabled tenant could not send at all. // Proven in production on 2026-08-23, at the cost of a short outage. func TestPolicyGrantsTheConfigSetItSendsThrough(t *testing.T) { doc, err := SendPolicyJSON( []string{"arn:aws:ses:eu-west-1:1234:identity/a.example.com"}, []string{"arn:aws:ses:eu-west-1:1234:configuration-set/carlos-bag"}, []string{"a.example.com"}, ) if err != nil { t.Fatalf("SendPolicyJSON: %v", err) } res := policyResources(t, doc) if !res["arn:aws:ses:eu-west-1:1234:identity/a.example.com"] { t.Error("the identity ARN is missing from Resource") } if !res["arn:aws:ses:eu-west-1:1234:configuration-set/carlos-bag"] { t.Error("the configuration-set ARN is missing from Resource — every send through a bound identity 554s without it") } } // TestPolicyWithNoConfigSetIsUnchanged: a deployment with no SNS topic binds // no configuration set, and its policy must stay exactly what it was. The // fix must not make event ingest a precondition for sending. func TestPolicyWithNoConfigSetIsUnchanged(t *testing.T) { doc, err := SendPolicyJSON( []string{"arn:aws:ses:eu-west-1:1234:identity/a.example.com"}, nil, []string{"a.example.com"}, ) if err != nil { t.Fatalf("SendPolicyJSON: %v", err) } res := policyResources(t, doc) if len(res) != 1 || !res["arn:aws:ses:eu-west-1:1234:identity/a.example.com"] { t.Fatalf("Resource = %v, want the identity ARN alone", res) } } // TestPolicyRefusesABadConfigSetARN: the config-set ARN reaches IAM as a // resource pattern exactly as the identity ARN does, so it gets the same // scrutiny. A wildcard would widen the grant to every configuration set in // the estate; a wrong resource type would be a silently ineffective grant. func TestPolicyRefusesABadConfigSetARN(t *testing.T) { for _, bad := range []string{ "arn:aws:ses:eu-west-1:1234:configuration-set/*", "arn:aws:ses:eu-west-1:1234:configuration-set/carlos-?ag", "arn:aws:ses:eu-west-1:1234:identity/a.example.com", "arn:aws:sns:eu-west-1:1234:configuration-set/carlos-bag", "arn:aws:ses::1234:configuration-set/carlos-bag", "arn:aws:ses:eu-west-1::configuration-set/carlos-bag", "not-an-arn", } { if _, err := SendPolicyJSON( []string{"arn:aws:ses:eu-west-1:1234:identity/a.example.com"}, []string{bad}, []string{"a.example.com"}, ); err == nil { t.Errorf("config-set ARN %q was accepted", bad) } } } // TestVerifiedConfigSetARNsSkipsWhatItShould: only a VERIFIED region with a // config set contributes one. A pending region, or a verified region that // has no set bound, must contribute nothing — the first because its identity // is not usable yet, the second because that is the ordinary no-SNS shape. func TestVerifiedConfigSetARNsSkipsWhatItShould(t *testing.T) { rec := &DomainRecord{Domain: "a.example.com", Regions: map[string]*RegionState{ "eu-west-1": {IdentityARN: "arn:aws:ses:eu-west-1:1234:identity/a.example.com", ConfigSet: "carlos-bag", Status: StatusVerified}, "us-east-1": {IdentityARN: "arn:aws:ses:us-east-1:1234:identity/a.example.com", ConfigSet: "carlos-bag", Status: StatusPending}, "eu-west-2": {IdentityARN: "arn:aws:ses:eu-west-2:1234:identity/a.example.com", ConfigSet: "", Status: StatusVerified}, }} got := rec.VerifiedConfigSetARNs() want := []string{"arn:aws:ses:eu-west-1:1234:configuration-set/carlos-bag"} if len(got) != len(want) || (len(got) == 1 && got[0] != want[0]) { t.Fatalf("VerifiedConfigSetARNs = %v, want %v", got, want) } } // policyResources pulls the Resource list out of a rendered policy. func policyResources(t *testing.T, doc string) map[string]bool { t.Helper() var parsed struct { Statement []struct { Resource []string `json:"Resource"` } `json:"Statement"` } if err := json.Unmarshal([]byte(doc), &parsed); err != nil { t.Fatalf("policy is not JSON: %v", err) } if len(parsed.Statement) != 1 { t.Fatalf("statements = %d, want 1", len(parsed.Statement)) } out := map[string]bool{} for _, r := range parsed.Statement[0].Resource { out[r] = true } return out } func TestPolicyRefusesAnEmptyGrant(t *testing.T) { if _, err := SendPolicyJSON(nil, nil, nil); err == nil { t.Fatal("SendPolicyJSON with no ARNs and no domains returned a policy, want an error") } if _, err := SendPolicyJSON([]string{"arn:x"}, nil, nil); err == nil { t.Fatal("SendPolicyJSON with no domains returned a policy, want an error") } if _, err := SendPolicyJSON(nil, nil, []string{"x.com"}); err == nil { t.Fatal("SendPolicyJSON with no ARNs returned a policy, want an error") } } // A domain that reached this far unvalidated could inject a wildcard into // the condition ("*" would match every address). Refuse rather than render. func TestPolicyRefusesAnInvalidDomain(t *testing.T) { if _, err := SendPolicyJSON([]string{"arn:x"}, nil, []string{"*"}); err == nil { t.Fatal("SendPolicyJSON accepted a wildcard domain") } } // IAM expands * and ? as match patterns in ALL ARN segments, not just the // identity name. Reject wildcards anywhere in the ARN, and validate the // entire structure: exactly 6 colon-delimited parts, region and account-id // non-empty, no trailing segments, no bypasses. func TestPolicyRefusesARNWildcards(t *testing.T) { tests := []struct { name string arn string }{ // Wildcards in different segments {"wildcard in region", "arn:aws:ses:*:1:identity/example.com"}, {"wildcard in account", "arn:aws:ses:eu-west-1:*:identity/example.com"}, {"wildcard in both", "arn:aws:ses:*:*:identity/example.com"}, {"question mark in region", "arn:aws:ses:?:1:identity/example.com"}, // Malformed structure {"trailing segment", "arn:aws:ses:eu-west-1:1:identity/x.com:extra"}, {"empty region", "arn:aws:ses::1:identity/example.com"}, {"empty account", "arn:aws:ses:eu-west-1::identity/example.com"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { _, err := SendPolicyJSON([]string{tt.arn}, nil, []string{"example.com"}) if err == nil { t.Fatalf("SendPolicyJSON accepted invalid ARN: %s", tt.arn) } }) } } // Legitimate production shape must pass: real 12-digit account ID, valid region. func TestPolicyAcceptsLegitimateARN(t *testing.T) { _, err := SendPolicyJSON( []string{"arn:aws:ses:eu-west-1:123456789012:identity/harbourmail.bag.oncarlos.com"}, nil, []string{"harbourmail.bag.oncarlos.com"}) if err != nil { t.Fatalf("SendPolicyJSON rejected legitimate ARN: %v", err) } } func TestTenantNameIsPrefixed(t *testing.T) { if got, want := TenantName("bag"), "carlos-bag"; got != want { t.Fatalf("TenantName = %q, want %q", got, want) } } func TestUserNamesAreStableAndDistinct(t *testing.T) { if got, want := UserName("bag", "harbourmail"), "carlos-mail-bag-harbourmail"; got != want { t.Fatalf("UserName = %q, want %q", got, want) } // A standalone credential MUST NOT share the delivered user: IAM allows // two access keys per user and both slots are spent on the delivered key // plus the rotation overlap. if StandaloneUserName("bag", "harbourmail", "laptop") == UserName("bag", "harbourmail") { t.Fatal("standalone credential collided with the delivered user") } // App names allow hyphens, so ensure no collision with a different separator. if StandaloneUserName("bag", "see", "laptop") == UserName("bag", "see-s-laptop") { t.Fatal("standalone credential collided with a hyphenated app name") } }