package console import ( "context" "database/sql" "errors" "os" "github.com/carlosframework/platform/internal/hostops" ) func (s *Store) BindClaimAppIdentity(accountID int64, app, appID string) error { if appID == "" { return hostops.ErrIdentity } result, err := s.db.Exec(`UPDATE apps SET app_id=? WHERE account_id=? AND app=? AND (app_id='' OR app_id=?)`, appID, accountID, app, appID) if err != nil { return err } n, err := result.RowsAffected() if err != nil { return err } if n != 1 { return hostops.ErrPending } return nil } func (s *Server) claimControl() (*hostops.ControlWriter, error) { if !s.cfg.HostClaim || s.cfg.ClaimAuthority == nil || s.cfg.ClaimAuthority.Control == nil { return nil, hostops.ErrContract } return s.cfg.ClaimAuthority.Control, nil } // claimAdmissionNameFree is claimApp's read-only exemption for its own // prefix: only a fully completed, retained admission (and a request naming // that retired incarnation) may remain. It never writes; EnrollNewApp // repeats the census and discards the request itself. func (s *Server) claimAdmissionNameFree(ctx context.Context, account, app string) (bool, error) { prefix := "apps/" + account + "/" + app + "/" keys, err := s.cfg.Releases.List(ctx, prefix) if err != nil { return false, err } if len(keys) == 0 { return true, nil } a, err := hostops.ReadCanonicalAdmission(ctx, s.cfg.Releases, account, app) if err != nil || !hostops.CompletedAdmission(a.Value) { return false, err } for _, key := range keys { if key != prefix+"admission.json" && key != prefix+"purge-request.json" { return false, nil } } return true, nil } // bindClaimAppIdentity writes the canonical app_id into this console's row // as an effect of a short exclusive "enroll" operation: the SQL gate admits // it on that permit, and no purge or other mutation can interleave. func (s *Server) bindClaimAppIdentity(ctx context.Context, acct *Account, app, appID string) error { var err error ctx, err = s.claimEnrollmentContext(ctx, acct, app, appID) if err != nil { return err } if _, err := s.claimControl(); err != nil { return err } scope := claimSQLScope{AccountID: acct.ID, App: app, AppID: appID} return s.claimAppMutation(ctx, acct, app, "enroll", func(ctx context.Context) error { // This local SQL call has joined when it returns. Its error cannot // authorize a late remote effect, and the next sweep retries the bind. return s.store.WithAppMutation(ctx, []sqlGrant{permitGrant(ctx, s.cfg.Releases, acct.Sqid, scope)}, func(st *Store) error { return st.BindClaimAppIdentity(acct.ID, app, appID) }) }) } // reserveClaimAppAdmission runs only after this request won SQL new-row // creation. The canonical identity is chosen by the bucket CAS; SQL binds it. func (s *Server) reserveClaimAppAdmission(ctx context.Context, acct *Account, app string) error { w, err := s.claimControl() if err != nil { return err } appID, err := w.EnrollNewApp(ctx, acct.Sqid, app) if err != nil { return err } return s.bindClaimAppIdentity(ctx, acct, app, appID) } func (s *Server) initializeBackfillClaimAppAdmission(ctx context.Context, acct *Account, app string) error { w, err := s.claimControl() if err != nil { return err } grant, err := s.cfg.ClaimAuthority.BackfillAppGrant(ctx, acct.Sqid, app) if err != nil { return err } appID, err := w.EnrollBackfillApp(ctx, grant) if err != nil { return err } return s.bindClaimAppIdentity(ctx, acct, app, appID) } func (s *Server) bindExistingClaimAppAdmission(ctx context.Context, acct *Account, app string) error { a, err := hostops.ReadCanonicalAdmission(ctx, s.cfg.Releases, acct.Sqid, app) if err != nil { return err } if hostops.CompletedAdmission(a.Value) { return hostops.ErrPending } bound, err := s.store.ClaimAppID(acct.ID, app) if err != nil && !errors.Is(err, hostops.ErrIdentity) { return err } if bound != "" { if bound != a.Value.AppID { return hostops.ErrPending } return nil } // A legacy unbound row carries no evidence that it belongs to a name's // new incarnation. Only creation binds new rows after a completed purge. if a.Value.LastPurge != nil { return hostops.ErrPending } return s.bindClaimAppIdentity(ctx, acct, app, a.Value.AppID) } func (s *Server) discardCompletedPurgeRequest(ctx context.Context, account, app string) (bool, error) { w, err := s.claimControl() if err != nil { return false, err } return w.DiscardPurgeRequest(ctx, account, app) } func (s *Server) SweepClaimAppIdentities(ctx context.Context) error { if !s.cfg.HostClaim { return nil } rows, err := s.store.AppRows() if err != nil { return err } var failures []error for _, row := range rows { if err := ctx.Err(); err != nil { return errors.Join(append(failures, err)...) } acct, err := s.store.AccountByID(row.AccountID) if err != nil { failures = append(failures, err) continue } if acct == nil || acct.Sqid != row.Sqid { failures = append(failures, hostops.ErrIdentity) continue } _, err = hostops.ReadCanonicalAdmission(ctx, s.cfg.Releases, row.Sqid, row.App) if errors.Is(err, os.ErrNotExist) { err = s.initializeBackfillClaimAppAdmission(ctx, acct, row.App) } else if err == nil { err = s.bindExistingClaimAppAdmission(ctx, acct, row.App) } if err != nil && !errors.Is(err, hostops.ErrPending) { failures = append(failures, err) } } return errors.Join(failures...) } // Enrollment alone may supply an identity before the local row is bound. // The value is private to this package and scoped to one account and app. // Existing nonempty identities must match; errors never authorize enrollment. type claimEnrollmentKey struct{} type claimEnrollmentIdentity struct{ account, app, id string } func (s *Server) claimEnrollmentContext(ctx context.Context, acct *Account, app, appID string) (context.Context, error) { if acct == nil || appID == "" { return ctx, hostops.ErrIdentity } id, err := s.store.ClaimAppID(acct.ID, app) if err != nil && !errors.Is(err, sql.ErrNoRows) && !errors.Is(err, hostops.ErrIdentity) { return ctx, err } if id != "" && id != appID { return ctx, hostops.ErrIdentity } return context.WithValue(ctx, claimEnrollmentKey{}, claimEnrollmentIdentity{acct.Sqid, app, appID}), nil } func (s *Server) mutationAppID(ctx context.Context, account, app string) (string, error) { acct, err := s.store.AccountBySqid(account) if err != nil { return "", err } if acct == nil { return "", hostops.ErrIdentity } id, err := s.store.ClaimAppID(acct.ID, app) if enrollment, ok := ctx.Value(claimEnrollmentKey{}).(claimEnrollmentIdentity); ok && enrollment.account == account && enrollment.app == app { if err != nil && !errors.Is(err, sql.ErrNoRows) && !errors.Is(err, hostops.ErrIdentity) { return "", err } if id != "" && id != enrollment.id { return "", hostops.ErrIdentity } return enrollment.id, nil } return id, err }