diff options
| -rw-r--r-- | go.mod | 2 | ||||
| -rw-r--r-- | go.sum | 4 | ||||
| -rw-r--r-- | vendor/github.com/minio/minio-go/v7/api-bucket-cors.go | 136 | ||||
| -rw-r--r-- | vendor/github.com/minio/minio-go/v7/api.go | 2 | ||||
| -rw-r--r-- | vendor/github.com/minio/minio-go/v7/core.go | 3 | ||||
| -rw-r--r-- | vendor/github.com/minio/minio-go/v7/functional_tests.go | 1102 | ||||
| -rw-r--r-- | vendor/github.com/minio/minio-go/v7/pkg/cors/cors.go | 91 | ||||
| -rw-r--r-- | vendor/github.com/minio/minio-go/v7/s3-error.go | 1 | ||||
| -rw-r--r-- | vendor/modules.txt | 3 | 
9 files changed, 1324 insertions, 20 deletions
| @@ -43,7 +43,7 @@ require (  	github.com/jackc/pgx/v5 v5.6.0  	github.com/microcosm-cc/bluemonday v1.0.27  	github.com/miekg/dns v1.1.61 -	github.com/minio/minio-go/v7 v7.0.74 +	github.com/minio/minio-go/v7 v7.0.75  	github.com/mitchellh/mapstructure v1.5.0  	github.com/ncruces/go-sqlite3 v0.18.0  	github.com/oklog/ulid v1.3.1 @@ -421,8 +421,8 @@ github.com/miekg/dns v1.1.61 h1:nLxbwF3XxhwVSm8g9Dghm9MHPaUZuqhPiGL+675ZmEs=  github.com/miekg/dns v1.1.61/go.mod h1:mnAarhS3nWaW+NVP2wTkYVIZyHNJ098SJZUki3eykwQ=  github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=  github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM= -github.com/minio/minio-go/v7 v7.0.74 h1:fTo/XlPBTSpo3BAMshlwKL5RspXRv9us5UeHEGYCFe0= -github.com/minio/minio-go/v7 v7.0.74/go.mod h1:qydcVzV8Hqtj1VtEocfxbmVFa2siu6HGa+LDEPogjD8= +github.com/minio/minio-go/v7 v7.0.75 h1:0uLrB6u6teY2Jt+cJUVi9cTvDRuBKWSRzSAcznRkwlE= +github.com/minio/minio-go/v7 v7.0.75/go.mod h1:qydcVzV8Hqtj1VtEocfxbmVFa2siu6HGa+LDEPogjD8=  github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=  github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=  github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= diff --git a/vendor/github.com/minio/minio-go/v7/api-bucket-cors.go b/vendor/github.com/minio/minio-go/v7/api-bucket-cors.go new file mode 100644 index 000000000..8bf537f73 --- /dev/null +++ b/vendor/github.com/minio/minio-go/v7/api-bucket-cors.go @@ -0,0 +1,136 @@ +/* + * MinIO Go Library for Amazon S3 Compatible Cloud Storage + * Copyright 2024 MinIO, Inc. + * 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 minio + +import ( +	"bytes" +	"context" +	"net/http" +	"net/url" + +	"github.com/minio/minio-go/v7/pkg/cors" +	"github.com/minio/minio-go/v7/pkg/s3utils" +) + +// SetBucketCors sets the cors configuration for the bucket +func (c *Client) SetBucketCors(ctx context.Context, bucketName string, corsConfig *cors.Config) error { +	if err := s3utils.CheckValidBucketName(bucketName); err != nil { +		return err +	} + +	if corsConfig == nil { +		return c.removeBucketCors(ctx, bucketName) +	} + +	return c.putBucketCors(ctx, bucketName, corsConfig) +} + +func (c *Client) putBucketCors(ctx context.Context, bucketName string, corsConfig *cors.Config) error { +	urlValues := make(url.Values) +	urlValues.Set("cors", "") + +	corsStr, err := corsConfig.ToXML() +	if err != nil { +		return err +	} + +	reqMetadata := requestMetadata{ +		bucketName:       bucketName, +		queryValues:      urlValues, +		contentBody:      bytes.NewReader(corsStr), +		contentLength:    int64(len(corsStr)), +		contentMD5Base64: sumMD5Base64([]byte(corsStr)), +	} + +	resp, err := c.executeMethod(ctx, http.MethodPut, reqMetadata) +	defer closeResponse(resp) +	if err != nil { +		return err +	} +	if resp != nil { +		if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusNoContent { +			return httpRespToErrorResponse(resp, bucketName, "") +		} +	} +	return nil +} + +func (c *Client) removeBucketCors(ctx context.Context, bucketName string) error { +	urlValues := make(url.Values) +	urlValues.Set("cors", "") + +	resp, err := c.executeMethod(ctx, http.MethodDelete, requestMetadata{ +		bucketName:       bucketName, +		queryValues:      urlValues, +		contentSHA256Hex: emptySHA256Hex, +	}) +	defer closeResponse(resp) +	if err != nil { +		return err +	} + +	if resp.StatusCode != http.StatusNoContent { +		return httpRespToErrorResponse(resp, bucketName, "") +	} + +	return nil +} + +// GetBucketCors returns the current cors +func (c *Client) GetBucketCors(ctx context.Context, bucketName string) (*cors.Config, error) { +	if err := s3utils.CheckValidBucketName(bucketName); err != nil { +		return nil, err +	} +	bucketCors, err := c.getBucketCors(ctx, bucketName) +	if err != nil { +		errResponse := ToErrorResponse(err) +		if errResponse.Code == "NoSuchCORSConfiguration" { +			return nil, nil +		} +		return nil, err +	} +	return bucketCors, nil +} + +func (c *Client) getBucketCors(ctx context.Context, bucketName string) (*cors.Config, error) { +	urlValues := make(url.Values) +	urlValues.Set("cors", "") + +	resp, err := c.executeMethod(ctx, http.MethodGet, requestMetadata{ +		bucketName:       bucketName, +		queryValues:      urlValues, +		contentSHA256Hex: emptySHA256Hex, // TODO: needed? copied over from other example, but not spec'd in API. +	}) + +	defer closeResponse(resp) +	if err != nil { +		return nil, err +	} + +	if resp != nil { +		if resp.StatusCode != http.StatusOK { +			return nil, httpRespToErrorResponse(resp, bucketName, "") +		} +	} + +	corsConfig, err := cors.ParseBucketCorsConfig(resp.Body) +	if err != nil { +		return nil, err +	} + +	return corsConfig, nil +} diff --git a/vendor/github.com/minio/minio-go/v7/api.go b/vendor/github.com/minio/minio-go/v7/api.go index 937551403..13c493d0f 100644 --- a/vendor/github.com/minio/minio-go/v7/api.go +++ b/vendor/github.com/minio/minio-go/v7/api.go @@ -129,7 +129,7 @@ type Options struct {  // Global constants.  const (  	libraryName    = "minio-go" -	libraryVersion = "v7.0.74" +	libraryVersion = "v7.0.75"  )  // User Agent should always following the below style. diff --git a/vendor/github.com/minio/minio-go/v7/core.go b/vendor/github.com/minio/minio-go/v7/core.go index 132ea702f..99b99db9b 100644 --- a/vendor/github.com/minio/minio-go/v7/core.go +++ b/vendor/github.com/minio/minio-go/v7/core.go @@ -91,6 +91,7 @@ type PutObjectPartOptions struct {  	Md5Base64, Sha256Hex  string  	SSE                   encrypt.ServerSide  	CustomHeader, Trailer http.Header +	DisableContentSha256  bool  }  // PutObjectPart - Upload an object part. @@ -107,7 +108,7 @@ func (c Core) PutObjectPart(ctx context.Context, bucket, object, uploadID string  		sha256Hex:    opts.Sha256Hex,  		size:         size,  		sse:          opts.SSE, -		streamSha256: true, +		streamSha256: !opts.DisableContentSha256,  		customHeader: opts.CustomHeader,  		trailer:      opts.Trailer,  	} diff --git a/vendor/github.com/minio/minio-go/v7/functional_tests.go b/vendor/github.com/minio/minio-go/v7/functional_tests.go index e77bf9d4a..871034bc7 100644 --- a/vendor/github.com/minio/minio-go/v7/functional_tests.go +++ b/vendor/github.com/minio/minio-go/v7/functional_tests.go @@ -52,6 +52,7 @@ import (  	"github.com/google/uuid"  	"github.com/minio/minio-go/v7" +	"github.com/minio/minio-go/v7/pkg/cors"  	"github.com/minio/minio-go/v7/pkg/credentials"  	"github.com/minio/minio-go/v7/pkg/encrypt"  	"github.com/minio/minio-go/v7/pkg/notification" @@ -2972,7 +2973,6 @@ func testGetObjectAttributes() {  			ContentType:    v.ContentType,  			SendContentMd5: v.SendContentMd5,  		}) -  		if err != nil {  			logError(testName, function, args, startTime, "", "PutObject failed", err)  			return @@ -7212,7 +7212,6 @@ func testFunctional() {  		"bucketName": bucketName,  	}  	exists, err = c.BucketExists(context.Background(), bucketName) -  	if err != nil {  		logError(testName, function, args, startTime, "", "BucketExists failed", err)  		return @@ -7275,7 +7274,6 @@ func testFunctional() {  		"bucketPolicy": writeOnlyPolicy,  	}  	err = c.SetBucketPolicy(context.Background(), bucketName, writeOnlyPolicy) -  	if err != nil {  		logError(testName, function, args, startTime, "", "SetBucketPolicy failed", err)  		return @@ -7304,7 +7302,6 @@ func testFunctional() {  		"bucketPolicy": readWritePolicy,  	}  	err = c.SetBucketPolicy(context.Background(), bucketName, readWritePolicy) -  	if err != nil {  		logError(testName, function, args, startTime, "", "SetBucketPolicy failed", err)  		return @@ -7481,7 +7478,6 @@ func testFunctional() {  		"fileName":   fileName + "-f",  	}  	err = c.FGetObject(context.Background(), bucketName, objectName, fileName+"-f", minio.GetObjectOptions{}) -  	if err != nil {  		logError(testName, function, args, startTime, "", "FGetObject failed", err)  		return @@ -7613,7 +7609,6 @@ func testFunctional() {  		"reqParams":  reqParams,  	}  	presignedGetURL, err = c.PresignedGetObject(context.Background(), bucketName, objectName, 3600*time.Second, reqParams) -  	if err != nil {  		logError(testName, function, args, startTime, "", "PresignedGetObject failed", err)  		return @@ -7770,14 +7765,12 @@ func testFunctional() {  		"objectName": objectName,  	}  	err = c.RemoveObject(context.Background(), bucketName, objectName, minio.RemoveObjectOptions{}) -  	if err != nil {  		logError(testName, function, args, startTime, "", "RemoveObject failed", err)  		return  	}  	args["objectName"] = objectName + "-f"  	err = c.RemoveObject(context.Background(), bucketName, objectName+"-f", minio.RemoveObjectOptions{}) -  	if err != nil {  		logError(testName, function, args, startTime, "", "RemoveObject failed", err)  		return @@ -7785,7 +7778,6 @@ func testFunctional() {  	args["objectName"] = objectName + "-nolength"  	err = c.RemoveObject(context.Background(), bucketName, objectName+"-nolength", minio.RemoveObjectOptions{}) -  	if err != nil {  		logError(testName, function, args, startTime, "", "RemoveObject failed", err)  		return @@ -7793,7 +7785,6 @@ func testFunctional() {  	args["objectName"] = objectName + "-presigned"  	err = c.RemoveObject(context.Background(), bucketName, objectName+"-presigned", minio.RemoveObjectOptions{}) -  	if err != nil {  		logError(testName, function, args, startTime, "", "RemoveObject failed", err)  		return @@ -7801,7 +7792,6 @@ func testFunctional() {  	args["objectName"] = objectName + "-presign-custom"  	err = c.RemoveObject(context.Background(), bucketName, objectName+"-presign-custom", minio.RemoveObjectOptions{}) -  	if err != nil {  		logError(testName, function, args, startTime, "", "RemoveObject failed", err)  		return @@ -7813,7 +7803,6 @@ func testFunctional() {  		"bucketName": bucketName,  	}  	err = c.RemoveBucket(context.Background(), bucketName) -  	if err != nil {  		logError(testName, function, args, startTime, "", "RemoveBucket failed", err)  		return @@ -12281,7 +12270,6 @@ func testFunctionalV2() {  		"bucketPolicy": readWritePolicy,  	}  	err = c.SetBucketPolicy(context.Background(), bucketName, readWritePolicy) -  	if err != nil {  		logError(testName, function, args, startTime, "", "SetBucketPolicy failed", err)  		return @@ -13012,7 +13000,6 @@ func testGetObjectACLContext() {  			ContentType:  "binary/octet-stream",  			UserMetadata: metaData,  		}) -  	if err != nil {  		logError(testName, function, args, startTime, "", "PutObject failed", err)  		return @@ -13491,6 +13478,849 @@ func testListObjects() {  	logSuccess(testName, function, args, startTime)  } +// testCors is runnable against S3 itself. +// Just provide the env var MINIO_GO_TEST_BUCKET_CORS with bucket that is public and WILL BE DELETED. +// Recreate this manually each time. Minio-go SDK does not support calling +// SetPublicBucket (put-public-access-block) on S3, otherwise we could script the whole thing. +func testCors() { +	ctx := context.Background() +	startTime := time.Now() +	testName := getFuncName() +	function := "SetBucketCors(bucketName, cors)" +	args := map[string]interface{}{ +		"bucketName": "", +		"cors":       "", +	} + +	// Instantiate new minio client object +	c, err := minio.New(os.Getenv(serverEndpoint), +		&minio.Options{ +			Creds:     credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), +			Transport: createHTTPTransport(), +			Secure:    mustParseBool(os.Getenv(enableHTTPS)), +		}) +	if err != nil { +		logFailure(testName, function, args, startTime, "", "MinIO client object creation failed", err) +		return +	} + +	// Enable tracing, write to stderr. +	// c.TraceOn(os.Stderr) + +	// Set user agent. +	c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) + +	// Create or reuse a bucket that will get cors settings applied to it and deleted when done +	bucketName := os.Getenv("MINIO_GO_TEST_BUCKET_CORS") +	if bucketName == "" { +		bucketName = randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") +		err = c.MakeBucket(ctx, bucketName, minio.MakeBucketOptions{Region: "us-east-1"}) +		if err != nil { +			logFailure(testName, function, args, startTime, "", "MakeBucket failed", err) +			return +		} +	} +	args["bucketName"] = bucketName +	defer cleanupBucket(bucketName, c) + +	publicPolicy := `{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"AWS":["*"]},"Action":["s3:*"],"Resource":["arn:aws:s3:::` + bucketName + `", "arn:aws:s3:::` + bucketName + `/*"]}]}` +	err = c.SetBucketPolicy(ctx, bucketName, publicPolicy) +	if err != nil { +		logFailure(testName, function, args, startTime, "", "SetBucketPolicy failed", err) +		return +	} + +	// Upload an object for testing. +	objectContents := `some-text-file-contents` +	reader := strings.NewReader(objectContents) +	bufSize := int64(len(objectContents)) + +	objectName := randString(60, rand.NewSource(time.Now().UnixNano()), "") +	args["objectName"] = objectName + +	_, err = c.PutObject(ctx, bucketName, objectName, reader, int64(bufSize), minio.PutObjectOptions{ContentType: "binary/octet-stream"}) +	if err != nil { +		logFailure(testName, function, args, startTime, "", "PutObject call failed", err) +		return +	} +	bucketURL := c.EndpointURL().String() + "/" + bucketName + "/" +	objectURL := bucketURL + objectName + +	transport, err := minio.DefaultTransport(mustParseBool(os.Getenv(enableHTTPS))) +	if err != nil { +		logFailure(testName, function, args, startTime, "", "DefaultTransport failed", err) +		return +	} +	httpClient := &http.Client{ +		Timeout:   30 * time.Second, +		Transport: transport, +	} + +	errStrAccessForbidden := `<Error><Code>AccessForbidden</Code><Message>CORSResponse: This CORS request is not allowed. This is usually because the evalution of Origin, request method / Access-Control-Request-Method or Access-Control-Request-Headers are not whitelisted` +	testCases := []struct { +		name string + +		// Cors rules to apply +		applyCorsRules []cors.Rule + +		// Outbound request info +		method  string +		url     string +		headers map[string]string + +		// Wanted response +		wantStatus       int +		wantHeaders      map[string]string +		wantBodyContains string +	}{ +		{ +			name: "apply bucket rules", +			applyCorsRules: []cors.Rule{ +				{ +					AllowedOrigin: []string{"https"}, // S3 documents 'https' origin, but it does not actually work, see test below. +					AllowedMethod: []string{"PUT"}, +					AllowedHeader: []string{"*"}, +				}, +				{ +					AllowedOrigin: []string{"http://www.example1.com"}, +					AllowedMethod: []string{"PUT"}, +					AllowedHeader: []string{"*"}, +					ExposeHeader:  []string{"x-amz-server-side-encryption", "x-amz-request-id"}, +					MaxAgeSeconds: 3600, +				}, +				{ +					AllowedOrigin: []string{"http://www.example2.com"}, +					AllowedMethod: []string{"POST"}, +					AllowedHeader: []string{"X-My-Special-Header"}, +					ExposeHeader:  []string{"X-AMZ-Request-ID"}, +				}, +				{ +					AllowedOrigin: []string{"http://www.example3.com"}, +					AllowedMethod: []string{"PUT"}, +					AllowedHeader: []string{"X-Example-3-Special-Header"}, +					MaxAgeSeconds: 10, +				}, +				{ +					AllowedOrigin: []string{"*"}, +					AllowedMethod: []string{"GET"}, +					AllowedHeader: []string{"*"}, +					ExposeHeader:  []string{"x-amz-request-id", "X-AMZ-server-side-encryption"}, +					MaxAgeSeconds: 3600, +				}, +				{ +					AllowedOrigin: []string{"http://multiplemethodstest.com"}, +					AllowedMethod: []string{"POST", "PUT", "DELETE"}, +					AllowedHeader: []string{"x-abc-*", "x-def-*"}, +				}, +				{ +					AllowedOrigin: []string{"http://UPPERCASEEXAMPLE.com"}, +					AllowedMethod: []string{"DELETE"}, +				}, +				{ +					AllowedOrigin: []string{"https://*"}, +					AllowedMethod: []string{"DELETE"}, +					AllowedHeader: []string{"x-abc-*", "x-def-*"}, +				}, +			}, +		}, +		{ +			name:   "preflight to object url matches example1 rule", +			method: http.MethodOptions, +			url:    objectURL, +			headers: map[string]string{ +				"Origin":                         "http://www.example1.com", +				"Access-Control-Request-Method":  "PUT", +				"Access-Control-Request-Headers": "x-another-header,x-could-be-anything", +			}, +			wantStatus: http.StatusOK, +			wantHeaders: map[string]string{ +				"Access-Control-Allow-Origin":      "http://www.example1.com", +				"Access-Control-Allow-Methods":     "PUT", +				"Access-Control-Allow-Headers":     "x-another-header,x-could-be-anything", +				"Access-Control-Allow-Credentials": "true", +				"Access-Control-Max-Age":           "3600", +				"Content-Length":                   "0", +				// S3 additionally sets the following headers here, MinIO follows fetch spec and does not: +				// "Access-Control-Expose-Headers":    "", +			}, +		}, +		{ +			name:   "preflight to bucket url matches example1 rule", +			method: http.MethodOptions, +			url:    bucketURL, +			headers: map[string]string{ +				"Origin":                         "http://www.example1.com", +				"Access-Control-Request-Method":  "PUT", +				"Access-Control-Request-Headers": "x-another-header,x-could-be-anything", +			}, +			wantStatus: http.StatusOK, +			wantHeaders: map[string]string{ +				"Access-Control-Allow-Origin":      "http://www.example1.com", +				"Access-Control-Allow-Methods":     "PUT", +				"Access-Control-Allow-Headers":     "x-another-header,x-could-be-anything", +				"Access-Control-Allow-Credentials": "true", +				"Access-Control-Max-Age":           "3600", +				"Content-Length":                   "0", +			}, +		}, +		{ +			name:   "preflight matches example2 rule with header given", +			method: http.MethodOptions, +			url:    objectURL, +			headers: map[string]string{ +				"Origin":                         "http://www.example2.com", +				"Access-Control-Request-Method":  "POST", +				"Access-Control-Request-Headers": "X-My-Special-Header", +			}, +			wantStatus: http.StatusOK, +			wantHeaders: map[string]string{ +				"Access-Control-Allow-Origin":      "http://www.example2.com", +				"Access-Control-Allow-Methods":     "POST", +				"Access-Control-Allow-Headers":     "x-my-special-header", +				"Access-Control-Allow-Credentials": "true", +				"Access-Control-Max-Age":           "", +				"Content-Length":                   "0", +			}, +		}, +		{ +			name:   "preflight matches example2 rule with no header given", +			method: http.MethodOptions, +			url:    objectURL, +			headers: map[string]string{ +				"Origin":                        "http://www.example2.com", +				"Access-Control-Request-Method": "POST", +			}, +			wantStatus: http.StatusOK, +			wantHeaders: map[string]string{ +				"Access-Control-Allow-Origin":      "http://www.example2.com", +				"Access-Control-Allow-Methods":     "POST", +				"Access-Control-Allow-Headers":     "", +				"Access-Control-Allow-Credentials": "true", +				"Access-Control-Max-Age":           "", +				"Content-Length":                   "0", +			}, +		}, +		{ +			name:   "preflight matches wildcard origin rule", +			method: http.MethodOptions, +			url:    objectURL, +			headers: map[string]string{ +				"Origin":                         "http://www.couldbeanything.com", +				"Access-Control-Request-Method":  "GET", +				"Access-Control-Request-Headers": "x-custom-header,x-other-custom-header", +			}, +			wantStatus: http.StatusOK, +			wantHeaders: map[string]string{ +				"Access-Control-Allow-Origin":      "*", +				"Access-Control-Allow-Methods":     "GET", +				"Access-Control-Allow-Headers":     "x-custom-header,x-other-custom-header", +				"Access-Control-Allow-Credentials": "", +				"Access-Control-Max-Age":           "3600", +				"Content-Length":                   "0", +			}, +		}, +		{ +			name:   "preflight does not match any rule", +			method: http.MethodOptions, +			url:    objectURL, +			headers: map[string]string{ +				"Origin":                        "http://www.couldbeanything.com", +				"Access-Control-Request-Method": "DELETE", +			}, +			wantStatus:       http.StatusForbidden, +			wantBodyContains: errStrAccessForbidden, +		}, +		{ +			name:   "preflight does not match example1 rule because of method", +			method: http.MethodOptions, +			url:    objectURL, +			headers: map[string]string{ +				"Origin":                        "http://www.example1.com", +				"Access-Control-Request-Method": "POST", +			}, +			wantStatus:       http.StatusForbidden, +			wantBodyContains: errStrAccessForbidden, +		}, +		{ +			name:   "s3 processes cors rules even when request is not preflight if cors headers present test get", +			method: http.MethodGet, +			url:    objectURL, +			headers: map[string]string{ +				"Origin":                         "http://www.example1.com", +				"Access-Control-Request-Headers": "x-another-header,x-could-be-anything", +				"Access-Control-Request-Method":  "PUT", +			}, +			wantStatus: http.StatusOK, +			wantHeaders: map[string]string{ +				"Access-Control-Allow-Credentials": "true", +				"Access-Control-Allow-Origin":      "http://www.example1.com", +				"Access-Control-Expose-Headers":    "x-amz-server-side-encryption,x-amz-request-id", +				// S3 additionally sets the following headers here, MinIO follows fetch spec and does not: +				// "Access-Control-Allow-Headers":     "x-another-header,x-could-be-anything", +				// "Access-Control-Allow-Methods":     "PUT", +				// "Access-Control-Max-Age":           "3600", +			}, +		}, +		{ +			name:   "s3 processes cors rules even when request is not preflight if cors headers present test put", +			method: http.MethodPut, +			url:    objectURL, +			headers: map[string]string{ +				"Origin":                        "http://www.example1.com", +				"Access-Control-Request-Method": "GET", +			}, +			wantStatus: http.StatusOK, +			wantHeaders: map[string]string{ +				"Access-Control-Allow-Credentials": "", +				"Access-Control-Allow-Origin":      "*", +				"Access-Control-Expose-Headers":    "x-amz-request-id,x-amz-server-side-encryption", +				// S3 additionally sets the following headers here, MinIO follows fetch spec and does not: +				// "Access-Control-Allow-Headers":     "x-another-header,x-could-be-anything", +				// "Access-Control-Allow-Methods":     "PUT", +				// "Access-Control-Max-Age":           "3600", +			}, +		}, +		{ +			name:   "s3 processes cors rules even when request is not preflight but there is no rule match", +			method: http.MethodGet, +			url:    objectURL, +			headers: map[string]string{ +				"Origin":                         "http://www.example1.com", +				"Access-Control-Request-Headers": "x-another-header,x-could-be-anything", +				"Access-Control-Request-Method":  "DELETE", +			}, +			wantStatus: http.StatusOK, +			wantHeaders: map[string]string{ +				"Access-Control-Allow-Methods":     "", +				"Access-Control-Allow-Origin":      "", +				"Access-Control-Allow-Headers":     "", +				"Access-Control-Allow-Credentials": "", +				"Access-Control-Expose-Headers":    "", +				"Access-Control-Max-Age":           "", +			}, +		}, +		{ +			name:   "get request matches wildcard origin rule and returns cors headers", +			method: http.MethodGet, +			url:    objectURL, +			headers: map[string]string{ +				"Origin": "http://www.example1.com", +			}, +			wantStatus: http.StatusOK, +			wantHeaders: map[string]string{ +				"Access-Control-Allow-Credentials": "", +				"Access-Control-Allow-Origin":      "*", +				"Access-Control-Allow-Headers":     "", +				"Access-Control-Expose-Headers":    "x-amz-request-id,X-AMZ-server-side-encryption", +				// S3 returns the following headers, MinIO follows fetch spec and does not: +				// "Access-Control-Max-Age":           "3600", +				// "Access-Control-Allow-Methods":     "GET", +			}, +		}, +		{ +			name:   "head request does not match rule and returns no cors headers", +			method: http.MethodHead, +			url:    objectURL, +			headers: map[string]string{ +				"Origin": "http://www.nomatchingdomainfound.com", +			}, +			wantStatus: http.StatusOK, +			wantHeaders: map[string]string{ +				"Access-Control-Allow-Credentials": "", +				"Access-Control-Allow-Methods":     "", +				"Access-Control-Allow-Origin":      "", +				"Access-Control-Allow-Headers":     "", +				"Access-Control-Expose-Headers":    "", +				"Access-Control-Max-Age":           "", +			}, +		}, +		{ +			name:   "put request with origin does not match rule and returns no cors headers", +			method: http.MethodPut, +			url:    objectURL, +			headers: map[string]string{ +				"Origin": "http://www.nomatchingdomainfound.com", +			}, +			wantStatus: http.StatusOK, +			wantHeaders: map[string]string{ +				"Access-Control-Allow-Credentials": "", +				"Access-Control-Allow-Methods":     "", +				"Access-Control-Allow-Origin":      "", +				"Access-Control-Allow-Headers":     "", +				"Access-Control-Expose-Headers":    "", +				"Access-Control-Max-Age":           "", +			}, +		}, +		{ +			name:       "put request with no origin does not match rule and returns no cors headers", +			method:     http.MethodPut, +			url:        objectURL, +			headers:    map[string]string{}, +			wantStatus: http.StatusOK, +			wantHeaders: map[string]string{ +				"Access-Control-Allow-Credentials": "", +				"Access-Control-Allow-Methods":     "", +				"Access-Control-Allow-Origin":      "", +				"Access-Control-Allow-Headers":     "", +				"Access-Control-Expose-Headers":    "", +				"Access-Control-Max-Age":           "", +			}, +		}, +		{ +			name:   "preflight for delete request with wildcard origin does not match", +			method: http.MethodOptions, +			url:    objectURL, +			headers: map[string]string{ +				"Origin":                        "http://www.notsecureexample.com", +				"Access-Control-Request-Method": "DELETE", +			}, +			wantStatus:       http.StatusForbidden, +			wantBodyContains: errStrAccessForbidden, +		}, +		{ +			name:   "preflight for delete request with wildcard https origin matches secureexample", +			method: http.MethodOptions, +			url:    objectURL, +			headers: map[string]string{ +				"Origin":                        "https://www.secureexample.com", +				"Access-Control-Request-Method": "DELETE", +			}, +			wantStatus: http.StatusOK, +			wantHeaders: map[string]string{ +				"Access-Control-Allow-Credentials": "true", +				"Access-Control-Allow-Methods":     "DELETE", +				"Access-Control-Allow-Origin":      "https://www.secureexample.com", +				"Access-Control-Allow-Headers":     "", +				"Access-Control-Expose-Headers":    "", +				"Access-Control-Max-Age":           "", +			}, +		}, +		{ +			name:   "preflight for delete request matches secureexample with wildcard https origin and request headers", +			method: http.MethodOptions, +			url:    objectURL, +			headers: map[string]string{ +				"Origin":                         "https://www.secureexample.com", +				"Access-Control-Request-Method":  "DELETE", +				"Access-Control-Request-Headers": "x-abc-1,x-abc-second,x-def-1", +			}, +			wantStatus: http.StatusOK, +			wantHeaders: map[string]string{ +				"Access-Control-Allow-Credentials": "true", +				"Access-Control-Allow-Methods":     "DELETE", +				"Access-Control-Allow-Origin":      "https://www.secureexample.com", +				"Access-Control-Allow-Headers":     "x-abc-1,x-abc-second,x-def-1", +				"Access-Control-Expose-Headers":    "", +				"Access-Control-Max-Age":           "", +			}, +		}, +		{ +			name:   "preflight for delete request matches secureexample rejected because request header does not match", +			method: http.MethodOptions, +			url:    objectURL, +			headers: map[string]string{ +				"Origin":                         "https://www.secureexample.com", +				"Access-Control-Request-Method":  "DELETE", +				"Access-Control-Request-Headers": "x-abc-1,x-abc-second,x-def-1,x-does-not-match", +			}, +			wantStatus:       http.StatusForbidden, +			wantBodyContains: errStrAccessForbidden, +		}, +		{ +			name:   "preflight with https origin is documented by s3 as matching but it does not match", +			method: http.MethodOptions, +			url:    objectURL, +			headers: map[string]string{ +				"Origin":                        "https://www.securebutdoesnotmatch.com", +				"Access-Control-Request-Method": "PUT", +			}, +			wantStatus:       http.StatusForbidden, +			wantBodyContains: errStrAccessForbidden, +		}, +		{ +			name:       "put no origin no match returns no cors headers", +			method:     http.MethodPut, +			url:        objectURL, +			headers:    map[string]string{}, +			wantStatus: http.StatusOK, + +			wantHeaders: map[string]string{ +				"Access-Control-Allow-Credentials": "", +				"Access-Control-Allow-Methods":     "", +				"Access-Control-Allow-Origin":      "", +				"Access-Control-Allow-Headers":     "", +				"Access-Control-Expose-Headers":    "", +				"Access-Control-Max-Age":           "", +			}, +		}, +		{ +			name:   "put with origin match example1 returns cors headers", +			method: http.MethodPut, +			url:    objectURL, +			headers: map[string]string{ +				"Origin": "http://www.example1.com", +			}, +			wantStatus: http.StatusOK, + +			wantHeaders: map[string]string{ +				"Access-Control-Allow-Credentials": "true", +				"Access-Control-Allow-Origin":      "http://www.example1.com", +				"Access-Control-Allow-Headers":     "", +				"Access-Control-Expose-Headers":    "x-amz-server-side-encryption,x-amz-request-id", +				// S3 returns the following headers, MinIO follows fetch spec and does not: +				// "Access-Control-Max-Age":           "3600", +				// "Access-Control-Allow-Methods":     "PUT", +			}, +		}, +		{ +			name:   "put with origin and header match example1 returns cors headers", +			method: http.MethodPut, +			url:    objectURL, +			headers: map[string]string{ +				"Origin":              "http://www.example1.com", +				"x-could-be-anything": "myvalue", +			}, +			wantStatus: http.StatusOK, + +			wantHeaders: map[string]string{ +				"Access-Control-Allow-Credentials": "true", +				"Access-Control-Allow-Origin":      "http://www.example1.com", +				"Access-Control-Allow-Headers":     "", +				"Access-Control-Expose-Headers":    "x-amz-server-side-encryption,x-amz-request-id", +				// S3 returns the following headers, MinIO follows fetch spec and does not: +				// "Access-Control-Max-Age":           "3600", +				// "Access-Control-Allow-Methods":     "PUT", +			}, +		}, +		{ +			name:   "put no match found returns no cors headers", +			method: http.MethodPut, +			url:    objectURL, +			headers: map[string]string{ +				"Origin": "http://www.unmatchingdomain.com", +			}, +			wantStatus: http.StatusOK, + +			wantHeaders: map[string]string{ +				"Access-Control-Allow-Credentials": "", +				"Access-Control-Allow-Methods":     "", +				"Access-Control-Allow-Origin":      "", +				"Access-Control-Allow-Headers":     "", +				"Access-Control-Expose-Headers":    "", +				"Access-Control-Max-Age":           "", +			}, +		}, +		{ +			name:   "put with origin match example3 returns cors headers", +			method: http.MethodPut, +			url:    objectURL, +			headers: map[string]string{ +				"Origin":              "http://www.example3.com", +				"X-My-Special-Header": "myvalue", +			}, +			wantStatus: http.StatusOK, + +			wantHeaders: map[string]string{ +				"Access-Control-Allow-Credentials": "true", +				"Access-Control-Allow-Origin":      "http://www.example3.com", +				"Access-Control-Allow-Headers":     "", +				"Access-Control-Expose-Headers":    "", +				// S3 returns the following headers, MinIO follows fetch spec and does not: +				// "Access-Control-Max-Age":           "10", +				// "Access-Control-Allow-Methods":     "PUT", +			}, +		}, +		{ +			name:   "preflight matches example1 rule headers case is incorrect", +			method: http.MethodOptions, +			url:    objectURL, +			headers: map[string]string{ +				"Origin":                        "http://www.example1.com", +				"Access-Control-Request-Method": "PUT", +				// Fetch standard guarantees that these are sent lowercase, here we test what happens when they are not. +				"Access-Control-Request-Headers": "X-Another-Header,X-Could-Be-Anything", +			}, +			wantStatus: http.StatusOK, +			wantHeaders: map[string]string{ +				"Access-Control-Allow-Origin":      "http://www.example1.com", +				"Access-Control-Allow-Methods":     "PUT", +				"Access-Control-Allow-Headers":     "x-another-header,x-could-be-anything", +				"Access-Control-Allow-Credentials": "true", +				"Access-Control-Max-Age":           "3600", +				"Content-Length":                   "0", +				// S3 returns the following headers, MinIO follows fetch spec and does not: +				// "Access-Control-Expose-Headers":    "x-amz-server-side-encryption,x-amz-request-id", +			}, +		}, +		{ +			name:   "preflight matches example1 rule headers are not sorted", +			method: http.MethodOptions, +			url:    objectURL, +			headers: map[string]string{ +				"Origin":                        "http://www.example1.com", +				"Access-Control-Request-Method": "PUT", +				// Fetch standard guarantees that these are sorted, test what happens when they are not. +				"Access-Control-Request-Headers": "a-customer-header,b-should-be-last", +			}, +			wantStatus: http.StatusOK, +			wantHeaders: map[string]string{ +				"Access-Control-Allow-Origin":      "http://www.example1.com", +				"Access-Control-Allow-Methods":     "PUT", +				"Access-Control-Allow-Headers":     "a-customer-header,b-should-be-last", +				"Access-Control-Allow-Credentials": "true", +				"Access-Control-Max-Age":           "3600", +				"Content-Length":                   "0", +				// S3 returns the following headers, MinIO follows fetch spec and does not: +				// "Access-Control-Expose-Headers":    "x-amz-server-side-encryption,x-amz-request-id", +			}, +		}, +		{ +			name:   "preflight with case sensitivity in origin matches uppercase", +			method: http.MethodOptions, +			url:    objectURL, +			headers: map[string]string{ +				"Origin":                        "http://UPPERCASEEXAMPLE.com", +				"Access-Control-Request-Method": "DELETE", +			}, +			wantStatus: http.StatusOK, +			wantHeaders: map[string]string{ +				"Access-Control-Allow-Credentials": "true", +				"Access-Control-Allow-Methods":     "DELETE", +				"Access-Control-Allow-Origin":      "http://UPPERCASEEXAMPLE.com", +				"Access-Control-Allow-Headers":     "", +				"Access-Control-Expose-Headers":    "", +				"Access-Control-Max-Age":           "", +			}, +		}, +		{ +			name:   "preflight with case sensitivity in origin does not match when lowercase", +			method: http.MethodOptions, +			url:    objectURL, +			headers: map[string]string{ +				"Origin":                        "http://uppercaseexample.com", +				"Access-Control-Request-Method": "DELETE", +			}, +			wantStatus:       http.StatusForbidden, +			wantBodyContains: errStrAccessForbidden, +		}, +		{ +			name:   "preflight match upper case with unknown header but no header restrictions", +			method: http.MethodOptions, +			url:    objectURL, +			headers: map[string]string{ +				"Origin":                         "http://UPPERCASEEXAMPLE.com", +				"Access-Control-Request-Method":  "DELETE", +				"Access-Control-Request-Headers": "x-unknown-1", +			}, +			wantStatus:       http.StatusForbidden, +			wantBodyContains: errStrAccessForbidden, +		}, +		{ +			name:   "preflight for delete request matches multiplemethodstest.com origin and request headers", +			method: http.MethodOptions, +			url:    objectURL, +			headers: map[string]string{ +				"Origin":                         "http://multiplemethodstest.com", +				"Access-Control-Request-Method":  "DELETE", +				"Access-Control-Request-Headers": "x-abc-1", +			}, +			wantStatus: http.StatusOK, +			wantHeaders: map[string]string{ +				"Access-Control-Allow-Credentials": "true", +				"Access-Control-Allow-Origin":      "http://multiplemethodstest.com", +				"Access-Control-Allow-Headers":     "x-abc-1", +				"Access-Control-Expose-Headers":    "", +				"Access-Control-Max-Age":           "", +				// S3 returns POST, PUT, DELETE here, MinIO does not as spec does not require it. +				// "Access-Control-Allow-Methods":     "DELETE", +			}, +		}, +		{ +			name:   "delete request goes ahead because cors is only for browsers and does not block on the server side", +			method: http.MethodDelete, +			url:    objectURL, +			headers: map[string]string{ +				"Origin": "http://www.justrandom.com", +			}, +			wantStatus: http.StatusNoContent, +		}, +	} + +	for i, test := range testCases { +		testName := fmt.Sprintf("%s_%d_%s", testName, i+1, strings.ReplaceAll(test.name, " ", "_")) + +		// Apply the CORS rules +		if test.applyCorsRules != nil { +			corsConfig := &cors.Config{ +				CORSRules: test.applyCorsRules, +			} +			err = c.SetBucketCors(ctx, bucketName, corsConfig) +			if err != nil { +				logFailure(testName, function, args, startTime, "", "SetBucketCors failed to apply", err) +				return +			} +		} + +		// Make request +		if test.method != "" && test.url != "" { +			req, err := http.NewRequestWithContext(ctx, test.method, test.url, nil) +			if err != nil { +				logFailure(testName, function, args, startTime, "", "HTTP request creation failed", err) +				return +			} +			req.Header.Set("User-Agent", "MinIO-go-FunctionalTest/"+appVersion) + +			for k, v := range test.headers { +				req.Header.Set(k, v) +			} +			resp, err := httpClient.Do(req) +			if err != nil { +				logFailure(testName, function, args, startTime, "", "HTTP request failed", err) +				return +			} +			defer resp.Body.Close() + +			// Check returned status code +			if resp.StatusCode != test.wantStatus { +				errStr := fmt.Sprintf(" incorrect status code in response, want: %d, got: %d", test.wantStatus, resp.StatusCode) +				logFailure(testName, function, args, startTime, "", errStr, nil) +				return +			} + +			// Check returned body +			if test.wantBodyContains != "" { +				body, err := io.ReadAll(resp.Body) +				if err != nil { +					logFailure(testName, function, args, startTime, "", "Failed to read response body", err) +					return +				} +				if !strings.Contains(string(body), test.wantBodyContains) { +					errStr := fmt.Sprintf(" incorrect body in response, want: %s, in got: %s", test.wantBodyContains, string(body)) +					logFailure(testName, function, args, startTime, "", errStr, nil) +					return +				} +			} + +			// Check returned response headers +			for k, v := range test.wantHeaders { +				gotVal := resp.Header.Get(k) +				if k == "Access-Control-Expose-Headers" { +					// MinIO returns this in canonical form, S3 does not. +					gotVal = strings.ToLower(gotVal) +					v = strings.ToLower(v) +				} +				// Remove all spaces, S3 adds spaces after CSV values in headers, MinIO does not. +				gotVal = strings.ReplaceAll(gotVal, " ", "") +				if gotVal != v { +					errStr := fmt.Sprintf(" incorrect header in response, want: %s: '%s', got: '%s'", k, v, gotVal) +					logFailure(testName, function, args, startTime, "", errStr, nil) +					return +				} +			} +		} +		logSuccess(testName, function, args, startTime) +	} +	logSuccess(testName, function, args, startTime) +} + +func testCorsSetGetDelete() { +	ctx := context.Background() +	startTime := time.Now() +	testName := getFuncName() +	function := "SetBucketCors(bucketName, cors)" +	args := map[string]interface{}{ +		"bucketName": "", +		"cors":       "", +	} + +	// Instantiate new minio client object +	c, err := minio.New(os.Getenv(serverEndpoint), +		&minio.Options{ +			Creds:     credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), +			Transport: createHTTPTransport(), +			Secure:    mustParseBool(os.Getenv(enableHTTPS)), +		}) +	if err != nil { +		logFailure(testName, function, args, startTime, "", "MinIO client object creation failed", err) +		return +	} + +	// Enable tracing, write to stderr. +	// c.TraceOn(os.Stderr) + +	// Set user agent. +	c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) + +	// Generate a new random bucket name. +	bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") +	args["bucketName"] = bucketName + +	// Make a new bucket. +	err = c.MakeBucket(ctx, bucketName, minio.MakeBucketOptions{Region: "us-east-1"}) +	if err != nil { +		logFailure(testName, function, args, startTime, "", "MakeBucket failed", err) +		return +	} +	defer cleanupBucket(bucketName, c) + +	// Set the CORS rules on the new bucket +	corsRules := []cors.Rule{ +		{ +			AllowedOrigin: []string{"http://www.example1.com"}, +			AllowedMethod: []string{"PUT"}, +			AllowedHeader: []string{"*"}, +		}, +		{ +			AllowedOrigin: []string{"http://www.example2.com"}, +			AllowedMethod: []string{"POST"}, +			AllowedHeader: []string{"X-My-Special-Header"}, +		}, +		{ +			AllowedOrigin: []string{"*"}, +			AllowedMethod: []string{"GET"}, +			AllowedHeader: []string{"*"}, +		}, +	} +	corsConfig := cors.NewConfig(corsRules) +	err = c.SetBucketCors(ctx, bucketName, corsConfig) +	if err != nil { +		logFailure(testName, function, args, startTime, "", "SetBucketCors failed to apply", err) +		return +	} + +	// Get the rules and check they match what we set +	gotCorsConfig, err := c.GetBucketCors(ctx, bucketName) +	if err != nil { +		logFailure(testName, function, args, startTime, "", "GetBucketCors failed", err) +		return +	} +	if !reflect.DeepEqual(corsConfig, gotCorsConfig) { +		msg := fmt.Sprintf("GetBucketCors returned unexpected rules, expected: %+v, got: %+v", corsConfig, gotCorsConfig) +		logFailure(testName, function, args, startTime, "", msg, nil) +		return +	} + +	// Delete the rules +	err = c.SetBucketCors(ctx, bucketName, nil) +	if err != nil { +		logFailure(testName, function, args, startTime, "", "SetBucketCors failed to delete", err) +		return +	} + +	// Get the rules and check they are now empty +	gotCorsConfig, err = c.GetBucketCors(ctx, bucketName) +	if err != nil { +		logFailure(testName, function, args, startTime, "", "GetBucketCors failed", err) +		return +	} +	if gotCorsConfig != nil { +		logFailure(testName, function, args, startTime, "", "GetBucketCors returned unexpected rules", nil) +		return +	} + +	logSuccess(testName, function, args, startTime) +} +  // Test deleting multiple objects with object retention set in Governance mode  func testRemoveObjects() {  	// initialize logging params @@ -13627,6 +14457,245 @@ func testRemoveObjects() {  	logSuccess(testName, function, args, startTime)  } +// Test get bucket tags +func testGetBucketTagging() { +	// initialize logging params +	startTime := time.Now() +	testName := getFuncName() +	function := "GetBucketTagging(bucketName)" +	args := map[string]interface{}{ +		"bucketName": "", +	} +	// Seed random based on current time. +	rand.Seed(time.Now().Unix()) + +	// Instantiate new minio client object. +	c, err := minio.New(os.Getenv(serverEndpoint), +		&minio.Options{ +			Creds:     credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), +			Transport: createHTTPTransport(), +			Secure:    mustParseBool(os.Getenv(enableHTTPS)), +		}) +	if err != nil { +		logError(testName, function, args, startTime, "", "MinIO client v4 object creation failed", err) +		return +	} + +	// Enable tracing, write to stderr. +	// c.TraceOn(os.Stderr) + +	// Set user agent. +	c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) + +	// Generate a new random bucket name. +	bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") +	args["bucketName"] = bucketName + +	// Make a new bucket. +	err = c.MakeBucket(context.Background(), bucketName, minio.MakeBucketOptions{Region: "us-east-1", ObjectLocking: true}) +	if err != nil { +		logError(testName, function, args, startTime, "", "MakeBucket failed", err) +		return +	} + +	_, err = c.GetBucketTagging(context.Background(), bucketName) +	if minio.ToErrorResponse(err).Code != "NoSuchTagSet" { +		logError(testName, function, args, startTime, "", "Invalid error from server failed", err) +		return +	} + +	if err = cleanupVersionedBucket(bucketName, c); err != nil { +		logError(testName, function, args, startTime, "", "CleanupBucket failed", err) +		return +	} + +	logSuccess(testName, function, args, startTime) +} + +// Test setting tags for bucket +func testSetBucketTagging() { +	// initialize logging params +	startTime := time.Now() +	testName := getFuncName() +	function := "SetBucketTagging(bucketName, tags)" +	args := map[string]interface{}{ +		"bucketName": "", +		"tags":       "", +	} +	// Seed random based on current time. +	rand.Seed(time.Now().Unix()) + +	// Instantiate new minio client object. +	c, err := minio.New(os.Getenv(serverEndpoint), +		&minio.Options{ +			Creds:     credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), +			Transport: createHTTPTransport(), +			Secure:    mustParseBool(os.Getenv(enableHTTPS)), +		}) +	if err != nil { +		logError(testName, function, args, startTime, "", "MinIO client v4 object creation failed", err) +		return +	} + +	// Enable tracing, write to stderr. +	// c.TraceOn(os.Stderr) + +	// Set user agent. +	c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) + +	// Generate a new random bucket name. +	bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") +	args["bucketName"] = bucketName + +	// Make a new bucket. +	err = c.MakeBucket(context.Background(), bucketName, minio.MakeBucketOptions{Region: "us-east-1", ObjectLocking: true}) +	if err != nil { +		logError(testName, function, args, startTime, "", "MakeBucket failed", err) +		return +	} + +	_, err = c.GetBucketTagging(context.Background(), bucketName) +	if minio.ToErrorResponse(err).Code != "NoSuchTagSet" { +		logError(testName, function, args, startTime, "", "Invalid error from server", err) +		return +	} + +	tag := randString(60, rand.NewSource(time.Now().UnixNano()), "") +	expectedValue := randString(60, rand.NewSource(time.Now().UnixNano()), "") + +	t, err := tags.MapToBucketTags(map[string]string{ +		tag: expectedValue, +	}) +	args["tags"] = t.String() +	if err != nil { +		logError(testName, function, args, startTime, "", "tags.MapToBucketTags failed", err) +		return +	} + +	err = c.SetBucketTagging(context.Background(), bucketName, t) +	if err != nil { +		logError(testName, function, args, startTime, "", "SetBucketTagging failed", err) +		return +	} + +	tagging, err := c.GetBucketTagging(context.Background(), bucketName) +	if err != nil { +		logError(testName, function, args, startTime, "", "GetBucketTagging failed", err) +		return +	} + +	if tagging.ToMap()[tag] != expectedValue { +		msg := fmt.Sprintf("Tag %s; got value %s; wanted %s", tag, tagging.ToMap()[tag], expectedValue) +		logError(testName, function, args, startTime, "", msg, err) +		return +	} + +	// Delete all objects and buckets +	if err = cleanupVersionedBucket(bucketName, c); err != nil { +		logError(testName, function, args, startTime, "", "CleanupBucket failed", err) +		return +	} + +	logSuccess(testName, function, args, startTime) +} + +// Test removing bucket tags +func testRemoveBucketTagging() { +	// initialize logging params +	startTime := time.Now() +	testName := getFuncName() +	function := "RemoveBucketTagging(bucketName)" +	args := map[string]interface{}{ +		"bucketName": "", +	} +	// Seed random based on current time. +	rand.Seed(time.Now().Unix()) + +	// Instantiate new minio client object. +	c, err := minio.New(os.Getenv(serverEndpoint), +		&minio.Options{ +			Creds:     credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), +			Transport: createHTTPTransport(), +			Secure:    mustParseBool(os.Getenv(enableHTTPS)), +		}) +	if err != nil { +		logError(testName, function, args, startTime, "", "MinIO client v4 object creation failed", err) +		return +	} + +	// Enable tracing, write to stderr. +	// c.TraceOn(os.Stderr) + +	// Set user agent. +	c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) + +	// Generate a new random bucket name. +	bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") +	args["bucketName"] = bucketName + +	// Make a new bucket. +	err = c.MakeBucket(context.Background(), bucketName, minio.MakeBucketOptions{Region: "us-east-1", ObjectLocking: true}) +	if err != nil { +		logError(testName, function, args, startTime, "", "MakeBucket failed", err) +		return +	} + +	_, err = c.GetBucketTagging(context.Background(), bucketName) +	if minio.ToErrorResponse(err).Code != "NoSuchTagSet" { +		logError(testName, function, args, startTime, "", "Invalid error from server", err) +		return +	} + +	tag := randString(60, rand.NewSource(time.Now().UnixNano()), "") +	expectedValue := randString(60, rand.NewSource(time.Now().UnixNano()), "") + +	t, err := tags.MapToBucketTags(map[string]string{ +		tag: expectedValue, +	}) +	if err != nil { +		logError(testName, function, args, startTime, "", "tags.MapToBucketTags failed", err) +		return +	} + +	err = c.SetBucketTagging(context.Background(), bucketName, t) +	if err != nil { +		logError(testName, function, args, startTime, "", "SetBucketTagging failed", err) +		return +	} + +	tagging, err := c.GetBucketTagging(context.Background(), bucketName) +	if err != nil { +		logError(testName, function, args, startTime, "", "GetBucketTagging failed", err) +		return +	} + +	if tagging.ToMap()[tag] != expectedValue { +		msg := fmt.Sprintf("Tag %s; got value %s; wanted %s", tag, tagging.ToMap()[tag], expectedValue) +		logError(testName, function, args, startTime, "", msg, err) +		return +	} + +	err = c.RemoveBucketTagging(context.Background(), bucketName) +	if err != nil { +		logError(testName, function, args, startTime, "", "RemoveBucketTagging failed", err) +		return +	} + +	_, err = c.GetBucketTagging(context.Background(), bucketName) +	if minio.ToErrorResponse(err).Code != "NoSuchTagSet" { +		logError(testName, function, args, startTime, "", "Invalid error from server", err) +		return +	} + +	// Delete all objects and buckets +	if err = cleanupVersionedBucket(bucketName, c); err != nil { +		logError(testName, function, args, startTime, "", "CleanupBucket failed", err) +		return +	} + +	logSuccess(testName, function, args, startTime) +} +  // Convert string to bool and always return false if any error  func mustParseBool(str string) bool {  	b, err := strconv.ParseBool(str) @@ -13660,6 +14729,8 @@ func main() {  	// execute tests  	if isFullMode() { +		testCorsSetGetDelete() +		testCors()  		testListMultipartUpload()  		testGetObjectAttributes()  		testGetObjectAttributesErrorCases() @@ -13731,6 +14802,9 @@ func main() {  		testObjectTaggingWithVersioning()  		testTrailingChecksums()  		testPutObjectWithAutomaticChecksums() +		testGetBucketTagging() +		testSetBucketTagging() +		testRemoveBucketTagging()  		// SSE-C tests will only work over TLS connection.  		if tls { diff --git a/vendor/github.com/minio/minio-go/v7/pkg/cors/cors.go b/vendor/github.com/minio/minio-go/v7/pkg/cors/cors.go new file mode 100644 index 000000000..e71864ee9 --- /dev/null +++ b/vendor/github.com/minio/minio-go/v7/pkg/cors/cors.go @@ -0,0 +1,91 @@ +/* + * MinIO Go Library for Amazon S3 Compatible Cloud Storage + * Copyright 2015-2024 MinIO, Inc. + * + * 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 cors + +import ( +	"encoding/xml" +	"fmt" +	"io" +	"strings" + +	"github.com/dustin/go-humanize" +) + +const defaultXMLNS = "http://s3.amazonaws.com/doc/2006-03-01/" + +// Config is the container for a CORS configuration for a bucket. +type Config struct { +	XMLNS     string   `xml:"xmlns,attr,omitempty"` +	XMLName   xml.Name `xml:"CORSConfiguration"` +	CORSRules []Rule   `xml:"CORSRule"` +} + +// Rule is a single rule in a CORS configuration. +type Rule struct { +	AllowedHeader []string `xml:"AllowedHeader,omitempty"` +	AllowedMethod []string `xml:"AllowedMethod,omitempty"` +	AllowedOrigin []string `xml:"AllowedOrigin,omitempty"` +	ExposeHeader  []string `xml:"ExposeHeader,omitempty"` +	ID            string   `xml:"ID,omitempty"` +	MaxAgeSeconds int      `xml:"MaxAgeSeconds,omitempty"` +} + +// NewConfig creates a new CORS configuration with the given rules. +func NewConfig(rules []Rule) *Config { +	return &Config{ +		XMLNS: defaultXMLNS, +		XMLName: xml.Name{ +			Local: "CORSConfiguration", +			Space: defaultXMLNS, +		}, +		CORSRules: rules, +	} +} + +// ParseBucketCorsConfig parses a CORS configuration in XML from an io.Reader. +func ParseBucketCorsConfig(reader io.Reader) (*Config, error) { +	var c Config + +	// Max size of cors document is 64KiB according to https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutBucketCors.html +	// This limiter is just for safety so has a max of 128KiB +	err := xml.NewDecoder(io.LimitReader(reader, 128*humanize.KiByte)).Decode(&c) +	if err != nil { +		return nil, fmt.Errorf("decoding xml: %w", err) +	} +	if c.XMLNS == "" { +		c.XMLNS = defaultXMLNS +	} +	for i, rule := range c.CORSRules { +		for j, method := range rule.AllowedMethod { +			c.CORSRules[i].AllowedMethod[j] = strings.ToUpper(method) +		} +	} +	return &c, nil +} + +// ToXML marshals the CORS configuration to XML. +func (c Config) ToXML() ([]byte, error) { +	if c.XMLNS == "" { +		c.XMLNS = defaultXMLNS +	} +	data, err := xml.Marshal(&c) +	if err != nil { +		return nil, fmt.Errorf("marshaling xml: %w", err) +	} +	return append([]byte(xml.Header), data...), nil +} diff --git a/vendor/github.com/minio/minio-go/v7/s3-error.go b/vendor/github.com/minio/minio-go/v7/s3-error.go index f365157ee..f7fad19f6 100644 --- a/vendor/github.com/minio/minio-go/v7/s3-error.go +++ b/vendor/github.com/minio/minio-go/v7/s3-error.go @@ -57,5 +57,6 @@ var s3ErrorResponseMap = map[string]string{  	"BucketAlreadyOwnedByYou":           "Your previous request to create the named bucket succeeded and you already own it.",  	"InvalidDuration":                   "Duration provided in the request is invalid.",  	"XAmzContentSHA256Mismatch":         "The provided 'x-amz-content-sha256' header does not match what was computed.", +	"NoSuchCORSConfiguration":           "The specified bucket does not have a CORS configuration.",  	// Add new API errors here.  } diff --git a/vendor/modules.txt b/vendor/modules.txt index 1c475f83c..fbb31c404 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -495,9 +495,10 @@ github.com/miekg/dns  # github.com/minio/md5-simd v1.1.2  ## explicit; go 1.14  github.com/minio/md5-simd -# github.com/minio/minio-go/v7 v7.0.74 +# github.com/minio/minio-go/v7 v7.0.75  ## explicit; go 1.21  github.com/minio/minio-go/v7 +github.com/minio/minio-go/v7/pkg/cors  github.com/minio/minio-go/v7/pkg/credentials  github.com/minio/minio-go/v7/pkg/encrypt  github.com/minio/minio-go/v7/pkg/lifecycle | 
