diff --git a/.github/workflows/run-mint.sh b/.github/workflows/run-mint.sh index 6a64465c9..df1bec9b7 100755 --- a/.github/workflows/run-mint.sh +++ b/.github/workflows/run-mint.sh @@ -16,6 +16,9 @@ docker volume rm $(docker volume ls -f dangling=true) || true ## change working directory cd .github/workflows/mint +## always pull latest +docker pull docker.io/minio/mint:edge + docker-compose -f minio-${MODE}.yaml up -d sleep 1m diff --git a/cmd/post-policy_test.go b/cmd/post-policy_test.go index e2f74bc72..236e6dd10 100644 --- a/cmd/post-policy_test.go +++ b/cmd/post-policy_test.go @@ -56,10 +56,12 @@ func newPostPolicyBytesV4WithContentRange(credential, bucketName, objectKey stri credentialConditionStr := fmt.Sprintf(`["eq", "$x-amz-credential", "%s"]`, credential) // Add the meta-uuid string, set to 1234 uuidConditionStr := fmt.Sprintf(`["eq", "$x-amz-meta-uuid", "%s"]`, "1234") + // Add the content-encoding string, set to gzip. + contentEncodingConditionStr := fmt.Sprintf(`["eq", "$content-encoding", "%s"]`, "gzip") // Combine all conditions into one string. - conditionStr := fmt.Sprintf(`"conditions":[%s, %s, %s, %s, %s, %s, %s]`, bucketConditionStr, - keyConditionStr, contentLengthCondStr, algorithmConditionStr, dateConditionStr, credentialConditionStr, uuidConditionStr) + conditionStr := fmt.Sprintf(`"conditions":[%s, %s, %s, %s, %s, %s, %s, %s]`, bucketConditionStr, + keyConditionStr, contentLengthCondStr, algorithmConditionStr, dateConditionStr, credentialConditionStr, uuidConditionStr, contentEncodingConditionStr) retStr := "{" retStr = retStr + expirationStr + "," retStr += conditionStr @@ -85,9 +87,11 @@ func newPostPolicyBytesV4(credential, bucketName, objectKey string, expiration t credentialConditionStr := fmt.Sprintf(`["eq", "$x-amz-credential", "%s"]`, credential) // Add the meta-uuid string, set to 1234 uuidConditionStr := fmt.Sprintf(`["eq", "$x-amz-meta-uuid", "%s"]`, "1234") + // Add the content-encoding string, set to gzip + contentEncodingConditionStr := fmt.Sprintf(`["eq", "$content-encoding", "%s"]`, "gzip") // Combine all conditions into one string. - conditionStr := fmt.Sprintf(`"conditions":[%s, %s, %s, %s, %s, %s]`, bucketConditionStr, keyConditionStr, algorithmConditionStr, dateConditionStr, credentialConditionStr, uuidConditionStr) + conditionStr := fmt.Sprintf(`"conditions":[%s, %s, %s, %s, %s, %s, %s]`, bucketConditionStr, keyConditionStr, algorithmConditionStr, dateConditionStr, credentialConditionStr, uuidConditionStr, contentEncodingConditionStr) retStr := "{" retStr = retStr + expirationStr + "," retStr += conditionStr @@ -331,7 +335,7 @@ func testPostPolicyBucketHandler(obj ObjectLayer, instanceType string, t TestErr accessKey: credentials.AccessKey, secretKey: credentials.SecretKey, dates: []interface{}{curTimePlus5Min.Format(iso8601TimeFormat), curTime.Format(iso8601DateFormat), curTime.Format(yyyymmdd)}, - policy: `{"expiration": "%s","conditions":[["eq", "$bucket", "` + bucketName + `"], ["starts-with", "$key", "test/"], ["eq", "$x-amz-algorithm", "AWS4-HMAC-SHA256"], ["eq", "$x-amz-date", "%s"], ["eq", "$x-amz-credential", "` + credentials.AccessKey + `/%s/us-east-1/s3/aws4_request"],["eq", "$x-amz-meta-uuid", "1234"]]}`, + policy: `{"expiration": "%s","conditions":[["eq", "$bucket", "` + bucketName + `"], ["starts-with", "$key", "test/"], ["eq", "$x-amz-algorithm", "AWS4-HMAC-SHA256"], ["eq", "$x-amz-date", "%s"], ["eq", "$x-amz-credential", "` + credentials.AccessKey + `/%s/us-east-1/s3/aws4_request"],["eq", "$x-amz-meta-uuid", "1234"],["eq", "$content-encoding", "gzip"]]}`, }, // Success case, no multipart filename. { @@ -341,7 +345,7 @@ func testPostPolicyBucketHandler(obj ObjectLayer, instanceType string, t TestErr accessKey: credentials.AccessKey, secretKey: credentials.SecretKey, dates: []interface{}{curTimePlus5Min.Format(iso8601TimeFormat), curTime.Format(iso8601DateFormat), curTime.Format(yyyymmdd)}, - policy: `{"expiration": "%s","conditions":[["eq", "$bucket", "` + bucketName + `"], ["starts-with", "$key", "test/"], ["eq", "$x-amz-algorithm", "AWS4-HMAC-SHA256"], ["eq", "$x-amz-date", "%s"], ["eq", "$x-amz-credential", "` + credentials.AccessKey + `/%s/us-east-1/s3/aws4_request"],["eq", "$x-amz-meta-uuid", "1234"]]}`, + policy: `{"expiration": "%s","conditions":[["eq", "$bucket", "` + bucketName + `"], ["starts-with", "$key", "test/"], ["eq", "$x-amz-algorithm", "AWS4-HMAC-SHA256"], ["eq", "$x-amz-date", "%s"], ["eq", "$x-amz-credential", "` + credentials.AccessKey + `/%s/us-east-1/s3/aws4_request"],["eq", "$x-amz-meta-uuid", "1234"],["eq", "$content-encoding", "gzip"]]}`, noFilename: true, }, // Success case, big body. @@ -352,7 +356,7 @@ func testPostPolicyBucketHandler(obj ObjectLayer, instanceType string, t TestErr accessKey: credentials.AccessKey, secretKey: credentials.SecretKey, dates: []interface{}{curTimePlus5Min.Format(iso8601TimeFormat), curTime.Format(iso8601DateFormat), curTime.Format(yyyymmdd)}, - policy: `{"expiration": "%s","conditions":[["eq", "$bucket", "` + bucketName + `"], ["starts-with", "$key", "test/"], ["eq", "$x-amz-algorithm", "AWS4-HMAC-SHA256"], ["eq", "$x-amz-date", "%s"], ["eq", "$x-amz-credential", "` + credentials.AccessKey + `/%s/us-east-1/s3/aws4_request"],["eq", "$x-amz-meta-uuid", "1234"]]}`, + policy: `{"expiration": "%s","conditions":[["eq", "$bucket", "` + bucketName + `"], ["starts-with", "$key", "test/"], ["eq", "$x-amz-algorithm", "AWS4-HMAC-SHA256"], ["eq", "$x-amz-date", "%s"], ["eq", "$x-amz-credential", "` + credentials.AccessKey + `/%s/us-east-1/s3/aws4_request"],["eq", "$x-amz-meta-uuid", "1234"],["eq", "$content-encoding", "gzip"]]}`, }, // Corrupted Base 64 result { @@ -447,7 +451,7 @@ func testPostPolicyBucketHandler(obj ObjectLayer, instanceType string, t TestErr malformedBody: false, ignoreContentLength: false, }, - // Failed with Content-Length not specified. + // Success with Content-Length not specified. { objectName: "test", data: bytes.Repeat([]byte("a"), 1025), @@ -547,7 +551,7 @@ func testPostPolicyBucketHandlerRedirect(obj ObjectLayer, instanceType string, t rec := httptest.NewRecorder() dates := []interface{}{curTimePlus5Min.Format(iso8601TimeFormat), curTime.Format(iso8601DateFormat), curTime.Format(yyyymmdd)} - policy := `{"expiration": "%s","conditions":[["eq", "$bucket", "` + bucketName + `"], {"success_action_redirect":"` + redirectURL.String() + `"},["starts-with", "$key", "test/"], ["eq", "$x-amz-meta-uuid", "1234"], ["eq", "$x-amz-algorithm", "AWS4-HMAC-SHA256"], ["eq", "$x-amz-date", "%s"], ["eq", "$x-amz-credential", "` + credentials.AccessKey + `/%s/us-east-1/s3/aws4_request"]]}` + policy := `{"expiration": "%s","conditions":[["eq", "$bucket", "` + bucketName + `"], {"success_action_redirect":"` + redirectURL.String() + `"},["starts-with", "$key", "test/"], ["eq", "$x-amz-meta-uuid", "1234"], ["eq", "$x-amz-algorithm", "AWS4-HMAC-SHA256"], ["eq", "$x-amz-date", "%s"], ["eq", "$x-amz-credential", "` + credentials.AccessKey + `/%s/us-east-1/s3/aws4_request"],["eq", "$content-encoding", "gzip"]]}` // Generate the final policy document policy = fmt.Sprintf(policy, dates...) diff --git a/cmd/postpolicyform.go b/cmd/postpolicyform.go index ad8aadc6a..74b5ef870 100644 --- a/cmd/postpolicyform.go +++ b/cmd/postpolicyform.go @@ -53,18 +53,6 @@ var startsWithConds = map[string]bool{ "$x-amz-date": false, } -var postPolicyIgnoreKeys = map[string]bool{ - "Policy": true, - xhttp.AmzSignature: true, - xhttp.ContentEncoding: true, - http.CanonicalHeaderKey(xhttp.AmzChecksumAlgo): true, - http.CanonicalHeaderKey(xhttp.AmzChecksumCRC32): true, - http.CanonicalHeaderKey(xhttp.AmzChecksumCRC32C): true, - http.CanonicalHeaderKey(xhttp.AmzChecksumSHA1): true, - http.CanonicalHeaderKey(xhttp.AmzChecksumSHA256): true, - http.CanonicalHeaderKey(xhttp.AmzChecksumMode): true, -} - // Add policy conditionals. const ( policyCondEqual = "eq" @@ -272,60 +260,57 @@ func checkPolicyCond(op string, input1, input2 string) bool { return false } +// S3 docs: "Each form field that you specify in a form (except x-amz-signature, file, policy, and field names +// that have an x-ignore- prefix) must appear in the list of conditions." +// https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-HTTPPOSTConstructPolicy.html +// keyInPolicyExceptions - list of keys that, when present in the form, can be missing in the conditions of the policy. +var keyInPolicyExceptions = map[string]bool{ + xhttp.AmzSignature: true, + "File": true, + "Policy": true, + + // MinIO specific exceptions to the general S3 rule above. + encrypt.SseKmsKeyID: true, + encrypt.SseEncryptionContext: true, + encrypt.SseCustomerAlgorithm: true, + encrypt.SseCustomerKey: true, + encrypt.SseCustomerKeyMD5: true, +} + // checkPostPolicy - apply policy conditions and validate input values. -// (http://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-HTTPPOSTConstructPolicy.html) +// Note that content-length-range is checked in the API handler function PostPolicyBucketHandler. +// formValues is the already-canonicalized form values from the POST request. func checkPostPolicy(formValues http.Header, postPolicyForm PostPolicyForm) error { // Check if policy document expiry date is still not reached if !postPolicyForm.Expiration.After(UTCNow()) { return fmt.Errorf("Invalid according to Policy: Policy expired") } - // check all formValues appear in postPolicyForm or return error. #https://github.com/minio/minio/issues/17391 - checkHeader := map[string][]string{} - ignoreKeys := map[string]bool{} - for key, value := range formValues { - switch { - case ignoreKeys[key], postPolicyIgnoreKeys[key], strings.HasPrefix(key, encrypt.SseGenericHeader): - continue - case strings.HasPrefix(key, "X-Amz-Ignore-"): - ignoreKey := strings.Replace(key, "X-Amz-Ignore-", "", 1) - ignoreKeys[ignoreKey] = true - // if it have already - delete(checkHeader, ignoreKey) - default: - checkHeader[key] = value - } - } - // map to store the metadata - metaMap := make(map[string]string) - for _, policy := range postPolicyForm.Conditions.Policies { - if strings.HasPrefix(policy.Key, "$x-amz-meta-") { - formCanonicalName := http.CanonicalHeaderKey(strings.TrimPrefix(policy.Key, "$")) - metaMap[formCanonicalName] = policy.Value - } - } - // Check if any extra metadata field is passed as input - for key := range formValues { - if strings.HasPrefix(key, "X-Amz-Meta-") { - if _, ok := metaMap[key]; !ok { - return fmt.Errorf("Invalid according to Policy: Extra input fields: %s", key) - } - } - } - // Flag to indicate if all policies conditions are satisfied - var condPassed bool + // mustFindInPolicy is a map to list all the keys that we must find in the policy as + // we process it below. At the end of checkPostPolicy function, if any key is left in + // this map, that's an error. + mustFindInPolicy := make(map[string][]string, len(formValues)) + for key, values := range formValues { + if keyInPolicyExceptions[key] || strings.HasPrefix(key, "X-Ignore-") { + continue + } + mustFindInPolicy[key] = values + } // Iterate over policy conditions and check them against received form fields for _, policy := range postPolicyForm.Conditions.Policies { // Form fields names are in canonical format, convert conditions names // to canonical for simplification purpose, so `$key` will become `Key` formCanonicalName := http.CanonicalHeaderKey(strings.TrimPrefix(policy.Key, "$")) + // Operator for the current policy condition op := policy.Operator - // Multiple values should not occur - if len(checkHeader[formCanonicalName]) >= 2 { - return fmt.Errorf("Invalid according to Policy: Policy Condition failed: [%s, %s, %s]. FormValues have multiple values: [%s]", op, policy.Key, policy.Value, strings.Join(checkHeader[formCanonicalName], ", ")) + + // Multiple values are not allowed for a single form field + if len(mustFindInPolicy[formCanonicalName]) >= 2 { + return fmt.Errorf("Invalid according to Policy: Policy Condition failed: [%s, %s, %s]. FormValues have multiple values: [%s]", op, policy.Key, policy.Value, strings.Join(mustFindInPolicy[formCanonicalName], ", ")) } + // If the current policy condition is known if startsWithSupported, condFound := startsWithConds[policy.Key]; condFound { // Check if the current condition supports starts-with operator @@ -333,35 +318,35 @@ func checkPostPolicy(formValues http.Header, postPolicyForm PostPolicyForm) erro return fmt.Errorf("Invalid according to Policy: Policy Condition failed") } // Check if current policy condition is satisfied - condPassed = checkPolicyCond(op, formValues.Get(formCanonicalName), policy.Value) - if !condPassed { + if !checkPolicyCond(op, formValues.Get(formCanonicalName), policy.Value) { return fmt.Errorf("Invalid according to Policy: Policy Condition failed") } } else if strings.HasPrefix(policy.Key, "$x-amz-meta-") || strings.HasPrefix(policy.Key, "$x-amz-") { // This covers all conditions X-Amz-Meta-* and X-Amz-* // Check if policy condition is satisfied - condPassed = checkPolicyCond(op, formValues.Get(formCanonicalName), policy.Value) - if !condPassed { + if !checkPolicyCond(op, formValues.Get(formCanonicalName), policy.Value) { return fmt.Errorf("Invalid according to Policy: Policy Condition failed: [%s, %s, %s]", op, policy.Key, policy.Value) } } - delete(checkHeader, formCanonicalName) + delete(mustFindInPolicy, formCanonicalName) } - // For SignV2 - Signature/AWSAccessKeyId field will be ignored. + + // For SignV2 - Signature/AWSAccessKeyId fields do not need to be in the policy if _, ok := formValues[xhttp.AmzSignatureV2]; ok { - delete(checkHeader, xhttp.AmzSignatureV2) - for k := range checkHeader { + delete(mustFindInPolicy, xhttp.AmzSignatureV2) + for k := range mustFindInPolicy { // case-insensitivity for AWSAccessKeyId if strings.EqualFold(k, xhttp.AmzAccessKeyID) { - delete(checkHeader, k) + delete(mustFindInPolicy, k) break } } } - if len(checkHeader) != 0 { - logKeys := make([]string, 0, len(checkHeader)) - for key := range checkHeader { + // Check mustFindInPolicy to see if any key is left, if so, it was not found in policy and we return an error. + if len(mustFindInPolicy) != 0 { + logKeys := make([]string, 0, len(mustFindInPolicy)) + for key := range mustFindInPolicy { logKeys = append(logKeys, key) } return fmt.Errorf("Each form field that you specify in a form must appear in the list of policy conditions. %q not specified in the policy.", strings.Join(logKeys, ", ")) diff --git a/cmd/postpolicyform_test.go b/cmd/postpolicyform_test.go index 8095c4d0f..0f86044b8 100644 --- a/cmd/postpolicyform_test.go +++ b/cmd/postpolicyform_test.go @@ -20,12 +20,12 @@ package cmd import ( "bytes" "encoding/base64" - "fmt" "net/http" "strings" "testing" minio "github.com/minio/minio-go/v7" + xhttp "github.com/minio/minio/internal/http" ) func TestParsePostPolicyForm(t *testing.T) { @@ -78,6 +78,28 @@ func TestParsePostPolicyForm(t *testing.T) { } } +type formValues struct { + http.Header +} + +func newFormValues() formValues { + return formValues{make(http.Header)} +} + +func (f formValues) Set(key, value string) formValues { + f.Header.Set(key, value) + return f +} + +func (f formValues) Add(key, value string) formValues { + f.Header.Add(key, value) + return f +} + +func (f formValues) Clone() formValues { + return formValues{f.Header.Clone()} +} + // Test Post Policy parsing and checking conditions func TestPostPolicyForm(t *testing.T) { pp := minio.NewPostPolicy() @@ -85,76 +107,193 @@ func TestPostPolicyForm(t *testing.T) { pp.SetContentType("image/jpeg") pp.SetUserMetadata("uuid", "14365123651274") pp.SetKeyStartsWith("user/user1/filename") - pp.SetContentLengthRange(1048579, 10485760) + pp.SetContentLengthRange(100, 999999) // not testable from this layer, condition is checked in the API handler. pp.SetSuccessStatusAction("201") + pp.SetCondition("eq", "X-Amz-Credential", "KVGKMDUQ23TCZXTLTHLP/20160727/us-east-1/s3/aws4_request") + pp.SetCondition("eq", "X-Amz-Algorithm", "AWS4-HMAC-SHA256") + pp.SetCondition("eq", xhttp.AmzDate, "20160727T000000Z") + + defaultFormVals := newFormValues() + defaultFormVals.Set("Bucket", "testbucket") + defaultFormVals.Set("Content-Type", "image/jpeg") + defaultFormVals.Set(xhttp.AmzMetaUUID, "14365123651274") + defaultFormVals.Set("Key", "user/user1/filename/${filename}/myfile.txt") + defaultFormVals.Set("X-Amz-Credential", "KVGKMDUQ23TCZXTLTHLP/20160727/us-east-1/s3/aws4_request") + defaultFormVals.Set("X-Amz-Algorithm", "AWS4-HMAC-SHA256") + defaultFormVals.Set(xhttp.AmzDate, "20160727T000000Z") + defaultFormVals.Set("Success_action_status", "201") + + policyCondFailedErr := "Invalid according to Policy: Policy Condition failed" type testCase struct { - Bucket string - Key string - XAmzDate string - XAmzAlgorithm string - XAmzCredential string - XAmzMetaUUID string - ContentType string - SuccessActionStatus string - Policy string - Expired bool - expectedErr error + name string + fv formValues + expired bool + wantErr string } + // Test case just contains fields we override from defaultFormVals. testCases := []testCase{ - // Everything is fine with this test - {Bucket: "testbucket", Key: "user/user1/filename/${filename}/myfile.txt", XAmzMetaUUID: "14365123651274", SuccessActionStatus: "201", XAmzCredential: "KVGKMDUQ23TCZXTLTHLP/20160727/us-east-1/s3/aws4_request", XAmzDate: "20160727T000000Z", XAmzAlgorithm: "AWS4-HMAC-SHA256", ContentType: "image/jpeg", expectedErr: nil}, - // Expired policy document - {Bucket: "testbucket", Key: "user/user1/filename/${filename}/myfile.txt", XAmzMetaUUID: "14365123651274", SuccessActionStatus: "201", XAmzCredential: "KVGKMDUQ23TCZXTLTHLP/20160727/us-east-1/s3/aws4_request", XAmzDate: "20160727T000000Z", XAmzAlgorithm: "AWS4-HMAC-SHA256", ContentType: "image/jpeg", Expired: true, expectedErr: fmt.Errorf("Invalid according to Policy: Policy expired")}, - // Different AMZ date - {Bucket: "testbucket", Key: "user/user1/filename/${filename}/myfile.txt", XAmzMetaUUID: "14365123651274", XAmzDate: "2017T000000Z", XAmzAlgorithm: "AWS4-HMAC-SHA256", ContentType: "image/jpeg", expectedErr: fmt.Errorf("Invalid according to Policy: Policy Condition failed")}, - // Key which doesn't start with user/user1/filename - {Bucket: "testbucket", Key: "myfile.txt", XAmzDate: "20160727T000000Z", XAmzMetaUUID: "14365123651274", XAmzAlgorithm: "AWS4-HMAC-SHA256", ContentType: "image/jpeg", expectedErr: fmt.Errorf("Invalid according to Policy: Policy Condition failed")}, - // Incorrect bucket name. - {Bucket: "incorrect", Key: "user/user1/filename/myfile.txt", XAmzMetaUUID: "14365123651274", XAmzDate: "20160727T000000Z", XAmzAlgorithm: "AWS4-HMAC-SHA256", ContentType: "image/jpeg", expectedErr: fmt.Errorf("Invalid according to Policy: Policy Condition failed")}, - // Incorrect key name - {Bucket: "testbucket", Key: "incorrect", XAmzDate: "20160727T000000Z", XAmzMetaUUID: "14365123651274", XAmzAlgorithm: "AWS4-HMAC-SHA256", ContentType: "image/jpeg", expectedErr: fmt.Errorf("Invalid according to Policy: Policy Condition failed")}, - // Incorrect date - {Bucket: "testbucket", Key: "user/user1/filename/${filename}/myfile.txt", XAmzMetaUUID: "14365123651274", XAmzDate: "incorrect", XAmzAlgorithm: "AWS4-HMAC-SHA256", ContentType: "image/jpeg", expectedErr: fmt.Errorf("Invalid according to Policy: Policy Condition failed")}, - // Incorrect ContentType - {Bucket: "testbucket", Key: "user/user1/filename/${filename}/myfile.txt", XAmzMetaUUID: "14365123651274", XAmzDate: "20160727T000000Z", XAmzAlgorithm: "AWS4-HMAC-SHA256", ContentType: "incorrect", expectedErr: fmt.Errorf("Invalid according to Policy: Policy Condition failed")}, - // Incorrect Metadata - {Bucket: "testbucket", Key: "user/user1/filename/${filename}/myfile.txt", XAmzMetaUUID: "151274", SuccessActionStatus: "201", XAmzCredential: "KVGKMDUQ23TCZXTLTHLP/20160727/us-east-1/s3/aws4_request", XAmzDate: "20160727T000000Z", XAmzAlgorithm: "AWS4-HMAC-SHA256", ContentType: "image/jpeg", expectedErr: fmt.Errorf("Invalid according to Policy: Policy Condition failed: [eq, $x-amz-meta-uuid, 14365123651274]")}, + { + name: "happy path no errors", + fv: defaultFormVals.Clone(), + wantErr: "", + }, + { + name: "expired policy document", + fv: defaultFormVals.Clone(), + expired: true, + wantErr: "Invalid according to Policy: Policy expired", + }, + { + name: "different AMZ date", + fv: defaultFormVals.Clone().Set(xhttp.AmzDate, "2017T000000Z"), + wantErr: policyCondFailedErr, + }, + { + name: "incorrect date", + fv: defaultFormVals.Clone().Set(xhttp.AmzDate, "incorrect"), + wantErr: policyCondFailedErr, + }, + { + name: "key which doesn't start with user/user1/filename", + fv: defaultFormVals.Clone().Set("Key", "myfile.txt"), + wantErr: policyCondFailedErr, + }, + { + name: "incorrect key name", + fv: defaultFormVals.Clone().Set("Key", "incorrect"), + wantErr: policyCondFailedErr, + }, + { + name: "incorrect bucket name", + fv: defaultFormVals.Clone().Set("Bucket", "incorrect"), + wantErr: policyCondFailedErr, + }, + { + name: "incorrect ContentType", + fv: defaultFormVals.Clone().Set(xhttp.ContentType, "incorrect"), + wantErr: policyCondFailedErr, + }, + { + name: "incorrect X-Amz-Algorithm", + fv: defaultFormVals.Clone().Set(xhttp.AmzAlgorithm, "incorrect"), + wantErr: policyCondFailedErr, + }, + { + name: "incorrect X-Amz-Credential", + fv: defaultFormVals.Clone().Set(xhttp.AmzCredential, "incorrect"), + wantErr: policyCondFailedErr, + }, + { + name: "incorrect metadata uuid", + fv: defaultFormVals.Clone().Set(xhttp.AmzMetaUUID, "151274"), + wantErr: "Invalid according to Policy: Policy Condition failed: [eq, $x-amz-meta-uuid, 14365123651274]", + }, + { + name: "unknown key XAmzMetaName is error as it does not appear in policy", + fv: defaultFormVals.Clone().Set(xhttp.AmzMetaName, "my-name"), + wantErr: `Each form field that you specify in a form must appear in the list of policy conditions. "X-Amz-Meta-Name" not specified in the policy.`, + }, + { + name: "unknown key XAmzChecksumAlgo is error as it does not appear in policy", + fv: defaultFormVals.Clone().Set(http.CanonicalHeaderKey(xhttp.AmzChecksumAlgo), "algo-val"), + wantErr: `Each form field that you specify in a form must appear in the list of policy conditions. "X-Amz-Checksum-Algorithm" not specified in the policy.`, + }, + { + name: "unknown key XAmzChecksumCRC32 is error as it does not appear in policy", + fv: defaultFormVals.Clone().Set(http.CanonicalHeaderKey(xhttp.AmzChecksumCRC32), "crc32-val"), + wantErr: `Each form field that you specify in a form must appear in the list of policy conditions. "X-Amz-Checksum-Crc32" not specified in the policy.`, + }, + { + name: "unknown key XAmzChecksumCRC32C is error as it does not appear in policy", + fv: defaultFormVals.Clone().Set(http.CanonicalHeaderKey(xhttp.AmzChecksumCRC32C), "crc32c-val"), + wantErr: `Each form field that you specify in a form must appear in the list of policy conditions. "X-Amz-Checksum-Crc32c" not specified in the policy.`, + }, + { + name: "unknown key XAmzChecksumSHA1 is error as it does not appear in policy", + fv: defaultFormVals.Clone().Set(http.CanonicalHeaderKey(xhttp.AmzChecksumSHA1), "sha1-val"), + wantErr: `Each form field that you specify in a form must appear in the list of policy conditions. "X-Amz-Checksum-Sha1" not specified in the policy.`, + }, + { + name: "unknown key XAmzChecksumSHA256 is error as it does not appear in policy", + fv: defaultFormVals.Clone().Set(http.CanonicalHeaderKey(xhttp.AmzChecksumSHA256), "sha256-val"), + wantErr: `Each form field that you specify in a form must appear in the list of policy conditions. "X-Amz-Checksum-Sha256" not specified in the policy.`, + }, + { + name: "unknown key XAmzChecksumMode is error as it does not appear in policy", + fv: defaultFormVals.Clone().Set(http.CanonicalHeaderKey(xhttp.AmzChecksumMode), "mode-val"), + wantErr: `Each form field that you specify in a form must appear in the list of policy conditions. "X-Amz-Checksum-Mode" not specified in the policy.`, + }, + { + name: "unknown key Content-Encoding is error as it does not appear in policy", + fv: defaultFormVals.Clone().Set(http.CanonicalHeaderKey(xhttp.ContentEncoding), "encoding-val"), + wantErr: `Each form field that you specify in a form must appear in the list of policy conditions. "Content-Encoding" not specified in the policy.`, + }, + { + name: "many bucket values", + fv: defaultFormVals.Clone().Add("Bucket", "anotherbucket"), + wantErr: "Invalid according to Policy: Policy Condition failed: [eq, $bucket, testbucket]. FormValues have multiple values: [testbucket, anotherbucket]", + }, + { + name: "XAmzSignature does not have to appear in policy", + fv: defaultFormVals.Clone().Set(xhttp.AmzSignature, "my-signature"), + }, + { + name: "XIgnoreFoo does not have to appear in policy", + fv: defaultFormVals.Clone().Set("X-Ignore-Foo", "my-foo-value"), + }, + { + name: "File does not have to appear in policy", + fv: defaultFormVals.Clone().Set("File", "file-value"), + }, + { + name: "Signature does not have to appear in policy", + fv: defaultFormVals.Clone().Set(xhttp.AmzSignatureV2, "signature-value"), + }, + { + name: "AWSAccessKeyID does not have to appear in policy", + fv: defaultFormVals.Clone().Set(xhttp.AmzAccessKeyID, "access").Set(xhttp.AmzSignatureV2, "signature-value"), + }, + { + name: "any form value starting with X-Amz-Server-Side-Encryption- does not have to appear in policy", + fv: defaultFormVals.Clone(). + Set(xhttp.AmzServerSideEncryptionKmsContext, "context-val"). + Set(xhttp.AmzServerSideEncryptionCustomerAlgorithm, "algo-val"), + }, } - // Validate all the test cases. - for i, tt := range testCases { - formValues := make(http.Header) - formValues.Set("Bucket", tt.Bucket) - formValues.Set("Key", tt.Key) - formValues.Set("Content-Type", tt.ContentType) - formValues.Set("X-Amz-Date", tt.XAmzDate) - formValues.Set("X-Amz-Meta-Uuid", tt.XAmzMetaUUID) - formValues.Set("X-Amz-Algorithm", tt.XAmzAlgorithm) - formValues.Set("X-Amz-Credential", tt.XAmzCredential) - if tt.Expired { - // Expired already. - pp.SetExpires(UTCNow().AddDate(0, 0, -10)) - } else { - // Expires in 10 days. - pp.SetExpires(UTCNow().AddDate(0, 0, 10)) - } - formValues.Set("Policy", base64.StdEncoding.EncodeToString([]byte(pp.String()))) - formValues.Set("Success_action_status", tt.SuccessActionStatus) - policyBytes, err := base64.StdEncoding.DecodeString(base64.StdEncoding.EncodeToString([]byte(pp.String()))) - if err != nil { - t.Fatal(err) - } + // Run tests + for _, tt := range testCases { + t.Run(tt.name, func(t *testing.T) { + if tt.expired { + // Expired already. + pp.SetExpires(UTCNow().AddDate(0, 0, -10)) + } else { + // Expires in 10 days. + pp.SetExpires(UTCNow().AddDate(0, 0, 10)) + } - postPolicyForm, err := parsePostPolicyForm(bytes.NewReader(policyBytes)) - if err != nil { - t.Fatal(err) - } + tt.fv.Set("Policy", base64.StdEncoding.EncodeToString([]byte(pp.String()))) - err = checkPostPolicy(formValues, postPolicyForm) - if err != nil && tt.expectedErr != nil && err.Error() != tt.expectedErr.Error() { - t.Fatalf("Test %d:, Expected %s, got %s", i+1, tt.expectedErr.Error(), err.Error()) - } + policyBytes, err := base64.StdEncoding.DecodeString(base64.StdEncoding.EncodeToString([]byte(pp.String()))) + if err != nil { + t.Fatal(err) + } + + postPolicyForm, err := parsePostPolicyForm(bytes.NewReader(policyBytes)) + if err != nil { + t.Fatal(err) + } + + errStr := "" + err = checkPostPolicy(tt.fv.Header, postPolicyForm) + if err != nil { + errStr = err.Error() + } + if errStr != tt.wantErr { + t.Errorf("test: '%s', want error: '%s', got error: '%s'", tt.name, tt.wantErr, errStr) + } + }) } } diff --git a/internal/http/headers.go b/internal/http/headers.go index d8968910f..edfca9d9b 100644 --- a/internal/http/headers.go +++ b/internal/http/headers.go @@ -177,6 +177,10 @@ const ( AmzChecksumSHA256 = "x-amz-checksum-sha256" AmzChecksumMode = "x-amz-checksum-mode" + // Post Policy related + AmzMetaUUID = "X-Amz-Meta-Uuid" + AmzMetaName = "X-Amz-Meta-Name" + // Delete special flag to force delete a bucket or a prefix MinIOForceDelete = "x-minio-force-delete"