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/subtle"
6 "encoding/json"
7 "fmt"
8 "log"
9 "net/http"
10 "sort"
11 "strings"
12 "time"
13
14 "github.com/carlosframework/platform/internal/objstore"
15 "github.com/carlosframework/platform/internal/usage"
16)
17
18// Only state crosses into account settings. In particular, audit detail is
19// not safe here: apply stores the note and decline stores the reason.
20type accountStoreView struct {
21 State string
22 CanApply bool
23 Unavailable bool
24 Events []Event
25}
26
27type storeQueueView struct {
28 Applications []*objstore.Entitlement
29 Buckets int
30 SweepDate string
31 CountKnown bool
32}
33
34func (s *Server) accountStorePane(ctx context.Context, acct *Account) *accountStoreView {
35 v := &accountStoreView{State: "Not applied", CanApply: true}
36 if s.cfg.Releases == nil {
37 v.Unavailable = true
38 v.CanApply = false
39 return v
40 }
41 ctx, cancel := context.WithTimeout(ctx, storeTimeout)
42 defer cancel()
43 e, _, err := objstore.GetEntitlement(ctx, s.cfg.Releases, acct.Sqid)
44 if err != nil {
45 log.Printf("console: storage settings %s: %v", acct.Sqid, err)
46 v.Unavailable = true
47 v.CanApply = false
48 } else if e != nil {
49 switch {
50 case e.Approved():
51 v.State = "Approved"
52 v.CanApply = false
53 case e.Pending():
54 v.State = "Pending"
55 v.CanApply = false
56 case e.Decision != nil:
57 v.State = "Declined"
58 }
59 }
60 events, err := s.store.Events(acct.ID, "", activityCap)
61 if err != nil {
62 log.Printf("console: account storage events %s: %v", acct.Sqid, err)
63 }
64 for _, e := range events {
65 switch e.Action {
66 case "store-apply", "store-approve", "store-decline":
67 // The deciding operator's identity also stays out of tenant pages.
68 detail := map[string]string{"store-apply": "Applied for object storage", "store-approve": "Object storage approved", "store-decline": "Object storage declined"}[e.Action]
69 v.Events = append(v.Events, Event{Action: e.Action, Detail: detail, CreatedAt: e.CreatedAt})
70 }
71 }
72 return v
73}
74
75func (s *Server) storeFormCSRF(w http.ResponseWriter, r *http.Request, raw string) bool {
76 r.Body = http.MaxBytesReader(w, r.Body, 1<<20)
77 if err := r.ParseForm(); err != nil {
78 s.renderMessage(w, r, http.StatusBadRequest, "Refused", "Could not read the form. Please try again.")
79 return false
80 }
81 if subtle.ConstantTimeCompare([]byte(r.FormValue("csrf")), []byte(raw)) != 1 {
82 s.renderMessage(w, r, http.StatusForbidden, "Refused", "Cross-site request blocked.")
83 return false
84 }
85 return true
86}
87
88func (s *Server) handleStoreApplyPage(w http.ResponseWriter, r *http.Request) {
89 addr, raw, ok := s.session(r)
90 if !ok {
91 http.NotFound(w, r)
92 return
93 }
94 if !s.storeFormCSRF(w, r, raw) {
95 return
96 }
97 _, _, acct, ok := s.owner(w, r)
98 if !ok {
99 return
100 }
101 if !s.allowWrite(Principal{Address: addr}, time.Now()) {
102 s.renderMessage(w, r, http.StatusTooManyRequests, "Refused", "too many requests — wait a minute and try again")
103 return
104 }
105 if s.cfg.Releases == nil {
106 s.renderMessage(w, r, http.StatusServiceUnavailable, "Error", "Object storage applications are unavailable on this deployment.")
107 return
108 }
109 ctx, cancel := context.WithTimeout(r.Context(), storeTimeout)
110 defer cancel()
111 _, status, msg := s.applyStoreEntitlement(ctx, acct, addr, r.FormValue("note"))
112 if msg != "" {
113 s.renderMessage(w, r, status, "Error", msg)
114 return
115 }
116 http.Redirect(w, r, accountSettingsURL(acct.Sqid), http.StatusSeeOther)
117}
118
119func (s *Server) storePageOperator(w http.ResponseWriter, r *http.Request) (string, string, bool) {
120 addr, raw, ok := s.operatorOnly(w, r)
121 if !ok {
122 return "", "", false
123 }
124 if !s.storeOperator(addr) {
125 http.NotFound(w, r)
126 return "", "", false
127 }
128 return addr, raw, true
129}
130
131func (s *Server) handleStoreApprovalsPage(w http.ResponseWriter, r *http.Request) {
132 addr, raw, ok := s.storePageOperator(w, r)
133 if !ok {
134 return
135 }
136 if s.cfg.Releases == nil {
137 s.renderMessage(w, r, http.StatusServiceUnavailable, "Error", "Object storage applications are unavailable on this deployment.")
138 return
139 }
140 ctx, cancel := context.WithTimeout(r.Context(), storeTimeout)
141 defer cancel()
142 applications, err := s.pendingStoreApplications(ctx)
143 if err != nil {
144 s.renderMessage(w, r, http.StatusBadGateway, "Error", "could not list storage applications. try again")
145 return
146 }
147 v := &storeQueueView{Applications: applications}
148 v.Buckets, v.SweepDate, v.CountKnown, err = s.storeBucketCount(ctx)
149 if err != nil {
150 log.Printf("console: storage bucket count: %v", err)
151 }
152 s.render(w, http.StatusOK, "store_approvals", page{Title: "Object storage applications", Address: addr, CSRF: raw, StoreQueue: v})
153}
154
155func (s *Server) handleStoreDecisionPage(w http.ResponseWriter, r *http.Request) {
156 addr, raw, ok := s.storePageOperator(w, r)
157 if !ok {
158 return
159 }
160 if !s.storeFormCSRF(w, r, raw) {
161 return
162 }
163 acct, ok := s.storeEntitlementTarget(w, r)
164 if !ok {
165 return
166 }
167 approved := r.PathValue("decision") == "approve"
168 if !approved && r.PathValue("decision") != "decline" {
169 http.NotFound(w, r)
170 return
171 }
172 reason := strings.TrimSpace(r.FormValue("reason"))
173 if !approved && reason == "" {
174 s.renderMessage(w, r, http.StatusBadRequest, "Refused", "a reason is required to decline storage")
175 return
176 }
177 // Approval grants future cloud spend; prove presence after every authority
178 // check. Decline only removes that authority and needs no step-up.
179 if approved && !s.requireFreshAuth(w, r, addr, raw) {
180 return
181 }
182 if !s.allowWrite(Principal{Address: addr}, time.Now()) {
183 s.renderMessage(w, r, http.StatusTooManyRequests, "Refused", "too many requests — wait a minute and try again")
184 return
185 }
186 if s.cfg.Releases == nil {
187 s.renderMessage(w, r, http.StatusServiceUnavailable, "Error", "Object storage applications are unavailable on this deployment.")
188 return
189 }
190 ctx, cancel := context.WithTimeout(r.Context(), storeTimeout)
191 defer cancel()
192 _, status, msg := s.decideStoreEntitlement(ctx, acct, addr, approved, reason)
193 if msg != "" {
194 s.renderMessage(w, r, status, "Error", msg)
195 return
196 }
197 http.Redirect(w, r, "/fleet/store", http.StatusSeeOther)
198}
199
200// A dated occupancy snapshot is not a live cloud inventory. Older snapshots
201// lack the boolean, so they cannot establish even an empty-store count.
202func (s *Server) storeBucketCount(ctx context.Context) (int, string, bool, error) {
203 keys, err := s.cfg.Releases.List(ctx, usage.StoragePrefix)
204 if err != nil {
205 return 0, "", false, err
206 }
207 var dates []string
208 for _, key := range keys {
209 date := strings.TrimSuffix(strings.TrimPrefix(key, usage.StoragePrefix), ".json")
210 if _, err := time.Parse("2006-01-02", date); err == nil && usage.StorageDayKey(date) == key {
211 dates = append(dates, date)
212 }
213 }
214 if len(dates) == 0 {
215 return 0, "", false, nil
216 }
217 sort.Strings(dates)
218 date := dates[len(dates)-1]
219 raw, _, err := s.cfg.Releases.Get(ctx, usage.StorageDayKey(date))
220 if err != nil {
221 return 0, date, false, err
222 }
223 var day struct {
224 V int `json:"v"`
225 Date string `json:"date"`
226 Apps map[string]struct {
227 Provisioned *bool `json:"store_provisioned"`
228 Unswept bool `json:"store_unswept"`
229 } `json:"apps"`
230 }
231 if err := json.Unmarshal(raw, &day); err != nil {
232 return 0, date, false, err
233 }
234 if day.V != 1 || day.Date != date || day.Apps == nil {
235 return 0, date, false, fmt.Errorf("invalid storage snapshot %s", date)
236 }
237 n := 0
238 for _, app := range day.Apps {
239 if app.Provisioned == nil || (!*app.Provisioned && app.Unswept) {
240 return 0, date, false, nil
241 }
242 if *app.Provisioned {
243 n++
244 }
245 }
246 return n, date, true, nil
247}
248