package release import ( "context" "errors" "fmt" "os" "github.com/carlosframework/platform/internal/updatewords" ) // ErrAuthorizeChanged is an authorize whose version was reshipped between the // plan and the confirmation; ErrAuthorizeRootOnly a scoped key. var ( ErrAuthorizeChanged = errors.New("release: the manifest changed since the authorize plan was made") ErrAuthorizeRootOnly = errors.New("release: authorizing a build needs the root key") ) // AuthorizePlan is what `carlos releases authorize` shows: the digest the // record will carry and every artifact's sha256 (spec §4.1). type AuthorizePlan struct { Account, App, Version, ManifestDigest string Artifacts map[string]Artifact } // PlanAuthorize reads the version's canonical manifest. It writes nothing. func PlanAuthorize(ctx context.Context, s Store, acct, app, version string) (*AuthorizePlan, error) { m, raw, err := GetManifestRaw(ctx, s, acct, app, version) if err != nil { return nil, fmt.Errorf("%s: %w", fmt.Sprintf(updatewords.Text("settings.authorize.not-shipped"), version), err) } return &AuthorizePlan{Account: acct, App: app, Version: version, ManifestDigest: ManifestDigest(raw), Artifacts: m.Artifacts}, nil } // claimRawAuthorize signs a record for exactly the plan's bytes: an explicit // act by the publisher, never automatic (spec §4.1). The manifest is read // again and must still hash to the plan's digest. already is true when a // record for these bytes exists, in which case nothing is written. func (sg Signer) claimRawAuthorize(ctx context.Context, s Store, plan *AuthorizePlan, actor string) (already bool, err error) { if sg.scope != nil || sg.grant != nil { return false, fmt.Errorf("%w: %s", ErrAuthorizeRootOnly, updatewords.Text("settings.authorize.root-only")) } _, raw, err := GetManifestRaw(ctx, s, plan.Account, plan.App, plan.Version) if err != nil { // If the version vanished (os.ErrNotExist), that's a change; keep the cause if errors.Is(err, os.ErrNotExist) { return false, fmt.Errorf("%w: %s: %w", ErrAuthorizeChanged, fmt.Sprintf(updatewords.Text("settings.authorize.changed"), plan.Version), err) } // Any other error is a real failure, not a change return false, fmt.Errorf("authorize: read the manifest of %s: %w", plan.Version, err) } // Read succeeded, check if digest changed if ManifestDigest(raw) != plan.ManifestDigest { return false, fmt.Errorf("%w: %s", ErrAuthorizeChanged, fmt.Sprintf(updatewords.Text("settings.authorize.changed"), plan.Version)) } if cur, err := ReadPromotionRecord(ctx, s, sg.verifyPub(), plan.Account, plan.App, plan.Version); err == nil && cur.ManifestDigest == plan.ManifestDigest { return true, nil } return false, sg.WritePromotionRecord(ctx, s, PromotionRecord{Account: plan.Account, App: plan.App, Version: plan.Version, ManifestDigest: plan.ManifestDigest, Authorized: true, Actor: actor}) }