diff --git a/cmd/policy.go b/cmd/policy.go index e5fbe4d1e..dfe35a2dd 100644 --- a/cmd/policy.go +++ b/cmd/policy.go @@ -20,6 +20,7 @@ import ( "bytes" "context" "encoding/json" + "fmt" "net/http" "path" "strings" @@ -183,7 +184,12 @@ func NewPolicySys() *PolicySys { } func getConditionValues(request *http.Request, locationConstraint string) map[string][]string { - args := make(map[string][]string) + args := map[string][]string{ + "SourceIp": {handlers.GetSourceIP(request)}, + "SecureTransport": {fmt.Sprintf("%t", request.TLS != nil)}, + "UserAgent": {request.UserAgent()}, + "Referer": {request.Referer()}, + } for key, values := range request.Header { if existingValues, found := args[key]; found { @@ -201,8 +207,6 @@ func getConditionValues(request *http.Request, locationConstraint string) map[st } } - args["SourceIp"] = []string{handlers.GetSourceIP(request)} - if locationConstraint != "" { args["LocationConstraint"] = []string{locationConstraint} } diff --git a/pkg/iam/policy/action.go b/pkg/iam/policy/action.go index f561ebf29..1dd30fb9c 100644 --- a/pkg/iam/policy/action.go +++ b/pkg/iam/policy/action.go @@ -174,39 +174,55 @@ func parseAction(s string) (Action, error) { // actionConditionKeyMap - holds mapping of supported condition key for an action. var actionConditionKeyMap = map[Action]condition.KeySet{ + AllActions: condition.NewKeySet(condition.AllSupportedKeys...), + AbortMultipartUploadAction: condition.NewKeySet( condition.AWSReferer, condition.AWSSourceIP, + condition.AWSUserAgent, + condition.AWSSecureTransport, ), CreateBucketAction: condition.NewKeySet( condition.AWSReferer, condition.AWSSourceIP, + condition.AWSUserAgent, + condition.AWSSecureTransport, ), DeleteBucketPolicyAction: condition.NewKeySet( condition.AWSReferer, condition.AWSSourceIP, + condition.AWSUserAgent, + condition.AWSSecureTransport, ), DeleteObjectAction: condition.NewKeySet( condition.AWSReferer, condition.AWSSourceIP, + condition.AWSUserAgent, + condition.AWSSecureTransport, ), GetBucketLocationAction: condition.NewKeySet( condition.AWSReferer, condition.AWSSourceIP, + condition.AWSUserAgent, + condition.AWSSecureTransport, ), GetBucketNotificationAction: condition.NewKeySet( condition.AWSReferer, condition.AWSSourceIP, + condition.AWSUserAgent, + condition.AWSSecureTransport, ), GetBucketPolicyAction: condition.NewKeySet( condition.AWSReferer, condition.AWSSourceIP, + condition.AWSUserAgent, + condition.AWSSecureTransport, ), GetObjectAction: condition.NewKeySet( @@ -215,16 +231,22 @@ var actionConditionKeyMap = map[Action]condition.KeySet{ condition.S3XAmzStorageClass, condition.AWSReferer, condition.AWSSourceIP, + condition.AWSUserAgent, + condition.AWSSecureTransport, ), HeadBucketAction: condition.NewKeySet( condition.AWSReferer, condition.AWSSourceIP, + condition.AWSUserAgent, + condition.AWSSecureTransport, ), ListAllMyBucketsAction: condition.NewKeySet( condition.AWSReferer, condition.AWSSourceIP, + condition.AWSUserAgent, + condition.AWSSecureTransport, ), ListBucketAction: condition.NewKeySet( @@ -233,31 +255,43 @@ var actionConditionKeyMap = map[Action]condition.KeySet{ condition.S3MaxKeys, condition.AWSReferer, condition.AWSSourceIP, + condition.AWSUserAgent, + condition.AWSSecureTransport, ), ListBucketMultipartUploadsAction: condition.NewKeySet( condition.AWSReferer, condition.AWSSourceIP, + condition.AWSUserAgent, + condition.AWSSecureTransport, ), ListenBucketNotificationAction: condition.NewKeySet( condition.AWSReferer, condition.AWSSourceIP, + condition.AWSUserAgent, + condition.AWSSecureTransport, ), ListMultipartUploadPartsAction: condition.NewKeySet( condition.AWSReferer, condition.AWSSourceIP, + condition.AWSUserAgent, + condition.AWSSecureTransport, ), PutBucketNotificationAction: condition.NewKeySet( condition.AWSReferer, condition.AWSSourceIP, + condition.AWSUserAgent, + condition.AWSSecureTransport, ), PutBucketPolicyAction: condition.NewKeySet( condition.AWSReferer, condition.AWSSourceIP, + condition.AWSUserAgent, + condition.AWSSecureTransport, ), PutObjectAction: condition.NewKeySet( @@ -268,5 +302,7 @@ var actionConditionKeyMap = map[Action]condition.KeySet{ condition.S3XAmzStorageClass, condition.AWSReferer, condition.AWSSourceIP, + condition.AWSUserAgent, + condition.AWSSecureTransport, ), } diff --git a/pkg/policy/action.go b/pkg/policy/action.go index 40743fe57..c95010224 100644 --- a/pkg/policy/action.go +++ b/pkg/policy/action.go @@ -161,36 +161,50 @@ var actionConditionKeyMap = map[Action]condition.KeySet{ AbortMultipartUploadAction: condition.NewKeySet( condition.AWSReferer, condition.AWSSourceIP, + condition.AWSUserAgent, + condition.AWSSecureTransport, ), CreateBucketAction: condition.NewKeySet( condition.AWSReferer, condition.AWSSourceIP, + condition.AWSUserAgent, + condition.AWSSecureTransport, ), DeleteBucketPolicyAction: condition.NewKeySet( condition.AWSReferer, condition.AWSSourceIP, + condition.AWSUserAgent, + condition.AWSSecureTransport, ), DeleteObjectAction: condition.NewKeySet( condition.AWSReferer, condition.AWSSourceIP, + condition.AWSUserAgent, + condition.AWSSecureTransport, ), GetBucketLocationAction: condition.NewKeySet( condition.AWSReferer, condition.AWSSourceIP, + condition.AWSUserAgent, + condition.AWSSecureTransport, ), GetBucketNotificationAction: condition.NewKeySet( condition.AWSReferer, condition.AWSSourceIP, + condition.AWSUserAgent, + condition.AWSSecureTransport, ), GetBucketPolicyAction: condition.NewKeySet( condition.AWSReferer, condition.AWSSourceIP, + condition.AWSUserAgent, + condition.AWSSecureTransport, ), GetObjectAction: condition.NewKeySet( @@ -199,16 +213,22 @@ var actionConditionKeyMap = map[Action]condition.KeySet{ condition.S3XAmzStorageClass, condition.AWSReferer, condition.AWSSourceIP, + condition.AWSUserAgent, + condition.AWSSecureTransport, ), HeadBucketAction: condition.NewKeySet( condition.AWSReferer, condition.AWSSourceIP, + condition.AWSUserAgent, + condition.AWSSecureTransport, ), ListAllMyBucketsAction: condition.NewKeySet( condition.AWSReferer, condition.AWSSourceIP, + condition.AWSUserAgent, + condition.AWSSecureTransport, ), ListBucketAction: condition.NewKeySet( @@ -217,31 +237,43 @@ var actionConditionKeyMap = map[Action]condition.KeySet{ condition.S3MaxKeys, condition.AWSReferer, condition.AWSSourceIP, + condition.AWSUserAgent, + condition.AWSSecureTransport, ), ListBucketMultipartUploadsAction: condition.NewKeySet( condition.AWSReferer, condition.AWSSourceIP, + condition.AWSUserAgent, + condition.AWSSecureTransport, ), ListenBucketNotificationAction: condition.NewKeySet( condition.AWSReferer, condition.AWSSourceIP, + condition.AWSUserAgent, + condition.AWSSecureTransport, ), ListMultipartUploadPartsAction: condition.NewKeySet( condition.AWSReferer, condition.AWSSourceIP, + condition.AWSUserAgent, + condition.AWSSecureTransport, ), PutBucketNotificationAction: condition.NewKeySet( condition.AWSReferer, condition.AWSSourceIP, + condition.AWSUserAgent, + condition.AWSSecureTransport, ), PutBucketPolicyAction: condition.NewKeySet( condition.AWSReferer, condition.AWSSourceIP, + condition.AWSUserAgent, + condition.AWSSecureTransport, ), PutObjectAction: condition.NewKeySet( @@ -252,5 +284,7 @@ var actionConditionKeyMap = map[Action]condition.KeySet{ condition.S3XAmzStorageClass, condition.AWSReferer, condition.AWSSourceIP, + condition.AWSUserAgent, + condition.AWSSecureTransport, ), } diff --git a/pkg/policy/condition/binaryequalsfunc.go b/pkg/policy/condition/binaryequalsfunc.go new file mode 100644 index 000000000..89239ae13 --- /dev/null +++ b/pkg/policy/condition/binaryequalsfunc.go @@ -0,0 +1,138 @@ +/* + * Minio Cloud Storage, (C) 2018 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 condition + +import ( + "encoding/base64" + "fmt" + "net/http" + "sort" + + "github.com/minio/minio-go/pkg/s3utils" + "github.com/minio/minio-go/pkg/set" +) + +func toBinaryEqualsFuncString(n name, key Key, values set.StringSet) string { + valueStrings := values.ToSlice() + sort.Strings(valueStrings) + + return fmt.Sprintf("%v:%v:%v", n, key, valueStrings) +} + +// binaryEqualsFunc - String equals function. It checks whether value by Key in given +// values map is in condition values. +// For example, +// - if values = ["mybucket/foo"], at evaluate() it returns whether string +// in value map for Key is in values. +type binaryEqualsFunc struct { + k Key + values set.StringSet +} + +// evaluate() - evaluates to check whether value by Key in given values is in +// condition values. +func (f binaryEqualsFunc) evaluate(values map[string][]string) bool { + requestValue, ok := values[http.CanonicalHeaderKey(f.k.Name())] + if !ok { + requestValue = values[f.k.Name()] + } + + return !f.values.Intersection(set.CreateStringSet(requestValue...)).IsEmpty() +} + +// key() - returns condition key which is used by this condition function. +func (f binaryEqualsFunc) key() Key { + return f.k +} + +// name() - returns "BinaryEquals" condition name. +func (f binaryEqualsFunc) name() name { + return binaryEquals +} + +func (f binaryEqualsFunc) String() string { + return toBinaryEqualsFuncString(binaryEquals, f.k, f.values) +} + +// toMap - returns map representation of this function. +func (f binaryEqualsFunc) toMap() map[Key]ValueSet { + if !f.k.IsValid() { + return nil + } + + values := NewValueSet() + for _, value := range f.values.ToSlice() { + values.Add(NewStringValue(base64.StdEncoding.EncodeToString([]byte(value)))) + } + + return map[Key]ValueSet{ + f.k: values, + } +} + +func validateBinaryEqualsValues(n name, key Key, values set.StringSet) error { + vslice := values.ToSlice() + for _, s := range vslice { + sbytes, err := base64.StdEncoding.DecodeString(s) + if err != nil { + return err + } + values.Remove(s) + s = string(sbytes) + switch key { + case S3XAmzCopySource: + bucket, object := path2BucketAndObject(s) + if object == "" { + return fmt.Errorf("invalid value '%v' for '%v' for %v condition", s, S3XAmzCopySource, n) + } + if err = s3utils.CheckValidBucketName(bucket); err != nil { + return err + } + case S3XAmzServerSideEncryption: + if s != "aws:kms" && s != "AES256" { + return fmt.Errorf("invalid value '%v' for '%v' for %v condition", s, S3XAmzServerSideEncryption, n) + } + case S3XAmzMetadataDirective: + if s != "COPY" && s != "REPLACE" { + return fmt.Errorf("invalid value '%v' for '%v' for %v condition", s, S3XAmzMetadataDirective, n) + } + } + values.Add(s) + } + + return nil +} + +// newBinaryEqualsFunc - returns new BinaryEquals function. +func newBinaryEqualsFunc(key Key, values ValueSet) (Function, error) { + valueStrings, err := valuesToStringSlice(binaryEquals, values) + if err != nil { + return nil, err + } + + return NewBinaryEqualsFunc(key, valueStrings...) +} + +// NewBinaryEqualsFunc - returns new BinaryEquals function. +func NewBinaryEqualsFunc(key Key, values ...string) (Function, error) { + sset := set.CreateStringSet(values...) + if err := validateBinaryEqualsValues(binaryEquals, key, sset); err != nil { + return nil, err + } + + return &binaryEqualsFunc{key, sset}, nil +} diff --git a/pkg/policy/condition/binaryequalsfunc_test.go b/pkg/policy/condition/binaryequalsfunc_test.go new file mode 100644 index 000000000..beb6250d8 --- /dev/null +++ b/pkg/policy/condition/binaryequalsfunc_test.go @@ -0,0 +1,387 @@ +/* + * Minio Cloud Storage, (C) 2018 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 condition + +import ( + "encoding/base64" + "reflect" + "testing" +) + +func TestBinaryEqualsFuncEvaluate(t *testing.T) { + case1Function, err := newBinaryEqualsFunc(S3XAmzCopySource, + NewValueSet(NewStringValue(base64.StdEncoding.EncodeToString([]byte("mybucket/myobject"))))) + if err != nil { + t.Fatalf("unexpected error. %v\n", err) + } + + case2Function, err := newBinaryEqualsFunc(S3XAmzServerSideEncryption, + NewValueSet(NewStringValue(base64.StdEncoding.EncodeToString([]byte("AES256"))))) + if err != nil { + t.Fatalf("unexpected error. %v\n", err) + } + + case3Function, err := newBinaryEqualsFunc(S3XAmzMetadataDirective, + NewValueSet(NewStringValue(base64.StdEncoding.EncodeToString([]byte("REPLACE"))))) + if err != nil { + t.Fatalf("unexpected error. %v\n", err) + } + + case4Function, err := newBinaryEqualsFunc(S3LocationConstraint, + NewValueSet(NewStringValue(base64.StdEncoding.EncodeToString([]byte("eu-west-1"))))) + if err != nil { + t.Fatalf("unexpected error. %v\n", err) + } + + testCases := []struct { + function Function + values map[string][]string + expectedResult bool + }{ + {case1Function, map[string][]string{"x-amz-copy-source": {"mybucket/myobject"}}, true}, + {case1Function, map[string][]string{"x-amz-copy-source": {"yourbucket/myobject"}}, false}, + {case1Function, map[string][]string{}, false}, + {case1Function, map[string][]string{"delimiter": {"/"}}, false}, + + {case2Function, map[string][]string{"x-amz-server-side-encryption": {"AES256"}}, true}, + {case2Function, map[string][]string{"x-amz-server-side-encryption": {"aws:kms"}}, false}, + {case2Function, map[string][]string{}, false}, + {case2Function, map[string][]string{"delimiter": {"/"}}, false}, + + {case3Function, map[string][]string{"x-amz-metadata-directive": {"REPLACE"}}, true}, + {case3Function, map[string][]string{"x-amz-metadata-directive": {"COPY"}}, false}, + {case3Function, map[string][]string{}, false}, + {case3Function, map[string][]string{"delimiter": {"/"}}, false}, + + {case4Function, map[string][]string{"LocationConstraint": {"eu-west-1"}}, true}, + {case4Function, map[string][]string{"LocationConstraint": {"us-east-1"}}, false}, + {case4Function, map[string][]string{}, false}, + {case4Function, map[string][]string{"delimiter": {"/"}}, false}, + } + + for i, testCase := range testCases { + result := testCase.function.evaluate(testCase.values) + + if result != testCase.expectedResult { + t.Fatalf("case %v: expected: %v, got: %v\n", i+1, testCase.expectedResult, result) + } + } +} + +func TestBinaryEqualsFuncKey(t *testing.T) { + case1Function, err := newBinaryEqualsFunc(S3XAmzCopySource, + NewValueSet(NewStringValue(base64.StdEncoding.EncodeToString([]byte("mybucket/myobject"))))) + if err != nil { + t.Fatalf("unexpected error. %v\n", err) + } + + case2Function, err := newBinaryEqualsFunc(S3XAmzServerSideEncryption, + NewValueSet(NewStringValue(base64.StdEncoding.EncodeToString([]byte("AES256"))))) + if err != nil { + t.Fatalf("unexpected error. %v\n", err) + } + + case3Function, err := newBinaryEqualsFunc(S3XAmzMetadataDirective, + NewValueSet(NewStringValue(base64.StdEncoding.EncodeToString([]byte("REPLACE"))))) + if err != nil { + t.Fatalf("unexpected error. %v\n", err) + } + + case4Function, err := newBinaryEqualsFunc(S3LocationConstraint, + NewValueSet(NewStringValue(base64.StdEncoding.EncodeToString([]byte("eu-west-1"))))) + if err != nil { + t.Fatalf("unexpected error. %v\n", err) + } + + testCases := []struct { + function Function + expectedResult Key + }{ + {case1Function, S3XAmzCopySource}, + {case2Function, S3XAmzServerSideEncryption}, + {case3Function, S3XAmzMetadataDirective}, + {case4Function, S3LocationConstraint}, + } + + for i, testCase := range testCases { + result := testCase.function.key() + + if result != testCase.expectedResult { + t.Fatalf("case %v: expected: %v, got: %v\n", i+1, testCase.expectedResult, result) + } + } +} + +func TestBinaryEqualsFuncToMap(t *testing.T) { + case1Function, err := newBinaryEqualsFunc(S3XAmzCopySource, + NewValueSet(NewStringValue(base64.StdEncoding.EncodeToString([]byte("mybucket/myobject"))))) + if err != nil { + t.Fatalf("unexpected error. %v\n", err) + } + + case1Result := map[Key]ValueSet{ + S3XAmzCopySource: NewValueSet(NewStringValue(base64.StdEncoding.EncodeToString([]byte("mybucket/myobject")))), + } + + case2Function, err := newBinaryEqualsFunc(S3XAmzCopySource, + NewValueSet( + NewStringValue(base64.StdEncoding.EncodeToString([]byte("mybucket/myobject"))), + NewStringValue(base64.StdEncoding.EncodeToString([]byte("yourbucket/myobject"))), + ), + ) + if err != nil { + t.Fatalf("unexpected error. %v\n", err) + } + + case2Result := map[Key]ValueSet{ + S3XAmzCopySource: NewValueSet( + NewStringValue(base64.StdEncoding.EncodeToString([]byte("mybucket/myobject"))), + NewStringValue(base64.StdEncoding.EncodeToString([]byte("yourbucket/myobject"))), + ), + } + + case3Function, err := newBinaryEqualsFunc(S3XAmzServerSideEncryption, + NewValueSet(NewStringValue(base64.StdEncoding.EncodeToString([]byte("AES256"))))) + if err != nil { + t.Fatalf("unexpected error. %v\n", err) + } + + case3Result := map[Key]ValueSet{ + S3XAmzServerSideEncryption: NewValueSet(NewStringValue(base64.StdEncoding.EncodeToString([]byte("AES256")))), + } + + case4Function, err := newBinaryEqualsFunc(S3XAmzServerSideEncryption, + NewValueSet( + NewStringValue(base64.StdEncoding.EncodeToString([]byte("AES256"))), + NewStringValue(base64.StdEncoding.EncodeToString([]byte("aws:kms"))), + ), + ) + if err != nil { + t.Fatalf("unexpected error. %v\n", err) + } + + case4Result := map[Key]ValueSet{ + S3XAmzServerSideEncryption: NewValueSet( + NewStringValue(base64.StdEncoding.EncodeToString([]byte("AES256"))), + NewStringValue(base64.StdEncoding.EncodeToString([]byte("aws:kms"))), + ), + } + + case5Function, err := newBinaryEqualsFunc(S3XAmzMetadataDirective, + NewValueSet(NewStringValue(base64.StdEncoding.EncodeToString([]byte("REPLACE"))))) + if err != nil { + t.Fatalf("unexpected error. %v\n", err) + } + + case5Result := map[Key]ValueSet{ + S3XAmzMetadataDirective: NewValueSet(NewStringValue(base64.StdEncoding.EncodeToString([]byte("REPLACE")))), + } + + case6Function, err := newBinaryEqualsFunc(S3XAmzMetadataDirective, + NewValueSet( + NewStringValue(base64.StdEncoding.EncodeToString([]byte("REPLACE"))), + NewStringValue(base64.StdEncoding.EncodeToString([]byte("COPY"))), + ), + ) + if err != nil { + t.Fatalf("unexpected error. %v\n", err) + } + + case6Result := map[Key]ValueSet{ + S3XAmzMetadataDirective: NewValueSet( + NewStringValue(base64.StdEncoding.EncodeToString([]byte("REPLACE"))), + NewStringValue(base64.StdEncoding.EncodeToString([]byte("COPY"))), + ), + } + + case7Function, err := newBinaryEqualsFunc(S3LocationConstraint, + NewValueSet(NewStringValue(base64.StdEncoding.EncodeToString([]byte("eu-west-1"))))) + if err != nil { + t.Fatalf("unexpected error. %v\n", err) + } + + case7Result := map[Key]ValueSet{ + S3LocationConstraint: NewValueSet(NewStringValue(base64.StdEncoding.EncodeToString([]byte("eu-west-1")))), + } + + case8Function, err := newBinaryEqualsFunc(S3LocationConstraint, + NewValueSet( + NewStringValue(base64.StdEncoding.EncodeToString([]byte("eu-west-1"))), + NewStringValue(base64.StdEncoding.EncodeToString([]byte("us-west-1"))), + ), + ) + if err != nil { + t.Fatalf("unexpected error. %v\n", err) + } + + case8Result := map[Key]ValueSet{ + S3LocationConstraint: NewValueSet( + NewStringValue(base64.StdEncoding.EncodeToString([]byte("eu-west-1"))), + NewStringValue(base64.StdEncoding.EncodeToString([]byte("us-west-1"))), + ), + } + + testCases := []struct { + f Function + expectedResult map[Key]ValueSet + }{ + {case1Function, case1Result}, + {case2Function, case2Result}, + {case3Function, case3Result}, + {case4Function, case4Result}, + {case5Function, case5Result}, + {case6Function, case6Result}, + {case7Function, case7Result}, + {case8Function, case8Result}, + {&binaryEqualsFunc{}, nil}, + } + + for i, testCase := range testCases { + result := testCase.f.toMap() + + if !reflect.DeepEqual(result, testCase.expectedResult) { + t.Fatalf("case %v: result: expected: %v, got: %v\n", i+1, testCase.expectedResult, result) + } + } +} + +func TestNewBinaryEqualsFunc(t *testing.T) { + case1Function, err := newBinaryEqualsFunc(S3XAmzCopySource, + NewValueSet(NewStringValue(base64.StdEncoding.EncodeToString([]byte("mybucket/myobject"))))) + if err != nil { + t.Fatalf("unexpected error. %v\n", err) + } + + case2Function, err := newBinaryEqualsFunc(S3XAmzCopySource, + NewValueSet( + NewStringValue(base64.StdEncoding.EncodeToString([]byte("mybucket/myobject"))), + NewStringValue(base64.StdEncoding.EncodeToString([]byte("yourbucket/myobject"))), + ), + ) + if err != nil { + t.Fatalf("unexpected error. %v\n", err) + } + + case3Function, err := newBinaryEqualsFunc(S3XAmzServerSideEncryption, + NewValueSet(NewStringValue(base64.StdEncoding.EncodeToString([]byte("AES256"))))) + if err != nil { + t.Fatalf("unexpected error. %v\n", err) + } + + case4Function, err := newBinaryEqualsFunc(S3XAmzServerSideEncryption, + NewValueSet( + NewStringValue(base64.StdEncoding.EncodeToString([]byte("AES256"))), + NewStringValue(base64.StdEncoding.EncodeToString([]byte("aws:kms"))), + ), + ) + if err != nil { + t.Fatalf("unexpected error. %v\n", err) + } + + case5Function, err := newBinaryEqualsFunc(S3XAmzMetadataDirective, + NewValueSet(NewStringValue(base64.StdEncoding.EncodeToString([]byte("REPLACE"))))) + if err != nil { + t.Fatalf("unexpected error. %v\n", err) + } + + case6Function, err := newBinaryEqualsFunc(S3XAmzMetadataDirective, + NewValueSet( + NewStringValue(base64.StdEncoding.EncodeToString([]byte("REPLACE"))), + NewStringValue(base64.StdEncoding.EncodeToString([]byte("COPY"))), + ), + ) + if err != nil { + t.Fatalf("unexpected error. %v\n", err) + } + + case7Function, err := newBinaryEqualsFunc(S3LocationConstraint, + NewValueSet(NewStringValue(base64.StdEncoding.EncodeToString([]byte("eu-west-1"))))) + if err != nil { + t.Fatalf("unexpected error. %v\n", err) + } + + case8Function, err := newBinaryEqualsFunc(S3LocationConstraint, + NewValueSet( + NewStringValue(base64.StdEncoding.EncodeToString([]byte("eu-west-1"))), + NewStringValue(base64.StdEncoding.EncodeToString([]byte("us-west-1"))), + ), + ) + if err != nil { + t.Fatalf("unexpected error. %v\n", err) + } + + testCases := []struct { + key Key + values ValueSet + expectedResult Function + expectErr bool + }{ + {S3XAmzCopySource, NewValueSet(NewStringValue(base64.StdEncoding.EncodeToString([]byte("mybucket/myobject")))), case1Function, false}, + {S3XAmzCopySource, + NewValueSet( + NewStringValue(base64.StdEncoding.EncodeToString([]byte("mybucket/myobject"))), + NewStringValue(base64.StdEncoding.EncodeToString([]byte("yourbucket/myobject"))), + ), case2Function, false}, + + {S3XAmzServerSideEncryption, NewValueSet(NewStringValue(base64.StdEncoding.EncodeToString([]byte("AES256")))), case3Function, false}, + {S3XAmzServerSideEncryption, + NewValueSet( + NewStringValue(base64.StdEncoding.EncodeToString([]byte("AES256"))), + NewStringValue(base64.StdEncoding.EncodeToString([]byte("aws:kms"))), + ), case4Function, false}, + + {S3XAmzMetadataDirective, NewValueSet(NewStringValue(base64.StdEncoding.EncodeToString([]byte("REPLACE")))), case5Function, false}, + {S3XAmzMetadataDirective, + NewValueSet( + NewStringValue(base64.StdEncoding.EncodeToString([]byte("REPLACE"))), + NewStringValue(base64.StdEncoding.EncodeToString([]byte("COPY"))), + ), case6Function, false}, + + {S3LocationConstraint, NewValueSet(NewStringValue(base64.StdEncoding.EncodeToString([]byte("eu-west-1")))), case7Function, false}, + {S3LocationConstraint, + NewValueSet( + NewStringValue(base64.StdEncoding.EncodeToString([]byte("eu-west-1"))), + NewStringValue(base64.StdEncoding.EncodeToString([]byte("us-west-1"))), + ), case8Function, false}, + + // Unsupported value error. + {S3XAmzCopySource, NewValueSet(NewStringValue(base64.StdEncoding.EncodeToString([]byte("mybucket/myobject"))), NewIntValue(7)), nil, true}, + {S3XAmzServerSideEncryption, NewValueSet(NewStringValue(base64.StdEncoding.EncodeToString([]byte("AES256"))), NewIntValue(7)), nil, true}, + {S3XAmzMetadataDirective, NewValueSet(NewStringValue(base64.StdEncoding.EncodeToString([]byte("REPLACE"))), NewIntValue(7)), nil, true}, + {S3LocationConstraint, NewValueSet(NewStringValue(base64.StdEncoding.EncodeToString([]byte("eu-west-1"))), NewIntValue(7)), nil, true}, + + // Invalid value error. + {S3XAmzCopySource, NewValueSet(NewStringValue(base64.StdEncoding.EncodeToString([]byte("mybucket")))), nil, true}, + {S3XAmzServerSideEncryption, NewValueSet(NewStringValue(base64.StdEncoding.EncodeToString([]byte("SSE-C")))), nil, true}, + {S3XAmzMetadataDirective, NewValueSet(NewStringValue(base64.StdEncoding.EncodeToString([]byte("DUPLICATE")))), nil, true}, + } + + for i, testCase := range testCases { + result, err := newBinaryEqualsFunc(testCase.key, testCase.values) + expectErr := (err != nil) + + if expectErr != testCase.expectErr { + t.Fatalf("case %v: error: expected: %v, got: %v\n", i+1, testCase.expectErr, expectErr) + } + + if !testCase.expectErr { + if !reflect.DeepEqual(result, testCase.expectedResult) { + t.Fatalf("case %v: result: expected: %v, got: %v\n", i+1, testCase.expectedResult, result) + } + } + } +} diff --git a/pkg/policy/condition/func.go b/pkg/policy/condition/func.go index 01805cbf1..b525b74fa 100644 --- a/pkg/policy/condition/func.go +++ b/pkg/policy/condition/func.go @@ -88,6 +88,20 @@ func (functions Functions) String() string { return fmt.Sprintf("%v", funcStrings) } +var conditionFuncMap = map[name]func(Key, ValueSet) (Function, error){ + stringEquals: newStringEqualsFunc, + stringNotEquals: newStringNotEqualsFunc, + stringEqualsIgnoreCase: newStringEqualsIgnoreCaseFunc, + stringNotEqualsIgnoreCase: newStringNotEqualsIgnoreCaseFunc, + binaryEquals: newBinaryEqualsFunc, + stringLike: newStringLikeFunc, + stringNotLike: newStringNotLikeFunc, + ipAddress: newIPAddressFunc, + notIPAddress: newNotIPAddressFunc, + null: newNullFunc, + // Add new conditions here. +} + // UnmarshalJSON - decodes JSON data to Functions. func (functions *Functions) UnmarshalJSON(data []byte) error { // As string kind, int kind then json.Unmarshaler is checked at @@ -119,38 +133,14 @@ func (functions *Functions) UnmarshalJSON(data []byte) error { return err } - var f Function - switch n { - case stringEquals: - if f, err = newStringEqualsFunc(key, values); err != nil { - return err - } - case stringNotEquals: - if f, err = newStringNotEqualsFunc(key, values); err != nil { - return err - } - case stringLike: - if f, err = newStringLikeFunc(key, values); err != nil { - return err - } - case stringNotLike: - if f, err = newStringNotLikeFunc(key, values); err != nil { - return err - } - case ipAddress: - if f, err = newIPAddressFunc(key, values); err != nil { - return err - } - case notIPAddress: - if f, err = newNotIPAddressFunc(key, values); err != nil { - return err - } - case null: - if f, err = newNullFunc(key, values); err != nil { - return err - } - default: - return fmt.Errorf("%v is not handled", n) + vfn, ok := conditionFuncMap[n] + if !ok { + return fmt.Errorf("condition %v is not handled", n) + } + + f, err := vfn(key, values) + if err != nil { + return err } funcs = append(funcs, f) diff --git a/pkg/policy/condition/func_test.go b/pkg/policy/condition/func_test.go index 4675e50fd..3a9121f3f 100644 --- a/pkg/policy/condition/func_test.go +++ b/pkg/policy/condition/func_test.go @@ -268,6 +268,11 @@ func TestFunctionsUnmarshalJSON(t *testing.T) { case3Data := []byte(`{}`) + // Remove this test after supporting date conditions. + case4Data := []byte(`{ +"DateEquals": { "aws:CurrentTime": "2013-06-30T00:00:00Z" } +}`) + testCases := []struct { data []byte expectedResult Functions @@ -278,6 +283,8 @@ func TestFunctionsUnmarshalJSON(t *testing.T) { {case2Data, NewFunctions(func6), false}, // empty condition error. {case3Data, nil, true}, + // unsupported condition error. + {case4Data, nil, true}, } for i, testCase := range testCases { diff --git a/pkg/policy/condition/key.go b/pkg/policy/condition/key.go index 0762d0365..d57ddd620 100644 --- a/pkg/policy/condition/key.go +++ b/pkg/policy/condition/key.go @@ -68,17 +68,38 @@ const ( // AWSSourceIP - key representing client's IP address (not intermittent proxies) of any API. AWSSourceIP = "aws:SourceIp" + + // AWSUserAgent - key representing UserAgent header for any API. + AWSUserAgent = "aws:UserAgent" + + // AWSSecureTransport - key representing if the clients request is authenticated or not. + AWSSecureTransport = "aws:SecureTransport" ) +// AllSupportedKeys - is list of all all supported keys. +var AllSupportedKeys = []Key{ + S3XAmzCopySource, + S3XAmzServerSideEncryption, + S3XAmzServerSideEncryptionAwsKMSKeyID, + S3XAmzMetadataDirective, + S3XAmzStorageClass, + S3LocationConstraint, + S3Prefix, + S3Delimiter, + S3MaxKeys, + AWSReferer, + AWSSourceIP, + AWSUserAgent, + AWSSecureTransport, + // Add new supported condition keys. +} + // IsValid - checks if key is valid or not. func (key Key) IsValid() bool { - switch key { - case S3XAmzCopySource, S3XAmzServerSideEncryption, S3XAmzServerSideEncryptionAwsKMSKeyID: - fallthrough - case S3XAmzMetadataDirective, S3XAmzStorageClass, S3LocationConstraint, S3Prefix: - fallthrough - case S3Delimiter, S3MaxKeys, AWSReferer, AWSSourceIP: - return true + for _, supKey := range AllSupportedKeys { + if supKey == key { + return true + } } return false diff --git a/pkg/policy/condition/name.go b/pkg/policy/condition/name.go index 0cecabb6c..7d7a6a8d3 100644 --- a/pkg/policy/condition/name.go +++ b/pkg/policy/condition/name.go @@ -24,20 +24,36 @@ import ( type name string const ( - stringEquals name = "StringEquals" - stringNotEquals = "StringNotEquals" - stringLike = "StringLike" - stringNotLike = "StringNotLike" - ipAddress = "IpAddress" - notIPAddress = "NotIpAddress" - null = "Null" + stringEquals name = "StringEquals" + stringNotEquals = "StringNotEquals" + stringEqualsIgnoreCase = "StringEqualsIgnoreCase" + stringNotEqualsIgnoreCase = "StringNotEqualsIgnoreCase" + stringLike = "StringLike" + stringNotLike = "StringNotLike" + binaryEquals = "BinaryEquals" + ipAddress = "IpAddress" + notIPAddress = "NotIpAddress" + null = "Null" ) // IsValid - checks if name is valid or not. func (n name) IsValid() bool { - switch n { - case stringEquals, stringNotEquals, stringLike, stringNotLike, ipAddress, notIPAddress, null: - return true + for _, supn := range []name{ + stringEquals, + stringNotEquals, + stringEqualsIgnoreCase, + stringNotEqualsIgnoreCase, + binaryEquals, + stringLike, + stringNotLike, + ipAddress, + notIPAddress, + null, + // Add new conditions here. + } { + if n == supn { + return true + } } return false diff --git a/pkg/policy/condition/stringequalsfunc.go b/pkg/policy/condition/stringequalsfunc.go index 9c84e73d9..b988917ec 100644 --- a/pkg/policy/condition/stringequalsfunc.go +++ b/pkg/policy/condition/stringequalsfunc.go @@ -20,8 +20,8 @@ import ( "fmt" "net/http" "sort" - "strings" + "github.com/minio/minio-go/pkg/s3utils" "github.com/minio/minio-go/pkg/set" ) @@ -127,11 +127,13 @@ func validateStringEqualsValues(n name, key Key, values set.StringSet) error { for _, s := range values.ToSlice() { switch key { case S3XAmzCopySource: - tokens := strings.SplitN(s, "/", 2) - if len(tokens) < 2 { + bucket, object := path2BucketAndObject(s) + if object == "" { return fmt.Errorf("invalid value '%v' for '%v' for %v condition", s, S3XAmzCopySource, n) } - // FIXME: tokens[0] must be a valid bucket name. + if err := s3utils.CheckValidBucketName(bucket); err != nil { + return err + } case S3XAmzServerSideEncryption: if s != "aws:kms" && s != "AES256" { return fmt.Errorf("invalid value '%v' for '%v' for %v condition", s, S3XAmzServerSideEncryption, n) diff --git a/pkg/policy/condition/stringequalsignorecasefunc.go b/pkg/policy/condition/stringequalsignorecasefunc.go new file mode 100644 index 000000000..9b713a066 --- /dev/null +++ b/pkg/policy/condition/stringequalsignorecasefunc.go @@ -0,0 +1,157 @@ +/* + * Minio Cloud Storage, (C) 2018 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 condition + +import ( + "fmt" + "net/http" + "sort" + "strings" + + "github.com/minio/minio-go/pkg/set" +) + +func toStringEqualsIgnoreCaseFuncString(n name, key Key, values set.StringSet) string { + valueStrings := values.ToSlice() + sort.Strings(valueStrings) + + return fmt.Sprintf("%v:%v:%v", n, key, valueStrings) +} + +// stringEqualsIgnoreCaseFunc - String equals function. It checks whether value by Key in given +// values map is in condition values. +// For example, +// - if values = ["mybucket/foo"], at evaluate() it returns whether string +// in value map for Key is in values. +type stringEqualsIgnoreCaseFunc struct { + k Key + values set.StringSet +} + +// evaluate() - evaluates to check whether value by Key in given values is in +// condition values, ignores case. +func (f stringEqualsIgnoreCaseFunc) evaluate(values map[string][]string) bool { + requestValue, ok := values[http.CanonicalHeaderKey(f.k.Name())] + if !ok { + requestValue = values[f.k.Name()] + } + + for _, v := range requestValue { + if !f.values.FuncMatch(strings.EqualFold, v).IsEmpty() { + return true + } + } + return false +} + +// key() - returns condition key which is used by this condition function. +func (f stringEqualsIgnoreCaseFunc) key() Key { + return f.k +} + +// name() - returns "StringEqualsIgnoreCase" condition name. +func (f stringEqualsIgnoreCaseFunc) name() name { + return stringEqualsIgnoreCase +} + +func (f stringEqualsIgnoreCaseFunc) String() string { + return toStringEqualsIgnoreCaseFuncString(stringEqualsIgnoreCase, f.k, f.values) +} + +// toMap - returns map representation of this function. +func (f stringEqualsIgnoreCaseFunc) toMap() map[Key]ValueSet { + if !f.k.IsValid() { + return nil + } + + values := NewValueSet() + for _, value := range f.values.ToSlice() { + values.Add(NewStringValue(value)) + } + + return map[Key]ValueSet{ + f.k: values, + } +} + +// stringNotEqualsIgnoreCaseFunc - String not equals function. It checks whether value by Key in +// given values is NOT in condition values. +// For example, +// - if values = ["mybucket/foo"], at evaluate() it returns whether string +// in value map for Key is NOT in values. +type stringNotEqualsIgnoreCaseFunc struct { + stringEqualsIgnoreCaseFunc +} + +// evaluate() - evaluates to check whether value by Key in given values is NOT in +// condition values. +func (f stringNotEqualsIgnoreCaseFunc) evaluate(values map[string][]string) bool { + return !f.stringEqualsIgnoreCaseFunc.evaluate(values) +} + +// name() - returns "StringNotEqualsIgnoreCase" condition name. +func (f stringNotEqualsIgnoreCaseFunc) name() name { + return stringNotEqualsIgnoreCase +} + +func (f stringNotEqualsIgnoreCaseFunc) String() string { + return toStringEqualsIgnoreCaseFuncString(stringNotEqualsIgnoreCase, f.stringEqualsIgnoreCaseFunc.k, f.stringEqualsIgnoreCaseFunc.values) +} + +func validateStringEqualsIgnoreCaseValues(n name, key Key, values set.StringSet) error { + return validateStringEqualsValues(n, key, values) +} + +// newStringEqualsIgnoreCaseFunc - returns new StringEqualsIgnoreCase function. +func newStringEqualsIgnoreCaseFunc(key Key, values ValueSet) (Function, error) { + valueStrings, err := valuesToStringSlice(stringEqualsIgnoreCase, values) + if err != nil { + return nil, err + } + + return NewStringEqualsIgnoreCaseFunc(key, valueStrings...) +} + +// NewStringEqualsIgnoreCaseFunc - returns new StringEqualsIgnoreCase function. +func NewStringEqualsIgnoreCaseFunc(key Key, values ...string) (Function, error) { + sset := set.CreateStringSet(values...) + if err := validateStringEqualsIgnoreCaseValues(stringEqualsIgnoreCase, key, sset); err != nil { + return nil, err + } + + return &stringEqualsIgnoreCaseFunc{key, sset}, nil +} + +// newStringNotEqualsIgnoreCaseFunc - returns new StringNotEqualsIgnoreCase function. +func newStringNotEqualsIgnoreCaseFunc(key Key, values ValueSet) (Function, error) { + valueStrings, err := valuesToStringSlice(stringNotEqualsIgnoreCase, values) + if err != nil { + return nil, err + } + + return NewStringNotEqualsIgnoreCaseFunc(key, valueStrings...) +} + +// NewStringNotEqualsIgnoreCaseFunc - returns new StringNotEqualsIgnoreCase function. +func NewStringNotEqualsIgnoreCaseFunc(key Key, values ...string) (Function, error) { + sset := set.CreateStringSet(values...) + if err := validateStringEqualsIgnoreCaseValues(stringNotEqualsIgnoreCase, key, sset); err != nil { + return nil, err + } + + return &stringNotEqualsIgnoreCaseFunc{stringEqualsIgnoreCaseFunc{key, sset}}, nil +} diff --git a/pkg/policy/condition/stringequalsignorecasefunc_test.go b/pkg/policy/condition/stringequalsignorecasefunc_test.go new file mode 100644 index 000000000..c28fd4aee --- /dev/null +++ b/pkg/policy/condition/stringequalsignorecasefunc_test.go @@ -0,0 +1,720 @@ +/* + * Minio Cloud Storage, (C) 2018 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 condition + +import ( + "reflect" + "testing" +) + +func TestStringEqualsIgnoreCaseFuncEvaluate(t *testing.T) { + case1Function, err := newStringEqualsIgnoreCaseFunc(S3XAmzCopySource, NewValueSet(NewStringValue("mybucket/myobject"))) + if err != nil { + t.Fatalf("unexpected error. %v\n", err) + } + + case2Function, err := newStringEqualsIgnoreCaseFunc(S3XAmzServerSideEncryption, NewValueSet(NewStringValue("AES256"))) + if err != nil { + t.Fatalf("unexpected error. %v\n", err) + } + + case3Function, err := newStringEqualsIgnoreCaseFunc(S3XAmzMetadataDirective, NewValueSet(NewStringValue("REPLACE"))) + if err != nil { + t.Fatalf("unexpected error. %v\n", err) + } + + case4Function, err := newStringEqualsIgnoreCaseFunc(S3LocationConstraint, NewValueSet(NewStringValue("eu-west-1"))) + if err != nil { + t.Fatalf("unexpected error. %v\n", err) + } + + testCases := []struct { + function Function + values map[string][]string + expectedResult bool + }{ + {case1Function, map[string][]string{"x-amz-copy-source": {"mybucket/myobject"}}, true}, + {case1Function, map[string][]string{"x-amz-copy-source": {"yourbucket/myobject"}}, false}, + {case1Function, map[string][]string{}, false}, + {case1Function, map[string][]string{"delimiter": {"/"}}, false}, + + {case2Function, map[string][]string{"x-amz-server-side-encryption": {"AES256"}}, true}, + {case2Function, map[string][]string{"x-amz-server-side-encryption": {"aes256"}}, true}, + {case2Function, map[string][]string{"x-amz-server-side-encryption": {"aws:kms"}}, false}, + {case2Function, map[string][]string{}, false}, + {case2Function, map[string][]string{"delimiter": {"/"}}, false}, + + {case3Function, map[string][]string{"x-amz-metadata-directive": {"REPLACE"}}, true}, + {case3Function, map[string][]string{"x-amz-metadata-directive": {"replace"}}, true}, + {case3Function, map[string][]string{"x-amz-metadata-directive": {"COPY"}}, false}, + {case3Function, map[string][]string{}, false}, + {case3Function, map[string][]string{"delimiter": {"/"}}, false}, + + {case4Function, map[string][]string{"LocationConstraint": {"eu-west-1"}}, true}, + {case4Function, map[string][]string{"LocationConstraint": {"us-east-1"}}, false}, + {case4Function, map[string][]string{}, false}, + {case4Function, map[string][]string{"delimiter": {"/"}}, false}, + } + + for i, testCase := range testCases { + result := testCase.function.evaluate(testCase.values) + + if result != testCase.expectedResult { + t.Fatalf("case %v: expected: %v, got: %v\n", i+1, testCase.expectedResult, result) + } + } +} + +func TestStringEqualsIgnoreCaseFuncKey(t *testing.T) { + case1Function, err := newStringEqualsIgnoreCaseFunc(S3XAmzCopySource, NewValueSet(NewStringValue("mybucket/myobject"))) + if err != nil { + t.Fatalf("unexpected error. %v\n", err) + } + + case2Function, err := newStringEqualsIgnoreCaseFunc(S3XAmzServerSideEncryption, NewValueSet(NewStringValue("AES256"))) + if err != nil { + t.Fatalf("unexpected error. %v\n", err) + } + + case3Function, err := newStringEqualsIgnoreCaseFunc(S3XAmzMetadataDirective, NewValueSet(NewStringValue("REPLACE"))) + if err != nil { + t.Fatalf("unexpected error. %v\n", err) + } + + case4Function, err := newStringEqualsIgnoreCaseFunc(S3LocationConstraint, NewValueSet(NewStringValue("eu-west-1"))) + if err != nil { + t.Fatalf("unexpected error. %v\n", err) + } + + testCases := []struct { + function Function + expectedResult Key + }{ + {case1Function, S3XAmzCopySource}, + {case2Function, S3XAmzServerSideEncryption}, + {case3Function, S3XAmzMetadataDirective}, + {case4Function, S3LocationConstraint}, + } + + for i, testCase := range testCases { + result := testCase.function.key() + + if result != testCase.expectedResult { + t.Fatalf("case %v: expected: %v, got: %v\n", i+1, testCase.expectedResult, result) + } + } +} + +func TestStringEqualsIgnoreCaseFuncToMap(t *testing.T) { + case1Function, err := newStringEqualsIgnoreCaseFunc(S3XAmzCopySource, NewValueSet(NewStringValue("mybucket/myobject"))) + if err != nil { + t.Fatalf("unexpected error. %v\n", err) + } + + case1Result := map[Key]ValueSet{ + S3XAmzCopySource: NewValueSet(NewStringValue("mybucket/myobject")), + } + + case2Function, err := newStringEqualsIgnoreCaseFunc(S3XAmzCopySource, + NewValueSet( + NewStringValue("mybucket/myobject"), + NewStringValue("yourbucket/myobject"), + ), + ) + if err != nil { + t.Fatalf("unexpected error. %v\n", err) + } + + case2Result := map[Key]ValueSet{ + S3XAmzCopySource: NewValueSet( + NewStringValue("mybucket/myobject"), + NewStringValue("yourbucket/myobject"), + ), + } + + case3Function, err := newStringEqualsIgnoreCaseFunc(S3XAmzServerSideEncryption, NewValueSet(NewStringValue("AES256"))) + if err != nil { + t.Fatalf("unexpected error. %v\n", err) + } + + case3Result := map[Key]ValueSet{ + S3XAmzServerSideEncryption: NewValueSet(NewStringValue("AES256")), + } + + case4Function, err := newStringEqualsIgnoreCaseFunc(S3XAmzServerSideEncryption, + NewValueSet( + NewStringValue("AES256"), + NewStringValue("aws:kms"), + ), + ) + if err != nil { + t.Fatalf("unexpected error. %v\n", err) + } + + case4Result := map[Key]ValueSet{ + S3XAmzServerSideEncryption: NewValueSet( + NewStringValue("AES256"), + NewStringValue("aws:kms"), + ), + } + + case5Function, err := newStringEqualsIgnoreCaseFunc(S3XAmzMetadataDirective, NewValueSet(NewStringValue("REPLACE"))) + if err != nil { + t.Fatalf("unexpected error. %v\n", err) + } + + case5Result := map[Key]ValueSet{ + S3XAmzMetadataDirective: NewValueSet(NewStringValue("REPLACE")), + } + + case6Function, err := newStringEqualsIgnoreCaseFunc(S3XAmzMetadataDirective, + NewValueSet( + NewStringValue("REPLACE"), + NewStringValue("COPY"), + ), + ) + if err != nil { + t.Fatalf("unexpected error. %v\n", err) + } + + case6Result := map[Key]ValueSet{ + S3XAmzMetadataDirective: NewValueSet( + NewStringValue("REPLACE"), + NewStringValue("COPY"), + ), + } + + case7Function, err := newStringEqualsIgnoreCaseFunc(S3LocationConstraint, NewValueSet(NewStringValue("eu-west-1"))) + if err != nil { + t.Fatalf("unexpected error. %v\n", err) + } + + case7Result := map[Key]ValueSet{ + S3LocationConstraint: NewValueSet(NewStringValue("eu-west-1")), + } + + case8Function, err := newStringEqualsIgnoreCaseFunc(S3LocationConstraint, + NewValueSet( + NewStringValue("eu-west-1"), + NewStringValue("us-west-1"), + ), + ) + if err != nil { + t.Fatalf("unexpected error. %v\n", err) + } + + case8Result := map[Key]ValueSet{ + S3LocationConstraint: NewValueSet( + NewStringValue("eu-west-1"), + NewStringValue("us-west-1"), + ), + } + + testCases := []struct { + f Function + expectedResult map[Key]ValueSet + }{ + {case1Function, case1Result}, + {case2Function, case2Result}, + {case3Function, case3Result}, + {case4Function, case4Result}, + {case5Function, case5Result}, + {case6Function, case6Result}, + {case7Function, case7Result}, + {case8Function, case8Result}, + {&stringEqualsFunc{}, nil}, + } + + for i, testCase := range testCases { + result := testCase.f.toMap() + + if !reflect.DeepEqual(result, testCase.expectedResult) { + t.Fatalf("case %v: result: expected: %v, got: %v\n", i+1, testCase.expectedResult, result) + } + } +} + +func TestStringNotEqualsIgnoreCaseFuncEvaluate(t *testing.T) { + case1Function, err := newStringNotEqualsIgnoreCaseFunc(S3XAmzCopySource, NewValueSet(NewStringValue("mybucket/myobject"))) + if err != nil { + t.Fatalf("unexpected error. %v\n", err) + } + + case2Function, err := newStringNotEqualsIgnoreCaseFunc(S3XAmzServerSideEncryption, NewValueSet(NewStringValue("AES256"))) + if err != nil { + t.Fatalf("unexpected error. %v\n", err) + } + + case3Function, err := newStringNotEqualsIgnoreCaseFunc(S3XAmzMetadataDirective, NewValueSet(NewStringValue("REPLACE"))) + if err != nil { + t.Fatalf("unexpected error. %v\n", err) + } + + case4Function, err := newStringNotEqualsIgnoreCaseFunc(S3LocationConstraint, NewValueSet(NewStringValue("eu-west-1"))) + if err != nil { + t.Fatalf("unexpected error. %v\n", err) + } + + testCases := []struct { + function Function + values map[string][]string + expectedResult bool + }{ + {case1Function, map[string][]string{"x-amz-copy-source": {"mybucket/myobject"}}, false}, + {case1Function, map[string][]string{"x-amz-copy-source": {"yourbucket/myobject"}}, true}, + {case1Function, map[string][]string{}, true}, + {case1Function, map[string][]string{"delimiter": {"/"}}, true}, + + {case2Function, map[string][]string{"x-amz-server-side-encryption": {"AES256"}}, false}, + {case2Function, map[string][]string{"x-amz-server-side-encryption": {"aws:kms"}}, true}, + {case2Function, map[string][]string{}, true}, + {case2Function, map[string][]string{"delimiter": {"/"}}, true}, + + {case3Function, map[string][]string{"x-amz-metadata-directive": {"REPLACE"}}, false}, + {case3Function, map[string][]string{"x-amz-metadata-directive": {"COPY"}}, true}, + {case3Function, map[string][]string{}, true}, + {case3Function, map[string][]string{"delimiter": {"/"}}, true}, + + {case4Function, map[string][]string{"LocationConstraint": {"eu-west-1"}}, false}, + {case4Function, map[string][]string{"LocationConstraint": {"us-east-1"}}, true}, + {case4Function, map[string][]string{}, true}, + {case4Function, map[string][]string{"delimiter": {"/"}}, true}, + } + + for i, testCase := range testCases { + result := testCase.function.evaluate(testCase.values) + + if result != testCase.expectedResult { + t.Fatalf("case %v: expected: %v, got: %v\n", i+1, testCase.expectedResult, result) + } + } +} + +func TestStringNotEqualsIgnoreCaseFuncKey(t *testing.T) { + case1Function, err := newStringNotEqualsIgnoreCaseFunc(S3XAmzCopySource, NewValueSet(NewStringValue("mybucket/myobject"))) + if err != nil { + t.Fatalf("unexpected error. %v\n", err) + } + + case2Function, err := newStringNotEqualsIgnoreCaseFunc(S3XAmzServerSideEncryption, NewValueSet(NewStringValue("AES256"))) + if err != nil { + t.Fatalf("unexpected error. %v\n", err) + } + + case3Function, err := newStringNotEqualsIgnoreCaseFunc(S3XAmzMetadataDirective, NewValueSet(NewStringValue("REPLACE"))) + if err != nil { + t.Fatalf("unexpected error. %v\n", err) + } + + case4Function, err := newStringNotEqualsIgnoreCaseFunc(S3LocationConstraint, NewValueSet(NewStringValue("eu-west-1"))) + if err != nil { + t.Fatalf("unexpected error. %v\n", err) + } + + testCases := []struct { + function Function + expectedResult Key + }{ + {case1Function, S3XAmzCopySource}, + {case2Function, S3XAmzServerSideEncryption}, + {case3Function, S3XAmzMetadataDirective}, + {case4Function, S3LocationConstraint}, + } + + for i, testCase := range testCases { + result := testCase.function.key() + + if result != testCase.expectedResult { + t.Fatalf("case %v: expected: %v, got: %v\n", i+1, testCase.expectedResult, result) + } + } +} + +func TestStringNotEqualsIgnoreCaseFuncToMap(t *testing.T) { + case1Function, err := newStringNotEqualsIgnoreCaseFunc(S3XAmzCopySource, NewValueSet(NewStringValue("mybucket/myobject"))) + if err != nil { + t.Fatalf("unexpected error. %v\n", err) + } + + case1Result := map[Key]ValueSet{ + S3XAmzCopySource: NewValueSet(NewStringValue("mybucket/myobject")), + } + + case2Function, err := newStringNotEqualsIgnoreCaseFunc(S3XAmzCopySource, + NewValueSet( + NewStringValue("mybucket/myobject"), + NewStringValue("yourbucket/myobject"), + ), + ) + if err != nil { + t.Fatalf("unexpected error. %v\n", err) + } + + case2Result := map[Key]ValueSet{ + S3XAmzCopySource: NewValueSet( + NewStringValue("mybucket/myobject"), + NewStringValue("yourbucket/myobject"), + ), + } + + case3Function, err := newStringNotEqualsIgnoreCaseFunc(S3XAmzServerSideEncryption, NewValueSet(NewStringValue("AES256"))) + if err != nil { + t.Fatalf("unexpected error. %v\n", err) + } + + case3Result := map[Key]ValueSet{ + S3XAmzServerSideEncryption: NewValueSet(NewStringValue("AES256")), + } + + case4Function, err := newStringNotEqualsIgnoreCaseFunc(S3XAmzServerSideEncryption, + NewValueSet( + NewStringValue("AES256"), + NewStringValue("aws:kms"), + ), + ) + if err != nil { + t.Fatalf("unexpected error. %v\n", err) + } + + case4Result := map[Key]ValueSet{ + S3XAmzServerSideEncryption: NewValueSet( + NewStringValue("AES256"), + NewStringValue("aws:kms"), + ), + } + + case5Function, err := newStringNotEqualsIgnoreCaseFunc(S3XAmzMetadataDirective, NewValueSet(NewStringValue("REPLACE"))) + if err != nil { + t.Fatalf("unexpected error. %v\n", err) + } + + case5Result := map[Key]ValueSet{ + S3XAmzMetadataDirective: NewValueSet(NewStringValue("REPLACE")), + } + + case6Function, err := newStringNotEqualsIgnoreCaseFunc(S3XAmzMetadataDirective, + NewValueSet( + NewStringValue("REPLACE"), + NewStringValue("COPY"), + ), + ) + if err != nil { + t.Fatalf("unexpected error. %v\n", err) + } + + case6Result := map[Key]ValueSet{ + S3XAmzMetadataDirective: NewValueSet( + NewStringValue("REPLACE"), + NewStringValue("COPY"), + ), + } + + case7Function, err := newStringNotEqualsIgnoreCaseFunc(S3LocationConstraint, NewValueSet(NewStringValue("eu-west-1"))) + if err != nil { + t.Fatalf("unexpected error. %v\n", err) + } + + case7Result := map[Key]ValueSet{ + S3LocationConstraint: NewValueSet(NewStringValue("eu-west-1")), + } + + case8Function, err := newStringNotEqualsIgnoreCaseFunc(S3LocationConstraint, + NewValueSet( + NewStringValue("eu-west-1"), + NewStringValue("us-west-1"), + ), + ) + if err != nil { + t.Fatalf("unexpected error. %v\n", err) + } + + case8Result := map[Key]ValueSet{ + S3LocationConstraint: NewValueSet( + NewStringValue("eu-west-1"), + NewStringValue("us-west-1"), + ), + } + + testCases := []struct { + f Function + expectedResult map[Key]ValueSet + }{ + {case1Function, case1Result}, + {case2Function, case2Result}, + {case3Function, case3Result}, + {case4Function, case4Result}, + {case5Function, case5Result}, + {case6Function, case6Result}, + {case7Function, case7Result}, + {case8Function, case8Result}, + {&stringNotEqualsFunc{}, nil}, + } + + for i, testCase := range testCases { + result := testCase.f.toMap() + + if !reflect.DeepEqual(result, testCase.expectedResult) { + t.Fatalf("case %v: result: expected: %v, got: %v\n", i+1, testCase.expectedResult, result) + } + } +} + +func TestNewStringEqualsIgnoreCaseFunc(t *testing.T) { + case1Function, err := newStringEqualsIgnoreCaseFunc(S3XAmzCopySource, NewValueSet(NewStringValue("mybucket/myobject"))) + if err != nil { + t.Fatalf("unexpected error. %v\n", err) + } + + case2Function, err := newStringEqualsIgnoreCaseFunc(S3XAmzCopySource, + NewValueSet( + NewStringValue("mybucket/myobject"), + NewStringValue("yourbucket/myobject"), + ), + ) + if err != nil { + t.Fatalf("unexpected error. %v\n", err) + } + + case3Function, err := newStringEqualsIgnoreCaseFunc(S3XAmzServerSideEncryption, NewValueSet(NewStringValue("AES256"))) + if err != nil { + t.Fatalf("unexpected error. %v\n", err) + } + + case4Function, err := newStringEqualsIgnoreCaseFunc(S3XAmzServerSideEncryption, + NewValueSet( + NewStringValue("AES256"), + NewStringValue("aws:kms"), + ), + ) + if err != nil { + t.Fatalf("unexpected error. %v\n", err) + } + + case5Function, err := newStringEqualsIgnoreCaseFunc(S3XAmzMetadataDirective, NewValueSet(NewStringValue("REPLACE"))) + if err != nil { + t.Fatalf("unexpected error. %v\n", err) + } + + case6Function, err := newStringEqualsIgnoreCaseFunc(S3XAmzMetadataDirective, + NewValueSet( + NewStringValue("REPLACE"), + NewStringValue("COPY"), + ), + ) + if err != nil { + t.Fatalf("unexpected error. %v\n", err) + } + + case7Function, err := newStringEqualsIgnoreCaseFunc(S3LocationConstraint, NewValueSet(NewStringValue("eu-west-1"))) + if err != nil { + t.Fatalf("unexpected error. %v\n", err) + } + + case8Function, err := newStringEqualsIgnoreCaseFunc(S3LocationConstraint, + NewValueSet( + NewStringValue("eu-west-1"), + NewStringValue("us-west-1"), + ), + ) + if err != nil { + t.Fatalf("unexpected error. %v\n", err) + } + + testCases := []struct { + key Key + values ValueSet + expectedResult Function + expectErr bool + }{ + {S3XAmzCopySource, NewValueSet(NewStringValue("mybucket/myobject")), case1Function, false}, + {S3XAmzCopySource, + NewValueSet( + NewStringValue("mybucket/myobject"), + NewStringValue("yourbucket/myobject"), + ), case2Function, false}, + + {S3XAmzServerSideEncryption, NewValueSet(NewStringValue("AES256")), case3Function, false}, + {S3XAmzServerSideEncryption, + NewValueSet( + NewStringValue("AES256"), + NewStringValue("aws:kms"), + ), case4Function, false}, + + {S3XAmzMetadataDirective, NewValueSet(NewStringValue("REPLACE")), case5Function, false}, + {S3XAmzMetadataDirective, + NewValueSet( + NewStringValue("REPLACE"), + NewStringValue("COPY"), + ), case6Function, false}, + + {S3LocationConstraint, NewValueSet(NewStringValue("eu-west-1")), case7Function, false}, + {S3LocationConstraint, + NewValueSet( + NewStringValue("eu-west-1"), + NewStringValue("us-west-1"), + ), case8Function, false}, + + // Unsupported value error. + {S3XAmzCopySource, NewValueSet(NewStringValue("mybucket/myobject"), NewIntValue(7)), nil, true}, + {S3XAmzServerSideEncryption, NewValueSet(NewStringValue("AES256"), NewIntValue(7)), nil, true}, + {S3XAmzMetadataDirective, NewValueSet(NewStringValue("REPLACE"), NewIntValue(7)), nil, true}, + {S3LocationConstraint, NewValueSet(NewStringValue("eu-west-1"), NewIntValue(7)), nil, true}, + + // Invalid value error. + {S3XAmzCopySource, NewValueSet(NewStringValue("mybucket")), nil, true}, + {S3XAmzServerSideEncryption, NewValueSet(NewStringValue("SSE-C")), nil, true}, + {S3XAmzMetadataDirective, NewValueSet(NewStringValue("DUPLICATE")), nil, true}, + } + + for i, testCase := range testCases { + result, err := newStringEqualsIgnoreCaseFunc(testCase.key, testCase.values) + expectErr := (err != nil) + + if expectErr != testCase.expectErr { + t.Fatalf("case %v: error: expected: %v, got: %v\n", i+1, testCase.expectErr, expectErr) + } + + if !testCase.expectErr { + if !reflect.DeepEqual(result, testCase.expectedResult) { + t.Fatalf("case %v: result: expected: %v, got: %v\n", i+1, testCase.expectedResult, result) + } + } + } +} + +func TestNewStringNotEqualsIgnoreCaseFunc(t *testing.T) { + case1Function, err := newStringNotEqualsIgnoreCaseFunc(S3XAmzCopySource, NewValueSet(NewStringValue("mybucket/myobject"))) + if err != nil { + t.Fatalf("unexpected error. %v\n", err) + } + + case2Function, err := newStringNotEqualsIgnoreCaseFunc(S3XAmzCopySource, + NewValueSet( + NewStringValue("mybucket/myobject"), + NewStringValue("yourbucket/myobject"), + ), + ) + if err != nil { + t.Fatalf("unexpected error. %v\n", err) + } + + case3Function, err := newStringNotEqualsIgnoreCaseFunc(S3XAmzServerSideEncryption, NewValueSet(NewStringValue("AES256"))) + if err != nil { + t.Fatalf("unexpected error. %v\n", err) + } + + case4Function, err := newStringNotEqualsIgnoreCaseFunc(S3XAmzServerSideEncryption, + NewValueSet( + NewStringValue("AES256"), + NewStringValue("aws:kms"), + ), + ) + if err != nil { + t.Fatalf("unexpected error. %v\n", err) + } + + case5Function, err := newStringNotEqualsIgnoreCaseFunc(S3XAmzMetadataDirective, NewValueSet(NewStringValue("REPLACE"))) + if err != nil { + t.Fatalf("unexpected error. %v\n", err) + } + + case6Function, err := newStringNotEqualsIgnoreCaseFunc(S3XAmzMetadataDirective, + NewValueSet( + NewStringValue("REPLACE"), + NewStringValue("COPY"), + ), + ) + if err != nil { + t.Fatalf("unexpected error. %v\n", err) + } + + case7Function, err := newStringNotEqualsIgnoreCaseFunc(S3LocationConstraint, NewValueSet(NewStringValue("eu-west-1"))) + if err != nil { + t.Fatalf("unexpected error. %v\n", err) + } + + case8Function, err := newStringNotEqualsIgnoreCaseFunc(S3LocationConstraint, + NewValueSet( + NewStringValue("eu-west-1"), + NewStringValue("us-west-1"), + ), + ) + if err != nil { + t.Fatalf("unexpected error. %v\n", err) + } + + testCases := []struct { + key Key + values ValueSet + expectedResult Function + expectErr bool + }{ + {S3XAmzCopySource, NewValueSet(NewStringValue("mybucket/myobject")), case1Function, false}, + {S3XAmzCopySource, + NewValueSet( + NewStringValue("mybucket/myobject"), + NewStringValue("yourbucket/myobject"), + ), case2Function, false}, + + {S3XAmzServerSideEncryption, NewValueSet(NewStringValue("AES256")), case3Function, false}, + {S3XAmzServerSideEncryption, + NewValueSet( + NewStringValue("AES256"), + NewStringValue("aws:kms"), + ), case4Function, false}, + + {S3XAmzMetadataDirective, NewValueSet(NewStringValue("REPLACE")), case5Function, false}, + {S3XAmzMetadataDirective, + NewValueSet( + NewStringValue("REPLACE"), + NewStringValue("COPY"), + ), case6Function, false}, + + {S3LocationConstraint, NewValueSet(NewStringValue("eu-west-1")), case7Function, false}, + {S3LocationConstraint, + NewValueSet( + NewStringValue("eu-west-1"), + NewStringValue("us-west-1"), + ), case8Function, false}, + + // Unsupported value error. + {S3XAmzCopySource, NewValueSet(NewStringValue("mybucket/myobject"), NewIntValue(7)), nil, true}, + {S3XAmzServerSideEncryption, NewValueSet(NewStringValue("AES256"), NewIntValue(7)), nil, true}, + {S3XAmzMetadataDirective, NewValueSet(NewStringValue("REPLACE"), NewIntValue(7)), nil, true}, + {S3LocationConstraint, NewValueSet(NewStringValue("eu-west-1"), NewIntValue(7)), nil, true}, + + // Invalid value error. + {S3XAmzCopySource, NewValueSet(NewStringValue("mybucket")), nil, true}, + {S3XAmzServerSideEncryption, NewValueSet(NewStringValue("SSE-C")), nil, true}, + {S3XAmzMetadataDirective, NewValueSet(NewStringValue("DUPLICATE")), nil, true}, + } + + for i, testCase := range testCases { + result, err := newStringNotEqualsIgnoreCaseFunc(testCase.key, testCase.values) + expectErr := (err != nil) + + if expectErr != testCase.expectErr { + t.Fatalf("case %v: error: expected: %v, got: %v\n", i+1, testCase.expectErr, expectErr) + } + + if !testCase.expectErr { + if !reflect.DeepEqual(result, testCase.expectedResult) { + t.Fatalf("case %v: result: expected: %v, got: %v\n", i+1, testCase.expectedResult, result) + } + } + } +} diff --git a/pkg/policy/condition/stringlikefunc.go b/pkg/policy/condition/stringlikefunc.go index ca05c688e..7927d4cd6 100644 --- a/pkg/policy/condition/stringlikefunc.go +++ b/pkg/policy/condition/stringlikefunc.go @@ -20,8 +20,8 @@ import ( "fmt" "net/http" "sort" - "strings" + "github.com/minio/minio-go/pkg/s3utils" "github.com/minio/minio-go/pkg/set" "github.com/minio/minio/pkg/wildcard" ) @@ -118,12 +118,13 @@ func validateStringLikeValues(n name, key Key, values set.StringSet) error { for _, s := range values.ToSlice() { switch key { case S3XAmzCopySource: - tokens := strings.SplitN(s, "/", 2) - if len(tokens) < 2 { - return fmt.Errorf("invalid value '%v' for '%v' in %v condition", s, key, n) + bucket, object := path2BucketAndObject(s) + if object == "" { + return fmt.Errorf("invalid value '%v' for '%v' for %v condition", s, S3XAmzCopySource, n) + } + if err := s3utils.CheckValidBucketName(bucket); err != nil { + return err } - - // FIXME: tokens[0] must be a valid bucket name. } } diff --git a/pkg/policy/condition/value.go b/pkg/policy/condition/value.go index 2e0ad938c..707555890 100644 --- a/pkg/policy/condition/value.go +++ b/pkg/policy/condition/value.go @@ -21,8 +21,26 @@ import ( "fmt" "reflect" "strconv" + "strings" ) +// Splits an incoming path into bucket and object components. +func path2BucketAndObject(path string) (bucket, object string) { + // Skip the first element if it is '/', split the rest. + path = strings.TrimPrefix(path, "/") + pathComponents := strings.SplitN(path, "/", 2) + + // Save the bucket and object extracted from path. + switch len(pathComponents) { + case 1: + bucket = pathComponents[0] + case 2: + bucket = pathComponents[0] + object = pathComponents[1] + } + return bucket, object +} + // Value - is enum type of string, int or bool. type Value struct { t reflect.Kind