diff --git a/auth/bucket_cors.go b/auth/bucket_cors.go index 309b93a..97ec7e4 100644 --- a/auth/bucket_cors.go +++ b/auth/bucket_cors.go @@ -45,13 +45,23 @@ func (ch CORSHeader) String() string { return string(ch) } +// ToLower converts the header to lower case +func (ch CORSHeader) ToLower() string { + return strings.ToLower(string(ch)) +} + // IsValid validates the cors http request method: // the methods are case sensitive func (cm CORSHTTPMethod) IsValid() bool { - return cm == http.MethodGet || cm == http.MethodHead || cm == http.MethodPut || + return cm.IsEmpty() || cm == http.MethodGet || cm == http.MethodHead || cm == http.MethodPut || cm == http.MethodPost || cm == http.MethodDelete } +// IsEmpty checks if the cors method is an empty string +func (cm CORSHTTPMethod) IsEmpty() bool { + return cm == "" +} + // String converts the method value to 'string' func (cm CORSHTTPMethod) String() string { return string(cm) @@ -88,12 +98,19 @@ type CORSAllowanceConfig struct { Methods string ExposedHeaders string AllowCredentials string + AllowHeaders string MaxAge *int32 } // IsAllowed walks through the CORS rules and finds the first one allowing access. // If no rule grants access, returns 'AccessForbidden' func (cc *CORSConfiguration) IsAllowed(origin string, method CORSHTTPMethod, headers []CORSHeader) (*CORSAllowanceConfig, error) { + // if method is empty, anyways cors is forbidden + // skip, without going through the rules + if method.IsEmpty() { + debuglogger.Logf("empty Access-Control-Request-Method") + return nil, s3err.GetAPIError(s3err.ErrCORSForbidden) + } for _, rule := range cc.Rules { // find the first rule granting access if isAllowed, wilcardOrigin := rule.Match(origin, method, headers); isAllowed { @@ -109,6 +126,7 @@ func (cc *CORSConfiguration) IsAllowed(origin string, method CORSHTTPMethod, hea AllowCredentials: allowCredentials, Methods: rule.GetAllowedMethods(), ExposedHeaders: rule.GetExposeHeaders(), + AllowHeaders: buildAllowedHeaders(headers), MaxAge: rule.MaxAgeSeconds, }, nil } @@ -189,7 +207,7 @@ func (cr *CORSRule) Match(origin string, method CORSHTTPMethod, headers []CORSHe for _, reqHeader := range headers { match := false for _, header := range cr.AllowedHeaders { - if wildcardMatch(header.String(), reqHeader.String()) { + if wildcardMatch(header.ToLower(), reqHeader.ToLower()) { match = true break } @@ -217,6 +235,20 @@ func (cr *CORSRule) GetExposeHeaders() string { return result.String() } +// buildAllowedHeaders builds a comma separated string from []CORSHeader +func buildAllowedHeaders(headers []CORSHeader) string { + var result strings.Builder + + for i, h := range headers { + if i > 0 { + result.WriteString(", ") + } + result.WriteString(h.ToLower()) + } + + return result.String() +} + // GetAllowedMethods returns comma separated CORS allowed methods func (cr *CORSRule) GetAllowedMethods() string { var result strings.Builder diff --git a/auth/bucket_cors_test.go b/auth/bucket_cors_test.go index 5ff7c07..8cc2282 100644 --- a/auth/bucket_cors_test.go +++ b/auth/bucket_cors_test.go @@ -50,6 +50,7 @@ func TestCORSHTTPMethod_IsValid(t *testing.T) { method CORSHTTPMethod want bool }{ + {"empty valid", "", true}, {"GET valid", http.MethodGet, true}, {"HEAD valid", http.MethodHead, true}, {"PUT valid", http.MethodPut, true}, @@ -71,6 +72,83 @@ func TestCORSHTTPMethod_IsValid(t *testing.T) { } } +func TestCORSHeader_ToLower(t *testing.T) { + tests := []struct { + name string + header CORSHeader + want string + }{ + { + name: "already lowercase", + header: CORSHeader("content-type"), + want: "content-type", + }, + { + name: "mixed case", + header: CORSHeader("X-CuStOm-HeAdEr"), + want: "x-custom-header", + }, + { + name: "uppercase", + header: CORSHeader("AUTHORIZATION"), + want: "authorization", + }, + { + name: "empty string", + header: CORSHeader(""), + want: "", + }, + { + name: "numeric and symbols", + header: CORSHeader("X-123-HEADER"), + want: "x-123-header", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := tt.header.ToLower() + assert.Equal(t, tt.want, got) + }) + } +} + +func TestCORSHTTPMethod_IsEmpty(t *testing.T) { + tests := []struct { + name string + method CORSHTTPMethod + want bool + }{ + { + name: "empty string is empty", + method: CORSHTTPMethod(""), + want: true, + }, + { + name: "GET method is not empty", + method: CORSHTTPMethod("GET"), + want: false, + }, + { + name: "random string is not empty", + method: CORSHTTPMethod("FOO"), + want: false, + }, + { + name: "lowercase get is not empty (case sensitive)", + method: CORSHTTPMethod("get"), + want: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := tt.method.IsEmpty() + assert.Equal(t, tt.want, got) + }) + } +} + func TestCORSConfiguration_Validate(t *testing.T) { tests := []struct { name string @@ -130,6 +208,7 @@ func TestCORSConfiguration_IsAllowed(t *testing.T) { Origin: "http://allowed.com", AllowCredentials: "true", Methods: http.MethodGet, + AllowHeaders: "x-test", ExposedHeaders: "", MaxAge: nil, }, @@ -152,6 +231,7 @@ func TestCORSConfiguration_IsAllowed(t *testing.T) { result: &CORSAllowanceConfig{ Origin: "*", AllowCredentials: "false", + AllowHeaders: "x-test", Methods: http.MethodGet, ExposedHeaders: "", MaxAge: nil, @@ -415,6 +495,52 @@ func TestGetExposeHeaders(t *testing.T) { } } +func TestBuildAllowedHeaders(t *testing.T) { + tests := []struct { + name string + headers []CORSHeader + want string + }{ + { + name: "empty slice returns empty string", + headers: []CORSHeader{}, + want: "", + }, + { + name: "single header lowercase", + headers: []CORSHeader{"Content-Type"}, + want: "content-type", + }, + { + name: "multiple headers lowercased with commas", + headers: []CORSHeader{"Content-Type", "X-Custom-Header", "Authorization"}, + want: "content-type, x-custom-header, authorization", + }, + { + name: "already lowercase header", + headers: []CORSHeader{"accept"}, + want: "accept", + }, + { + name: "mixed case headers", + headers: []CORSHeader{"ACCEPT", "x-Powered-By"}, + want: "accept, x-powered-by", + }, + { + name: "empty header value", + headers: []CORSHeader{""}, + want: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := buildAllowedHeaders(tt.headers) + assert.Equal(t, tt.want, got) + }) + } +} + func TestGetAllowedMethods(t *testing.T) { tests := []struct { name string diff --git a/s3api/controllers/options.go b/s3api/controllers/options.go index c15c195..e498da8 100644 --- a/s3api/controllers/options.go +++ b/s3api/controllers/options.go @@ -102,6 +102,7 @@ func (s S3ApiController) CORSOptions(ctx *fiber.Ctx) (*Response, error) { "Access-Control-Allow-Methods": &allowConfig.Methods, "Access-Control-Expose-Headers": &allowConfig.ExposedHeaders, "Access-Control-Allow-Credentials": &allowConfig.AllowCredentials, + "Access-Control-Allow-Headers": &allowConfig.AllowHeaders, "Access-Control-Max-Age": utils.ConvertPtrToStringPtr(allowConfig.MaxAge), "Vary": &middlewares.VaryHdr, }, diff --git a/s3api/controllers/options_test.go b/s3api/controllers/options_test.go new file mode 100644 index 0000000..2d9f633 --- /dev/null +++ b/s3api/controllers/options_test.go @@ -0,0 +1,241 @@ +// Copyright 2023 Versity Software +// This file is licensed under the Apache License, Version 2.0 +// (the "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package controllers + +import ( + "context" + "encoding/xml" + "errors" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/versity/versitygw/auth" + "github.com/versity/versitygw/s3api/middlewares" + "github.com/versity/versitygw/s3api/utils" + "github.com/versity/versitygw/s3err" +) + +func TestS3ApiController_CORSOptions(t *testing.T) { + maxAge := int32(10000) + cors, err := xml.Marshal(auth.CORSConfiguration{ + Rules: []auth.CORSRule{ + { + AllowedOrigins: []string{"example.com"}, + AllowedMethods: []auth.CORSHTTPMethod{http.MethodGet, http.MethodPost}, + AllowedHeaders: []auth.CORSHeader{"Content-Type", "Content-Disposition"}, + ExposeHeaders: []auth.CORSHeader{"Content-Encoding", "date"}, + MaxAgeSeconds: &maxAge, + }, + }, + }) + assert.NoError(t, err) + + tests := []struct { + name string + input testInput + output testOutput + }{ + { + name: "missing origin", + input: testInput{ + locals: defaultLocals, + headers: map[string]string{ + "Access-Control-Request-Method": "GET", + "Access-Control-Request-Headers": "Content-Type", + }, + }, + output: testOutput{ + response: &Response{ + MetaOpts: &MetaOptions{ + BucketOwner: "root", + }, + }, + err: s3err.GetAPIError(s3err.ErrMissingCORSOrigin), + }, + }, + { + name: "invalid method", + input: testInput{ + locals: defaultLocals, + headers: map[string]string{ + "Origin": "example.com", + "Access-Control-Request-Method": "invalid_method", + "Access-Control-Request-Headers": "Content-Type", + }, + }, + output: testOutput{ + response: &Response{ + MetaOpts: &MetaOptions{ + BucketOwner: "root", + }, + }, + err: s3err.GetInvalidCORSMethodErr("invalid_method"), + }, + }, + { + name: "invalid headers", + input: testInput{ + locals: defaultLocals, + headers: map[string]string{ + "Origin": "example.com", + "Access-Control-Request-Method": "GET", + "Access-Control-Request-Headers": "Content Type", + }, + }, + output: testOutput{ + response: &Response{ + MetaOpts: &MetaOptions{ + BucketOwner: "root", + }, + }, + err: s3err.GetInvalidCORSRequestHeaderErr("Content Type"), + }, + }, + { + name: "fails to get bucket cors", + input: testInput{ + locals: defaultLocals, + headers: map[string]string{ + "Origin": "example.com", + "Access-Control-Request-Method": "GET", + "Access-Control-Request-Headers": "Content-Type", + }, + beRes: []byte{}, + beErr: s3err.GetAPIError(s3err.ErrNoSuchBucket), + }, + output: testOutput{ + response: &Response{ + MetaOpts: &MetaOptions{ + BucketOwner: "root", + }, + }, + err: s3err.GetAPIError(s3err.ErrNoSuchBucket), + }, + }, + { + name: "bucket cors is not enabled", + input: testInput{ + locals: defaultLocals, + headers: map[string]string{ + "Origin": "example.com", + "Access-Control-Request-Method": "GET", + "Access-Control-Request-Headers": "Content-Type", + }, + beRes: []byte{}, + beErr: s3err.GetAPIError(s3err.ErrNoSuchCORSConfiguration), + }, + output: testOutput{ + response: &Response{ + MetaOpts: &MetaOptions{ + BucketOwner: "root", + }, + }, + err: s3err.GetAPIError(s3err.ErrCORSIsNotEnabled), + }, + }, + { + name: "fails to parse bucket cors", + input: testInput{ + locals: defaultLocals, + headers: map[string]string{ + "Origin": "example.com", + "Access-Control-Request-Method": "GET", + "Access-Control-Request-Headers": "Content-Type", + }, + beRes: []byte("invalid_cors"), + }, + output: testOutput{ + response: &Response{ + MetaOpts: &MetaOptions{ + BucketOwner: "root", + }, + }, + err: errors.New("failed to parse cors config:"), + }, + }, + { + name: "cors is not allowed", + input: testInput{ + locals: defaultLocals, + headers: map[string]string{ + "Origin": "example.com", + "Access-Control-Request-Method": "PUT", + "Access-Control-Request-Headers": "Content-Type", + }, + beRes: cors, + }, + output: testOutput{ + response: &Response{ + MetaOpts: &MetaOptions{ + BucketOwner: "root", + }, + }, + err: s3err.GetAPIError(s3err.ErrCORSForbidden), + }, + }, + { + name: "success: cors is allowed", + input: testInput{ + locals: defaultLocals, + headers: map[string]string{ + "Origin": "example.com", + "Access-Control-Request-Method": "GET", + "Access-Control-Request-Headers": "content-type, Content-Disposition", + }, + beRes: cors, + }, + output: testOutput{ + response: &Response{ + Headers: map[string]*string{ + "Access-Control-Allow-Origin": utils.GetStringPtr("example.com"), + "Access-Control-Allow-Methods": utils.GetStringPtr("GET, POST"), + "Access-Control-Expose-Headers": utils.GetStringPtr("Content-Encoding, date"), + "Access-Control-Allow-Credentials": utils.GetStringPtr("true"), + "Access-Control-Allow-Headers": utils.GetStringPtr("content-type, content-disposition"), + "Access-Control-Max-Age": utils.ConvertToStringPtr(maxAge), + "Vary": &middlewares.VaryHdr, + }, + MetaOpts: &MetaOptions{ + BucketOwner: "root", + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + be := &BackendMock{ + GetBucketCorsFunc: func(contextMoqParam context.Context, bucket string) ([]byte, error) { + return tt.input.beRes.([]byte), tt.input.beErr + }, + } + + ctrl := S3ApiController{ + be: be, + } + + testController( + t, + ctrl.CORSOptions, + tt.output.response, + tt.output.err, + ctxInputs{ + locals: tt.input.locals, + headers: tt.input.headers, + }) + }) + } +} diff --git a/s3api/middlewares/apply-bucket-cors.go b/s3api/middlewares/apply-bucket-cors.go index 71eb46f..7f9aabe 100644 --- a/s3api/middlewares/apply-bucket-cors.go +++ b/s3api/middlewares/apply-bucket-cors.go @@ -62,7 +62,7 @@ func ApplyBucketCORS(be backend.Backend) fiber.Handler { // if request method is not specified with Access-Control-Request-Method // override it with the actual request method - if method == "" { + if method.IsEmpty() { method = auth.CORSHTTPMethod(ctx.Request().Header.Method()) } else if !method.IsValid() { // check if allowed method is valid @@ -92,6 +92,7 @@ func ApplyBucketCORS(be backend.Backend) fiber.Handler { "Access-Control-Allow-Methods": allowConfig.Methods, "Access-Control-Expose-Headers": allowConfig.ExposedHeaders, "Access-Control-Allow-Credentials": allowConfig.AllowCredentials, + "Access-Control-Allow-Headers": allowConfig.AllowHeaders, "Vary": VaryHdr, } { if val != "" { diff --git a/tests/integration/tests.go b/tests/integration/tests.go index cf8b852..c068a53 100644 --- a/tests/integration/tests.go +++ b/tests/integration/tests.go @@ -14217,22 +14217,22 @@ func PreflightOPTIONS_access_granted(s *S3Conf) error { result PreflightResult }{ // first rule matches - {"http://example.com", http.MethodGet, "X-Amz-Date", PreflightResult{"http://example.com", "GET, HEAD", "Content-Type, Content-Length", "100", "true", varyHdr, nil}}, - {"http://example.com", http.MethodGet, "X-Amz-Content-Sha256", PreflightResult{"http://example.com", "GET, HEAD", "Content-Type, Content-Length", "100", "true", varyHdr, nil}}, - {"http://example.com", http.MethodHead, "", PreflightResult{"http://example.com", "GET, HEAD", "Content-Type, Content-Length", "100", "true", varyHdr, nil}}, - {"https://example.com", http.MethodGet, "X-Amz-Date,X-Amz-Content-Sha256", PreflightResult{"https://example.com", "GET, HEAD", "Content-Type, Content-Length", "100", "true", varyHdr, nil}}, + {"http://example.com", http.MethodGet, "X-Amz-Date", PreflightResult{"http://example.com", "GET, HEAD", "x-amz-date", "Content-Type, Content-Length", "100", "true", varyHdr, nil}}, + {"http://example.com", http.MethodGet, "X-Amz-Content-Sha256", PreflightResult{"http://example.com", "GET, HEAD", "x-amz-content-sha256", "Content-Type, Content-Length", "100", "true", varyHdr, nil}}, + {"http://example.com", http.MethodHead, "", PreflightResult{"http://example.com", "GET, HEAD", "", "Content-Type, Content-Length", "100", "true", varyHdr, nil}}, + {"https://example.com", http.MethodGet, "X-Amz-Date,X-Amz-Content-Sha256", PreflightResult{"https://example.com", "GET, HEAD", "x-amz-date, x-amz-content-sha256", "Content-Type, Content-Length", "100", "true", varyHdr, nil}}, // second rule matches: origin is a wildcard - {"http://anything.com", http.MethodHead, "X-Amz-Meta-Something", PreflightResult{"*", "HEAD", "", "", "false", varyHdr, nil}}, - {"hello.com", http.MethodHead, "", PreflightResult{"*", "HEAD", "", "", "false", varyHdr, nil}}, + {"http://anything.com", http.MethodHead, "X-Amz-Meta-Something", PreflightResult{"*", "HEAD", "x-amz-meta-something", "", "", "false", varyHdr, nil}}, + {"hello.com", http.MethodHead, "", PreflightResult{"*", "HEAD", "", "", "", "false", varyHdr, nil}}, // third rule matches - {"something.net", http.MethodPut, "Authorization", PreflightResult{"something.net", "POST, PUT", "Content-Disposition, Content-Encoding", "3000", "true", varyHdr, nil}}, - {"something.net", http.MethodPost, "", PreflightResult{"something.net", "POST, PUT", "Content-Disposition, Content-Encoding", "3000", "true", varyHdr, nil}}, + {"something.net", http.MethodPut, "Authorization", PreflightResult{"something.net", "POST, PUT", "authorization", "Content-Disposition, Content-Encoding", "3000", "true", varyHdr, nil}}, + {"something.net", http.MethodPost, "", PreflightResult{"something.net", "POST, PUT", "", "Content-Disposition, Content-Encoding", "3000", "true", varyHdr, nil}}, // forth rule matches: origin contains wildcard - {"http://www.hello.world.com", http.MethodGet, "", PreflightResult{"http://www.hello.world.com", "GET", "X-Amz-Expected-Bucket-Owner", "5000", "true", varyHdr, nil}}, - {"http://www.example.com", http.MethodGet, "x-amz-server-side-encryption", PreflightResult{"http://www.example.com", "GET", "X-Amz-Expected-Bucket-Owner", "5000", "true", varyHdr, nil}}, + {"http://www.hello.world.com", http.MethodGet, "", PreflightResult{"http://www.hello.world.com", "GET", "", "X-Amz-Expected-Bucket-Owner", "5000", "true", varyHdr, nil}}, + {"http://www.example.com", http.MethodGet, "x-amz-server-side-encryption", PreflightResult{"http://www.example.com", "GET", "x-amz-server-side-encryption", "X-Amz-Expected-Bucket-Owner", "5000", "true", varyHdr, nil}}, // fifth rule matches: allowed headers contains wildcard - {"http://uniquie-origin.net", http.MethodPost, "X-Amz-anything-Suffix", PreflightResult{"http://uniquie-origin.net", "POST, PUT", "Authorization, Content-Type", "2000", "true", varyHdr, nil}}, - {"http://uniquie-origin.net", http.MethodPut, "X-Amz-yyy-xxx-Suffix", PreflightResult{"http://uniquie-origin.net", "POST, PUT", "Authorization, Content-Type", "2000", "true", varyHdr, nil}}, + {"http://uniquie-origin.net", http.MethodPost, "X-Amz-anything-Suffix", PreflightResult{"http://uniquie-origin.net", "POST, PUT", "x-amz-anything-suffix", "Authorization, Content-Type", "2000", "true", varyHdr, nil}}, + {"http://uniquie-origin.net", http.MethodPut, "X-Amz-yyy-xxx-Suffix", PreflightResult{"http://uniquie-origin.net", "POST, PUT", "x-amz-yyy-xxx-suffix", "Authorization, Content-Type", "2000", "true", varyHdr, nil}}, } { err := testOPTIONSEdnpoint(s, bucket, test.origin, test.method, test.headers, &test.result) if err != nil { @@ -14439,16 +14439,16 @@ func CORSMiddleware_access_granted(s *S3Conf) error { result PreflightResult }{ // first rule matches - {"http://example.com", http.MethodGet, "X-Amz-Date", PreflightResult{"http://example.com", "GET, HEAD", "Content-Type, Content-Length", "100", "true", varyHdr, nil}}, - {"http://example.com", http.MethodGet, "X-Amz-Content-Sha256", PreflightResult{"http://example.com", "GET, HEAD", "Content-Type, Content-Length", "100", "true", varyHdr, nil}}, - {"http://example.com", http.MethodHead, "", PreflightResult{"http://example.com", "GET, HEAD", "Content-Type, Content-Length", "100", "true", varyHdr, nil}}, - {"https://example.com", http.MethodGet, "X-Amz-Date,X-Amz-Content-Sha256", PreflightResult{"https://example.com", "GET, HEAD", "Content-Type, Content-Length", "100", "true", varyHdr, nil}}, + {"http://example.com", http.MethodGet, "X-Amz-Date", PreflightResult{"http://example.com", "GET, HEAD", "x-amz-date, x-amz-content-sha256", "Content-Type, Content-Length", "100", "true", varyHdr, nil}}, + {"http://example.com", http.MethodGet, "X-Amz-Content-Sha256", PreflightResult{"http://example.com", "GET, HEAD", "x-amz-date, x-amz-content-sha256", "Content-Type, Content-Length", "100", "true", varyHdr, nil}}, + {"http://example.com", http.MethodHead, "", PreflightResult{"http://example.com", "GET, HEAD", "", "Content-Type, Content-Length", "100", "true", varyHdr, nil}}, + {"https://example.com", http.MethodGet, "X-Amz-Date,X-Amz-Content-Sha256", PreflightResult{"https://example.com", "GET, HEAD", "x-amz-date, x-amz-content-sha256", "Content-Type, Content-Length", "100", "true", varyHdr, nil}}, // second rule matches - {"http://anything.com", http.MethodHead, "X-Amz-Meta-Something", PreflightResult{"*", "HEAD", "", "", "false", varyHdr, nil}}, - {"hello.com", http.MethodHead, "", PreflightResult{"*", "HEAD", "", "", "false", varyHdr, nil}}, + {"http://anything.com", http.MethodHead, "X-Amz-Meta-Something", PreflightResult{"*", "HEAD", "x-amz-meta-something", "", "", "false", varyHdr, nil}}, + {"hello.com", http.MethodHead, "", PreflightResult{"*", "HEAD", "", "", "", "false", varyHdr, nil}}, // third rule matches - {"something.net", http.MethodPut, "Authorization", PreflightResult{"something.net", "POST, PUT", "Content-Disposition, Content-Encoding", "3000", "true", varyHdr, nil}}, - {"something.net", http.MethodPost, "", PreflightResult{"something.net", "POST, PUT", "Content-Disposition, Content-Encoding", "3000", "true", varyHdr, nil}}, + {"something.net", http.MethodPut, "Authorization", PreflightResult{"something.net", "POST, PUT", "authorization", "Content-Disposition, Content-Encoding", "3000", "true", varyHdr, nil}}, + {"something.net", http.MethodPost, "", PreflightResult{"something.net", "POST, PUT", "", "Content-Disposition, Content-Encoding", "3000", "true", varyHdr, nil}}, } { req, err := createSignedReq(http.MethodPut, s.endpoint, bucket+"/my-obj", s.awsID, s.awsSecret, "s3", s.awsRegion, nil, time.Now(), map[string]string{ "Origin": test.origin, diff --git a/tests/integration/utils.go b/tests/integration/utils.go index 23d34a7..6671863 100644 --- a/tests/integration/utils.go +++ b/tests/integration/utils.go @@ -1685,6 +1685,7 @@ func compareCorsConfig(expected, got []types.CORSRule) error { type PreflightResult struct { Origin string Methods string + AllowHeaders string ExposeHeaders string MaxAge string AllowCredentials string @@ -1715,6 +1716,7 @@ func extractCORSHeaders(resp *http.Response) (*PreflightResult, error) { Methods: resp.Header.Get("Access-Control-Allow-Methods"), ExposeHeaders: resp.Header.Get("Access-Control-Expose-Headers"), MaxAge: resp.Header.Get("Access-Control-Max-Age"), + AllowHeaders: resp.Header.Get("Access-Control-Allow-Headers"), AllowCredentials: resp.Header.Get("Access-Control-Allow-Credentials"), Vary: resp.Header.Get("Vary"), }, nil @@ -1769,6 +1771,9 @@ func comparePreflightResult(expected, got *PreflightResult) error { if expected.Methods != got.Methods { return fmt.Errorf("expected the allowed methods to be %v, instead got %v", expected.Methods, got.Methods) } + if expected.AllowHeaders != got.AllowHeaders { + return fmt.Errorf("expected the allow headers to be %v, instead got %v", expected.AllowHeaders, got.AllowHeaders) + } if expected.ExposeHeaders != got.ExposeHeaders { return fmt.Errorf("expected the expose headers to be %v, instead got %v", expected.ExposeHeaders, got.ExposeHeaders) }