carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package console
2
3import (
4 "context"
5 "net/http"
6 "sync/atomic"
7 "testing"
8
9 "github.com/carlosframework/platform/internal/appconfig"
10 "github.com/carlosframework/platform/internal/objstore"
11 "github.com/carlosframework/platform/internal/release"
12)
13
14// racingRunner is a claims-on AppMutationRunner whose first permit for action
15// lets a concurrent writer land before the callback runs.
16type racingRunner struct {
17 release.Store
18 action string
19 race func(context.Context)
20 raced atomic.Bool
21 puts atomic.Int64
22}
23
24func (r *racingRunner) RunAppMutation(ctx context.Context, account, app, action string, f func(context.Context) error) error {
25 if action == r.action && r.raced.CompareAndSwap(false, true) {
26 r.race(ctx)
27 }
28 return f(ctx)
29}
30
31func (r *racingRunner) Put(ctx context.Context, key string, body []byte, etag string) (string, error) {
32 if r.raced.Load() {
33 r.puts.Add(1)
34 }
35 return r.Store.Put(ctx, key, body, etag)
36}
37
38func rotationRaceFixture(t *testing.T, code string) (*consoleFixture, string, string) {
39 t.Helper()
40 fx, _, token, priv := storeArmed(t, code)
41 grantStore(t, fx, token)
42 fx.srv.cfg.StoreScanner = objstore.NewMemScanner()
43 enableDeploymentScanning(t, fx)
44 grantScanning(t, fx, token, "titogo")
45 return fx, token, priv
46}
47
48func editRecord(t *testing.T, fx *consoleFixture, edit func(*objstore.Record)) {
49 t.Helper()
50 ctx := context.Background()
51 rec, etag, err := objstore.Get(ctx, fx.rel, fx.sqid, "titogo")
52 if err != nil || rec == nil {
53 t.Fatalf("read record: %v %v", rec, err)
54 }
55 edit(rec)
56 if _, err := objstore.Put(ctx, fx.rel, fx.sqid, "titogo", rec, etag); err != nil {
57 t.Fatal(err)
58 }
59}
60
61func revokeBehindTheDoor(t *testing.T, fx *consoleFixture) {
62 t.Helper()
63 editRecord(t, fx, func(rec *objstore.Record) {
64 rec.Scanning.Enabled = nil
65 rec.Scanning.Enforce = nil
66 })
67 if _, err := appconfig.Apply(context.Background(), fx.rel, fx.sqid, "titogo", nil,
68 []string{objstore.DefaultEnvPrefix + "_SCAN_CALLBACK_SECRET"}); err != nil {
69 t.Fatal(err)
70 }
71}
72
73func TestDeploymentRotateDeliversTheGenerationHeldUnderThePermit(t *testing.T) {
74 fx, _, priv := rotationRaceFixture(t, "code-rotate-race-gen")
75 fx.srv.cfg.CallbackSigner = callbackTestSigner(t, "race-master", testWebhookMaster)
76 staleKeyID := storedRecord(t, fx, "titogo").Scanning.WebhookKeyID
77 runner := &racingRunner{Store: fx.rel, action: "scan-webhook-rotate", race: func(context.Context) {
78 editRecord(t, fx, func(rec *objstore.Record) { rec.Scanning.WebhookGen++ })
79 }}
80 fx.srv.cfg.Releases = runner
81 pub, _ := appconfig.ParsePublicKey(fx.srv.cfg.SecretsPub, "test")
82
83 remaining, changed, err := fx.srv.rotateWebhookBudget(context.Background(), pub)
84 if err != nil {
85 t.Fatal(err)
86 }
87 if remaining != 0 || changed != 1 {
88 t.Fatalf("rotation = remaining %d changed %d, want 0/1", remaining, changed)
89 }
90 rec := storedRecord(t, fx, "titogo")
91 if rec.Scanning.WebhookKeyID == staleKeyID || rec.Scanning.WebhookKeyID != fx.srv.cfg.CallbackSigner.CurrentKeyID() {
92 t.Fatalf("webhook_key_id = %q, want current", rec.Scanning.WebhookKeyID)
93 }
94 want := string(fx.srv.cfg.CallbackSigner.DerivedKey(fx.sqid, "titogo", rec.Scanning.WebhookGen))
95 if got := callbackSecret(t, fx, priv, "titogo"); got != want {
96 t.Fatal("delivered callback secret does not verify against the recorded generation")
97 }
98}
99
100func TestDeploymentRotateSkipsWorkFinishedBeforeThePermit(t *testing.T) {
101 fx, _, priv := rotationRaceFixture(t, "code-rotate-race-done")
102 fx.srv.cfg.CallbackSigner = callbackTestSigner(t, "race-master", testWebhookMaster)
103 pub, _ := appconfig.ParsePublicKey(fx.srv.cfg.SecretsPub, "test")
104 var runner *racingRunner
105 runner = &racingRunner{Store: fx.rel, action: "scan-webhook-rotate", race: func(ctx context.Context) {
106 cur := storedRecord(t, fx, "titogo")
107 gen := cur.Scanning.WebhookGen + 1
108 if err := fx.srv.deliverCallbackSecret(ctx, pub, fx.sqid, "titogo", cur, gen); err != nil {
109 t.Fatal(err)
110 }
111 manifest, err := appconfig.Get(ctx, fx.rel, fx.sqid, "titogo")
112 if err != nil {
113 t.Fatal(err)
114 }
115 editRecord(t, fx, func(rec *objstore.Record) {
116 rec.Scanning.WebhookGen = gen
117 rec.Scanning.WebhookKeyID = fx.srv.cfg.CallbackSigner.CurrentKeyID()
118 rec.Scanning.WebhookConfigHash = scanSecretHash(manifest.Vars[rec.Declared.EnvPrefix+"_SCAN_CALLBACK_SECRET"])
119 })
120 runner.puts.Store(0)
121 }}
122 fx.srv.cfg.Releases = runner
123
124 remaining, changed, err := fx.srv.rotateWebhookBudget(context.Background(), pub)
125 if err != nil {
126 t.Fatal(err)
127 }
128 if remaining != 0 || changed != 0 || runner.puts.Load() != 0 {
129 t.Fatalf("rotation = remaining %d changed %d puts %d, want 0/0/0", remaining, changed, runner.puts.Load())
130 }
131 rec := storedRecord(t, fx, "titogo")
132 want := string(fx.srv.cfg.CallbackSigner.DerivedKey(fx.sqid, "titogo", rec.Scanning.WebhookGen))
133 if got := callbackSecret(t, fx, priv, "titogo"); got != want {
134 t.Fatal("rotation overwrote the concurrent rotation's callback secret")
135 }
136}
137
138func TestDeploymentRotateLeavesARevokedAppAlone(t *testing.T) {
139 fx, _, _ := rotationRaceFixture(t, "code-rotate-race-revoke")
140 fx.srv.cfg.CallbackSigner = callbackTestSigner(t, "race-master", testWebhookMaster)
141 before := storedRecord(t, fx, "titogo").Scanning.WebhookKeyID
142 runner := &racingRunner{Store: fx.rel, action: "scan-webhook-rotate", race: func(context.Context) {
143 revokeBehindTheDoor(t, fx)
144 }}
145 fx.srv.cfg.Releases = runner
146 pub, _ := appconfig.ParsePublicKey(fx.srv.cfg.SecretsPub, "test")
147
148 remaining, changed, err := fx.srv.rotateWebhookBudget(context.Background(), pub)
149 if err != nil {
150 t.Fatal(err)
151 }
152 if remaining != 0 || changed != 0 || runner.puts.Load() != 0 {
153 t.Fatalf("rotation = remaining %d changed %d puts %d, want 0/0/0", remaining, changed, runner.puts.Load())
154 }
155 if _, ok := appVars(t, fx, "titogo")[objstore.DefaultEnvPrefix+"_SCAN_CALLBACK_SECRET"]; ok {
156 t.Fatal("rotation delivered a callback secret to a revoked app")
157 }
158 rec := storedRecord(t, fx, "titogo")
159 if rec.Scanning.Enabled != nil || rec.Scanning.WebhookKeyID != before {
160 t.Fatalf("rotation rewrote a revoked scanning block: %+v", rec.Scanning)
161 }
162}
163
164func TestPerAppRotateRefusesARevokeLandedBeforeThePermit(t *testing.T) {
165 fx, token, _ := rotationRaceFixture(t, "code-rotate-race-app-revoke")
166 before := storedRecord(t, fx, "titogo").Scanning.WebhookGen
167 runner := &racingRunner{Store: fx.rel, action: "store-scan-callback-rotate", race: func(context.Context) {
168 revokeBehindTheDoor(t, fx)
169 }}
170 fx.srv.cfg.Releases = runner
171
172 s, o := postJSON(t, scanURL(fx, "titogo")+"/callback-key/rotate", map[string]any{}, token)
173 if s != http.StatusConflict || o["error"] != "scanning is not enabled on this store — run `carlos store scan grant` first" {
174 t.Fatalf("per-app rotate after revoke = %d %v, want 409", s, o)
175 }
176 if runner.puts.Load() != 0 {
177 t.Fatalf("per-app rotate after revoke wrote %d objects, want none", runner.puts.Load())
178 }
179 if _, ok := appVars(t, fx, "titogo")[objstore.DefaultEnvPrefix+"_SCAN_CALLBACK_SECRET"]; ok {
180 t.Fatal("per-app rotate delivered a callback secret to a revoked app")
181 }
182 if got := storedRecord(t, fx, "titogo").Scanning.WebhookGen; got != before {
183 t.Fatalf("webhook_gen = %d, want unchanged %d", got, before)
184 }
185}
186