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 "crypto/ecdh"
6 "crypto/sha256"
7 "encoding/hex"
8 "encoding/json"
9 "errors"
10
11 "github.com/carlosframework/platform/internal/appconfig"
12 "github.com/carlosframework/platform/internal/objstore"
13 "github.com/carlosframework/platform/internal/release"
14)
15
16func scanSecretHash(v appconfig.Var) string {
17 if v.Kind != "secret" || v.Envelope == "" {
18 return ""
19 }
20 raw, _ := json.Marshal(v)
21 digest := sha256.Sum256(raw)
22 return hex.EncodeToString(digest[:])
23}
24
25func (s *Server) callbackKeyCurrent(ctx context.Context, acct, app string, rec *objstore.Record) (bool, error) {
26 if rec == nil || rec.Declared == nil || rec.Scanning == nil || rec.Scanning.Enabled == nil ||
27 rec.Scanning.WebhookKeyID != s.cfg.CallbackSigner.CurrentKeyID() || rec.Scanning.WebhookConfigHash == "" {
28 return false, nil
29 }
30 manifest, err := appconfig.Get(ctx, s.cfg.Releases, acct, app)
31 if err != nil {
32 return false, err
33 }
34 return scanSecretHash(manifest.Vars[rec.Declared.EnvPrefix+"_SCAN_CALLBACK_SECRET"]) == rec.Scanning.WebhookConfigHash, nil
35}
36
37func (s *Server) deliverCallbackRevision(ctx context.Context, pub *ecdh.PublicKey, acct, app string, rec *objstore.Record, gen int) (string, error) {
38 if s.cfg.CallbackSigner == nil || rec.Declared == nil {
39 return "", ErrNoCallbackKey
40 }
41 envelope, err := appconfig.Seal(pub, appconfig.Binding{Account: acct, App: app}, s.cfg.CallbackSigner.DerivedKey(acct, app, gen))
42 if err != nil {
43 return "", err
44 }
45 v := appconfig.Var{Kind: "secret", Envelope: envelope}
46 _, err = appconfig.Apply(ctx, s.cfg.Releases, acct, app, map[string]appconfig.Var{
47 rec.Declared.EnvPrefix + "_SCAN_CALLBACK_SECRET": v,
48 }, nil)
49 return scanSecretHash(v), err
50}
51
52// The completion record names the exact sealed value. A concurrent console
53// can overwrite config after a successful rotation, even when the master ID
54// is unchanged; the next pass must detect and repair that incomplete state.
55func (s *Server) convergeCallbackKey(ctx context.Context, pub *ecdh.PublicKey, acct *Account, app string) (changed bool, result *objstore.Record, err error) {
56 for attempt := 0; attempt < 4; attempt++ {
57 tomb, err := release.ReadAppTombstone(ctx, s.cfg.Releases, acct.Sqid, app)
58 if err != nil {
59 return changed, nil, err
60 }
61 if tomb != nil {
62 return changed, nil, objstore.ErrQueueNotPermitted
63 }
64 rec, _, err := objstore.Get(ctx, s.cfg.Releases, acct.Sqid, app)
65 if err != nil {
66 return changed, nil, err
67 }
68 if rec == nil || rec.Declared == nil || rec.Provisioned == nil || rec.Scanning == nil || rec.Scanning.Enabled == nil {
69 return changed, rec, objstore.ErrQueueNotPermitted
70 }
71 if current, err := s.callbackKeyCurrent(ctx, acct.Sqid, app, rec); err != nil || current {
72 return changed, rec, err
73 }
74 gen, prefix := rec.Scanning.WebhookGen, rec.Declared.EnvPrefix
75 hash, err := s.deliverCallbackRevision(ctx, pub, acct.Sqid, app, rec, gen)
76 if errors.Is(err, release.ErrCASConflict) {
77 continue
78 }
79 if err != nil {
80 return changed, rec, err
81 }
82 changed = true
83 _, err = s.writeScanRecord(ctx, acct, app, func(fresh *objstore.Record) error {
84 if fresh.Declared == nil || fresh.Scanning == nil || fresh.Scanning.Enabled == nil {
85 return objstore.ErrQueueNotPermitted
86 }
87 if fresh.Scanning.WebhookGen != gen || fresh.Declared.EnvPrefix != prefix {
88 return release.ErrCASConflict
89 }
90 fresh.Scanning.WebhookKeyID = s.cfg.CallbackSigner.CurrentKeyID()
91 fresh.Scanning.WebhookConfigHash = hash
92 return nil
93 })
94 if errors.Is(err, objstore.ErrQueueNotPermitted) {
95 if _, cleanupErr := appconfig.Apply(ctx, s.cfg.Releases, acct.Sqid, app, nil, []string{prefix + "_SCAN_CALLBACK_SECRET"}); cleanupErr != nil {
96 return changed, rec, cleanupErr
97 }
98 continue
99 }
100 if err != nil && !errors.Is(err, release.ErrCASConflict) {
101 return changed, rec, err
102 }
103 }
104 return changed, nil, release.ErrCASConflict
105}
106