From b408d0e87dcaaabf28d9730954b6e0f307e18018 Mon Sep 17 00:00:00 2001 From: Krishnan Parthasarathi Date: Mon, 30 Jan 2017 09:15:11 +0530 Subject: [PATCH] Add aws:Referer condition key support. (#3641) This change implements bucket policy enhancements required to restrict access based on HTTP referer. See https://docs.aws.amazon.com/AmazonS3/latest/dev/example-bucket-policies.html#example-bucket-policies-use-case-4 for more information. Fixes #3540 --- cmd/auth-handler.go | 3 +- cmd/bucket-handlers.go | 15 ++-- cmd/bucket-policy-handlers.go | 46 +++++++++--- cmd/bucket-policy-handlers_test.go | 28 ++++++++ cmd/bucket-policy-parser.go | 4 +- cmd/bucket-policy-parser_test.go | 111 ++++++++++++++++++++++++----- cmd/object-handlers.go | 13 ++-- docs/bucket/policy/README.md | 3 + 8 files changed, 181 insertions(+), 42 deletions(-) diff --git a/cmd/auth-handler.go b/cmd/auth-handler.go index a50219d59..a48989f12 100644 --- a/cmd/auth-handler.go +++ b/cmd/auth-handler.go @@ -125,7 +125,8 @@ func checkRequestAuthType(r *http.Request, bucket, policyAction, region string) if reqAuthType == authTypeAnonymous && policyAction != "" { // http://docs.aws.amazon.com/AmazonS3/latest/dev/using-with-s3-actions.html - return enforceBucketPolicy(bucket, policyAction, r.URL) + return enforceBucketPolicy(bucket, policyAction, r.URL.Path, + r.Referer(), r.URL.Query()) } // By default return ErrAccessDenied diff --git a/cmd/bucket-handlers.go b/cmd/bucket-handlers.go index 95227f4af..db942d755 100644 --- a/cmd/bucket-handlers.go +++ b/cmd/bucket-handlers.go @@ -33,7 +33,7 @@ import ( // http://docs.aws.amazon.com/AmazonS3/latest/dev/using-with-s3-actions.html // Enforces bucket policies for a bucket for a given tatusaction. -func enforceBucketPolicy(bucket string, action string, reqURL *url.URL) (s3Error APIErrorCode) { +func enforceBucketPolicy(bucket, action, resource, referer string, queryParams url.Values) (s3Error APIErrorCode) { // Verify if bucket actually exists if err := checkBucketExist(bucket, newObjectLayerFn()); err != nil { err = errorCause(err) @@ -57,16 +57,21 @@ func enforceBucketPolicy(bucket string, action string, reqURL *url.URL) (s3Error } // Construct resource in 'arn:aws:s3:::examplebucket/object' format. - resource := bucketARNPrefix + strings.TrimSuffix(strings.TrimPrefix(reqURL.Path, "/"), "/") + arn := bucketARNPrefix + strings.TrimSuffix(strings.TrimPrefix(resource, "/"), "/") // Get conditions for policy verification. conditionKeyMap := make(map[string]set.StringSet) - for queryParam := range reqURL.Query() { - conditionKeyMap[queryParam] = set.CreateStringSet(reqURL.Query().Get(queryParam)) + for queryParam := range queryParams { + conditionKeyMap[queryParam] = set.CreateStringSet(queryParams.Get(queryParam)) + } + + // Add request referer to conditionKeyMap if present. + if referer != "" { + conditionKeyMap["referer"] = set.CreateStringSet(referer) } // Validate action, resource and conditions with current policy statements. - if !bucketPolicyEvalStatements(action, resource, conditionKeyMap, policy.Statements) { + if !bucketPolicyEvalStatements(action, arn, conditionKeyMap, policy.Statements) { return ErrAccessDenied } return ErrNone diff --git a/cmd/bucket-policy-handlers.go b/cmd/bucket-policy-handlers.go index 7a525dd95..c856e38e8 100644 --- a/cmd/bucket-policy-handlers.go +++ b/cmd/bucket-policy-handlers.go @@ -71,6 +71,10 @@ func actionMatch(pattern, action string) bool { return wildcard.MatchSimple(pattern, action) } +func refererMatch(pattern, referer string) bool { + return wildcard.MatchSimple(pattern, referer) +} + // Verify if given resource matches with policy statement. func bucketPolicyResourceMatch(resource string, statement policyStatement) bool { // the resource rule for object could contain "*" wild card. @@ -85,33 +89,55 @@ func bucketPolicyConditionMatch(conditions map[string]set.StringSet, statement p // Supports following conditions. // - StringEquals // - StringNotEquals + // - StringLike + // - StringNotLike // // Supported applicable condition keys for each conditions. // - s3:prefix // - s3:max-keys - var conditionMatches = true + // - s3:aws-Referer + for condition, conditionKeyVal := range statement.Conditions { if condition == "StringEquals" { if !conditionKeyVal["s3:prefix"].Equals(conditions["prefix"]) { - conditionMatches = false - break + return false } if !conditionKeyVal["s3:max-keys"].Equals(conditions["max-keys"]) { - conditionMatches = false - break + return false } } else if condition == "StringNotEquals" { if !conditionKeyVal["s3:prefix"].Equals(conditions["prefix"]) { - conditionMatches = false - break + return false } if !conditionKeyVal["s3:max-keys"].Equals(conditions["max-keys"]) { - conditionMatches = false - break + return false + } + } else if condition == "StringLike" { + awsReferers := conditionKeyVal["aws:Referer"] + // wildcard match of referer in statement was not empty. + // StringLike has a match, i.e, condition evaluates to true. + for referer := range conditions["referer"] { + if !awsReferers.FuncMatch(refererMatch, referer).IsEmpty() { + return true + } + } + // No matching referer found, so the condition is false. + return false + } else if condition == "StringNotLike" { + awsReferers := conditionKeyVal["aws:Referer"] + // wildcard match of referer in statement was not empty. + // StringNotLike has a match, i.e, condition evaluates to false. + for referer := range conditions["referer"] { + if !awsReferers.FuncMatch(refererMatch, referer).IsEmpty() { + return false + } } + // No matching referer found, so the condition is true. + return true } } - return conditionMatches + // No conditions were present in the statement, so trivially true (always). + return true } // PutBucketPolicyHandler - PUT Bucket policy diff --git a/cmd/bucket-policy-handlers_test.go b/cmd/bucket-policy-handlers_test.go index cfc690ef7..999dc5530 100644 --- a/cmd/bucket-policy-handlers_test.go +++ b/cmd/bucket-policy-handlers_test.go @@ -942,6 +942,34 @@ func TestBucketPolicyConditionMatch(t *testing.T) { expectedMatch: false, }, + // Test case - 9. + // StringLike condition matches. + { + statementCondition: getStatementWithCondition("StringLike", "aws:Referer", "http://www.example.com/"), + condition: getInnerMap("referer", "http://www.example.com/"), + expectedMatch: true, + }, + // Test case - 10. + // StringLike condition doesn't match. + { + statementCondition: getStatementWithCondition("StringLike", "aws:Referer", "http://www.example.com/"), + condition: getInnerMap("referer", "www.somethingelse.com"), + expectedMatch: false, + }, + // Test case - 11. + // StringNotLike condition evaluates to false. + { + statementCondition: getStatementWithCondition("StringNotLike", "aws:Referer", "http://www.example.com/"), + condition: getInnerMap("referer", "http://www.example.com/"), + expectedMatch: false, + }, + // Test case - 12. + // StringNotLike condition evaluates to true. + { + statementCondition: getStatementWithCondition("StringNotLike", "aws:Referer", "http://www.example.com/"), + condition: getInnerMap("referer", "http://somethingelse.com/"), + expectedMatch: true, + }, } for i, tc := range testCases { diff --git a/cmd/bucket-policy-parser.go b/cmd/bucket-policy-parser.go index 1264a610c..f9324846c 100644 --- a/cmd/bucket-policy-parser.go +++ b/cmd/bucket-policy-parser.go @@ -35,11 +35,11 @@ var supportedActionMap = set.CreateStringSet("*", "s3:*", "s3:GetObject", "s3:AbortMultipartUpload", "s3:ListBucketMultipartUploads", "s3:ListMultipartUploadParts") // supported Conditions type. -var supportedConditionsType = set.CreateStringSet("StringEquals", "StringNotEquals") +var supportedConditionsType = set.CreateStringSet("StringEquals", "StringNotEquals", "StringLike", "StringNotLike") // Validate s3:prefix, s3:max-keys are present if not // supported keys for the conditions. -var supportedConditionsKey = set.CreateStringSet("s3:prefix", "s3:max-keys") +var supportedConditionsKey = set.CreateStringSet("s3:prefix", "s3:max-keys", "aws:Referer") // supportedEffectMap - supported effects. var supportedEffectMap = set.CreateStringSet("Allow", "Deny") diff --git a/cmd/bucket-policy-parser_test.go b/cmd/bucket-policy-parser_test.go index 522dc2c63..a9418db1f 100644 --- a/cmd/bucket-policy-parser_test.go +++ b/cmd/bucket-policy-parser_test.go @@ -22,6 +22,7 @@ import ( "fmt" "testing" + "github.com/minio/minio-go/pkg/policy" "github.com/minio/minio-go/pkg/set" ) @@ -342,6 +343,18 @@ func TestIsValidPrincipals(t *testing.T) { } } +// getEmptyConditionKeyMap - returns a function that generates a +// condition key map for a given key. +func getEmptyConditionKeyMap(conditionKey string) func() map[string]map[string]set.StringSet { + emptyConditonGenerator := func() map[string]map[string]set.StringSet { + emptyMap := make(map[string]set.StringSet) + conditions := make(map[string]map[string]set.StringSet) + conditions[conditionKey] = emptyMap + return conditions + } + return emptyConditonGenerator +} + // Tests validate policyStatement condition validator. func TestIsValidConditions(t *testing.T) { // returns empty conditions map. @@ -350,22 +363,17 @@ func TestIsValidConditions(t *testing.T) { } // returns map with the "StringEquals" set to empty map. - setEmptyStringEquals := func() map[string]map[string]set.StringSet { - emptyMap := make(map[string]set.StringSet) - conditions := make(map[string]map[string]set.StringSet) - conditions["StringEquals"] = emptyMap - return conditions - - } + setEmptyStringEquals := getEmptyConditionKeyMap("StringEquals") // returns map with the "StringNotEquals" set to empty map. - setEmptyStringNotEquals := func() map[string]map[string]set.StringSet { - emptyMap := make(map[string]set.StringSet) - conditions := make(map[string]map[string]set.StringSet) - conditions["StringNotEquals"] = emptyMap - return conditions + setEmptyStringNotEquals := getEmptyConditionKeyMap("StringNotEquals") + + // returns map with the "StringLike" set to empty map. + setEmptyStringLike := getEmptyConditionKeyMap("StringLike") + + // returns map with the "StringNotLike" set to empty map. + setEmptyStringNotLike := getEmptyConditionKeyMap("StringNotLike") - } // Generate conditions. generateConditions := func(key1, key2, value string) map[string]map[string]set.StringSet { innerMap := make(map[string]set.StringSet) @@ -377,11 +385,11 @@ func TestIsValidConditions(t *testing.T) { // generate ambigious conditions. generateAmbigiousConditions := func() map[string]map[string]set.StringSet { - innerMap := make(map[string]set.StringSet) - innerMap["s3:prefix"] = set.CreateStringSet("Asia/") + prefixMap := make(map[string]set.StringSet) + prefixMap["s3:prefix"] = set.CreateStringSet("Asia/") conditions := make(map[string]map[string]set.StringSet) - conditions["StringEquals"] = innerMap - conditions["StringNotEquals"] = innerMap + conditions["StringEquals"] = prefixMap + conditions["StringNotEquals"] = prefixMap return conditions } @@ -417,6 +425,8 @@ func TestIsValidConditions(t *testing.T) { setEmptyConditions(), setEmptyStringEquals(), setEmptyStringNotEquals(), + setEmptyStringLike(), + setEmptyStringNotLike(), generateConditions("StringEquals", "s3:prefix", "Asia/"), generateConditions("StringEquals", "s3:max-keys", "100"), generateConditions("StringNotEquals", "s3:prefix", "Asia/"), @@ -464,7 +474,11 @@ func TestIsValidConditions(t *testing.T) { {testConditions[9], nil, true}, // Test case - 11. {testConditions[10], nil, true}, - // Test case 10. + // Test case - 12. + {testConditions[11], nil, true}, + // Test case - 13. + {testConditions[11], nil, true}, + // Test case - 14. {testConditions[11], nil, true}, } for i, testCase := range testCases { @@ -709,3 +723,64 @@ func TestParseBucketPolicy(t *testing.T) { } } } + +func TestAWSRefererCondition(t *testing.T) { + resource := set.CreateStringSet([]string{ + fmt.Sprintf("%s%s", bucketARNPrefix, "minio-bucket"+"/"+"Asia"+"*"), + }...) + + conditionsKeyMap := make(policy.ConditionKeyMap) + conditionsKeyMap.Add("aws:Referer", + set.CreateStringSet("www.example.com", + "http://www.example.com")) + + requestConditionKeyMap := make(map[string]set.StringSet) + requestConditionKeyMap["referer"] = set.CreateStringSet("www.example.com") + + testCases := []struct { + effect string + conditionKey string + match bool + }{ + { + effect: "Allow", + conditionKey: "StringLike", + match: true, + }, + { + effect: "Allow", + conditionKey: "StringNotLike", + match: false, + }, + { + effect: "Deny", + conditionKey: "StringLike", + match: true, + }, + { + effect: "Deny", + conditionKey: "StringNotLike", + match: false, + }, + } + + for i, test := range testCases { + conditions := make(map[string]map[string]set.StringSet) + conditions[test.conditionKey] = conditionsKeyMap + + allowStatement := policyStatement{ + Sid: "Testing AWS referer condition", + Effect: test.effect, + Principal: map[string]interface{}{ + "AWS": "*", + }, + Resources: resource, + Conditions: conditions, + } + + if result := bucketPolicyConditionMatch(requestConditionKeyMap, allowStatement); result != test.match { + t.Errorf("Test %d - Expected conditons to evaluate to %v but got %v", + i+1, test.match, result) + } + } +} diff --git a/cmd/object-handlers.go b/cmd/object-handlers.go index 73b10b32e..d7c2bec79 100644 --- a/cmd/object-handlers.go +++ b/cmd/object-handlers.go @@ -56,10 +56,9 @@ func setGetRespHeaders(w http.ResponseWriter, reqParams url.Values) { func errAllowableObjectNotFound(bucket string, r *http.Request) APIErrorCode { if getRequestAuthType(r) == authTypeAnonymous { //we care about the bucket as a whole, not a particular resource - url := *r.URL - url.Path = "/" + bucket - - if s3Error := enforceBucketPolicy(bucket, "s3:ListBucket", &url); s3Error != ErrNone { + resource := "/" + bucket + if s3Error := enforceBucketPolicy(bucket, "s3:ListBucket", resource, + r.Referer(), r.URL.Query()); s3Error != ErrNone { return ErrAccessDenied } } @@ -440,7 +439,8 @@ func (api objectAPIHandlers) PutObjectHandler(w http.ResponseWriter, r *http.Req return case authTypeAnonymous: // http://docs.aws.amazon.com/AmazonS3/latest/dev/using-with-s3-actions.html - if s3Error := enforceBucketPolicy(bucket, "s3:PutObject", r.URL); s3Error != ErrNone { + if s3Error := enforceBucketPolicy(bucket, "s3:PutObject", r.URL.Path, + r.Referer(), r.URL.Query()); s3Error != ErrNone { writeErrorResponse(w, s3Error, r.URL) return } @@ -600,7 +600,8 @@ func (api objectAPIHandlers) PutObjectPartHandler(w http.ResponseWriter, r *http return case authTypeAnonymous: // http://docs.aws.amazon.com/AmazonS3/latest/dev/mpuAndPermissions.html - if s3Error := enforceBucketPolicy(bucket, "s3:PutObject", r.URL); s3Error != ErrNone { + if s3Error := enforceBucketPolicy(bucket, "s3:PutObject", r.URL.Path, + r.Referer(), r.URL.Query()); s3Error != ErrNone { writeErrorResponse(w, s3Error, r.URL) return } diff --git a/docs/bucket/policy/README.md b/docs/bucket/policy/README.md index b642de294..a738ff2c9 100644 --- a/docs/bucket/policy/README.md +++ b/docs/bucket/policy/README.md @@ -22,11 +22,14 @@ This package implements parsing and validating bucket access policies based on A StringEquals StringNotEquals + StringLike + StringNotLike Supported applicable condition keys for each conditions. s3:prefix s3:max-keys + aws:Referer ### Nested policy support.