package console import ( "context" "crypto/subtle" "encoding/json" "fmt" "log" "net/http" "sort" "strings" "time" "github.com/carlosframework/platform/internal/objstore" "github.com/carlosframework/platform/internal/usage" ) // Only state crosses into account settings. In particular, audit detail is // not safe here: apply stores the note and decline stores the reason. type accountStoreView struct { State string CanApply bool Unavailable bool Events []Event } type storeQueueView struct { Applications []*objstore.Entitlement Buckets int SweepDate string CountKnown bool } func (s *Server) accountStorePane(ctx context.Context, acct *Account) *accountStoreView { v := &accountStoreView{State: "Not applied", CanApply: true} if s.cfg.Releases == nil { v.Unavailable = true v.CanApply = false return v } ctx, cancel := context.WithTimeout(ctx, storeTimeout) defer cancel() e, _, err := objstore.GetEntitlement(ctx, s.cfg.Releases, acct.Sqid) if err != nil { log.Printf("console: storage settings %s: %v", acct.Sqid, err) v.Unavailable = true v.CanApply = false } else if e != nil { switch { case e.Approved(): v.State = "Approved" v.CanApply = false case e.Pending(): v.State = "Pending" v.CanApply = false case e.Decision != nil: v.State = "Declined" } } events, err := s.store.Events(acct.ID, "", activityCap) if err != nil { log.Printf("console: account storage events %s: %v", acct.Sqid, err) } for _, e := range events { switch e.Action { case "store-apply", "store-approve", "store-decline": // The deciding operator's identity also stays out of tenant pages. detail := map[string]string{"store-apply": "Applied for object storage", "store-approve": "Object storage approved", "store-decline": "Object storage declined"}[e.Action] v.Events = append(v.Events, Event{Action: e.Action, Detail: detail, CreatedAt: e.CreatedAt}) } } return v } func (s *Server) storeFormCSRF(w http.ResponseWriter, r *http.Request, raw string) bool { r.Body = http.MaxBytesReader(w, r.Body, 1<<20) if err := r.ParseForm(); err != nil { s.renderMessage(w, r, http.StatusBadRequest, "Refused", "Could not read the form. Please try again.") return false } if subtle.ConstantTimeCompare([]byte(r.FormValue("csrf")), []byte(raw)) != 1 { s.renderMessage(w, r, http.StatusForbidden, "Refused", "Cross-site request blocked.") return false } return true } func (s *Server) handleStoreApplyPage(w http.ResponseWriter, r *http.Request) { addr, raw, ok := s.session(r) if !ok { http.NotFound(w, r) return } if !s.storeFormCSRF(w, r, raw) { return } _, _, acct, ok := s.owner(w, r) if !ok { return } if !s.allowWrite(Principal{Address: addr}, time.Now()) { s.renderMessage(w, r, http.StatusTooManyRequests, "Refused", "too many requests — wait a minute and try again") return } if s.cfg.Releases == nil { s.renderMessage(w, r, http.StatusServiceUnavailable, "Error", "Object storage applications are unavailable on this deployment.") return } ctx, cancel := context.WithTimeout(r.Context(), storeTimeout) defer cancel() _, status, msg := s.applyStoreEntitlement(ctx, acct, addr, r.FormValue("note")) if msg != "" { s.renderMessage(w, r, status, "Error", msg) return } http.Redirect(w, r, accountSettingsURL(acct.Sqid), http.StatusSeeOther) } func (s *Server) storePageOperator(w http.ResponseWriter, r *http.Request) (string, string, bool) { addr, raw, ok := s.operatorOnly(w, r) if !ok { return "", "", false } if !s.storeOperator(addr) { http.NotFound(w, r) return "", "", false } return addr, raw, true } func (s *Server) handleStoreApprovalsPage(w http.ResponseWriter, r *http.Request) { addr, raw, ok := s.storePageOperator(w, r) if !ok { return } if s.cfg.Releases == nil { s.renderMessage(w, r, http.StatusServiceUnavailable, "Error", "Object storage applications are unavailable on this deployment.") return } ctx, cancel := context.WithTimeout(r.Context(), storeTimeout) defer cancel() applications, err := s.pendingStoreApplications(ctx) if err != nil { s.renderMessage(w, r, http.StatusBadGateway, "Error", "could not list storage applications. try again") return } v := &storeQueueView{Applications: applications} v.Buckets, v.SweepDate, v.CountKnown, err = s.storeBucketCount(ctx) if err != nil { log.Printf("console: storage bucket count: %v", err) } s.render(w, http.StatusOK, "store_approvals", page{Title: "Object storage applications", Address: addr, CSRF: raw, StoreQueue: v}) } func (s *Server) handleStoreDecisionPage(w http.ResponseWriter, r *http.Request) { addr, raw, ok := s.storePageOperator(w, r) if !ok { return } if !s.storeFormCSRF(w, r, raw) { return } acct, ok := s.storeEntitlementTarget(w, r) if !ok { return } approved := r.PathValue("decision") == "approve" if !approved && r.PathValue("decision") != "decline" { http.NotFound(w, r) return } reason := strings.TrimSpace(r.FormValue("reason")) if !approved && reason == "" { s.renderMessage(w, r, http.StatusBadRequest, "Refused", "a reason is required to decline storage") return } // Approval grants future cloud spend; prove presence after every authority // check. Decline only removes that authority and needs no step-up. if approved && !s.requireFreshAuth(w, r, addr, raw) { return } if !s.allowWrite(Principal{Address: addr}, time.Now()) { s.renderMessage(w, r, http.StatusTooManyRequests, "Refused", "too many requests — wait a minute and try again") return } if s.cfg.Releases == nil { s.renderMessage(w, r, http.StatusServiceUnavailable, "Error", "Object storage applications are unavailable on this deployment.") return } ctx, cancel := context.WithTimeout(r.Context(), storeTimeout) defer cancel() _, status, msg := s.decideStoreEntitlement(ctx, acct, addr, approved, reason) if msg != "" { s.renderMessage(w, r, status, "Error", msg) return } http.Redirect(w, r, "/fleet/store", http.StatusSeeOther) } // A dated occupancy snapshot is not a live cloud inventory. Older snapshots // lack the boolean, so they cannot establish even an empty-store count. func (s *Server) storeBucketCount(ctx context.Context) (int, string, bool, error) { keys, err := s.cfg.Releases.List(ctx, usage.StoragePrefix) if err != nil { return 0, "", false, err } var dates []string for _, key := range keys { date := strings.TrimSuffix(strings.TrimPrefix(key, usage.StoragePrefix), ".json") if _, err := time.Parse("2006-01-02", date); err == nil && usage.StorageDayKey(date) == key { dates = append(dates, date) } } if len(dates) == 0 { return 0, "", false, nil } sort.Strings(dates) date := dates[len(dates)-1] raw, _, err := s.cfg.Releases.Get(ctx, usage.StorageDayKey(date)) if err != nil { return 0, date, false, err } var day struct { V int `json:"v"` Date string `json:"date"` Apps map[string]struct { Provisioned *bool `json:"store_provisioned"` Unswept bool `json:"store_unswept"` } `json:"apps"` } if err := json.Unmarshal(raw, &day); err != nil { return 0, date, false, err } if day.V != 1 || day.Date != date || day.Apps == nil { return 0, date, false, fmt.Errorf("invalid storage snapshot %s", date) } n := 0 for _, app := range day.Apps { if app.Provisioned == nil || (!*app.Provisioned && app.Unswept) { return 0, date, false, nil } if *app.Provisioned { n++ } } return n, date, true, nil }