github.com/treeverse/lakefs@v1.24.1-0.20240520134607-95648127bfb0/pkg/gateway/operations/base.go (about) 1 package operations 2 3 import ( 4 "bytes" 5 "encoding/xml" 6 "errors" 7 "fmt" 8 "io" 9 "net/http" 10 "slices" 11 12 "github.com/treeverse/lakefs/pkg/auth" 13 "github.com/treeverse/lakefs/pkg/auth/keys" 14 "github.com/treeverse/lakefs/pkg/block" 15 "github.com/treeverse/lakefs/pkg/catalog" 16 gwerrors "github.com/treeverse/lakefs/pkg/gateway/errors" 17 "github.com/treeverse/lakefs/pkg/gateway/multipart" 18 "github.com/treeverse/lakefs/pkg/httputil" 19 "github.com/treeverse/lakefs/pkg/kv" 20 "github.com/treeverse/lakefs/pkg/logging" 21 "github.com/treeverse/lakefs/pkg/permissions" 22 "github.com/treeverse/lakefs/pkg/upload" 23 ) 24 25 const StorageClassHeader = "x-amz-storage-class" 26 27 type OperationID string 28 29 const ( 30 OperationIDDeleteObject OperationID = "delete_object" 31 OperationIDDeleteObjects OperationID = "delete_objects" 32 OperationIDGetObject OperationID = "get_object" 33 OperationIDHeadBucket OperationID = "head_bucket" 34 OperationIDHeadObject OperationID = "head_object" 35 OperationIDListBuckets OperationID = "list_buckets" 36 OperationIDListObjects OperationID = "list_objects" 37 OperationIDPostObject OperationID = "post_object" 38 OperationIDPutObject OperationID = "put_object" 39 OperationIDPutBucket OperationID = "put_bucket" 40 41 OperationIDUnsupportedOperation OperationID = "unsupported" 42 OperationIDOperationNotFound OperationID = "not_found" 43 ) 44 45 type ActionIncr func(action, userID, repository, ref string) 46 47 type Operation struct { 48 OperationID OperationID 49 Region string 50 FQDN string 51 Catalog *catalog.Catalog 52 MultipartTracker multipart.Tracker 53 BlockStore block.Adapter 54 Auth auth.GatewayService 55 Incr ActionIncr 56 MatchedHost bool 57 PathProvider upload.PathProvider 58 VerifyUnsupported bool 59 } 60 61 func StorageClassFromHeader(header http.Header) *string { 62 storageClass := header.Get(StorageClassHeader) 63 if storageClass == "" { 64 return nil 65 } 66 return &storageClass 67 } 68 69 func (o *Operation) Log(req *http.Request) logging.Logger { 70 return logging.FromContext(req.Context()) 71 } 72 73 func EncodeXMLBytes(w http.ResponseWriter, t []byte, statusCode int) error { 74 w.WriteHeader(statusCode) 75 var b bytes.Buffer 76 b.WriteString(xml.Header) 77 b.Write(t) 78 _, err := b.WriteTo(w) 79 return err 80 } 81 82 func (o *Operation) EncodeXMLBytes(w http.ResponseWriter, req *http.Request, t []byte, statusCode int) { 83 err := EncodeXMLBytes(w, t, statusCode) 84 if err != nil { 85 o.Log(req).WithError(err).Error("failed to encode XML to response") 86 } 87 } 88 89 func (o *Operation) HandleUnsupported(w http.ResponseWriter, req *http.Request, keys ...string) bool { 90 if !o.VerifyUnsupported { 91 return false 92 } 93 query := req.URL.Query() 94 if slices.ContainsFunc(keys, query.Has) { 95 _ = o.EncodeError(w, req, nil, gwerrors.ERRLakeFSNotSupported.ToAPIErr()) 96 return true 97 } 98 return false 99 } 100 101 func EncodeResponse(w http.ResponseWriter, entity interface{}, statusCode int) error { 102 // We don't indent the XML document because of Java. 103 // See: https://github.com/spulec/moto/issues/1870 104 payload, err := xml.Marshal(entity) 105 if err != nil { 106 return err 107 } 108 return EncodeXMLBytes(w, payload, statusCode) 109 } 110 111 func (o *Operation) EncodeResponse(w http.ResponseWriter, req *http.Request, entity interface{}, statusCode int) { 112 err := EncodeResponse(w, entity, statusCode) 113 if err != nil { 114 o.Log(req).WithError(err).Error("encoding response failed") 115 } 116 } 117 118 func DecodeXMLBody(reader io.Reader, entity interface{}) error { 119 body := reader 120 content, err := io.ReadAll(body) 121 if err != nil { 122 return err 123 } 124 err = xml.Unmarshal(content, entity) 125 if err != nil { 126 return err 127 } 128 return nil 129 } 130 131 // SetHeader sets a header on the response while preserving its case 132 func (o *Operation) SetHeader(w http.ResponseWriter, key, value string) { 133 w.Header()[key] = []string{value} 134 } 135 136 // DeleteHeader deletes a header from the response 137 func (o *Operation) DeleteHeader(w http.ResponseWriter, key string) { 138 w.Header().Del(key) 139 } 140 141 // SetHeaders sets a map of headers on the response while preserving the header's case 142 func (o *Operation) SetHeaders(w http.ResponseWriter, headers http.Header) { 143 h := w.Header() 144 for k, v := range headers { 145 for _, val := range v { 146 h.Add(k, val) 147 } 148 } 149 } 150 151 func (o *Operation) EncodeError(w http.ResponseWriter, req *http.Request, originalError error, fallbackError gwerrors.APIError) *http.Request { 152 err := fallbackError 153 if errors.Is(originalError, kv.ErrSlowDown) { 154 err = gwerrors.ErrSlowDown.ToAPIErr() 155 } 156 req, rid := httputil.RequestID(req) 157 writeErr := EncodeResponse(w, gwerrors.APIErrorResponse{ 158 Code: err.Code, 159 Message: err.Description, 160 BucketName: "", 161 Key: "", 162 Resource: "", 163 Region: o.Region, 164 RequestID: rid, 165 HostID: generateHostID(), // just for compatibility, meaningless in our case 166 }, err.HTTPStatusCode) 167 if writeErr != nil { 168 o.Log(req).WithError(writeErr).Error("encoding response failed") 169 } 170 return req 171 } 172 173 func generateHostID() string { 174 const generatedHostIDLength = 8 175 return keys.HexStringGenerator(generatedHostIDLength) 176 } 177 178 type AuthorizedOperation struct { 179 *Operation 180 Principal string 181 } 182 183 type RepoOperation struct { 184 *AuthorizedOperation 185 Repository *catalog.Repository 186 MatchedHost bool 187 } 188 189 func (o *RepoOperation) EncodeError(w http.ResponseWriter, req *http.Request, originalError error, fallbackError gwerrors.APIError) *http.Request { 190 err := fallbackError 191 if errors.Is(originalError, kv.ErrSlowDown) { 192 err = gwerrors.ErrSlowDown.ToAPIErr() 193 } 194 req, rid := httputil.RequestID(req) 195 writeErr := EncodeResponse(w, gwerrors.APIErrorResponse{ 196 Code: err.Code, 197 Message: err.Description, 198 BucketName: o.Repository.Name, 199 Key: "", 200 Resource: o.Repository.Name, 201 Region: o.Region, 202 RequestID: rid, 203 HostID: generateHostID(), 204 }, err.HTTPStatusCode) 205 if writeErr != nil { 206 o.Log(req).WithError(writeErr).Error("encoding response failed") 207 } 208 return req 209 } 210 211 type RefOperation struct { 212 *RepoOperation 213 Reference string 214 } 215 216 type PathOperation struct { 217 *RefOperation 218 Path string 219 } 220 221 func (o *PathOperation) EncodeError(w http.ResponseWriter, req *http.Request, originalError error, fallbackError gwerrors.APIError) *http.Request { 222 err := fallbackError 223 if errors.Is(originalError, kv.ErrSlowDown) { 224 err = gwerrors.ErrSlowDown.ToAPIErr() 225 } 226 req, rid := httputil.RequestID(req) 227 writeErr := EncodeResponse(w, gwerrors.APIErrorResponse{ 228 Code: err.Code, 229 Message: err.Description, 230 BucketName: o.Repository.Name, 231 Key: o.Path, 232 Resource: fmt.Sprintf("%s@%s", o.Reference, o.Repository.Name), 233 Region: o.Region, 234 RequestID: rid, 235 HostID: generateHostID(), 236 }, err.HTTPStatusCode) 237 if writeErr != nil { 238 o.Log(req).WithError(writeErr).Error("encoding response failed") 239 } 240 return req 241 } 242 243 type OperationHandler interface { 244 RequiredPermissions(req *http.Request) (permissions.Node, error) 245 Handle(w http.ResponseWriter, req *http.Request, op *Operation) 246 } 247 248 type AuthenticatedOperationHandler interface { 249 RequiredPermissions(req *http.Request) (permissions.Node, error) 250 Handle(w http.ResponseWriter, req *http.Request, op *AuthorizedOperation) 251 } 252 253 type RepoOperationHandler interface { 254 RequiredPermissions(req *http.Request, repository string) (permissions.Node, error) 255 Handle(w http.ResponseWriter, req *http.Request, op *RepoOperation) 256 } 257 258 type BranchOperationHandler interface { 259 RequiredPermissions(req *http.Request, repository, branch string) (permissions.Node, error) 260 Handle(w http.ResponseWriter, req *http.Request, op *RefOperation) 261 } 262 263 type PathOperationHandler interface { 264 RequiredPermissions(req *http.Request, repository, branch, path string) (permissions.Node, error) 265 Handle(w http.ResponseWriter, req *http.Request, op *PathOperation) 266 }