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  }