| 1 | package release |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "errors" |
| 6 | "fmt" |
| 7 | "os" |
| 8 | |
| 9 | "github.com/carlosframework/platform/internal/updatewords" |
| 10 | ) |
| 11 | |
| 12 | // ErrAuthorizeChanged is an authorize whose version was reshipped between the |
| 13 | // plan and the confirmation; ErrAuthorizeRootOnly a scoped key. |
| 14 | var ( |
| 15 | ErrAuthorizeChanged = errors.New("release: the manifest changed since the authorize plan was made") |
| 16 | ErrAuthorizeRootOnly = errors.New("release: authorizing a build needs the root key") |
| 17 | ) |
| 18 | |
| 19 | // AuthorizePlan is what `carlos releases authorize` shows: the digest the |
| 20 | // record will carry and every artifact's sha256 (spec §4.1). |
| 21 | type AuthorizePlan struct { |
| 22 | Account, App, Version, ManifestDigest string |
| 23 | Artifacts map[string]Artifact |
| 24 | } |
| 25 | |
| 26 | // PlanAuthorize reads the version's canonical manifest. It writes nothing. |
| 27 | func PlanAuthorize(ctx context.Context, s Store, acct, app, version string) (*AuthorizePlan, error) { |
| 28 | m, raw, err := GetManifestRaw(ctx, s, acct, app, version) |
| 29 | if err != nil { |
| 30 | return nil, fmt.Errorf("%s: %w", fmt.Sprintf(updatewords.Text("settings.authorize.not-shipped"), version), err) |
| 31 | } |
| 32 | return &AuthorizePlan{Account: acct, App: app, Version: version, ManifestDigest: ManifestDigest(raw), Artifacts: m.Artifacts}, nil |
| 33 | } |
| 34 | |
| 35 | // claimRawAuthorize signs a record for exactly the plan's bytes: an explicit |
| 36 | // act by the publisher, never automatic (spec §4.1). The manifest is read |
| 37 | // again and must still hash to the plan's digest. already is true when a |
| 38 | // record for these bytes exists, in which case nothing is written. |
| 39 | func (sg Signer) claimRawAuthorize(ctx context.Context, s Store, plan *AuthorizePlan, actor string) (already bool, err error) { |
| 40 | if sg.scope != nil || sg.grant != nil { |
| 41 | return false, fmt.Errorf("%w: %s", ErrAuthorizeRootOnly, updatewords.Text("settings.authorize.root-only")) |
| 42 | } |
| 43 | _, raw, err := GetManifestRaw(ctx, s, plan.Account, plan.App, plan.Version) |
| 44 | if err != nil { |
| 45 | // If the version vanished (os.ErrNotExist), that's a change; keep the cause |
| 46 | if errors.Is(err, os.ErrNotExist) { |
| 47 | return false, fmt.Errorf("%w: %s: %w", ErrAuthorizeChanged, fmt.Sprintf(updatewords.Text("settings.authorize.changed"), plan.Version), err) |
| 48 | } |
| 49 | // Any other error is a real failure, not a change |
| 50 | return false, fmt.Errorf("authorize: read the manifest of %s: %w", plan.Version, err) |
| 51 | } |
| 52 | // Read succeeded, check if digest changed |
| 53 | if ManifestDigest(raw) != plan.ManifestDigest { |
| 54 | return false, fmt.Errorf("%w: %s", ErrAuthorizeChanged, fmt.Sprintf(updatewords.Text("settings.authorize.changed"), plan.Version)) |
| 55 | } |
| 56 | if cur, err := ReadPromotionRecord(ctx, s, sg.verifyPub(), plan.Account, plan.App, plan.Version); err == nil && cur.ManifestDigest == plan.ManifestDigest { |
| 57 | return true, nil |
| 58 | } |
| 59 | return false, sg.WritePromotionRecord(ctx, s, PromotionRecord{Account: plan.Account, App: plan.App, Version: plan.Version, |
| 60 | ManifestDigest: plan.ManifestDigest, Authorized: true, Actor: actor}) |
| 61 | } |
| 62 | |