| index 53da9b1ac..dabdc6f48 100644 |
| --- a/internal/mail/walk_lifecycle_test.go |
| +++ b/internal/mail/walk_lifecycle_test.go |
| @@ -62,7 +62,11 @@ func TestRotateDoesNotLiftAPauseAtIAM(t *testing.T) { |
| t.Fatal("rotate flipped the record back to active") |
| } |
| user := UserName("bag", "seapointish") |
| - if p.Users[user].Keys[rec.Provisioned.KeyID].Active { |
| + k, ok := p.Users[user].Keys[rec.Provisioned.KeyID] |
| + if !ok { |
| + t.Fatalf("minted key %s does not exist at all", rec.Provisioned.KeyID) |
| + } |
| + if k.Active { |
| t.Fatal("rotate minted a LIVE key for a paused app — the pause was silently lifted at IAM") |
| } |
| } |
| @@ -114,7 +118,11 @@ func TestEnableRepairDoesNotLiftAPauseAtIAM(t *testing.T) { |
| t.Fatal("repair re-walk flipped the record back to active") |
| } |
| user := UserName("bag", "seapointish") |
| - if p.Users[user].Keys[rec.Provisioned.KeyID].Active { |
| + k, ok := p.Users[user].Keys[rec.Provisioned.KeyID] |
| + if !ok { |
| + t.Fatalf("minted key %s does not exist at all", rec.Provisioned.KeyID) |
| + } |
| + if k.Active { |
| t.Fatal("repair re-walk minted a LIVE key for a paused app — the pause was silently lifted at IAM") |
| } |
| } |
| @@ -212,18 +220,118 @@ func TestPauseReachesStandaloneCredentials(t *testing.T) { |
| if _, err := SetPaused(ctx, s, p, "bag", "seapointish", true, now); err != nil { |
| t.Fatalf("SetPaused(true): %v", err) |
| } |
| - if p.Users[standaloneUser].Keys[keyID].Active { |
| + su, ok := p.Users[standaloneUser] |
| + if !ok { |
| + t.Fatalf("standalone user %s does not exist at all", standaloneUser) |
| + } |
| + k, ok := su.Keys[keyID] |
| + if !ok { |
| + t.Fatalf("standalone key %s does not exist at all", keyID) |
| + } |
| + if k.Active { |
| t.Fatal("pause did not reach the standalone credential — it can still send") |
| } |
| |
| if _, err := SetPaused(ctx, s, p, "bag", "seapointish", false, now); err != nil { |
| t.Fatalf("SetPaused(false): %v", err) |
| } |
| - if !p.Users[standaloneUser].Keys[keyID].Active { |
| + su, ok = p.Users[standaloneUser] |
| + if !ok { |
| + t.Fatalf("resume left the standalone user %s gone entirely", standaloneUser) |
| + } |
| + k, ok = su.Keys[keyID] |
| + if !ok { |
| + t.Fatalf("resume left the standalone key %s gone entirely", keyID) |
| + } |
| + if !k.Active { |
| t.Fatal("resume did not reach the standalone credential") |
| } |
| } |
| |
| +// Round 2, item 1: CreateStandalone is a THIRD mint path and must go |
| +// through the same enforcement point as Enable and Rotate. Without it, a |
| +// member verb (`carlos email credentials create`) could hand a paused |
| +// tenant a live sending credential on the same verified domain the pause |
| +// was meant to protect — exactly the C1 shape, on a path C1's original fix |
| +// did not cover. |
| +func TestCreateStandaloneDoesNotBypassAPause(t *testing.T) { |
| + ctx, s, p, pub, now := enableFixture(t) |
| + if _, err := Enable(ctx, s, p, pub, testDeployment(), "bag", "seapointish", now); err != nil { |
| + t.Fatalf("Enable: %v", err) |
| + } |
| + if _, err := SetPaused(ctx, s, p, "bag", "seapointish", true, now); err != nil { |
| + t.Fatalf("SetPaused(true): %v", err) |
| + } |
| + |
| + creds, err := CreateStandalone(ctx, s, p, testDeployment(), "bag", "seapointish", |
| + "laptop", "paul@rslw.com", now) |
| + if err != nil { |
| + t.Fatalf("CreateStandalone: %v", err) |
| + } |
| + standaloneUser := StandaloneUserName("bag", "seapointish", "laptop") |
| + su, ok := p.Users[standaloneUser] |
| + if !ok { |
| + t.Fatalf("standalone user %s does not exist at all", standaloneUser) |
| + } |
| + k, ok := su.Keys[creds.Username] |
| + if !ok { |
| + t.Fatalf("minted standalone key %s does not exist at all", creds.Username) |
| + } |
| + if k.Active { |
| + t.Fatal("CreateStandalone minted a LIVE key for a paused app — the pause was bypassed") |
| + } |
| +} |
| + |
| +// A re-mint for an existing standalone name while paused must not reactivate |
| +// it either, and a resume must reach a standalone credential created while |
| +// paused. |
| +func TestCreateStandaloneReMintWhilePausedStaysInactiveAndResumeReactivates(t *testing.T) { |
| + ctx, s, p, pub, now := enableFixture(t) |
| + if _, err := Enable(ctx, s, p, pub, testDeployment(), "bag", "seapointish", now); err != nil { |
| + t.Fatalf("Enable: %v", err) |
| + } |
| + if _, err := SetPaused(ctx, s, p, "bag", "seapointish", true, now); err != nil { |
| + t.Fatalf("SetPaused(true): %v", err) |
| + } |
| + if _, err := CreateStandalone(ctx, s, p, testDeployment(), "bag", "seapointish", |
| + "laptop", "paul@rslw.com", now); err != nil { |
| + t.Fatalf("CreateStandalone: %v", err) |
| + } |
| + // Re-mint the same name while still paused. |
| + creds, err := CreateStandalone(ctx, s, p, testDeployment(), "bag", "seapointish", |
| + "laptop", "paul@rslw.com", now) |
| + if err != nil { |
| + t.Fatalf("CreateStandalone (re-mint): %v", err) |
| + } |
| + standaloneUser := StandaloneUserName("bag", "seapointish", "laptop") |
| + su, ok := p.Users[standaloneUser] |
| + if !ok { |
| + t.Fatalf("standalone user %s does not exist at all", standaloneUser) |
| + } |
| + k, ok := su.Keys[creds.Username] |
| + if !ok { |
| + t.Fatalf("minted standalone key %s does not exist at all", creds.Username) |
| + } |
| + if k.Active { |
| + t.Fatal("a re-mint while paused produced a LIVE key") |
| + } |
| + |
| + if _, err := SetPaused(ctx, s, p, "bag", "seapointish", false, now); err != nil { |
| + t.Fatalf("SetPaused(false): %v", err) |
| + } |
| + su, ok = p.Users[standaloneUser] |
| + if !ok { |
| + t.Fatalf("resume left the standalone user %s gone entirely", standaloneUser) |
| + } |
| + k, ok = su.Keys[creds.Username] |
| + if !ok { |
| + t.Fatalf("resume left the standalone key %s gone entirely", creds.Username) |
| + } |
| + if !k.Active { |
| + t.Fatal("resume did not reach a standalone credential created while paused") |
| + } |
| +} |
| + |
| // A standalone credential lives on its OWN IAM user, because the delivered |
| // user's two key slots are spent on the delivered key and the rotation |
| // overlap. Sharing would work until the first rotation and then break it. |