| new file mode 100644 |
| index 000000000..5ffa6df1d |
| --- /dev/null |
| +++ b/internal/console/deploysign_test.go |
| @@ -0,0 +1,226 @@ |
| +package console |
| + |
| +import ( |
| + "context" |
| + "crypto/ed25519" |
| + "encoding/base64" |
| + "encoding/json" |
| + "errors" |
| + "net/http" |
| + "os" |
| + "strings" |
| + "testing" |
| + "time" |
| + |
| + "github.com/carlosframework/platform/internal/release" |
| + "github.com/carlosframework/platform/internal/svcrole" |
| +) |
| + |
| +// deployFixture is a console with app hello in bab, v1 and v2 shipped by |
| +// paul, and a deploy credential ci over hello with rungs canary and edge. |
| +func deployFixture(t *testing.T) (fx *consoleFixture, person, deploy string) { |
| + t.Helper() |
| + fx = newConsoleFixture(t) |
| + if _, err := fx.st.CreateAppIn(OpsAccountID, "hello", "paul@keymail.dev", time.Now()); err != nil { |
| + t.Fatal(err) |
| + } |
| + person = cliToken(t, fx, "paul@keymail.dev", "code-deploy-sign") |
| + liftShipBudget(fx) |
| + for _, v := range []string{"v1", "v2"} { |
| + status, out := postShip(t, fx.ts.URL+"/api/cli/apps/bab/hello/releases", person, |
| + []shipPart{metaPart(t, v, "binary"), {name: "linux-arm64", body: []byte("bin " + v)}}) |
| + if status != http.StatusCreated { |
| + t.Fatalf("ship %s = %d %v", v, status, out) |
| + } |
| + } |
| + tok, err := fx.st.NewServicePrincipalScoped(OpsAccountID, "ci", "hello", svcrole.Deploy, []string{"canary", "edge"}, "paul@keymail.dev", time.Now()) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + return fx, person, tok |
| +} |
| + |
| +// pointerGrant returns the decoded payload of the grant carried by a raw |
| +// channel pointer, or nil when the pointer is root-signed (no grant field). |
| +func pointerGrant(t *testing.T, fx *consoleFixture, channel string) map[string]any { |
| + t.Helper() |
| + raw, _, err := release.GetQualified(context.Background(), fx.rel, release.ChannelKey("bab", "hello", channel)) |
| + if err != nil { |
| + t.Fatalf("read %s pointer: %v", channel, err) |
| + } |
| + var wrap struct { |
| + Grant *struct { |
| + Payload string `json:"payload"` |
| + Sig string `json:"sig"` |
| + } `json:"grant"` |
| + } |
| + if err := json.Unmarshal(raw, &wrap); err != nil { |
| + t.Fatalf("pointer is not JSON: %v", err) |
| + } |
| + if wrap.Grant == nil { |
| + return nil |
| + } |
| + pl, err := base64.StdEncoding.DecodeString(wrap.Grant.Payload) |
| + if err != nil { |
| + pl, err = base64.RawURLEncoding.DecodeString(wrap.Grant.Payload) |
| + if err != nil { |
| + t.Fatalf("grant payload is not base64: %v", err) |
| + } |
| + } |
| + var g map[string]any |
| + if err := json.Unmarshal(pl, &g); err != nil { |
| + t.Fatalf("grant payload is not JSON: %v", err) |
| + } |
| + return g |
| +} |
| + |
| +func grantRungs(t *testing.T, g map[string]any) []string { |
| + t.Helper() |
| + var out []string |
| + rs, _ := g["rungs"].([]any) |
| + for _, r := range rs { |
| + s, _ := r.(string) |
| + out = append(out, s) |
| + } |
| + return out |
| +} |
| + |
| +func serialNow(t *testing.T, fx *consoleFixture) int64 { |
| + t.Helper() |
| + doc, _, err := release.ReadSerial(context.Background(), fx.rel) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + return doc.Serial |
| +} |
| + |
| +func TestDeployPromoteIsSignedByAScopedGrant(t *testing.T) { |
| + fx, _, deploy := deployFixture(t) |
| + before := serialNow(t, fx) |
| + status, out := postJSON(t, fx.ts.URL+"/api/cli/apps/bab/hello/promote", |
| + map[string]any{"version": "v1", "channel": "edge"}, deploy) |
| + if status != http.StatusOK || str(t, out, "result") != "promoted" { |
| + t.Fatalf("deploy promote = %d %v", status, out) |
| + } |
| + if after := serialNow(t, fx); after != before+1 { |
| + t.Fatalf("serial %d -> %d, want +1", before, after) |
| + } |
| + evs, err := fx.st.Events(OpsAccountID, "hello", 10) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + found := false |
| + for _, e := range evs { |
| + if e.Action == "promote" { |
| + found = true |
| + } |
| + } |
| + if !found { |
| + t.Fatalf("no promote event in %v", evs) |
| + } |
| + cp, err := release.Channel(context.Background(), fx.rel, fx.pub, "bab", "hello", "edge") |
| + if err != nil { |
| + t.Fatalf("edge does not verify: %v", err) |
| + } |
| + if cp.Version != "v1" { |
| + t.Fatalf("edge = %q, want v1", cp.Version) |
| + } |
| + g := pointerGrant(t, fx, "edge") |
| + if g == nil { |
| + t.Fatal("edge pointer carries no grant") |
| + } |
| + if got := strings.Join(grantRungs(t, g), ","); got != "canary,edge" { |
| + t.Fatalf("grant rungs = %q, want canary,edge", got) |
| + } |
| + if g["name"] != "deploy.ci" { |
| + t.Fatalf("grant name = %v, want deploy.ci", g["name"]) |
| + } |
| +} |
| + |
| +func TestDeployRepeatIsANoOp(t *testing.T) { |
| + fx, _, deploy := deployFixture(t) |
| + body := map[string]any{"version": "v1", "channel": "edge"} |
| + if status, out := postJSON(t, fx.ts.URL+"/api/cli/apps/bab/hello/promote", body, deploy); status != http.StatusOK || str(t, out, "result") != "promoted" { |
| + t.Fatalf("first promote = %d %v", status, out) |
| + } |
| + before := serialNow(t, fx) |
| + status, out := postJSON(t, fx.ts.URL+"/api/cli/apps/bab/hello/promote", body, deploy) |
| + if status != http.StatusOK || str(t, out, "result") != "noop" { |
| + t.Fatalf("repeat promote = %d %v, want 200 noop", status, out) |
| + } |
| + if after := serialNow(t, fx); after != before { |
| + t.Fatalf("serial %d -> %d on a no-op", before, after) |
| + } |
| +} |
| + |
| +func TestDeploySignerRefusesWithoutTheScopeFunction(t *testing.T) { |
| + fx, _, _ := deployFixture(t) |
| + sg, err := fx.srv.deploySigner(Principal{Kind: PrincipalService, Role: svcrole.Deploy, Name: "ci", App: "hello", Rungs: []string{"edge"}}) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + ctx := context.Background() |
| + for _, ch := range []string{"beta", "stable"} { |
| + if _, err := sg.PromoteOnLadder(ctx, fx.rel, "bab", "hello", ch, "v1", release.PromoteOptions{Hotfix: true}); err == nil { |
| + t.Errorf("signer promoted %s with no Authorize hook", ch) |
| + } |
| + } |
| +} |
| + |
| +func TestDeploySignerFailureFailsClosed(t *testing.T) { |
| + fx, _, deploy := deployFixture(t) |
| + pub, _, err := ed25519.GenerateKey(nil) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + fx.srv.cfg.ReleasePub = pub |
| + status, out := postJSON(t, fx.ts.URL+"/api/cli/apps/bab/hello/promote", |
| + map[string]any{"version": "v1", "channel": "edge"}, deploy) |
| + if status != http.StatusServiceUnavailable { |
| + t.Fatalf("promote = %d %v, want 503", status, out) |
| + } |
| + if got := str(t, out, "error"); got != "could not prepare a signing key for this deploy credential. Nothing was promoted." { |
| + t.Fatalf("error = %q", got) |
| + } |
| + if _, _, err := release.GetQualified(context.Background(), fx.rel, release.ChannelKey("bab", "hello", "edge")); !errors.Is(err, os.ErrNotExist) { |
| + t.Fatalf("edge pointer after a failed signer: err = %v, want ErrNotExist", err) |
| + } |
| +} |
| + |
| +func TestPersonPromoteUnchanged(t *testing.T) { |
| + fx, person, _ := deployFixture(t) |
| + status, out := postJSON(t, fx.ts.URL+"/api/cli/apps/bab/hello/promote", |
| + map[string]any{"version": "v1", "channel": "edge"}, person) |
| + if status != http.StatusOK { |
| + t.Fatalf("person promote = %d %v", status, out) |
| + } |
| + if g := pointerGrant(t, fx, "edge"); g != nil { |
| + t.Fatalf("person-signed pointer carries a grant: %v", g) |
| + } |
| +} |
| + |
| +func TestDeployRollback(t *testing.T) { |
| + fx, person, deploy := deployFixture(t) |
| + for _, v := range []string{"v1", "v2"} { |
| + if status, out := postJSON(t, fx.ts.URL+"/api/cli/apps/bab/hello/promote", |
| + map[string]any{"version": v, "channel": "edge"}, person); status != http.StatusOK { |
| + t.Fatalf("person promote %s = %d %v", v, status, out) |
| + } |
| + } |
| + status, out := postJSON(t, fx.ts.URL+"/api/cli/apps/bab/hello/rollback", |
| + map[string]any{"channel": "edge"}, deploy) |
| + if status != http.StatusOK || str(t, out, "result") != "rolled back" { |
| + t.Fatalf("deploy rollback = %d %v", status, out) |
| + } |
| + cp, err := release.Channel(context.Background(), fx.rel, fx.pub, "bab", "hello", "edge") |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if cp.Version != "v1" { |
| + t.Fatalf("edge = %q, want v1", cp.Version) |
| + } |
| + g := pointerGrant(t, fx, "edge") |
| + if g == nil || g["name"] != "deploy.ci" { |
| + t.Fatalf("rolled-back pointer grant = %v, want name deploy.ci", g) |
| + } |
| +} |