Skip to content
Snippets Groups Projects
Commit fdc6f90b authored by Jacob Vosmaer (GitLab)'s avatar Jacob Vosmaer (GitLab)
Browse files

Prepare for lazy upload preauth

These are Workhorse refactors to prepare for having lazy
pre-authentication of multipart MIME file uploads.

Changelog: other
parent 67fd2190
No related branches found
No related tags found
No related merge requests found
Loading
@@ -43,16 +43,12 @@ type artifactsUploadProcessor struct {
Loading
@@ -43,16 +43,12 @@ type artifactsUploadProcessor struct {
// Artifacts is like a Multipart but specific for artifacts upload. // Artifacts is like a Multipart but specific for artifacts upload.
func Artifacts(myAPI *api.API, h http.Handler, p Preparer) http.Handler { func Artifacts(myAPI *api.API, h http.Handler, p Preparer) http.Handler {
return myAPI.PreAuthorizeHandler(func(w http.ResponseWriter, r *http.Request, a *api.Response) { return myAPI.PreAuthorizeHandler(func(w http.ResponseWriter, r *http.Request, a *api.Response) {
opts, err := p.Prepare(a)
if err != nil {
helper.Fail500(w, r, fmt.Errorf("UploadArtifacts: error preparing file storage options"))
return
}
format := r.URL.Query().Get(ArtifactFormatKey) format := r.URL.Query().Get(ArtifactFormatKey)
mg := &artifactsUploadProcessor{
mg := &artifactsUploadProcessor{format: format, SavedFileTracker: SavedFileTracker{Request: r}} format: format,
interceptMultipartFiles(w, r, h, a, mg, opts) SavedFileTracker: SavedFileTracker{Request: r},
}
interceptMultipartFiles(w, r, h, mg, &eagerAuthorizer{a}, p)
}, "/authorize") }, "/authorize")
} }
   
Loading
Loading
package upload package upload
   
import ( import (
"fmt"
"net/http" "net/http"
   
"gitlab.com/gitlab-org/gitlab/workhorse/internal/api" "gitlab.com/gitlab-org/gitlab/workhorse/internal/api"
"gitlab.com/gitlab-org/gitlab/workhorse/internal/helper"
) )
   
// Multipart is a request middleware. If the request has a MIME multipart // Multipart is a request middleware. If the request has a MIME multipart
Loading
@@ -17,12 +15,19 @@ func Multipart(rails PreAuthorizer, h http.Handler, p Preparer) http.Handler {
Loading
@@ -17,12 +15,19 @@ func Multipart(rails PreAuthorizer, h http.Handler, p Preparer) http.Handler {
return rails.PreAuthorizeHandler(func(w http.ResponseWriter, r *http.Request, a *api.Response) { return rails.PreAuthorizeHandler(func(w http.ResponseWriter, r *http.Request, a *api.Response) {
s := &SavedFileTracker{Request: r} s := &SavedFileTracker{Request: r}
   
opts, err := p.Prepare(a) interceptMultipartFiles(w, r, h, s, &eagerAuthorizer{a}, p)
if err != nil {
helper.Fail500(w, r, fmt.Errorf("Multipart: error preparing file storage options"))
return
}
interceptMultipartFiles(w, r, h, a, s, opts)
}, "/authorize") }, "/authorize")
} }
// SkipRailsPreAuthMultipart behaves like Multipart except it does not
// pre-authorize with Rails. It is intended for use on catch-all routes
// where we cannot pre-authorize both because we don't know which Rails
// endpoint to call, and because eagerly pre-authorizing would add too
// much overhead.
func SkipRailsPreAuthMultipart(tempPath string, h http.Handler, p Preparer) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
s := &SavedFileTracker{Request: r}
fa := &eagerAuthorizer{&api.Response{TempPath: tempPath}}
interceptMultipartFiles(w, r, h, s, fa, p)
})
}
Loading
@@ -62,13 +62,14 @@ var (
Loading
@@ -62,13 +62,14 @@ var (
) )
   
type rewriter struct { type rewriter struct {
writer *multipart.Writer writer *multipart.Writer
preauth *api.Response fileAuthorizer
Preparer
filter MultipartFormProcessor filter MultipartFormProcessor
finalizedFields map[string]bool finalizedFields map[string]bool
} }
   
func rewriteFormFilesFromMultipart(r *http.Request, writer *multipart.Writer, preauth *api.Response, filter MultipartFormProcessor, opts *destination.UploadOpts) error { func rewriteFormFilesFromMultipart(r *http.Request, writer *multipart.Writer, filter MultipartFormProcessor, fa fileAuthorizer, preparer Preparer) error {
// Create multipart reader // Create multipart reader
reader, err := r.MultipartReader() reader, err := r.MultipartReader()
if err != nil { if err != nil {
Loading
@@ -83,7 +84,8 @@ func rewriteFormFilesFromMultipart(r *http.Request, writer *multipart.Writer, pr
Loading
@@ -83,7 +84,8 @@ func rewriteFormFilesFromMultipart(r *http.Request, writer *multipart.Writer, pr
   
rew := &rewriter{ rew := &rewriter{
writer: writer, writer: writer,
preauth: preauth, fileAuthorizer: fa,
Preparer: preparer,
filter: filter, filter: filter,
finalizedFields: make(map[string]bool), finalizedFields: make(map[string]bool),
} }
Loading
@@ -108,7 +110,7 @@ func rewriteFormFilesFromMultipart(r *http.Request, writer *multipart.Writer, pr
Loading
@@ -108,7 +110,7 @@ func rewriteFormFilesFromMultipart(r *http.Request, writer *multipart.Writer, pr
} }
   
if filename != "" { if filename != "" {
err = rew.handleFilePart(r.Context(), name, p, opts) err = rew.handleFilePart(r, name, p)
} else { } else {
err = rew.copyPart(r.Context(), name, p) err = rew.copyPart(r.Context(), name, p)
} }
Loading
@@ -128,7 +130,7 @@ func parseAndNormalizeContentDisposition(header textproto.MIMEHeader) (string, s
Loading
@@ -128,7 +130,7 @@ func parseAndNormalizeContentDisposition(header textproto.MIMEHeader) (string, s
return params["name"], params["filename"] return params["name"], params["filename"]
} }
   
func (rew *rewriter) handleFilePart(ctx context.Context, name string, p *multipart.Part, opts *destination.UploadOpts) error { func (rew *rewriter) handleFilePart(r *http.Request, name string, p *multipart.Part) error {
if rew.filter.Count() >= maxFilesAllowed { if rew.filter.Count() >= maxFilesAllowed {
return ErrTooManyFilesUploaded return ErrTooManyFilesUploaded
} }
Loading
@@ -141,22 +143,28 @@ func (rew *rewriter) handleFilePart(ctx context.Context, name string, p *multipa
Loading
@@ -141,22 +143,28 @@ func (rew *rewriter) handleFilePart(ctx context.Context, name string, p *multipa
return fmt.Errorf("illegal filename: %q", filename) return fmt.Errorf("illegal filename: %q", filename)
} }
   
var inputReader io.ReadCloser apiResponse, err := rew.AuthorizeFile(r)
var err error if err != nil {
return err
}
opts, err := rew.Prepare(apiResponse)
if err != nil {
return err
}
   
imageType := exif.FileTypeFromSuffix(filename) var inputReader io.ReadCloser
switch { ctx := r.Context()
case imageType != exif.TypeUnknown: if imageType := exif.FileTypeFromSuffix(filename); imageType != exif.TypeUnknown {
inputReader, err = handleExifUpload(ctx, p, filename, imageType) inputReader, err = handleExifUpload(ctx, p, filename, imageType)
if err != nil { if err != nil {
return err return err
} }
case rew.preauth.ProcessLsif: } else if apiResponse.ProcessLsif {
inputReader, err = handleLsifUpload(ctx, p, opts.LocalTempPath, filename, rew.preauth) inputReader, err = handleLsifUpload(ctx, p, opts.LocalTempPath, filename)
if err != nil { if err != nil {
return err return err
} }
default: } else {
inputReader = ioutil.NopCloser(p) inputReader = ioutil.NopCloser(p)
} }
   
Loading
@@ -265,7 +273,7 @@ func isJPEG(r io.Reader) bool {
Loading
@@ -265,7 +273,7 @@ func isJPEG(r io.Reader) bool {
return http.DetectContentType(buf) == "image/jpeg" return http.DetectContentType(buf) == "image/jpeg"
} }
   
func handleLsifUpload(ctx context.Context, reader io.Reader, tempPath, filename string, preauth *api.Response) (io.ReadCloser, error) { func handleLsifUpload(ctx context.Context, reader io.Reader, tempPath, filename string) (io.ReadCloser, error) {
parserConfig := parser.Config{ parserConfig := parser.Config{
TempPath: tempPath, TempPath: tempPath,
} }
Loading
@@ -289,3 +297,15 @@ func (rew *rewriter) copyPart(ctx context.Context, name string, p *multipart.Par
Loading
@@ -289,3 +297,15 @@ func (rew *rewriter) copyPart(ctx context.Context, name string, p *multipart.Par
   
return nil return nil
} }
type fileAuthorizer interface {
AuthorizeFile(*http.Request) (*api.Response, error)
}
type eagerAuthorizer struct{ response *api.Response }
func (ea *eagerAuthorizer) AuthorizeFile(r *http.Request) (*api.Response, error) {
return ea.response, nil
}
var _ fileAuthorizer = &eagerAuthorizer{}
package upload
import (
"net/http"
"gitlab.com/gitlab-org/gitlab/workhorse/internal/api"
)
// SkipRailsAuthorizer implements a fake PreAuthorizer that does not call
// the gitlab-rails API. It must be fast because it gets called on each
// request proxied to Rails.
type SkipRailsAuthorizer struct {
// TempPath is a directory where workhorse can store files that can later
// be accessed by gitlab-rails.
TempPath string
}
func (l *SkipRailsAuthorizer) PreAuthorizeHandler(next api.HandleFunc, _ string) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
next(w, r, &api.Response{TempPath: l.TempPath})
})
}
Loading
@@ -40,13 +40,13 @@ type MultipartFormProcessor interface {
Loading
@@ -40,13 +40,13 @@ type MultipartFormProcessor interface {
   
// interceptMultipartFiles is the core of the implementation of // interceptMultipartFiles is the core of the implementation of
// Multipart. // Multipart.
func interceptMultipartFiles(w http.ResponseWriter, r *http.Request, h http.Handler, preauth *api.Response, filter MultipartFormProcessor, opts *destination.UploadOpts) { func interceptMultipartFiles(w http.ResponseWriter, r *http.Request, h http.Handler, filter MultipartFormProcessor, fa fileAuthorizer, p Preparer) {
var body bytes.Buffer var body bytes.Buffer
writer := multipart.NewWriter(&body) writer := multipart.NewWriter(&body)
defer writer.Close() defer writer.Close()
   
// Rewrite multipart form data // Rewrite multipart form data
err := rewriteFormFilesFromMultipart(r, writer, preauth, filter, opts) err := rewriteFormFilesFromMultipart(r, writer, filter, fa, p)
if err != nil { if err != nil {
switch err { switch err {
case ErrInjectedClientParam: case ErrInjectedClientParam:
Loading
Loading
Loading
@@ -71,12 +71,10 @@ func TestUploadHandlerForwardingRawData(t *testing.T) {
Loading
@@ -71,12 +71,10 @@ func TestUploadHandlerForwardingRawData(t *testing.T) {
response := httptest.NewRecorder() response := httptest.NewRecorder()
   
handler := newProxy(ts.URL) handler := newProxy(ts.URL)
apiResponse := &api.Response{TempPath: tempPath} fa := &eagerAuthorizer{&api.Response{TempPath: tempPath}}
preparer := &DefaultPreparer{} preparer := &DefaultPreparer{}
opts, err := preparer.Prepare(apiResponse)
require.NoError(t, err)
   
interceptMultipartFiles(response, httpRequest, handler, apiResponse, nil, opts) interceptMultipartFiles(response, httpRequest, handler, nil, fa, preparer)
   
require.Equal(t, 202, response.Code) require.Equal(t, 202, response.Code)
require.Equal(t, "RESPONSE", response.Body.String(), "response body") require.Equal(t, "RESPONSE", response.Body.String(), "response body")
Loading
@@ -139,12 +137,10 @@ func TestUploadHandlerRewritingMultiPartData(t *testing.T) {
Loading
@@ -139,12 +137,10 @@ func TestUploadHandlerRewritingMultiPartData(t *testing.T) {
   
handler := newProxy(ts.URL) handler := newProxy(ts.URL)
   
apiResponse := &api.Response{TempPath: tempPath} fa := &eagerAuthorizer{&api.Response{TempPath: tempPath}}
preparer := &DefaultPreparer{} preparer := &DefaultPreparer{}
opts, err := preparer.Prepare(apiResponse)
require.NoError(t, err)
   
interceptMultipartFiles(response, httpRequest, handler, apiResponse, &testFormProcessor{}, opts) interceptMultipartFiles(response, httpRequest, handler, &testFormProcessor{}, fa, preparer)
require.Equal(t, 202, response.Code) require.Equal(t, 202, response.Code)
   
cancel() // this will trigger an async cleanup cancel() // this will trigger an async cleanup
Loading
@@ -303,11 +299,10 @@ func TestUploadProcessingFile(t *testing.T) {
Loading
@@ -303,11 +299,10 @@ func TestUploadProcessingFile(t *testing.T) {
httpRequest.Header.Set("Content-Type", writer.FormDataContentType()) httpRequest.Header.Set("Content-Type", writer.FormDataContentType())
   
response := httptest.NewRecorder() response := httptest.NewRecorder()
fa := &eagerAuthorizer{test.preauth}
preparer := &DefaultPreparer{} preparer := &DefaultPreparer{}
opts, err := preparer.Prepare(test.preauth)
require.NoError(t, err)
   
interceptMultipartFiles(response, httpRequest, nilHandler, test.preauth, &testFormProcessor{}, opts) interceptMultipartFiles(response, httpRequest, nilHandler, &testFormProcessor{}, fa, preparer)
   
require.Equal(t, 200, response.Code) require.Equal(t, 200, response.Code)
require.Equal(t, "test", string(test.content(t))) require.Equal(t, "test", string(test.content(t)))
Loading
@@ -317,7 +312,6 @@ func TestUploadProcessingFile(t *testing.T) {
Loading
@@ -317,7 +312,6 @@ func TestUploadProcessingFile(t *testing.T) {
   
func TestInvalidFileNames(t *testing.T) { func TestInvalidFileNames(t *testing.T) {
testhelper.ConfigureSecret() testhelper.ConfigureSecret()
tempPath := t.TempDir()
   
for _, testCase := range []struct { for _, testCase := range []struct {
filename string filename string
Loading
@@ -345,12 +339,7 @@ func TestInvalidFileNames(t *testing.T) {
Loading
@@ -345,12 +339,7 @@ func TestInvalidFileNames(t *testing.T) {
httpRequest.Header.Set("Content-Type", writer.FormDataContentType()) httpRequest.Header.Set("Content-Type", writer.FormDataContentType())
   
response := httptest.NewRecorder() response := httptest.NewRecorder()
apiResponse := &api.Response{TempPath: tempPath} testInterceptMultipartFiles(t, response, httpRequest, nilHandler, &SavedFileTracker{Request: httpRequest})
preparer := &DefaultPreparer{}
opts, err := preparer.Prepare(apiResponse)
require.NoError(t, err)
interceptMultipartFiles(response, httpRequest, nilHandler, apiResponse, &SavedFileTracker{Request: httpRequest}, opts)
require.Equal(t, testCase.code, response.Code) require.Equal(t, testCase.code, response.Code)
} }
} }
Loading
@@ -542,10 +531,8 @@ func waitUntilDeleted(t *testing.T, path string) {
Loading
@@ -542,10 +531,8 @@ func waitUntilDeleted(t *testing.T, path string) {
func testInterceptMultipartFiles(t *testing.T, w http.ResponseWriter, r *http.Request, h http.Handler, filter MultipartFormProcessor) { func testInterceptMultipartFiles(t *testing.T, w http.ResponseWriter, r *http.Request, h http.Handler, filter MultipartFormProcessor) {
t.Helper() t.Helper()
   
apiResponse := &api.Response{TempPath: t.TempDir()} fa := &eagerAuthorizer{&api.Response{TempPath: t.TempDir()}}
preparer := &DefaultPreparer{} preparer := &DefaultPreparer{}
opts, err := preparer.Prepare(apiResponse)
require.NoError(t, err)
   
interceptMultipartFiles(w, r, h, apiResponse, filter, opts) interceptMultipartFiles(w, r, h, filter, fa, preparer)
} }
Loading
@@ -223,7 +223,7 @@ func configureRoutes(u *upstream) {
Loading
@@ -223,7 +223,7 @@ func configureRoutes(u *upstream) {
mimeMultipartUploader := upload.Multipart(api, signingProxy, preparer) mimeMultipartUploader := upload.Multipart(api, signingProxy, preparer)
   
uploadPath := path.Join(u.DocumentRoot, "uploads/tmp") uploadPath := path.Join(u.DocumentRoot, "uploads/tmp")
tempfileMultipartProxy := upload.Multipart(&upload.SkipRailsAuthorizer{TempPath: uploadPath}, proxy, preparer) tempfileMultipartProxy := upload.SkipRailsPreAuthMultipart(uploadPath, proxy, preparer)
ciAPIProxyQueue := queueing.QueueRequests("ci_api_job_requests", tempfileMultipartProxy, u.APILimit, u.APIQueueLimit, u.APIQueueTimeout) ciAPIProxyQueue := queueing.QueueRequests("ci_api_job_requests", tempfileMultipartProxy, u.APILimit, u.APIQueueLimit, u.APIQueueTimeout)
ciAPILongPolling := builds.RegisterHandler(ciAPIProxyQueue, redis.WatchKey, u.APICILongPollingDuration) ciAPILongPolling := builds.RegisterHandler(ciAPIProxyQueue, redis.WatchKey, u.APICILongPollingDuration)
   
Loading
Loading
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment