summaryrefslogtreecommitdiff
path: root/vendor/github.com/minio/minio-go/v7/functional_tests.go
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/github.com/minio/minio-go/v7/functional_tests.go')
-rw-r--r--vendor/github.com/minio/minio-go/v7/functional_tests.go1102
1 files changed, 1088 insertions, 14 deletions
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 {