carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package mail
2
3import (
4 "encoding/json"
5 "testing"
6)
7
8type policyDoc struct {
9 Version string `json:"Version"`
10 Statement []struct {
11 Effect string `json:"Effect"`
12 Action []string `json:"Action"`
13 Resource []string `json:"Resource"`
14 Condition map[string]map[string][]string
15 } `json:"Statement"`
16}
17
18func parsePolicy(t *testing.T, s string) policyDoc {
19 t.Helper()
20 var d policyDoc
21 if err := json.Unmarshal([]byte(s), &d); err != nil {
22 t.Fatalf("policy is not valid JSON: %v\n%s", err, s)
23 }
24 return d
25}
26
27// BOTH rails or neither. Resource alone authorises sending AS the identity,
28// and a subdomain identity covers From addresses the app must not claim.
29// The condition alone does not bind the identity used to sign. A change that
30// drops either must fail a test, not a review (spec §4).
31func TestPolicyCarriesBothIsolationRails(t *testing.T) {
32 got, err := SendPolicyJSON(
33 []string{"arn:aws:ses:eu-west-1:1:identity/harbourmail.bag.oncarlos.com"},
34 nil,
35 []string{"harbourmail.bag.oncarlos.com"})
36 if err != nil {
37 t.Fatalf("SendPolicyJSON: %v", err)
38 }
39 d := parsePolicy(t, got)
40 if len(d.Statement) != 1 {
41 t.Fatalf("want exactly one statement, got %d", len(d.Statement))
42 }
43 st := d.Statement[0]
44 if st.Effect != "Allow" {
45 t.Fatalf("Effect = %q, want Allow", st.Effect)
46 }
47 if len(st.Action) != 1 || st.Action[0] != "ses:SendRawEmail" {
48 t.Fatalf("Action = %v, want [ses:SendRawEmail] — the contract is SMTP-only", st.Action)
49 }
50 if len(st.Resource) != 1 || st.Resource[0] != "arn:aws:ses:eu-west-1:1:identity/harbourmail.bag.oncarlos.com" {
51 t.Fatalf("Resource = %v — rail one is missing or wrong", st.Resource)
52 }
53 from := st.Condition["StringLike"]["ses:FromAddress"]
54 if len(from) != 1 || from[0] != "*@harbourmail.bag.oncarlos.com" {
55 t.Fatalf("ses:FromAddress = %v — rail two is missing or wrong", from)
56 }
57}
58
59// Multi-region: one credential, one policy, an ARN per (domain, region).
60func TestPolicyListsEveryVerifiedARN(t *testing.T) {
61 got, err := SendPolicyJSON([]string{
62 "arn:aws:ses:eu-west-1:1:identity/x.com",
63 "arn:aws:ses:us-east-1:1:identity/x.com",
64 }, nil, []string{"x.com"})
65 if err != nil {
66 t.Fatalf("SendPolicyJSON: %v", err)
67 }
68 if n := len(parsePolicy(t, got).Statement[0].Resource); n != 2 {
69 t.Fatalf("Resource count = %d, want 2 — a credential spans regions", n)
70 }
71}
72
73// Steady state is multiple domains per app. Untested single-domain case would
74// allow an implementation that rendered only froms[0] to pass the suite.
75func TestPolicyWithMultipleDomainsAndARNs(t *testing.T) {
76 got, err := SendPolicyJSON([]string{
77 "arn:aws:ses:eu-west-1:1:identity/platform.com",
78 "arn:aws:ses:us-east-1:1:identity/custom.app",
79 }, nil, []string{"platform.com", "custom.app"})
80 if err != nil {
81 t.Fatalf("SendPolicyJSON: %v", err)
82 }
83 d := parsePolicy(t, got)
84 st := d.Statement[0]
85
86 // Check resources
87 if len(st.Resource) != 2 {
88 t.Fatalf("Resource count = %d, want 2", len(st.Resource))
89 }
90 resourceSet := make(map[string]bool)
91 for _, r := range st.Resource {
92 resourceSet[r] = true
93 }
94 if !resourceSet["arn:aws:ses:eu-west-1:1:identity/platform.com"] {
95 t.Fatalf("Resource missing arn:aws:ses:eu-west-1:1:identity/platform.com")
96 }
97 if !resourceSet["arn:aws:ses:us-east-1:1:identity/custom.app"] {
98 t.Fatalf("Resource missing arn:aws:ses:us-east-1:1:identity/custom.app")
99 }
100
101 // Check from addresses
102 froms := st.Condition["StringLike"]["ses:FromAddress"]
103 if len(froms) != 2 {
104 t.Fatalf("ses:FromAddress count = %d, want 2", len(froms))
105 }
106 fromSet := make(map[string]bool)
107 for _, f := range froms {
108 fromSet[f] = true
109 }
110 if !fromSet["*@platform.com"] {
111 t.Fatalf("ses:FromAddress missing *@platform.com")
112 }
113 if !fromSet["*@custom.app"] {
114 t.Fatalf("ses:FromAddress missing *@custom.app")
115 }
116}
117
118// An empty grant must not render a policy that permits everything. Refusing
119// is the only safe direction: a wildcard here would let one app send as any
120// identity in the estate.
121// TestPolicyGrantsTheConfigSetItSendsThrough is issue #271, and it is the
122// difference between a primitive that can emit events and one that can send
123// mail — before this, it could not do both.
124//
125// SES authorises a configuration set as a SEPARATE resource. The moment
126// SetIdentityConfigSet binds one to an identity, every send through that
127// identity needs ses:SendRawEmail on the config-set ARN too, and a policy
128// naming only the identity is refused with a 554:
129//
130// Access denied: User '...' is not authorized to perform 'ses:SendRawEmail'
131// on resource 'arn:aws:ses:eu-west-1:...:configuration-set/carlos-bag'
132//
133// That is not hypothetical and it is not retrofit-specific: EnsureDomain
134// binds a config set whenever SNSTopicARN is set, so on any deployment with
135// event ingest configured a freshly enabled tenant could not send at all.
136// Proven in production on 2026-08-23, at the cost of a short outage.
137func TestPolicyGrantsTheConfigSetItSendsThrough(t *testing.T) {
138 doc, err := SendPolicyJSON(
139 []string{"arn:aws:ses:eu-west-1:1234:identity/a.example.com"},
140 []string{"arn:aws:ses:eu-west-1:1234:configuration-set/carlos-bag"},
141 []string{"a.example.com"},
142 )
143 if err != nil {
144 t.Fatalf("SendPolicyJSON: %v", err)
145 }
146 res := policyResources(t, doc)
147 if !res["arn:aws:ses:eu-west-1:1234:identity/a.example.com"] {
148 t.Error("the identity ARN is missing from Resource")
149 }
150 if !res["arn:aws:ses:eu-west-1:1234:configuration-set/carlos-bag"] {
151 t.Error("the configuration-set ARN is missing from Resource — every send through a bound identity 554s without it")
152 }
153}
154
155// TestPolicyWithNoConfigSetIsUnchanged: a deployment with no SNS topic binds
156// no configuration set, and its policy must stay exactly what it was. The
157// fix must not make event ingest a precondition for sending.
158func TestPolicyWithNoConfigSetIsUnchanged(t *testing.T) {
159 doc, err := SendPolicyJSON(
160 []string{"arn:aws:ses:eu-west-1:1234:identity/a.example.com"},
161 nil,
162 []string{"a.example.com"},
163 )
164 if err != nil {
165 t.Fatalf("SendPolicyJSON: %v", err)
166 }
167 res := policyResources(t, doc)
168 if len(res) != 1 || !res["arn:aws:ses:eu-west-1:1234:identity/a.example.com"] {
169 t.Fatalf("Resource = %v, want the identity ARN alone", res)
170 }
171}
172
173// TestPolicyRefusesABadConfigSetARN: the config-set ARN reaches IAM as a
174// resource pattern exactly as the identity ARN does, so it gets the same
175// scrutiny. A wildcard would widen the grant to every configuration set in
176// the estate; a wrong resource type would be a silently ineffective grant.
177func TestPolicyRefusesABadConfigSetARN(t *testing.T) {
178 for _, bad := range []string{
179 "arn:aws:ses:eu-west-1:1234:configuration-set/*",
180 "arn:aws:ses:eu-west-1:1234:configuration-set/carlos-?ag",
181 "arn:aws:ses:eu-west-1:1234:identity/a.example.com",
182 "arn:aws:sns:eu-west-1:1234:configuration-set/carlos-bag",
183 "arn:aws:ses::1234:configuration-set/carlos-bag",
184 "arn:aws:ses:eu-west-1::configuration-set/carlos-bag",
185 "not-an-arn",
186 } {
187 if _, err := SendPolicyJSON(
188 []string{"arn:aws:ses:eu-west-1:1234:identity/a.example.com"},
189 []string{bad},
190 []string{"a.example.com"},
191 ); err == nil {
192 t.Errorf("config-set ARN %q was accepted", bad)
193 }
194 }
195}
196
197// TestVerifiedConfigSetARNsSkipsWhatItShould: only a VERIFIED region with a
198// config set contributes one. A pending region, or a verified region that
199// has no set bound, must contribute nothing — the first because its identity
200// is not usable yet, the second because that is the ordinary no-SNS shape.
201func TestVerifiedConfigSetARNsSkipsWhatItShould(t *testing.T) {
202 rec := &DomainRecord{Domain: "a.example.com", Regions: map[string]*RegionState{
203 "eu-west-1": {IdentityARN: "arn:aws:ses:eu-west-1:1234:identity/a.example.com",
204 ConfigSet: "carlos-bag", Status: StatusVerified},
205 "us-east-1": {IdentityARN: "arn:aws:ses:us-east-1:1234:identity/a.example.com",
206 ConfigSet: "carlos-bag", Status: StatusPending},
207 "eu-west-2": {IdentityARN: "arn:aws:ses:eu-west-2:1234:identity/a.example.com",
208 ConfigSet: "", Status: StatusVerified},
209 }}
210 got := rec.VerifiedConfigSetARNs()
211 want := []string{"arn:aws:ses:eu-west-1:1234:configuration-set/carlos-bag"}
212 if len(got) != len(want) || (len(got) == 1 && got[0] != want[0]) {
213 t.Fatalf("VerifiedConfigSetARNs = %v, want %v", got, want)
214 }
215}
216
217// policyResources pulls the Resource list out of a rendered policy.
218func policyResources(t *testing.T, doc string) map[string]bool {
219 t.Helper()
220 var parsed struct {
221 Statement []struct {
222 Resource []string `json:"Resource"`
223 } `json:"Statement"`
224 }
225 if err := json.Unmarshal([]byte(doc), &parsed); err != nil {
226 t.Fatalf("policy is not JSON: %v", err)
227 }
228 if len(parsed.Statement) != 1 {
229 t.Fatalf("statements = %d, want 1", len(parsed.Statement))
230 }
231 out := map[string]bool{}
232 for _, r := range parsed.Statement[0].Resource {
233 out[r] = true
234 }
235 return out
236}
237
238func TestPolicyRefusesAnEmptyGrant(t *testing.T) {
239 if _, err := SendPolicyJSON(nil, nil, nil); err == nil {
240 t.Fatal("SendPolicyJSON with no ARNs and no domains returned a policy, want an error")
241 }
242 if _, err := SendPolicyJSON([]string{"arn:x"}, nil, nil); err == nil {
243 t.Fatal("SendPolicyJSON with no domains returned a policy, want an error")
244 }
245 if _, err := SendPolicyJSON(nil, nil, []string{"x.com"}); err == nil {
246 t.Fatal("SendPolicyJSON with no ARNs returned a policy, want an error")
247 }
248}
249
250// A domain that reached this far unvalidated could inject a wildcard into
251// the condition ("*" would match every address). Refuse rather than render.
252func TestPolicyRefusesAnInvalidDomain(t *testing.T) {
253 if _, err := SendPolicyJSON([]string{"arn:x"}, nil, []string{"*"}); err == nil {
254 t.Fatal("SendPolicyJSON accepted a wildcard domain")
255 }
256}
257
258// IAM expands * and ? as match patterns in ALL ARN segments, not just the
259// identity name. Reject wildcards anywhere in the ARN, and validate the
260// entire structure: exactly 6 colon-delimited parts, region and account-id
261// non-empty, no trailing segments, no bypasses.
262func TestPolicyRefusesARNWildcards(t *testing.T) {
263 tests := []struct {
264 name string
265 arn string
266 }{
267 // Wildcards in different segments
268 {"wildcard in region", "arn:aws:ses:*:1:identity/example.com"},
269 {"wildcard in account", "arn:aws:ses:eu-west-1:*:identity/example.com"},
270 {"wildcard in both", "arn:aws:ses:*:*:identity/example.com"},
271 {"question mark in region", "arn:aws:ses:?:1:identity/example.com"},
272
273 // Malformed structure
274 {"trailing segment", "arn:aws:ses:eu-west-1:1:identity/x.com:extra"},
275 {"empty region", "arn:aws:ses::1:identity/example.com"},
276 {"empty account", "arn:aws:ses:eu-west-1::identity/example.com"},
277 }
278
279 for _, tt := range tests {
280 t.Run(tt.name, func(t *testing.T) {
281 _, err := SendPolicyJSON([]string{tt.arn}, nil, []string{"example.com"})
282 if err == nil {
283 t.Fatalf("SendPolicyJSON accepted invalid ARN: %s", tt.arn)
284 }
285 })
286 }
287}
288
289// Legitimate production shape must pass: real 12-digit account ID, valid region.
290func TestPolicyAcceptsLegitimateARN(t *testing.T) {
291 _, err := SendPolicyJSON(
292 []string{"arn:aws:ses:eu-west-1:123456789012:identity/harbourmail.bag.oncarlos.com"},
293 nil,
294 []string{"harbourmail.bag.oncarlos.com"})
295 if err != nil {
296 t.Fatalf("SendPolicyJSON rejected legitimate ARN: %v", err)
297 }
298}
299
300func TestTenantNameIsPrefixed(t *testing.T) {
301 if got, want := TenantName("bag"), "carlos-bag"; got != want {
302 t.Fatalf("TenantName = %q, want %q", got, want)
303 }
304}
305
306func TestUserNamesAreStableAndDistinct(t *testing.T) {
307 if got, want := UserName("bag", "harbourmail"), "carlos-mail-bag-harbourmail"; got != want {
308 t.Fatalf("UserName = %q, want %q", got, want)
309 }
310 // A standalone credential MUST NOT share the delivered user: IAM allows
311 // two access keys per user and both slots are spent on the delivered key
312 // plus the rotation overlap.
313 if StandaloneUserName("bag", "harbourmail", "laptop") == UserName("bag", "harbourmail") {
314 t.Fatal("standalone credential collided with the delivered user")
315 }
316 // App names allow hyphens, so ensure no collision with a different separator.
317 if StandaloneUserName("bag", "see", "laptop") == UserName("bag", "see-s-laptop") {
318 t.Fatal("standalone credential collided with a hyphenated app name")
319 }
320}
321