Enhance policy handling to support SSE and WORM (#5790)

- remove old bucket policy handling
- add new policy handling
- add new policy handling unit tests

This patch brings support to bucket policy to have more control not
limiting to anonymous.  Bucket owner controls to allow/deny any rest
API.

For example server side encryption can be controlled by allowing
PUT/GET objects with encryptions including bucket owner.
This commit is contained in:
Bala FA
2018-04-25 04:23:30 +05:30
committed by kannappanr
parent 21a3c0f482
commit 0d52126023
77 changed files with 9811 additions and 2633 deletions

267
pkg/policy/action.go Normal file
View File

@@ -0,0 +1,267 @@
/*
* 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 policy
import (
"encoding/json"
"fmt"
"github.com/minio/minio/pkg/policy/condition"
)
// Action - policy action.
// Refer https://docs.aws.amazon.com/IAM/latest/UserGuide/list_s3.html
// for more information about available actions.
type Action string
const (
// AbortMultipartUploadAction - AbortMultipartUpload Rest API action.
AbortMultipartUploadAction Action = "s3:AbortMultipartUpload"
// CreateBucketAction - CreateBucket Rest API action.
CreateBucketAction = "s3:CreateBucket"
// DeleteBucketAction - DeleteBucket Rest API action.
DeleteBucketAction = "s3:DeleteBucket"
// DeleteBucketPolicyAction - DeleteBucketPolicy Rest API action.
DeleteBucketPolicyAction = "s3:DeleteBucketPolicy"
// DeleteObjectAction - DeleteObject Rest API action.
DeleteObjectAction = "s3:DeleteObject"
// GetBucketLocationAction - GetBucketLocation Rest API action.
GetBucketLocationAction = "s3:GetBucketLocation"
// GetBucketNotificationAction - GetBucketNotification Rest API action.
GetBucketNotificationAction = "s3:GetBucketNotification"
// GetBucketPolicyAction - GetBucketPolicy Rest API action.
GetBucketPolicyAction = "s3:GetBucketPolicy"
// GetObjectAction - GetObject Rest API action.
GetObjectAction = "s3:GetObject"
// HeadBucketAction - HeadBucket Rest API action. This action is unused in minio.
HeadBucketAction = "s3:HeadBucket"
// ListAllMyBucketsAction - ListAllMyBuckets (List buckets) Rest API action.
ListAllMyBucketsAction = "s3:ListAllMyBuckets"
// ListBucketAction - ListBucket Rest API action.
ListBucketAction = "s3:ListBucket"
// ListBucketMultipartUploadsAction - ListMultipartUploads Rest API action.
ListBucketMultipartUploadsAction = "s3:ListBucketMultipartUploads"
// ListenBucketNotificationAction - ListenBucketNotification Rest API action.
// This is Minio extension.
ListenBucketNotificationAction = "s3:ListenBucketNotification"
// ListMultipartUploadPartsAction - ListParts Rest API action.
ListMultipartUploadPartsAction = "s3:ListMultipartUploadParts"
// ListObjectsAction - ListObjects Rest API action exactly same behavior as ListBucketAction.
ListObjectsAction = "s3:ListObjects"
// PutBucketNotificationAction - PutObjectNotification Rest API action.
PutBucketNotificationAction = "s3:PutBucketNotification"
// PutBucketPolicyAction - PutBucketPolicy Rest API action.
PutBucketPolicyAction = "s3:PutBucketPolicy"
// PutObjectAction - PutObject Rest API action.
PutObjectAction = "s3:PutObject"
)
// isObjectAction - returns whether action is object type or not.
func (action Action) isObjectAction() bool {
switch action {
case AbortMultipartUploadAction, DeleteObjectAction, GetObjectAction:
fallthrough
case ListMultipartUploadPartsAction, PutObjectAction:
return true
}
return false
}
// IsValid - checks if action is valid or not.
func (action Action) IsValid() bool {
switch action {
case AbortMultipartUploadAction, CreateBucketAction, DeleteBucketAction:
fallthrough
case DeleteBucketPolicyAction, DeleteObjectAction, GetBucketLocationAction:
fallthrough
case GetBucketNotificationAction, GetBucketPolicyAction, GetObjectAction:
fallthrough
case HeadBucketAction, ListAllMyBucketsAction, ListBucketAction:
fallthrough
case ListBucketMultipartUploadsAction, ListenBucketNotificationAction:
fallthrough
case ListMultipartUploadPartsAction, ListObjectsAction, PutBucketNotificationAction:
fallthrough
case PutBucketPolicyAction, PutObjectAction:
return true
}
return false
}
// MarshalJSON - encodes Action to JSON data.
func (action Action) MarshalJSON() ([]byte, error) {
if action.IsValid() {
return json.Marshal(string(action))
}
return nil, fmt.Errorf("invalid action '%v'", action)
}
// UnmarshalJSON - decodes JSON data to Action.
func (action *Action) UnmarshalJSON(data []byte) error {
var s string
if err := json.Unmarshal(data, &s); err != nil {
return err
}
a := Action(s)
if !a.IsValid() {
return fmt.Errorf("invalid action '%v'", s)
}
*action = a
return nil
}
func parseAction(s string) (Action, error) {
action := Action(s)
if action.IsValid() {
return action, nil
}
return action, fmt.Errorf("unsupported action '%v'", s)
}
// actionConditionKeyMap - holds mapping of supported condition key for an action.
var actionConditionKeyMap = map[Action]condition.KeySet{
AbortMultipartUploadAction: condition.NewKeySet(
condition.AWSReferer,
condition.AWSSourceIP,
),
CreateBucketAction: condition.NewKeySet(
condition.AWSReferer,
condition.AWSSourceIP,
),
DeleteBucketPolicyAction: condition.NewKeySet(
condition.AWSReferer,
condition.AWSSourceIP,
),
DeleteObjectAction: condition.NewKeySet(
condition.AWSReferer,
condition.AWSSourceIP,
),
GetBucketLocationAction: condition.NewKeySet(
condition.AWSReferer,
condition.AWSSourceIP,
),
GetBucketNotificationAction: condition.NewKeySet(
condition.AWSReferer,
condition.AWSSourceIP,
),
GetBucketPolicyAction: condition.NewKeySet(
condition.AWSReferer,
condition.AWSSourceIP,
),
GetObjectAction: condition.NewKeySet(
condition.S3XAmzServerSideEncryption,
condition.S3XAmzServerSideEncryptionAwsKMSKeyID,
condition.S3XAmzStorageClass,
condition.AWSReferer,
condition.AWSSourceIP,
),
HeadBucketAction: condition.NewKeySet(
condition.AWSReferer,
condition.AWSSourceIP,
),
ListAllMyBucketsAction: condition.NewKeySet(
condition.AWSReferer,
condition.AWSSourceIP,
),
ListBucketAction: condition.NewKeySet(
condition.S3Prefix,
condition.S3Delimiter,
condition.S3MaxKeys,
condition.AWSReferer,
condition.AWSSourceIP,
),
ListBucketMultipartUploadsAction: condition.NewKeySet(
condition.AWSReferer,
condition.AWSSourceIP,
),
ListenBucketNotificationAction: condition.NewKeySet(
condition.AWSReferer,
condition.AWSSourceIP,
),
ListMultipartUploadPartsAction: condition.NewKeySet(
condition.AWSReferer,
condition.AWSSourceIP,
),
ListObjectsAction: condition.NewKeySet(
condition.S3Prefix,
condition.S3Delimiter,
condition.S3MaxKeys,
condition.AWSReferer,
condition.AWSSourceIP,
),
PutBucketNotificationAction: condition.NewKeySet(
condition.AWSReferer,
condition.AWSSourceIP,
),
PutBucketPolicyAction: condition.NewKeySet(
condition.AWSReferer,
condition.AWSSourceIP,
),
PutObjectAction: condition.NewKeySet(
condition.S3XAmzCopySource,
condition.S3XAmzServerSideEncryption,
condition.S3XAmzServerSideEncryptionAwsKMSKeyID,
condition.S3XAmzMetadataDirective,
condition.S3XAmzStorageClass,
condition.AWSReferer,
condition.AWSSourceIP,
),
}

116
pkg/policy/action_test.go Normal file
View File

@@ -0,0 +1,116 @@
/*
* 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 policy
import (
"encoding/json"
"reflect"
"testing"
)
func TestActionIsObjectAction(t *testing.T) {
testCases := []struct {
action Action
expectedResult bool
}{
{AbortMultipartUploadAction, true},
{DeleteObjectAction, true},
{GetObjectAction, true},
{ListMultipartUploadPartsAction, true},
{PutObjectAction, true},
{CreateBucketAction, false},
}
for i, testCase := range testCases {
result := testCase.action.isObjectAction()
if testCase.expectedResult != result {
t.Fatalf("case %v: expected: %v, got: %v", i+1, testCase.expectedResult, result)
}
}
}
func TestActionIsValid(t *testing.T) {
testCases := []struct {
action Action
expectedResult bool
}{
{AbortMultipartUploadAction, true},
{Action("foo"), false},
}
for i, testCase := range testCases {
result := testCase.action.IsValid()
if testCase.expectedResult != result {
t.Fatalf("case %v: expected: %v, got: %v", i+1, testCase.expectedResult, result)
}
}
}
func TestActionMarshalJSON(t *testing.T) {
testCases := []struct {
action Action
expectedResult []byte
expectErr bool
}{
{PutObjectAction, []byte(`"s3:PutObject"`), false},
{Action("foo"), nil, true},
}
for i, testCase := range testCases {
result, err := json.Marshal(testCase.action)
expectErr := (err != nil)
if testCase.expectErr != expectErr {
t.Fatalf("case %v: error: expected: %v, got: %v", i+1, testCase.expectErr, expectErr)
}
if !testCase.expectErr {
if !reflect.DeepEqual(result, testCase.expectedResult) {
t.Fatalf("case %v: result: expected: %v, got: %v", i+1, testCase.expectedResult, result)
}
}
}
}
func TestActionUnmarshalJSON(t *testing.T) {
testCases := []struct {
data []byte
expectedResult Action
expectErr bool
}{
{[]byte(`"s3:PutObject"`), PutObjectAction, false},
{[]byte(`"foo"`), Action(""), true},
}
for i, testCase := range testCases {
var result Action
err := json.Unmarshal(testCase.data, &result)
expectErr := (err != nil)
if testCase.expectErr != expectErr {
t.Fatalf("case %v: error: expected: %v, got: %v", i+1, testCase.expectErr, expectErr)
}
if !testCase.expectErr {
if testCase.expectedResult != result {
t.Fatalf("case %v: result: expected: %v, got: %v", i+1, testCase.expectedResult, result)
}
}
}
}

114
pkg/policy/actionset.go Normal file
View File

@@ -0,0 +1,114 @@
/*
* 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 policy
import (
"encoding/json"
"fmt"
"sort"
"github.com/minio/minio-go/pkg/set"
)
// ActionSet - set of actions.
type ActionSet map[Action]struct{}
// Add - add action to the set.
func (actionSet ActionSet) Add(action Action) {
actionSet[action] = struct{}{}
}
// Contains - checks given action exists in the action set.
func (actionSet ActionSet) Contains(action Action) bool {
_, found := actionSet[action]
return found
}
// Intersection - returns actions available in both ActionSet.
func (actionSet ActionSet) Intersection(sset ActionSet) ActionSet {
nset := NewActionSet()
for k := range actionSet {
if _, ok := sset[k]; ok {
nset.Add(k)
}
}
return nset
}
// MarshalJSON - encodes ActionSet to JSON data.
func (actionSet ActionSet) MarshalJSON() ([]byte, error) {
if len(actionSet) == 0 {
return nil, fmt.Errorf("empty action set")
}
return json.Marshal(actionSet.ToSlice())
}
func (actionSet ActionSet) String() string {
actions := []string{}
for action := range actionSet {
actions = append(actions, string(action))
}
sort.Strings(actions)
return fmt.Sprintf("%v", actions)
}
// ToSlice - returns slice of actions from the action set.
func (actionSet ActionSet) ToSlice() []Action {
actions := []Action{}
for action := range actionSet {
actions = append(actions, action)
}
return actions
}
// UnmarshalJSON - decodes JSON data to ActionSet.
func (actionSet *ActionSet) UnmarshalJSON(data []byte) error {
var sset set.StringSet
if err := json.Unmarshal(data, &sset); err != nil {
return err
}
if len(sset) == 0 {
return fmt.Errorf("empty action set")
}
*actionSet = make(ActionSet)
for _, s := range sset.ToSlice() {
action, err := parseAction(s)
if err != nil {
return err
}
actionSet.Add(action)
}
return nil
}
// NewActionSet - creates new action set.
func NewActionSet(actions ...Action) ActionSet {
actionSet := make(ActionSet)
for _, action := range actions {
actionSet.Add(action)
}
return actionSet
}

View File

@@ -0,0 +1,158 @@
/*
* 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 policy
import (
"encoding/json"
"reflect"
"testing"
)
func TestActionSetAdd(t *testing.T) {
testCases := []struct {
set ActionSet
action Action
expectedResult ActionSet
}{
{NewActionSet(), PutObjectAction, NewActionSet(PutObjectAction)},
{NewActionSet(PutObjectAction), PutObjectAction, NewActionSet(PutObjectAction)},
}
for i, testCase := range testCases {
testCase.set.Add(testCase.action)
if !reflect.DeepEqual(testCase.expectedResult, testCase.set) {
t.Fatalf("case %v: expected: %v, got: %v\n", i+1, testCase.expectedResult, testCase.set)
}
}
}
func TestActionSetContains(t *testing.T) {
testCases := []struct {
set ActionSet
action Action
expectedResult bool
}{
{NewActionSet(PutObjectAction), PutObjectAction, true},
{NewActionSet(PutObjectAction, GetObjectAction), PutObjectAction, true},
{NewActionSet(PutObjectAction, GetObjectAction), AbortMultipartUploadAction, false},
}
for i, testCase := range testCases {
result := testCase.set.Contains(testCase.action)
if result != testCase.expectedResult {
t.Fatalf("case %v: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
}
}
}
func TestActionSetIntersection(t *testing.T) {
testCases := []struct {
set ActionSet
setToIntersect ActionSet
expectedResult ActionSet
}{
{NewActionSet(), NewActionSet(PutObjectAction), NewActionSet()},
{NewActionSet(PutObjectAction), NewActionSet(), NewActionSet()},
{NewActionSet(PutObjectAction), NewActionSet(PutObjectAction, GetObjectAction), NewActionSet(PutObjectAction)},
}
for i, testCase := range testCases {
result := testCase.set.Intersection(testCase.setToIntersect)
if !reflect.DeepEqual(result, testCase.expectedResult) {
t.Fatalf("case %v: expected: %v, got: %v\n", i+1, testCase.expectedResult, testCase.set)
}
}
}
func TestActionSetMarshalJSON(t *testing.T) {
testCases := []struct {
actionSet ActionSet
expectedResult []byte
expectErr bool
}{
{NewActionSet(PutObjectAction), []byte(`["s3:PutObject"]`), false},
{NewActionSet(), nil, true},
}
for i, testCase := range testCases {
result, err := json.Marshal(testCase.actionSet)
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, string(testCase.expectedResult), string(result))
}
}
}
}
func TestActionSetToSlice(t *testing.T) {
testCases := []struct {
actionSet ActionSet
expectedResult []Action
}{
{NewActionSet(PutObjectAction), []Action{PutObjectAction}},
{NewActionSet(), []Action{}},
}
for i, testCase := range testCases {
result := testCase.actionSet.ToSlice()
if !reflect.DeepEqual(result, testCase.expectedResult) {
t.Fatalf("case %v: result: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
}
}
}
func TestActionSetUnmarshalJSON(t *testing.T) {
testCases := []struct {
data []byte
expectedResult ActionSet
expectErr bool
}{
{[]byte(`"s3:PutObject"`), NewActionSet(PutObjectAction), false},
{[]byte(`["s3:PutObject"]`), NewActionSet(PutObjectAction), false},
{[]byte(`["s3:PutObject", "s3:GetObject"]`), NewActionSet(PutObjectAction, GetObjectAction), false},
{[]byte(`["s3:PutObject", "s3:GetObject", "s3:PutObject"]`), NewActionSet(PutObjectAction, GetObjectAction), false},
{[]byte(`[]`), NewActionSet(), true}, // Empty array.
{[]byte(`"foo"`), nil, true}, // Invalid action.
{[]byte(`["s3:PutObject", "foo"]`), nil, true}, // Invalid action.
}
for i, testCase := range testCases {
result := make(ActionSet)
err := json.Unmarshal(testCase.data, &result)
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)
}
}
}
}

View File

@@ -0,0 +1,168 @@
/*
* 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/json"
"fmt"
"sort"
)
// Function - condition function interface.
type Function interface {
// evaluate() - evaluates this condition function with given values.
evaluate(values map[string][]string) bool
// key() - returns condition key used in this function.
key() Key
// name() - returns condition name of this function.
name() name
// String() - returns string representation of function.
String() string
// toMap - returns map representation of this function.
toMap() map[Key]ValueSet
}
// Functions - list of functions.
type Functions []Function
// Evaluate - evaluates all functions with given values map. Each function is evaluated
// sequencely and next function is called only if current function succeeds.
func (functions Functions) Evaluate(values map[string][]string) bool {
for _, f := range functions {
if !f.evaluate(values) {
return false
}
}
return true
}
// Keys - returns list of keys used in all functions.
func (functions Functions) Keys() KeySet {
keySet := NewKeySet()
for _, f := range functions {
keySet.Add(f.key())
}
return keySet
}
// MarshalJSON - encodes Functions to JSON data.
func (functions Functions) MarshalJSON() ([]byte, error) {
nm := make(map[name]map[Key]ValueSet)
for _, f := range functions {
nm[f.name()] = f.toMap()
}
return json.Marshal(nm)
}
func (functions Functions) String() string {
funcStrings := []string{}
for _, f := range functions {
s := fmt.Sprintf("%v", f)
funcStrings = append(funcStrings, s)
}
sort.Strings(funcStrings)
return fmt.Sprintf("%v", funcStrings)
}
// UnmarshalJSON - decodes JSON data to Functions.
func (functions *Functions) UnmarshalJSON(data []byte) error {
// As string kind, int kind then json.Unmarshaler is checked at
// https://github.com/golang/go/blob/master/src/encoding/json/decode.go#L618
// UnmarshalJSON() is not called for types extending string
// see https://play.golang.org/p/HrSsKksHvrS, better way to do is
// https://play.golang.org/p/y9ElWpBgVAB
//
// Due to this issue, name and Key types cannot be used as map keys below.
nm := make(map[string]map[string]ValueSet)
if err := json.Unmarshal(data, &nm); err != nil {
return err
}
if len(nm) == 0 {
return fmt.Errorf("condition must not be empty")
}
funcs := []Function{}
for nameString, args := range nm {
n, err := parseName(nameString)
if err != nil {
return err
}
for keyString, values := range args {
key, err := parseKey(keyString)
if err != nil {
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)
}
funcs = append(funcs, f)
}
}
*functions = funcs
return nil
}
// NewFunctions - returns new Functions with given function list.
func NewFunctions(functions ...Function) Functions {
return Functions(functions)
}

View File

@@ -0,0 +1,298 @@
/*
* 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/json"
"reflect"
"testing"
)
func TestFunctionsEvaluate(t *testing.T) {
func1, err := newNullFunc(S3XAmzCopySource, NewValueSet(NewBoolValue(true)))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
func2, err := newIPAddressFunc(AWSSourceIP, NewValueSet(NewStringValue("192.168.1.0/24")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
func3, err := newStringEqualsFunc(S3XAmzCopySource, NewValueSet(NewStringValue("mybucket/myobject")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
func4, err := newStringLikeFunc(S3XAmzCopySource, NewValueSet(NewStringValue("mybucket/myobject*")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case1Function := NewFunctions(func1, func2, func3, func4)
testCases := []struct {
functions Functions
values map[string][]string
expectedResult bool
}{
{case1Function, map[string][]string{
"x-amz-copy-source": {"mybucket/myobject"},
"SourceIp": {"192.168.1.10"},
}, true},
{case1Function, map[string][]string{
"x-amz-copy-source": {"mybucket/myobject"},
"SourceIp": {"192.168.1.10"},
"Refer": {"http://example.org/"},
}, true},
{case1Function, map[string][]string{"x-amz-copy-source": {"mybucket/myobject"}}, false},
{case1Function, map[string][]string{"SourceIp": {"192.168.1.10"}}, false},
{case1Function, map[string][]string{
"x-amz-copy-source": {"mybucket/yourobject"},
"SourceIp": {"192.168.1.10"},
}, false},
{case1Function, map[string][]string{
"x-amz-copy-source": {"mybucket/myobject"},
"SourceIp": {"192.168.2.10"},
}, false},
{case1Function, map[string][]string{
"x-amz-copy-source": {"mybucket/myobject"},
"Refer": {"http://example.org/"},
}, false},
}
for i, testCase := range testCases {
result := testCase.functions.Evaluate(testCase.values)
if result != testCase.expectedResult {
t.Fatalf("case %v: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
}
}
}
func TestFunctionsKeys(t *testing.T) {
func1, err := newNullFunc(S3XAmzCopySource, NewValueSet(NewBoolValue(true)))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
func2, err := newIPAddressFunc(AWSSourceIP, NewValueSet(NewStringValue("192.168.1.0/24")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
func3, err := newStringEqualsFunc(S3XAmzCopySource, NewValueSet(NewStringValue("mybucket/myobject")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
func4, err := newStringLikeFunc(S3XAmzCopySource, NewValueSet(NewStringValue("mybucket/myobject*")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
testCases := []struct {
functions Functions
expectedResult KeySet
}{
{NewFunctions(func1, func2, func3, func4), NewKeySet(S3XAmzCopySource, AWSSourceIP)},
}
for i, testCase := range testCases {
result := testCase.functions.Keys()
if !reflect.DeepEqual(result, testCase.expectedResult) {
t.Fatalf("case %v: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
}
}
}
func TestFunctionsMarshalJSON(t *testing.T) {
func1, err := newStringLikeFunc(S3XAmzMetadataDirective, NewValueSet(NewStringValue("REPL*")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
func2, err := newStringEqualsFunc(S3XAmzCopySource, NewValueSet(NewStringValue("mybucket/myobject")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
func3, err := newStringNotEqualsFunc(S3XAmzServerSideEncryption, NewValueSet(NewStringValue("AES256")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
func4, err := newNotIPAddressFunc(AWSSourceIP,
NewValueSet(NewStringValue("10.1.10.0/24")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
func5, err := newStringNotLikeFunc(S3XAmzStorageClass, NewValueSet(NewStringValue("STANDARD")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
func6, err := newNullFunc(S3XAmzServerSideEncryptionAwsKMSKeyID, NewValueSet(NewBoolValue(true)))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
func7, err := newIPAddressFunc(AWSSourceIP,
NewValueSet(NewStringValue("192.168.1.0/24")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case1Result := []byte(`{"IpAddress":{"aws:SourceIp":["192.168.1.0/24"]},"NotIpAddress":{"aws:SourceIp":["10.1.10.0/24"]},"Null":{"s3:x-amz-server-side-encryption-aws-kms-key-id":[true]},"StringEquals":{"s3:x-amz-copy-source":["mybucket/myobject"]},"StringLike":{"s3:x-amz-metadata-directive":["REPL*"]},"StringNotEquals":{"s3:x-amz-server-side-encryption":["AES256"]},"StringNotLike":{"s3:x-amz-storage-class":["STANDARD"]}}`)
case2Result := []byte(`{"Null":{"s3:x-amz-server-side-encryption-aws-kms-key-id":[true]}}`)
testCases := []struct {
functions Functions
expectedResult []byte
expectErr bool
}{
{NewFunctions(func1, func2, func3, func4, func5, func6, func7), case1Result, false},
{NewFunctions(func6), case2Result, false},
{NewFunctions(), []byte(`{}`), false},
{nil, []byte(`{}`), false},
}
for i, testCase := range testCases {
result, err := json.Marshal(testCase.functions)
expectErr := (err != nil)
if testCase.expectErr != expectErr {
t.Fatalf("case %v: error: expected: %v, got: %v", i+1, testCase.expectErr, expectErr)
}
if !testCase.expectErr {
if !reflect.DeepEqual(result, testCase.expectedResult) {
t.Fatalf("case %v: result: expected: %v, got: %v", i+1, string(testCase.expectedResult), string(result))
}
}
}
}
func TestFunctionsUnmarshalJSON(t *testing.T) {
case1Data := []byte(`{
"StringLike": {
"s3:x-amz-metadata-directive": "REPL*"
},
"StringEquals": {
"s3:x-amz-copy-source": "mybucket/myobject"
},
"StringNotEquals": {
"s3:x-amz-server-side-encryption": "AES256"
},
"NotIpAddress": {
"aws:SourceIp": [
"10.1.10.0/24",
"10.10.1.0/24"
]
},
"StringNotLike": {
"s3:x-amz-storage-class": "STANDARD"
},
"Null": {
"s3:x-amz-server-side-encryption-aws-kms-key-id": true
},
"IpAddress": {
"aws:SourceIp": [
"192.168.1.0/24",
"192.168.2.0/24"
]
}
}`)
func1, err := newStringLikeFunc(S3XAmzMetadataDirective, NewValueSet(NewStringValue("REPL*")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
func2, err := newStringEqualsFunc(S3XAmzCopySource, NewValueSet(NewStringValue("mybucket/myobject")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
func3, err := newStringNotEqualsFunc(S3XAmzServerSideEncryption, NewValueSet(NewStringValue("AES256")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
func4, err := newNotIPAddressFunc(AWSSourceIP,
NewValueSet(NewStringValue("10.1.10.0/24"), NewStringValue("10.10.1.0/24")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
func5, err := newStringNotLikeFunc(S3XAmzStorageClass, NewValueSet(NewStringValue("STANDARD")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
func6, err := newNullFunc(S3XAmzServerSideEncryptionAwsKMSKeyID, NewValueSet(NewBoolValue(true)))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
func7, err := newIPAddressFunc(AWSSourceIP,
NewValueSet(NewStringValue("192.168.1.0/24"), NewStringValue("192.168.2.0/24")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case2Data := []byte(`{
"Null": {
"s3:x-amz-server-side-encryption-aws-kms-key-id": true
},
"Null": {
"s3:x-amz-server-side-encryption-aws-kms-key-id": "true"
}
}`)
case3Data := []byte(`{}`)
testCases := []struct {
data []byte
expectedResult Functions
expectErr bool
}{
{case1Data, NewFunctions(func1, func2, func3, func4, func5, func6, func7), false},
// duplicate condition error.
{case2Data, NewFunctions(func6), false},
// empty condition error.
{case3Data, nil, true},
}
for i, testCase := range testCases {
result := new(Functions)
err := json.Unmarshal(testCase.data, result)
expectErr := (err != nil)
if testCase.expectErr != expectErr {
t.Fatalf("case %v: error: expected: %v, got: %v", i+1, testCase.expectErr, expectErr)
}
if !testCase.expectErr {
if (*result).String() != testCase.expectedResult.String() {
t.Fatalf("case %v: result: expected: %v, got: %v", i+1, testCase.expectedResult, *result)
}
}
}
}

View File

@@ -0,0 +1,180 @@
/*
* 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"
"sort"
)
func toIPAddressFuncString(n name, key Key, values []*net.IPNet) string {
valueStrings := []string{}
for _, value := range values {
valueStrings = append(valueStrings, value.String())
}
sort.Strings(valueStrings)
return fmt.Sprintf("%v:%v:%v", n, key, valueStrings)
}
// ipAddressFunc - IP address function. It checks whether value by Key in given
// values is in IP network. Here Key must be AWSSourceIP.
// For example,
// - if values = [192.168.1.0/24], at evaluate() it returns whether IP address
// in value map for AWSSourceIP falls in the network 192.168.1.10/24.
type ipAddressFunc struct {
k Key
values []*net.IPNet
}
// evaluate() - evaluates to check whether IP address in values map for AWSSourceIP
// falls in one of network or not.
func (f ipAddressFunc) evaluate(values map[string][]string) bool {
IPs := []net.IP{}
for _, s := range values[f.k.Name()] {
IP := net.ParseIP(s)
if IP == nil {
panic(fmt.Errorf("invalid IP address '%v'", s))
}
IPs = append(IPs, IP)
}
for _, IP := range IPs {
for _, IPNet := range f.values {
if IPNet.Contains(IP) {
return true
}
}
}
return false
}
// key() - returns condition key which is used by this condition function.
// Key is always AWSSourceIP.
func (f ipAddressFunc) key() Key {
return f.k
}
// name() - returns "IpAddress" condition name.
func (f ipAddressFunc) name() name {
return ipAddress
}
func (f ipAddressFunc) String() string {
return toIPAddressFuncString(ipAddress, f.k, f.values)
}
// toMap - returns map representation of this function.
func (f ipAddressFunc) toMap() map[Key]ValueSet {
if !f.k.IsValid() {
return nil
}
values := NewValueSet()
for _, value := range f.values {
values.Add(NewStringValue(value.String()))
}
return map[Key]ValueSet{
f.k: values,
}
}
// notIPAddressFunc - Not IP address function. It checks whether value by Key in given
// values is NOT in IP network. Here Key must be AWSSourceIP.
// For example,
// - if values = [192.168.1.0/24], at evaluate() it returns whether IP address
// in value map for AWSSourceIP does not fall in the network 192.168.1.10/24.
type notIPAddressFunc struct {
ipAddressFunc
}
// evaluate() - evaluates to check whether IP address in values map for AWSSourceIP
// does not fall in one of network.
func (f notIPAddressFunc) evaluate(values map[string][]string) bool {
return !f.ipAddressFunc.evaluate(values)
}
// name() - returns "NotIpAddress" condition name.
func (f notIPAddressFunc) name() name {
return notIPAddress
}
func (f notIPAddressFunc) String() string {
return toIPAddressFuncString(notIPAddress, f.ipAddressFunc.k, f.ipAddressFunc.values)
}
func valuesToIPNets(n name, values ValueSet) ([]*net.IPNet, error) {
IPNets := []*net.IPNet{}
for v := range values {
s, err := v.GetString()
if err != nil {
return nil, fmt.Errorf("value %v must be string representation of CIDR for %v condition", v, n)
}
var IPNet *net.IPNet
_, IPNet, err = net.ParseCIDR(s)
if err != nil {
return nil, fmt.Errorf("value %v must be CIDR string for %v condition", s, n)
}
IPNets = append(IPNets, IPNet)
}
return IPNets, nil
}
// newIPAddressFunc - returns new IP address function.
func newIPAddressFunc(key Key, values ValueSet) (Function, error) {
IPNets, err := valuesToIPNets(ipAddress, values)
if err != nil {
return nil, err
}
return NewIPAddressFunc(key, IPNets...)
}
// NewIPAddressFunc - returns new IP address function.
func NewIPAddressFunc(key Key, IPNets ...*net.IPNet) (Function, error) {
if key != AWSSourceIP {
return nil, fmt.Errorf("only %v key is allowed for %v condition", AWSSourceIP, ipAddress)
}
return &ipAddressFunc{key, IPNets}, nil
}
// newNotIPAddressFunc - returns new Not IP address function.
func newNotIPAddressFunc(key Key, values ValueSet) (Function, error) {
IPNets, err := valuesToIPNets(notIPAddress, values)
if err != nil {
return nil, err
}
return NewNotIPAddressFunc(key, IPNets...)
}
// NewNotIPAddressFunc - returns new Not IP address function.
func NewNotIPAddressFunc(key Key, IPNets ...*net.IPNet) (Function, error) {
if key != AWSSourceIP {
return nil, fmt.Errorf("only %v key is allowed for %v condition", AWSSourceIP, notIPAddress)
}
return &notIPAddressFunc{ipAddressFunc{key, IPNets}}, nil
}

View File

@@ -0,0 +1,278 @@
/*
* 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 TestIPAddressFuncEvaluate(t *testing.T) {
case1Function, err := newIPAddressFunc(AWSSourceIP, NewValueSet(NewStringValue("192.168.1.0/24")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
testCases := []struct {
function Function
values map[string][]string
expectedResult bool
}{
{case1Function, map[string][]string{"SourceIp": {"192.168.1.10"}}, true},
{case1Function, map[string][]string{"SourceIp": {"192.168.2.10"}}, false},
{case1Function, map[string][]string{}, false},
{case1Function, 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 TestIPAddressFuncKey(t *testing.T) {
case1Function, err := newIPAddressFunc(AWSSourceIP, NewValueSet(NewStringValue("192.168.1.0/24")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
testCases := []struct {
function Function
expectedResult Key
}{
{case1Function, AWSSourceIP},
}
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 TestIPAddressFuncToMap(t *testing.T) {
case1Function, err := newIPAddressFunc(AWSSourceIP, NewValueSet(NewStringValue("192.168.1.0/24")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case2Function, err := newIPAddressFunc(AWSSourceIP, NewValueSet(NewStringValue("192.168.1.0/24"), NewStringValue("10.1.10.1/32")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case1Result := map[Key]ValueSet{
AWSSourceIP: NewValueSet(NewStringValue("192.168.1.0/24")),
}
case2Result := map[Key]ValueSet{
AWSSourceIP: NewValueSet(NewStringValue("192.168.1.0/24"), NewStringValue("10.1.10.1/32")),
}
testCases := []struct {
f Function
expectedResult map[Key]ValueSet
}{
{case1Function, case1Result},
{case2Function, case2Result},
{&ipAddressFunc{}, 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 TestNotIPAddressFuncEvaluate(t *testing.T) {
case1Function, err := newNotIPAddressFunc(AWSSourceIP, NewValueSet(NewStringValue("192.168.1.0/24")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
testCases := []struct {
function Function
values map[string][]string
expectedResult bool
}{
{case1Function, map[string][]string{"SourceIp": {"192.168.2.10"}}, true},
{case1Function, map[string][]string{}, true},
{case1Function, map[string][]string{"delimiter": {"/"}}, true},
{case1Function, map[string][]string{"SourceIp": {"192.168.1.10"}}, 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 TestNotIPAddressFuncKey(t *testing.T) {
case1Function, err := newNotIPAddressFunc(AWSSourceIP, NewValueSet(NewStringValue("192.168.1.0/24")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
testCases := []struct {
function Function
expectedResult Key
}{
{case1Function, AWSSourceIP},
}
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 TestNotIPAddressFuncToMap(t *testing.T) {
case1Function, err := newNotIPAddressFunc(AWSSourceIP, NewValueSet(NewStringValue("192.168.1.0/24")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case2Function, err := newNotIPAddressFunc(AWSSourceIP, NewValueSet(NewStringValue("192.168.1.0/24"), NewStringValue("10.1.10.1/32")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case1Result := map[Key]ValueSet{
AWSSourceIP: NewValueSet(NewStringValue("192.168.1.0/24")),
}
case2Result := map[Key]ValueSet{
AWSSourceIP: NewValueSet(NewStringValue("192.168.1.0/24"), NewStringValue("10.1.10.1/32")),
}
testCases := []struct {
f Function
expectedResult map[Key]ValueSet
}{
{case1Function, case1Result},
{case2Function, case2Result},
{&notIPAddressFunc{}, 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 TestNewIPAddressFunc(t *testing.T) {
case1Function, err := newIPAddressFunc(AWSSourceIP, NewValueSet(NewStringValue("192.168.1.0/24")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case2Function, err := newIPAddressFunc(AWSSourceIP, NewValueSet(NewStringValue("192.168.1.0/24"), NewStringValue("10.1.10.1/32")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
testCases := []struct {
key Key
values ValueSet
expectedResult Function
expectErr bool
}{
{AWSSourceIP, NewValueSet(NewStringValue("192.168.1.0/24")), case1Function, false},
{AWSSourceIP, NewValueSet(NewStringValue("192.168.1.0/24"), NewStringValue("10.1.10.1/32")), case2Function, false},
// Unsupported key error.
{S3Prefix, NewValueSet(NewStringValue("192.168.1.0/24")), nil, true},
// Invalid value error.
{AWSSourceIP, NewValueSet(NewStringValue("node1.example.org")), nil, true},
// Invalid CIDR format error.
{AWSSourceIP, NewValueSet(NewStringValue("192.168.1.0.0/24")), nil, true},
}
for i, testCase := range testCases {
result, err := newIPAddressFunc(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 result.String() != testCase.expectedResult.String() {
t.Fatalf("case %v: result: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
}
}
}
}
func TestNewNotIPAddressFunc(t *testing.T) {
case1Function, err := newNotIPAddressFunc(AWSSourceIP, NewValueSet(NewStringValue("192.168.1.0/24")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case2Function, err := newNotIPAddressFunc(AWSSourceIP, NewValueSet(NewStringValue("192.168.1.0/24"), NewStringValue("10.1.10.1/32")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
testCases := []struct {
key Key
values ValueSet
expectedResult Function
expectErr bool
}{
{AWSSourceIP, NewValueSet(NewStringValue("192.168.1.0/24")), case1Function, false},
{AWSSourceIP, NewValueSet(NewStringValue("192.168.1.0/24"), NewStringValue("10.1.10.1/32")), case2Function, false},
// Unsupported key error.
{S3Prefix, NewValueSet(NewStringValue("192.168.1.0/24")), nil, true},
// Invalid value error.
{AWSSourceIP, NewValueSet(NewStringValue("node1.example.org")), nil, true},
// Invalid CIDR format error.
{AWSSourceIP, NewValueSet(NewStringValue("192.168.1.0.0/24")), nil, true},
}
for i, testCase := range testCases {
result, err := newNotIPAddressFunc(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 result.String() != testCase.expectedResult.String() {
t.Fatalf("case %v: result: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
}
}
}
}

186
pkg/policy/condition/key.go Normal file
View File

@@ -0,0 +1,186 @@
/*
* 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/json"
"fmt"
"strings"
)
// Key - conditional key which is used to fetch values for any condition.
// Refer https://docs.aws.amazon.com/IAM/latest/UserGuide/list_s3.html
// for more information about available condition keys.
type Key string
const (
// S3XAmzCopySource - key representing x-amz-copy-source HTTP header applicable to PutObject API only.
S3XAmzCopySource Key = "s3:x-amz-copy-source"
// S3XAmzServerSideEncryption - key representing x-amz-server-side-encryption HTTP header applicable
// to PutObject API only.
S3XAmzServerSideEncryption = "s3:x-amz-server-side-encryption"
// S3XAmzServerSideEncryptionAwsKMSKeyID - key representing x-amz-server-side-encryption-aws-kms-key-id
// HTTP header applicable to PutObject API only.
S3XAmzServerSideEncryptionAwsKMSKeyID = "s3:x-amz-server-side-encryption-aws-kms-key-id"
// S3XAmzServerSideEncryptionCustomerAlgorithm - key representing
// x-amz-server-side-encryption-customer-algorithm HTTP header applicable to PutObject API only.
S3XAmzServerSideEncryptionCustomerAlgorithm = "s3:x-amz-server-side-encryption-customer-algorithm"
// S3XAmzMetadataDirective - key representing x-amz-metadata-directive HTTP header applicable to
// PutObject API only.
S3XAmzMetadataDirective = "s3:x-amz-metadata-directive"
// S3XAmzStorageClass - key representing x-amz-storage-class HTTP header applicable to PutObject API
// only.
S3XAmzStorageClass = "s3:x-amz-storage-class"
// S3LocationConstraint - key representing LocationConstraint XML tag of CreateBucket API only.
S3LocationConstraint = "s3:LocationConstraint"
// S3Prefix - key representing prefix query parameter of ListBucket API only.
S3Prefix = "s3:prefix"
// S3Delimiter - key representing delimiter query parameter of ListBucket API only.
S3Delimiter = "s3:delimiter"
// S3MaxKeys - key representing max-keys query parameter of ListBucket API only.
S3MaxKeys = "s3:max-keys"
// AWSReferer - key representing Referer header of any API.
AWSReferer = "aws:Referer"
// AWSSourceIP - key representing client's IP address (not intermittent proxies) of any API.
AWSSourceIP = "aws:SourceIp"
)
// 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
}
return false
}
// MarshalJSON - encodes Key to JSON data.
func (key Key) MarshalJSON() ([]byte, error) {
if !key.IsValid() {
return nil, fmt.Errorf("unknown key %v", key)
}
return json.Marshal(string(key))
}
// Name - returns key name which is stripped value of prefixes "aws:" and "s3:"
func (key Key) Name() string {
keyString := string(key)
if strings.HasPrefix(keyString, "aws:") {
return strings.TrimPrefix(keyString, "aws:")
}
return strings.TrimPrefix(keyString, "s3:")
}
// UnmarshalJSON - decodes JSON data to Key.
func (key *Key) UnmarshalJSON(data []byte) error {
var s string
if err := json.Unmarshal(data, &s); err != nil {
return err
}
parsedKey, err := parseKey(s)
if err != nil {
return err
}
*key = parsedKey
return nil
}
func parseKey(s string) (Key, error) {
key := Key(s)
if key.IsValid() {
return key, nil
}
return key, fmt.Errorf("invalid condition key '%v'", s)
}
// KeySet - set representation of slice of keys.
type KeySet map[Key]struct{}
// Add - add a key to key set.
func (set KeySet) Add(key Key) {
set[key] = struct{}{}
}
// Difference - returns a key set contains difference of two keys.
// Example:
// keySet1 := ["one", "two", "three"]
// keySet2 := ["two", "four", "three"]
// keySet1.Difference(keySet2) == ["one"]
func (set KeySet) Difference(sset KeySet) KeySet {
nset := make(KeySet)
for k := range set {
if _, ok := sset[k]; !ok {
nset.Add(k)
}
}
return nset
}
// IsEmpty - returns whether key set is empty or not.
func (set KeySet) IsEmpty() bool {
return len(set) == 0
}
func (set KeySet) String() string {
return fmt.Sprintf("%v", set.ToSlice())
}
// ToSlice - returns slice of keys.
func (set KeySet) ToSlice() []Key {
keys := []Key{}
for key := range set {
keys = append(keys, key)
}
return keys
}
// NewKeySet - returns new KeySet contains given keys.
func NewKeySet(keys ...Key) KeySet {
set := make(KeySet)
for _, key := range keys {
set.Add(key)
}
return set
}

View File

@@ -0,0 +1,214 @@
/*
* 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/json"
"reflect"
"testing"
)
func TestKeyIsValid(t *testing.T) {
testCases := []struct {
key Key
expectedResult bool
}{
{S3XAmzCopySource, true},
{S3XAmzServerSideEncryption, true},
{S3XAmzServerSideEncryptionAwsKMSKeyID, true},
{S3XAmzMetadataDirective, true},
{S3XAmzStorageClass, true},
{S3LocationConstraint, true},
{S3Prefix, true},
{S3Delimiter, true},
{S3MaxKeys, true},
{AWSReferer, true},
{AWSSourceIP, true},
{Key("foo"), false},
}
for i, testCase := range testCases {
result := testCase.key.IsValid()
if testCase.expectedResult != result {
t.Fatalf("case %v: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
}
}
}
func TestKeyMarshalJSON(t *testing.T) {
testCases := []struct {
key Key
expectedResult []byte
expectErr bool
}{
{S3XAmzCopySource, []byte(`"s3:x-amz-copy-source"`), false},
{Key("foo"), nil, true},
}
for i, testCase := range testCases {
result, err := json.Marshal(testCase.key)
expectErr := (err != nil)
if testCase.expectErr != 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: key: expected: %v, got: %v\n", i+1, string(testCase.expectedResult), string(result))
}
}
}
}
func TestKeyName(t *testing.T) {
testCases := []struct {
key Key
expectedResult string
}{
{S3XAmzCopySource, "x-amz-copy-source"},
{AWSReferer, "Referer"},
}
for i, testCase := range testCases {
result := testCase.key.Name()
if testCase.expectedResult != result {
t.Fatalf("case %v: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
}
}
}
func TestKeyUnmarshalJSON(t *testing.T) {
testCases := []struct {
data []byte
expectedKey Key
expectErr bool
}{
{[]byte(`"s3:x-amz-copy-source"`), S3XAmzCopySource, false},
{[]byte(`"foo"`), Key(""), true},
}
for i, testCase := range testCases {
var key Key
err := json.Unmarshal(testCase.data, &key)
expectErr := (err != nil)
if testCase.expectErr != expectErr {
t.Fatalf("case %v: error: expected: %v, got: %v\n", i+1, testCase.expectErr, expectErr)
}
if !testCase.expectErr {
if testCase.expectedKey != key {
t.Fatalf("case %v: key: expected: %v, got: %v\n", i+1, testCase.expectedKey, key)
}
}
}
}
func TestKeySetAdd(t *testing.T) {
testCases := []struct {
set KeySet
key Key
expectedResult KeySet
}{
{NewKeySet(), S3XAmzCopySource, NewKeySet(S3XAmzCopySource)},
{NewKeySet(S3XAmzCopySource), S3XAmzCopySource, NewKeySet(S3XAmzCopySource)},
}
for i, testCase := range testCases {
testCase.set.Add(testCase.key)
if !reflect.DeepEqual(testCase.expectedResult, testCase.set) {
t.Fatalf("case %v: expected: %v, got: %v\n", i+1, testCase.expectedResult, testCase.set)
}
}
}
func TestKeySetDifference(t *testing.T) {
testCases := []struct {
set KeySet
setToDiff KeySet
expectedResult KeySet
}{
{NewKeySet(), NewKeySet(S3XAmzCopySource), NewKeySet()},
{NewKeySet(S3Prefix, S3Delimiter, S3MaxKeys), NewKeySet(S3Delimiter, S3MaxKeys), NewKeySet(S3Prefix)},
}
for i, testCase := range testCases {
result := testCase.set.Difference(testCase.setToDiff)
if !reflect.DeepEqual(testCase.expectedResult, result) {
t.Fatalf("case %v: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
}
}
}
func TestKeySetIsEmpty(t *testing.T) {
testCases := []struct {
set KeySet
expectedResult bool
}{
{NewKeySet(), true},
{NewKeySet(S3Delimiter), false},
}
for i, testCase := range testCases {
result := testCase.set.IsEmpty()
if testCase.expectedResult != result {
t.Fatalf("case %v: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
}
}
}
func TestKeySetString(t *testing.T) {
testCases := []struct {
set KeySet
expectedResult string
}{
{NewKeySet(), `[]`},
{NewKeySet(S3Delimiter), `[s3:delimiter]`},
}
for i, testCase := range testCases {
result := testCase.set.String()
if testCase.expectedResult != result {
t.Fatalf("case %v: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
}
}
}
func TestKeySetToSlice(t *testing.T) {
testCases := []struct {
set KeySet
expectedResult []Key
}{
{NewKeySet(), []Key{}},
{NewKeySet(S3Delimiter), []Key{S3Delimiter}},
}
for i, testCase := range testCases {
result := testCase.set.ToSlice()
if !reflect.DeepEqual(testCase.expectedResult, result) {
t.Fatalf("case %v: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
}
}
}

View File

@@ -0,0 +1,79 @@
/*
* 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/json"
"fmt"
)
type name string
const (
stringEquals name = "StringEquals"
stringNotEquals = "StringNotEquals"
stringLike = "StringLike"
stringNotLike = "StringNotLike"
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
}
return false
}
// MarshalJSON - encodes name to JSON data.
func (n name) MarshalJSON() ([]byte, error) {
if !n.IsValid() {
return nil, fmt.Errorf("invalid name %v", n)
}
return json.Marshal(string(n))
}
// UnmarshalJSON - decodes JSON data to condition name.
func (n *name) UnmarshalJSON(data []byte) error {
var s string
if err := json.Unmarshal(data, &s); err != nil {
return err
}
parsedName, err := parseName(s)
if err != nil {
return err
}
*n = parsedName
return nil
}
func parseName(s string) (name, error) {
n := name(s)
if n.IsValid() {
return n, nil
}
return n, fmt.Errorf("invalid condition name '%v'", s)
}

View File

@@ -0,0 +1,106 @@
/*
* 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/json"
"reflect"
"testing"
)
func TestNameIsValid(t *testing.T) {
testCases := []struct {
n name
expectedResult bool
}{
{stringEquals, true},
{stringNotEquals, true},
{stringLike, true},
{stringNotLike, true},
{ipAddress, true},
{notIPAddress, true},
{null, true},
{name("foo"), false},
}
for i, testCase := range testCases {
result := testCase.n.IsValid()
if testCase.expectedResult != result {
t.Fatalf("case %v: expected: %v, got: %v", i+1, testCase.expectedResult, result)
}
}
}
func TestNameMarshalJSON(t *testing.T) {
testCases := []struct {
n name
expectedResult []byte
expectErr bool
}{
{stringEquals, []byte(`"StringEquals"`), false},
{stringNotEquals, []byte(`"StringNotEquals"`), false},
{stringLike, []byte(`"StringLike"`), false},
{stringNotLike, []byte(`"StringNotLike"`), false},
{ipAddress, []byte(`"IpAddress"`), false},
{notIPAddress, []byte(`"NotIpAddress"`), false},
{null, []byte(`"Null"`), false},
{name("foo"), nil, true},
}
for i, testCase := range testCases {
result, err := json.Marshal(testCase.n)
expectErr := (err != nil)
if testCase.expectErr != expectErr {
t.Fatalf("case %v: error: expected: %v, got: %v", i+1, testCase.expectErr, expectErr)
}
if !testCase.expectErr {
if !reflect.DeepEqual(result, testCase.expectedResult) {
t.Fatalf("case %v: result: expected: %v, got: %v", i+1, string(testCase.expectedResult), string(result))
}
}
}
}
func TestNameUnmarshalJSON(t *testing.T) {
testCases := []struct {
data []byte
expectedResult name
expectErr bool
}{
{[]byte(`"StringEquals"`), stringEquals, false},
{[]byte(`"foo"`), name(""), true},
}
for i, testCase := range testCases {
var result name
err := json.Unmarshal(testCase.data, &result)
expectErr := (err != nil)
if testCase.expectErr != expectErr {
t.Fatalf("case %v: error: expected: %v, got: %v", i+1, testCase.expectErr, expectErr)
}
if !testCase.expectErr {
if testCase.expectedResult != result {
t.Fatalf("case %v: result: expected: %v, got: %v", i+1, testCase.expectedResult, result)
}
}
}
}

View File

@@ -0,0 +1,101 @@
/*
* 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"
"reflect"
"strconv"
)
// nullFunc - Null condition function. It checks whether Key is present in given
// values or not.
// For example,
// 1. if Key = S3XAmzCopySource and Value = true, at evaluate() it returns whether
// S3XAmzCopySource is in given value map or not.
// 2. if Key = S3XAmzCopySource and Value = false, at evaluate() it returns whether
// S3XAmzCopySource is NOT in given value map or not.
type nullFunc struct {
k Key
value bool
}
// evaluate() - evaluates to check whether Key is present in given values or not.
// Depending on condition boolean value, this function returns true or false.
func (f nullFunc) evaluate(values map[string][]string) bool {
requestValue := values[f.k.Name()]
if f.value {
return len(requestValue) != 0
}
return len(requestValue) == 0
}
// key() - returns condition key which is used by this condition function.
func (f nullFunc) key() Key {
return f.k
}
// name() - returns "Null" condition name.
func (f nullFunc) name() name {
return null
}
func (f nullFunc) String() string {
return fmt.Sprintf("%v:%v:%v", null, f.k, f.value)
}
// toMap - returns map representation of this function.
func (f nullFunc) toMap() map[Key]ValueSet {
if !f.k.IsValid() {
return nil
}
return map[Key]ValueSet{
f.k: NewValueSet(NewBoolValue(f.value)),
}
}
func newNullFunc(key Key, values ValueSet) (Function, error) {
if len(values) != 1 {
return nil, fmt.Errorf("only one value is allowed for Null condition")
}
var value bool
for v := range values {
switch v.GetType() {
case reflect.Bool:
value, _ = v.GetBool()
case reflect.String:
var err error
s, _ := v.GetString()
if value, err = strconv.ParseBool(s); err != nil {
return nil, fmt.Errorf("value must be a boolean string for Null condition")
}
default:
return nil, fmt.Errorf("value must be a boolean for Null condition")
}
}
return &nullFunc{key, value}, nil
}
// NewNullFunc - returns new Null function.
func NewNullFunc(key Key, value bool) (Function, error) {
return &nullFunc{key, value}, nil
}

View File

@@ -0,0 +1,161 @@
/*
* 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 TestNullFuncEvaluate(t *testing.T) {
case1Function, err := newNullFunc(S3Prefix, NewValueSet(NewBoolValue(true)))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case2Function, err := newNullFunc(S3Prefix, NewValueSet(NewBoolValue(false)))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
testCases := []struct {
function Function
values map[string][]string
expectedResult bool
}{
{case1Function, map[string][]string{"prefix": {"true"}}, true},
{case1Function, map[string][]string{"prefix": {"false"}}, true},
{case1Function, map[string][]string{"prefix": {"mybucket/foo"}}, true},
{case1Function, map[string][]string{}, false},
{case1Function, map[string][]string{"delimiter": {"/"}}, false},
{case2Function, map[string][]string{"prefix": {"true"}}, false},
{case2Function, map[string][]string{"prefix": {"false"}}, false},
{case2Function, map[string][]string{"prefix": {"mybucket/foo"}}, false},
{case2Function, map[string][]string{}, true},
{case2Function, 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 TestNullFuncKey(t *testing.T) {
case1Function, err := newNullFunc(S3XAmzCopySource, NewValueSet(NewBoolValue(true)))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
testCases := []struct {
function Function
expectedResult Key
}{
{case1Function, S3XAmzCopySource},
}
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 TestNullFuncToMap(t *testing.T) {
case1Function, err := newNullFunc(S3Prefix, NewValueSet(NewBoolValue(true)))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case1Result := map[Key]ValueSet{
S3Prefix: NewValueSet(NewBoolValue(true)),
}
case2Function, err := newNullFunc(S3Prefix, NewValueSet(NewBoolValue(false)))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case2Result := map[Key]ValueSet{
S3Prefix: NewValueSet(NewBoolValue(false)),
}
testCases := []struct {
f Function
expectedResult map[Key]ValueSet
}{
{case1Function, case1Result},
{case2Function, case2Result},
{&nullFunc{}, 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 TestNewNullFunc(t *testing.T) {
case1Function, err := newNullFunc(S3Prefix, NewValueSet(NewBoolValue(true)))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case2Function, err := newNullFunc(S3Prefix, NewValueSet(NewBoolValue(false)))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
testCases := []struct {
key Key
values ValueSet
expectedResult Function
expectErr bool
}{
{S3Prefix, NewValueSet(NewBoolValue(true)), case1Function, false},
{S3Prefix, NewValueSet(NewStringValue("false")), case2Function, false},
// Multiple values error.
{S3Prefix, NewValueSet(NewBoolValue(true), NewBoolValue(false)), nil, true},
// Invalid boolean string error.
{S3Prefix, NewValueSet(NewStringValue("foo")), nil, true},
// Invalid value error.
{S3Prefix, NewValueSet(NewIntValue(7)), nil, true},
}
for i, testCase := range testCases {
result, err := newNullFunc(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)
}
}
}
}

View File

@@ -0,0 +1,182 @@
/*
* 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"
"sort"
"strings"
"github.com/minio/minio-go/pkg/set"
)
func toStringEqualsFuncString(n name, key Key, values set.StringSet) string {
valueStrings := values.ToSlice()
sort.Strings(valueStrings)
return fmt.Sprintf("%v:%v:%v", n, key, valueStrings)
}
// stringEqualsFunc - 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 stringEqualsFunc struct {
k Key
values set.StringSet
}
// evaluate() - evaluates to check whether value by Key in given values is in
// condition values.
func (f stringEqualsFunc) evaluate(values map[string][]string) bool {
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 stringEqualsFunc) key() Key {
return f.k
}
// name() - returns "StringEquals" condition name.
func (f stringEqualsFunc) name() name {
return stringEquals
}
func (f stringEqualsFunc) String() string {
return toStringEqualsFuncString(stringEquals, f.k, f.values)
}
// toMap - returns map representation of this function.
func (f stringEqualsFunc) 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,
}
}
// stringNotEqualsFunc - 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 stringNotEqualsFunc struct {
stringEqualsFunc
}
// evaluate() - evaluates to check whether value by Key in given values is NOT in
// condition values.
func (f stringNotEqualsFunc) evaluate(values map[string][]string) bool {
return !f.stringEqualsFunc.evaluate(values)
}
// name() - returns "StringNotEquals" condition name.
func (f stringNotEqualsFunc) name() name {
return stringNotEquals
}
func (f stringNotEqualsFunc) String() string {
return toStringEqualsFuncString(stringNotEquals, f.stringEqualsFunc.k, f.stringEqualsFunc.values)
}
func valuesToStringSlice(n name, values ValueSet) ([]string, error) {
valueStrings := []string{}
for value := range values {
// FIXME: if AWS supports non-string values, we would need to support it.
s, err := value.GetString()
if err != nil {
return nil, fmt.Errorf("value must be a string for %v condition", n)
}
valueStrings = append(valueStrings, s)
}
return valueStrings, nil
}
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 {
return fmt.Errorf("invalid value '%v' for '%v' for %v condition", s, S3XAmzCopySource, n)
}
// FIXME: tokens[0] must be a valid bucket name.
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)
}
}
}
return nil
}
// newStringEqualsFunc - returns new StringEquals function.
func newStringEqualsFunc(key Key, values ValueSet) (Function, error) {
valueStrings, err := valuesToStringSlice(stringEquals, values)
if err != nil {
return nil, err
}
return NewStringEqualsFunc(key, valueStrings...)
}
// NewStringEqualsFunc - returns new StringEquals function.
func NewStringEqualsFunc(key Key, values ...string) (Function, error) {
sset := set.CreateStringSet(values...)
if err := validateStringEqualsValues(stringEquals, key, sset); err != nil {
return nil, err
}
return &stringEqualsFunc{key, sset}, nil
}
// newStringNotEqualsFunc - returns new StringNotEquals function.
func newStringNotEqualsFunc(key Key, values ValueSet) (Function, error) {
valueStrings, err := valuesToStringSlice(stringNotEquals, values)
if err != nil {
return nil, err
}
return NewStringNotEqualsFunc(key, valueStrings...)
}
// NewStringNotEqualsFunc - returns new StringNotEquals function.
func NewStringNotEqualsFunc(key Key, values ...string) (Function, error) {
sset := set.CreateStringSet(values...)
if err := validateStringEqualsValues(stringNotEquals, key, sset); err != nil {
return nil, err
}
return &stringNotEqualsFunc{stringEqualsFunc{key, sset}}, nil
}

View File

@@ -0,0 +1,718 @@
/*
* 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 TestStringEqualsFuncEvaluate(t *testing.T) {
case1Function, err := newStringEqualsFunc(S3XAmzCopySource, NewValueSet(NewStringValue("mybucket/myobject")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case2Function, err := newStringEqualsFunc(S3XAmzServerSideEncryption, NewValueSet(NewStringValue("AES256")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case3Function, err := newStringEqualsFunc(S3XAmzMetadataDirective, NewValueSet(NewStringValue("REPLACE")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case4Function, err := newStringEqualsFunc(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": {"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 TestStringEqualsFuncKey(t *testing.T) {
case1Function, err := newStringEqualsFunc(S3XAmzCopySource, NewValueSet(NewStringValue("mybucket/myobject")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case2Function, err := newStringEqualsFunc(S3XAmzServerSideEncryption, NewValueSet(NewStringValue("AES256")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case3Function, err := newStringEqualsFunc(S3XAmzMetadataDirective, NewValueSet(NewStringValue("REPLACE")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case4Function, err := newStringEqualsFunc(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 TestStringEqualsFuncToMap(t *testing.T) {
case1Function, err := newStringEqualsFunc(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 := newStringEqualsFunc(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 := newStringEqualsFunc(S3XAmzServerSideEncryption, NewValueSet(NewStringValue("AES256")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case3Result := map[Key]ValueSet{
S3XAmzServerSideEncryption: NewValueSet(NewStringValue("AES256")),
}
case4Function, err := newStringEqualsFunc(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 := newStringEqualsFunc(S3XAmzMetadataDirective, NewValueSet(NewStringValue("REPLACE")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case5Result := map[Key]ValueSet{
S3XAmzMetadataDirective: NewValueSet(NewStringValue("REPLACE")),
}
case6Function, err := newStringEqualsFunc(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 := newStringEqualsFunc(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 := newStringEqualsFunc(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 TestStringNotEqualsFuncEvaluate(t *testing.T) {
case1Function, err := newStringNotEqualsFunc(S3XAmzCopySource, NewValueSet(NewStringValue("mybucket/myobject")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case2Function, err := newStringNotEqualsFunc(S3XAmzServerSideEncryption, NewValueSet(NewStringValue("AES256")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case3Function, err := newStringNotEqualsFunc(S3XAmzMetadataDirective, NewValueSet(NewStringValue("REPLACE")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case4Function, err := newStringNotEqualsFunc(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 TestStringNotEqualsFuncKey(t *testing.T) {
case1Function, err := newStringNotEqualsFunc(S3XAmzCopySource, NewValueSet(NewStringValue("mybucket/myobject")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case2Function, err := newStringNotEqualsFunc(S3XAmzServerSideEncryption, NewValueSet(NewStringValue("AES256")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case3Function, err := newStringNotEqualsFunc(S3XAmzMetadataDirective, NewValueSet(NewStringValue("REPLACE")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case4Function, err := newStringNotEqualsFunc(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 TestStringNotEqualsFuncToMap(t *testing.T) {
case1Function, err := newStringNotEqualsFunc(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 := newStringNotEqualsFunc(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 := newStringNotEqualsFunc(S3XAmzServerSideEncryption, NewValueSet(NewStringValue("AES256")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case3Result := map[Key]ValueSet{
S3XAmzServerSideEncryption: NewValueSet(NewStringValue("AES256")),
}
case4Function, err := newStringNotEqualsFunc(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 := newStringNotEqualsFunc(S3XAmzMetadataDirective, NewValueSet(NewStringValue("REPLACE")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case5Result := map[Key]ValueSet{
S3XAmzMetadataDirective: NewValueSet(NewStringValue("REPLACE")),
}
case6Function, err := newStringNotEqualsFunc(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 := newStringNotEqualsFunc(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 := newStringNotEqualsFunc(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 TestNewStringEqualsFunc(t *testing.T) {
case1Function, err := newStringEqualsFunc(S3XAmzCopySource, NewValueSet(NewStringValue("mybucket/myobject")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case2Function, err := newStringEqualsFunc(S3XAmzCopySource,
NewValueSet(
NewStringValue("mybucket/myobject"),
NewStringValue("yourbucket/myobject"),
),
)
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case3Function, err := newStringEqualsFunc(S3XAmzServerSideEncryption, NewValueSet(NewStringValue("AES256")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case4Function, err := newStringEqualsFunc(S3XAmzServerSideEncryption,
NewValueSet(
NewStringValue("AES256"),
NewStringValue("aws:kms"),
),
)
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case5Function, err := newStringEqualsFunc(S3XAmzMetadataDirective, NewValueSet(NewStringValue("REPLACE")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case6Function, err := newStringEqualsFunc(S3XAmzMetadataDirective,
NewValueSet(
NewStringValue("REPLACE"),
NewStringValue("COPY"),
),
)
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case7Function, err := newStringEqualsFunc(S3LocationConstraint, NewValueSet(NewStringValue("eu-west-1")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case8Function, err := newStringEqualsFunc(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 := newStringEqualsFunc(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 TestNewStringNotEqualsFunc(t *testing.T) {
case1Function, err := newStringNotEqualsFunc(S3XAmzCopySource, NewValueSet(NewStringValue("mybucket/myobject")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case2Function, err := newStringNotEqualsFunc(S3XAmzCopySource,
NewValueSet(
NewStringValue("mybucket/myobject"),
NewStringValue("yourbucket/myobject"),
),
)
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case3Function, err := newStringNotEqualsFunc(S3XAmzServerSideEncryption, NewValueSet(NewStringValue("AES256")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case4Function, err := newStringNotEqualsFunc(S3XAmzServerSideEncryption,
NewValueSet(
NewStringValue("AES256"),
NewStringValue("aws:kms"),
),
)
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case5Function, err := newStringNotEqualsFunc(S3XAmzMetadataDirective, NewValueSet(NewStringValue("REPLACE")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case6Function, err := newStringNotEqualsFunc(S3XAmzMetadataDirective,
NewValueSet(
NewStringValue("REPLACE"),
NewStringValue("COPY"),
),
)
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case7Function, err := newStringNotEqualsFunc(S3LocationConstraint, NewValueSet(NewStringValue("eu-west-1")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case8Function, err := newStringNotEqualsFunc(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 := newStringNotEqualsFunc(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)
}
}
}
}

View File

@@ -0,0 +1,165 @@
/*
* 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"
"sort"
"strings"
"github.com/minio/minio-go/pkg/set"
"github.com/minio/minio/pkg/wildcard"
)
func toStringLikeFuncString(n name, key Key, values set.StringSet) string {
valueStrings := values.ToSlice()
sort.Strings(valueStrings)
return fmt.Sprintf("%v:%v:%v", n, key, valueStrings)
}
// stringLikeFunc - String like function. It checks whether value by Key in given
// values map is widcard matching in condition values.
// For example,
// - if values = ["mybucket/foo*"], at evaluate() it returns whether string
// in value map for Key is wildcard matching in values.
type stringLikeFunc struct {
k Key
values set.StringSet
}
// evaluate() - evaluates to check whether value by Key in given values is wildcard
// matching in condition values.
func (f stringLikeFunc) evaluate(values map[string][]string) bool {
for _, v := range values[f.k.Name()] {
if !f.values.FuncMatch(wildcard.Match, v).IsEmpty() {
return true
}
}
return false
}
// key() - returns condition key which is used by this condition function.
func (f stringLikeFunc) key() Key {
return f.k
}
// name() - returns "StringLike" function name.
func (f stringLikeFunc) name() name {
return stringLike
}
func (f stringLikeFunc) String() string {
return toStringLikeFuncString(stringLike, f.k, f.values)
}
// toMap - returns map representation of this function.
func (f stringLikeFunc) 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,
}
}
// stringNotLikeFunc - String not like function. It checks whether value by Key in given
// values map is NOT widcard matching in condition values.
// For example,
// - if values = ["mybucket/foo*"], at evaluate() it returns whether string
// in value map for Key is NOT wildcard matching in values.
type stringNotLikeFunc struct {
stringLikeFunc
}
// evaluate() - evaluates to check whether value by Key in given values is NOT wildcard
// matching in condition values.
func (f stringNotLikeFunc) evaluate(values map[string][]string) bool {
return !f.stringLikeFunc.evaluate(values)
}
// name() - returns "StringNotLike" function name.
func (f stringNotLikeFunc) name() name {
return stringNotLike
}
func (f stringNotLikeFunc) String() string {
return toStringLikeFuncString(stringNotLike, f.stringLikeFunc.k, f.stringLikeFunc.values)
}
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)
}
// FIXME: tokens[0] must be a valid bucket name.
}
}
return nil
}
// newStringLikeFunc - returns new StringLike function.
func newStringLikeFunc(key Key, values ValueSet) (Function, error) {
valueStrings, err := valuesToStringSlice(stringLike, values)
if err != nil {
return nil, err
}
return NewStringLikeFunc(key, valueStrings...)
}
// NewStringLikeFunc - returns new StringLike function.
func NewStringLikeFunc(key Key, values ...string) (Function, error) {
sset := set.CreateStringSet(values...)
if err := validateStringLikeValues(stringLike, key, sset); err != nil {
return nil, err
}
return &stringLikeFunc{key, sset}, nil
}
// newStringNotLikeFunc - returns new StringNotLike function.
func newStringNotLikeFunc(key Key, values ValueSet) (Function, error) {
valueStrings, err := valuesToStringSlice(stringNotLike, values)
if err != nil {
return nil, err
}
return NewStringNotLikeFunc(key, valueStrings...)
}
// NewStringNotLikeFunc - returns new StringNotLike function.
func NewStringNotLikeFunc(key Key, values ...string) (Function, error) {
sset := set.CreateStringSet(values...)
if err := validateStringLikeValues(stringNotLike, key, sset); err != nil {
return nil, err
}
return &stringNotLikeFunc{stringLikeFunc{key, sset}}, nil
}

View File

@@ -0,0 +1,810 @@
/*
* 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 TestStringLikeFuncEvaluate(t *testing.T) {
case1Function, err := newStringLikeFunc(S3XAmzCopySource, NewValueSet(NewStringValue("mybucket/myobject*")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case2Function, err := newStringLikeFunc(S3XAmzCopySource, NewValueSet(NewStringValue("mybucket/myobject")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case3Function, err := newStringLikeFunc(S3XAmzServerSideEncryption, NewValueSet(NewStringValue("AES*")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case4Function, err := newStringLikeFunc(S3XAmzServerSideEncryption, NewValueSet(NewStringValue("AES256")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case5Function, err := newStringLikeFunc(S3XAmzMetadataDirective, NewValueSet(NewStringValue("REPL*")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case6Function, err := newStringLikeFunc(S3XAmzMetadataDirective, NewValueSet(NewStringValue("REPLACE")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case7Function, err := newStringLikeFunc(S3LocationConstraint, NewValueSet(NewStringValue("eu-west-*")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case8Function, err := newStringLikeFunc(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": {"mybucket/myobject.png"}}, 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-copy-source": {"mybucket/myobject"}}, true},
{case2Function, map[string][]string{"x-amz-copy-source": {"mybucket/myobject.png"}}, false},
{case2Function, map[string][]string{"x-amz-copy-source": {"yourbucket/myobject"}}, false},
{case2Function, map[string][]string{}, false},
{case2Function, map[string][]string{"delimiter": {"/"}}, false},
{case3Function, map[string][]string{"x-amz-server-side-encryption": {"AES256"}}, true},
{case3Function, map[string][]string{"x-amz-server-side-encryption": {"AES512"}}, true},
{case3Function, map[string][]string{"x-amz-server-side-encryption": {"aws:kms"}}, false},
{case3Function, map[string][]string{}, false},
{case3Function, map[string][]string{"delimiter": {"/"}}, false},
{case4Function, map[string][]string{"x-amz-server-side-encryption": {"AES256"}}, true},
{case4Function, map[string][]string{"x-amz-server-side-encryption": {"AES512"}}, false},
{case4Function, map[string][]string{"x-amz-server-side-encryption": {"aws:kms"}}, false},
{case4Function, map[string][]string{}, false},
{case4Function, map[string][]string{"delimiter": {"/"}}, false},
{case5Function, map[string][]string{"x-amz-metadata-directive": {"REPLACE"}}, true},
{case5Function, map[string][]string{"x-amz-metadata-directive": {"REPLACE/COPY"}}, true},
{case5Function, map[string][]string{"x-amz-metadata-directive": {"COPY"}}, false},
{case5Function, map[string][]string{}, false},
{case5Function, map[string][]string{"delimiter": {"/"}}, false},
{case6Function, map[string][]string{"x-amz-metadata-directive": {"REPLACE"}}, true},
{case6Function, map[string][]string{"x-amz-metadata-directive": {"REPLACE/COPY"}}, false},
{case6Function, map[string][]string{"x-amz-metadata-directive": {"COPY"}}, false},
{case6Function, map[string][]string{}, false},
{case6Function, map[string][]string{"delimiter": {"/"}}, false},
{case7Function, map[string][]string{"LocationConstraint": {"eu-west-1"}}, true},
{case7Function, map[string][]string{"LocationConstraint": {"eu-west-2"}}, true},
{case7Function, map[string][]string{"LocationConstraint": {"us-east-1"}}, false},
{case7Function, map[string][]string{}, false},
{case7Function, map[string][]string{"delimiter": {"/"}}, false},
{case8Function, map[string][]string{"LocationConstraint": {"eu-west-1"}}, true},
{case8Function, map[string][]string{"LocationConstraint": {"eu-west-2"}}, false},
{case8Function, map[string][]string{"LocationConstraint": {"us-east-1"}}, false},
{case8Function, map[string][]string{}, false},
{case8Function, 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 TestStringLikeFuncKey(t *testing.T) {
case1Function, err := newStringLikeFunc(S3XAmzCopySource, NewValueSet(NewStringValue("mybucket/myobject")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case2Function, err := newStringLikeFunc(S3XAmzServerSideEncryption, NewValueSet(NewStringValue("AES256")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case3Function, err := newStringLikeFunc(S3XAmzMetadataDirective, NewValueSet(NewStringValue("REPLACE")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case4Function, err := newStringLikeFunc(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 TestStringLikeFuncToMap(t *testing.T) {
case1Function, err := newStringLikeFunc(S3XAmzCopySource, NewValueSet(NewStringValue("mybucket/*")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case1Result := map[Key]ValueSet{
S3XAmzCopySource: NewValueSet(NewStringValue("mybucket/*")),
}
case2Function, err := newStringLikeFunc(S3XAmzCopySource,
NewValueSet(
NewStringValue("mybucket/*"),
NewStringValue("yourbucket/myobject*"),
),
)
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case2Result := map[Key]ValueSet{
S3XAmzCopySource: NewValueSet(
NewStringValue("mybucket/*"),
NewStringValue("yourbucket/myobject*"),
),
}
case3Function, err := newStringLikeFunc(S3XAmzServerSideEncryption, NewValueSet(NewStringValue("AES*")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case3Result := map[Key]ValueSet{
S3XAmzServerSideEncryption: NewValueSet(NewStringValue("AES*")),
}
case4Function, err := newStringLikeFunc(S3XAmzServerSideEncryption,
NewValueSet(
NewStringValue("AES*"),
NewStringValue("aws:*"),
),
)
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case4Result := map[Key]ValueSet{
S3XAmzServerSideEncryption: NewValueSet(
NewStringValue("AES*"),
NewStringValue("aws:*"),
),
}
case5Function, err := newStringLikeFunc(S3XAmzMetadataDirective, NewValueSet(NewStringValue("REPL*")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case5Result := map[Key]ValueSet{
S3XAmzMetadataDirective: NewValueSet(NewStringValue("REPL*")),
}
case6Function, err := newStringLikeFunc(S3XAmzMetadataDirective,
NewValueSet(
NewStringValue("REPL*"),
NewStringValue("COPY*"),
),
)
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case6Result := map[Key]ValueSet{
S3XAmzMetadataDirective: NewValueSet(
NewStringValue("REPL*"),
NewStringValue("COPY*"),
),
}
case7Function, err := newStringLikeFunc(S3LocationConstraint, NewValueSet(NewStringValue("eu-west-*")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case7Result := map[Key]ValueSet{
S3LocationConstraint: NewValueSet(NewStringValue("eu-west-*")),
}
case8Function, err := newStringLikeFunc(S3LocationConstraint,
NewValueSet(
NewStringValue("eu-west-*"),
NewStringValue("us-west-*"),
),
)
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case8Result := map[Key]ValueSet{
S3LocationConstraint: NewValueSet(
NewStringValue("eu-west-*"),
NewStringValue("us-west-*"),
),
}
testCases := []struct {
f Function
expectedResult map[Key]ValueSet
}{
{case1Function, case1Result},
{case2Function, case2Result},
{case3Function, case3Result},
{case4Function, case4Result},
{case5Function, case5Result},
{case6Function, case6Result},
{case7Function, case7Result},
{case8Function, case8Result},
{&stringLikeFunc{}, 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 TestStringNotLikeFuncEvaluate(t *testing.T) {
case1Function, err := newStringNotLikeFunc(S3XAmzCopySource, NewValueSet(NewStringValue("mybucket/myobject*")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case2Function, err := newStringNotLikeFunc(S3XAmzCopySource, NewValueSet(NewStringValue("mybucket/myobject")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case3Function, err := newStringNotLikeFunc(S3XAmzServerSideEncryption, NewValueSet(NewStringValue("AES*")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case4Function, err := newStringNotLikeFunc(S3XAmzServerSideEncryption, NewValueSet(NewStringValue("AES256")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case5Function, err := newStringNotLikeFunc(S3XAmzMetadataDirective, NewValueSet(NewStringValue("REPL*")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case6Function, err := newStringNotLikeFunc(S3XAmzMetadataDirective, NewValueSet(NewStringValue("REPLACE")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case7Function, err := newStringNotLikeFunc(S3LocationConstraint, NewValueSet(NewStringValue("eu-west-*")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case8Function, err := newStringNotLikeFunc(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": {"mybucket/myobject.png"}}, 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-copy-source": {"mybucket/myobject"}}, false},
{case2Function, map[string][]string{"x-amz-copy-source": {"mybucket/myobject.png"}}, true},
{case2Function, map[string][]string{"x-amz-copy-source": {"yourbucket/myobject"}}, true},
{case2Function, map[string][]string{}, true},
{case2Function, map[string][]string{"delimiter": {"/"}}, true},
{case3Function, map[string][]string{"x-amz-server-side-encryption": {"AES256"}}, false},
{case3Function, map[string][]string{"x-amz-server-side-encryption": {"AES512"}}, false},
{case3Function, map[string][]string{"x-amz-server-side-encryption": {"aws:kms"}}, true},
{case3Function, map[string][]string{}, true},
{case3Function, map[string][]string{"delimiter": {"/"}}, true},
{case4Function, map[string][]string{"x-amz-server-side-encryption": {"AES256"}}, false},
{case4Function, map[string][]string{"x-amz-server-side-encryption": {"AES512"}}, true},
{case4Function, map[string][]string{"x-amz-server-side-encryption": {"aws:kms"}}, true},
{case4Function, map[string][]string{}, true},
{case4Function, map[string][]string{"delimiter": {"/"}}, true},
{case5Function, map[string][]string{"x-amz-metadata-directive": {"REPLACE"}}, false},
{case5Function, map[string][]string{"x-amz-metadata-directive": {"REPLACE/COPY"}}, false},
{case5Function, map[string][]string{"x-amz-metadata-directive": {"COPY"}}, true},
{case5Function, map[string][]string{}, true},
{case5Function, map[string][]string{"delimiter": {"/"}}, true},
{case6Function, map[string][]string{"x-amz-metadata-directive": {"REPLACE"}}, false},
{case6Function, map[string][]string{"x-amz-metadata-directive": {"REPLACE/COPY"}}, true},
{case6Function, map[string][]string{"x-amz-metadata-directive": {"COPY"}}, true},
{case6Function, map[string][]string{}, true},
{case6Function, map[string][]string{"delimiter": {"/"}}, true},
{case7Function, map[string][]string{"LocationConstraint": {"eu-west-1"}}, false},
{case7Function, map[string][]string{"LocationConstraint": {"eu-west-2"}}, false},
{case7Function, map[string][]string{"LocationConstraint": {"us-east-1"}}, true},
{case7Function, map[string][]string{}, true},
{case7Function, map[string][]string{"delimiter": {"/"}}, true},
{case8Function, map[string][]string{"LocationConstraint": {"eu-west-1"}}, false},
{case8Function, map[string][]string{"LocationConstraint": {"eu-west-2"}}, true},
{case8Function, map[string][]string{"LocationConstraint": {"us-east-1"}}, true},
{case8Function, map[string][]string{}, true},
{case8Function, 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 TestStringNotLikeFuncKey(t *testing.T) {
case1Function, err := newStringNotLikeFunc(S3XAmzCopySource, NewValueSet(NewStringValue("mybucket/myobject")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case2Function, err := newStringNotLikeFunc(S3XAmzServerSideEncryption, NewValueSet(NewStringValue("AES256")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case3Function, err := newStringNotLikeFunc(S3XAmzMetadataDirective, NewValueSet(NewStringValue("REPLACE")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case4Function, err := newStringNotLikeFunc(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 TestStringNotLikeFuncToMap(t *testing.T) {
case1Function, err := newStringNotLikeFunc(S3XAmzCopySource, NewValueSet(NewStringValue("mybucket/*")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case1Result := map[Key]ValueSet{
S3XAmzCopySource: NewValueSet(NewStringValue("mybucket/*")),
}
case2Function, err := newStringNotLikeFunc(S3XAmzCopySource,
NewValueSet(
NewStringValue("mybucket/*"),
NewStringValue("yourbucket/myobject*"),
),
)
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case2Result := map[Key]ValueSet{
S3XAmzCopySource: NewValueSet(
NewStringValue("mybucket/*"),
NewStringValue("yourbucket/myobject*"),
),
}
case3Function, err := newStringNotLikeFunc(S3XAmzServerSideEncryption, NewValueSet(NewStringValue("AES*")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case3Result := map[Key]ValueSet{
S3XAmzServerSideEncryption: NewValueSet(NewStringValue("AES*")),
}
case4Function, err := newStringNotLikeFunc(S3XAmzServerSideEncryption,
NewValueSet(
NewStringValue("AES*"),
NewStringValue("aws:*"),
),
)
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case4Result := map[Key]ValueSet{
S3XAmzServerSideEncryption: NewValueSet(
NewStringValue("AES*"),
NewStringValue("aws:*"),
),
}
case5Function, err := newStringNotLikeFunc(S3XAmzMetadataDirective, NewValueSet(NewStringValue("REPL*")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case5Result := map[Key]ValueSet{
S3XAmzMetadataDirective: NewValueSet(NewStringValue("REPL*")),
}
case6Function, err := newStringNotLikeFunc(S3XAmzMetadataDirective,
NewValueSet(
NewStringValue("REPL*"),
NewStringValue("COPY*"),
),
)
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case6Result := map[Key]ValueSet{
S3XAmzMetadataDirective: NewValueSet(
NewStringValue("REPL*"),
NewStringValue("COPY*"),
),
}
case7Function, err := newStringNotLikeFunc(S3LocationConstraint, NewValueSet(NewStringValue("eu-west-*")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case7Result := map[Key]ValueSet{
S3LocationConstraint: NewValueSet(NewStringValue("eu-west-*")),
}
case8Function, err := newStringNotLikeFunc(S3LocationConstraint,
NewValueSet(
NewStringValue("eu-west-*"),
NewStringValue("us-west-*"),
),
)
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case8Result := map[Key]ValueSet{
S3LocationConstraint: NewValueSet(
NewStringValue("eu-west-*"),
NewStringValue("us-west-*"),
),
}
testCases := []struct {
f Function
expectedResult map[Key]ValueSet
}{
{case1Function, case1Result},
{case2Function, case2Result},
{case3Function, case3Result},
{case4Function, case4Result},
{case5Function, case5Result},
{case6Function, case6Result},
{case7Function, case7Result},
{case8Function, case8Result},
{&stringNotLikeFunc{}, 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 TestNewStringLikeFunc(t *testing.T) {
case1Function, err := newStringLikeFunc(S3XAmzCopySource, NewValueSet(NewStringValue("mybucket/*")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case2Function, err := newStringLikeFunc(S3XAmzCopySource,
NewValueSet(
NewStringValue("mybucket/*"),
NewStringValue("yourbucket/myobject*"),
),
)
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case3Function, err := newStringLikeFunc(S3XAmzServerSideEncryption, NewValueSet(NewStringValue("AES*")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case4Function, err := newStringLikeFunc(S3XAmzServerSideEncryption,
NewValueSet(
NewStringValue("AES*"),
NewStringValue("aws:*"),
),
)
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case5Function, err := newStringLikeFunc(S3XAmzMetadataDirective, NewValueSet(NewStringValue("REPL*")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case6Function, err := newStringLikeFunc(S3XAmzMetadataDirective,
NewValueSet(
NewStringValue("REPL*"),
NewStringValue("COPY*"),
),
)
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case7Function, err := newStringLikeFunc(S3LocationConstraint, NewValueSet(NewStringValue("eu-west-*")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case8Function, err := newStringLikeFunc(S3LocationConstraint,
NewValueSet(
NewStringValue("eu-west-*"),
NewStringValue("us-west-*"),
),
)
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
testCases := []struct {
key Key
values ValueSet
expectedResult Function
expectErr bool
}{
{S3XAmzCopySource, NewValueSet(NewStringValue("mybucket/*")), case1Function, false},
{S3XAmzCopySource,
NewValueSet(
NewStringValue("mybucket/*"),
NewStringValue("yourbucket/myobject*"),
), case2Function, false},
{S3XAmzServerSideEncryption, NewValueSet(NewStringValue("AES*")), case3Function, false},
{S3XAmzServerSideEncryption,
NewValueSet(
NewStringValue("AES*"),
NewStringValue("aws:*"),
), case4Function, false},
{S3XAmzMetadataDirective, NewValueSet(NewStringValue("REPL*")), case5Function, false},
{S3XAmzMetadataDirective,
NewValueSet(
NewStringValue("REPL*"),
NewStringValue("COPY*"),
), case6Function, false},
{S3LocationConstraint, NewValueSet(NewStringValue("eu-west-*")), case7Function, false},
{S3LocationConstraint,
NewValueSet(
NewStringValue("eu-west-*"),
NewStringValue("us-west-*"),
), 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},
}
for i, testCase := range testCases {
result, err := newStringLikeFunc(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 TestNewStringNotLikeFunc(t *testing.T) {
case1Function, err := newStringNotLikeFunc(S3XAmzCopySource, NewValueSet(NewStringValue("mybucket/*")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case2Function, err := newStringNotLikeFunc(S3XAmzCopySource,
NewValueSet(
NewStringValue("mybucket/*"),
NewStringValue("yourbucket/myobject*"),
),
)
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case3Function, err := newStringNotLikeFunc(S3XAmzServerSideEncryption, NewValueSet(NewStringValue("AES*")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case4Function, err := newStringNotLikeFunc(S3XAmzServerSideEncryption,
NewValueSet(
NewStringValue("AES*"),
NewStringValue("aws:*"),
),
)
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case5Function, err := newStringNotLikeFunc(S3XAmzMetadataDirective, NewValueSet(NewStringValue("REPL*")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case6Function, err := newStringNotLikeFunc(S3XAmzMetadataDirective,
NewValueSet(
NewStringValue("REPL*"),
NewStringValue("COPY*"),
),
)
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case7Function, err := newStringNotLikeFunc(S3LocationConstraint, NewValueSet(NewStringValue("eu-west-*")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case8Function, err := newStringNotLikeFunc(S3LocationConstraint,
NewValueSet(
NewStringValue("eu-west-*"),
NewStringValue("us-west-*"),
),
)
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
testCases := []struct {
key Key
values ValueSet
expectedResult Function
expectErr bool
}{
{S3XAmzCopySource, NewValueSet(NewStringValue("mybucket/*")), case1Function, false},
{S3XAmzCopySource,
NewValueSet(
NewStringValue("mybucket/*"),
NewStringValue("yourbucket/myobject*"),
), case2Function, false},
{S3XAmzServerSideEncryption, NewValueSet(NewStringValue("AES*")), case3Function, false},
{S3XAmzServerSideEncryption,
NewValueSet(
NewStringValue("AES*"),
NewStringValue("aws:*"),
), case4Function, false},
{S3XAmzMetadataDirective, NewValueSet(NewStringValue("REPL*")), case5Function, false},
{S3XAmzMetadataDirective,
NewValueSet(
NewStringValue("REPL*"),
NewStringValue("COPY*"),
), case6Function, false},
{S3LocationConstraint, NewValueSet(NewStringValue("eu-west-*")), case7Function, false},
{S3LocationConstraint,
NewValueSet(
NewStringValue("eu-west-*"),
NewStringValue("us-west-*"),
), 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},
}
for i, testCase := range testCases {
result, err := newStringNotLikeFunc(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)
}
}
}
}

View File

@@ -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 (
"encoding/json"
"fmt"
"reflect"
"strconv"
)
// Value - is enum type of string, int or bool.
type Value struct {
t reflect.Kind
s string
i int
b bool
}
// GetBool - gets stored bool value.
func (v Value) GetBool() (bool, error) {
var err error
if v.t != reflect.Bool {
err = fmt.Errorf("not a bool Value")
}
return v.b, err
}
// GetInt - gets stored int value.
func (v Value) GetInt() (int, error) {
var err error
if v.t != reflect.Int {
err = fmt.Errorf("not a int Value")
}
return v.i, err
}
// GetString - gets stored string value.
func (v Value) GetString() (string, error) {
var err error
if v.t != reflect.String {
err = fmt.Errorf("not a string Value")
}
return v.s, err
}
// GetType - gets enum type.
func (v Value) GetType() reflect.Kind {
return v.t
}
// MarshalJSON - encodes Value to JSON data.
func (v Value) MarshalJSON() ([]byte, error) {
switch v.t {
case reflect.String:
return json.Marshal(v.s)
case reflect.Int:
return json.Marshal(v.i)
case reflect.Bool:
return json.Marshal(v.b)
}
return nil, fmt.Errorf("unknown value kind %v", v.t)
}
// StoreBool - stores bool value.
func (v *Value) StoreBool(b bool) {
*v = Value{t: reflect.Bool, b: b}
}
// StoreInt - stores int value.
func (v *Value) StoreInt(i int) {
*v = Value{t: reflect.Int, i: i}
}
// StoreString - stores string value.
func (v *Value) StoreString(s string) {
*v = Value{t: reflect.String, s: s}
}
// String - returns string representation of value.
func (v Value) String() string {
switch v.t {
case reflect.String:
return v.s
case reflect.Int:
return strconv.Itoa(v.i)
case reflect.Bool:
return strconv.FormatBool(v.b)
}
return ""
}
// UnmarshalJSON - decodes JSON data.
func (v *Value) UnmarshalJSON(data []byte) error {
var b bool
if err := json.Unmarshal(data, &b); err == nil {
v.StoreBool(b)
return nil
}
var i int
if err := json.Unmarshal(data, &i); err == nil {
v.StoreInt(i)
return nil
}
var s string
if err := json.Unmarshal(data, &s); err == nil {
v.StoreString(s)
return nil
}
return fmt.Errorf("unknown json data '%v'", data)
}
// NewBoolValue - returns new bool value.
func NewBoolValue(b bool) Value {
value := &Value{}
value.StoreBool(b)
return *value
}
// NewIntValue - returns new int value.
func NewIntValue(i int) Value {
value := &Value{}
value.StoreInt(i)
return *value
}
// NewStringValue - returns new string value.
func NewStringValue(s string) Value {
value := &Value{}
value.StoreString(s)
return *value
}

View File

@@ -0,0 +1,260 @@
/*
* 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/json"
"reflect"
"testing"
)
func TestValueGetBool(t *testing.T) {
testCases := []struct {
value Value
expectedResult bool
expectErr bool
}{
{NewBoolValue(true), true, false},
{NewIntValue(7), false, true},
{Value{}, false, true},
}
for i, testCase := range testCases {
result, err := testCase.value.GetBool()
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 result != testCase.expectedResult {
t.Fatalf("case %v: result: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
}
}
}
}
func TestValueGetInt(t *testing.T) {
testCases := []struct {
value Value
expectedResult int
expectErr bool
}{
{NewIntValue(7), 7, false},
{NewBoolValue(true), 0, true},
{Value{}, 0, true},
}
for i, testCase := range testCases {
result, err := testCase.value.GetInt()
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 result != testCase.expectedResult {
t.Fatalf("case %v: result: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
}
}
}
}
func TestValueGetString(t *testing.T) {
testCases := []struct {
value Value
expectedResult string
expectErr bool
}{
{NewStringValue("foo"), "foo", false},
{NewBoolValue(true), "", true},
{Value{}, "", true},
}
for i, testCase := range testCases {
result, err := testCase.value.GetString()
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 result != testCase.expectedResult {
t.Fatalf("case %v: result: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
}
}
}
}
func TestValueGetType(t *testing.T) {
testCases := []struct {
value Value
expectedResult reflect.Kind
}{
{NewBoolValue(true), reflect.Bool},
{NewIntValue(7), reflect.Int},
{NewStringValue("foo"), reflect.String},
{Value{}, reflect.Invalid},
}
for i, testCase := range testCases {
result := testCase.value.GetType()
if result != testCase.expectedResult {
t.Fatalf("case %v: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
}
}
}
func TestValueMarshalJSON(t *testing.T) {
testCases := []struct {
value Value
expectedResult []byte
expectErr bool
}{
{NewBoolValue(true), []byte("true"), false},
{NewIntValue(7), []byte("7"), false},
{NewStringValue("foo"), []byte(`"foo"`), false},
{Value{}, nil, true},
}
for i, testCase := range testCases {
result, err := json.Marshal(testCase.value)
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 TestValueStoreBool(t *testing.T) {
testCases := []struct {
value bool
expectedResult Value
}{
{false, NewBoolValue(false)},
{true, NewBoolValue(true)},
}
for i, testCase := range testCases {
var result Value
result.StoreBool(testCase.value)
if !reflect.DeepEqual(result, testCase.expectedResult) {
t.Fatalf("case %v: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
}
}
}
func TestValueStoreInt(t *testing.T) {
testCases := []struct {
value int
expectedResult Value
}{
{0, NewIntValue(0)},
{7, NewIntValue(7)},
}
for i, testCase := range testCases {
var result Value
result.StoreInt(testCase.value)
if !reflect.DeepEqual(result, testCase.expectedResult) {
t.Fatalf("case %v: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
}
}
}
func TestValueStoreString(t *testing.T) {
testCases := []struct {
value string
expectedResult Value
}{
{"", NewStringValue("")},
{"foo", NewStringValue("foo")},
}
for i, testCase := range testCases {
var result Value
result.StoreString(testCase.value)
if !reflect.DeepEqual(result, testCase.expectedResult) {
t.Fatalf("case %v: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
}
}
}
func TestValueString(t *testing.T) {
testCases := []struct {
value Value
expectedResult string
}{
{NewBoolValue(true), "true"},
{NewIntValue(7), "7"},
{NewStringValue("foo"), "foo"},
{Value{}, ""},
}
for i, testCase := range testCases {
result := testCase.value.String()
if result != testCase.expectedResult {
t.Fatalf("case %v: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
}
}
}
func TestValueUnmarshalJSON(t *testing.T) {
testCases := []struct {
data []byte
expectedResult Value
expectErr bool
}{
{[]byte("true"), NewBoolValue(true), false},
{[]byte("7"), NewIntValue(7), false},
{[]byte(`"foo"`), NewStringValue("foo"), false},
{[]byte("True"), Value{}, true},
{[]byte("7.1"), Value{}, true},
{[]byte(`["foo"]`), Value{}, true},
}
for i, testCase := range testCases {
var result Value
err := json.Unmarshal(testCase.data, &result)
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)
}
}
}
}

View File

@@ -0,0 +1,85 @@
/*
* 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/json"
"fmt"
)
// ValueSet - unique list of values.
type ValueSet map[Value]struct{}
// Add - adds given value to value set.
func (set ValueSet) Add(value Value) {
set[value] = struct{}{}
}
// MarshalJSON - encodes ValueSet to JSON data.
func (set ValueSet) MarshalJSON() ([]byte, error) {
var values []Value
for k := range set {
values = append(values, k)
}
if len(values) == 0 {
return nil, fmt.Errorf("invalid value set %v", set)
}
return json.Marshal(values)
}
// UnmarshalJSON - decodes JSON data.
func (set *ValueSet) UnmarshalJSON(data []byte) error {
var v Value
if err := json.Unmarshal(data, &v); err == nil {
*set = make(ValueSet)
set.Add(v)
return nil
}
var values []Value
if err := json.Unmarshal(data, &values); err != nil {
return err
}
if len(values) < 1 {
return fmt.Errorf("invalid value")
}
*set = make(ValueSet)
for _, v = range values {
if _, found := (*set)[v]; found {
return fmt.Errorf("duplicate value found '%v'", v)
}
set.Add(v)
}
return nil
}
// NewValueSet - returns new value set containing given values.
func NewValueSet(values ...Value) ValueSet {
set := make(ValueSet)
for _, value := range values {
set.Add(value)
}
return set
}

View File

@@ -0,0 +1,118 @@
/*
* 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/json"
"reflect"
"testing"
)
func TestValueSetAdd(t *testing.T) {
testCases := []struct {
value Value
expectedResult ValueSet
}{
{NewBoolValue(true), NewValueSet(NewBoolValue(true))},
{NewIntValue(7), NewValueSet(NewIntValue(7))},
{NewStringValue("foo"), NewValueSet(NewStringValue("foo"))},
}
for i, testCase := range testCases {
result := NewValueSet()
result.Add(testCase.value)
if !reflect.DeepEqual(result, testCase.expectedResult) {
t.Fatalf("case %v: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
}
}
}
func TestValueSetMarshalJSON(t *testing.T) {
testCases := []struct {
set ValueSet
expectedResult string
expectErr bool
}{
{NewValueSet(NewBoolValue(true)), `[true]`, false},
{NewValueSet(NewIntValue(7)), `[7]`, false},
{NewValueSet(NewStringValue("foo")), `["foo"]`, false},
{NewValueSet(NewBoolValue(true)), `[true]`, false},
{NewValueSet(NewStringValue("7")), `["7"]`, false},
{NewValueSet(NewStringValue("foo")), `["foo"]`, false},
{make(ValueSet), "", true},
}
for i, testCase := range testCases {
result, err := json.Marshal(testCase.set)
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 string(result) != testCase.expectedResult {
t.Fatalf("case %v: result: expected: %v, got: %v\n", i+1, testCase.expectedResult, string(result))
}
}
}
}
func TestValueSetUnmarshalJSON(t *testing.T) {
set1 := NewValueSet(
NewBoolValue(true),
NewStringValue("false"),
NewIntValue(7),
NewStringValue("7"),
NewStringValue("foo"),
NewStringValue("192.168.1.100/24"),
)
testCases := []struct {
data []byte
expectedResult ValueSet
expectErr bool
}{
{[]byte(`true`), NewValueSet(NewBoolValue(true)), false},
{[]byte(`7`), NewValueSet(NewIntValue(7)), false},
{[]byte(`"foo"`), NewValueSet(NewStringValue("foo")), false},
{[]byte(`[true]`), NewValueSet(NewBoolValue(true)), false},
{[]byte(`[7]`), NewValueSet(NewIntValue(7)), false},
{[]byte(`["foo"]`), NewValueSet(NewStringValue("foo")), false},
{[]byte(`[true, "false", 7, "7", "foo", "192.168.1.100/24"]`), set1, false},
{[]byte(`{}`), nil, true}, // Unsupported data.
{[]byte(`[]`), nil, true}, // Empty array.
{[]byte(`[7, 7, true]`), nil, true}, // Duplicate value.
}
for i, testCase := range testCases {
result := make(ValueSet)
err := json.Unmarshal(testCase.data, &result)
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)
}
}
}
}

78
pkg/policy/effect.go Normal file
View File

@@ -0,0 +1,78 @@
/*
* 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 policy
import (
"encoding/json"
"fmt"
)
// Effect - policy statement effect Allow or Deny.
type Effect string
const (
// Allow - allow effect.
Allow Effect = "Allow"
// Deny - deny effect.
Deny = "Deny"
)
// IsAllowed - returns if given check is allowed or not.
func (effect Effect) IsAllowed(b bool) bool {
if effect == Allow {
return b
}
return !b
}
// IsValid - checks if Effect is valid or not
func (effect Effect) IsValid() bool {
switch effect {
case Allow, Deny:
return true
}
return false
}
// MarshalJSON - encodes Effect to JSON data.
func (effect Effect) MarshalJSON() ([]byte, error) {
if !effect.IsValid() {
return nil, fmt.Errorf("invalid effect '%v'", effect)
}
return json.Marshal(string(effect))
}
// UnmarshalJSON - decodes JSON data to Effect.
func (effect *Effect) UnmarshalJSON(data []byte) error {
var s string
if err := json.Unmarshal(data, &s); err != nil {
return err
}
e := Effect(s)
if !e.IsValid() {
return fmt.Errorf("invalid effect '%v'", s)
}
*effect = e
return nil
}

122
pkg/policy/effect_test.go Normal file
View File

@@ -0,0 +1,122 @@
/*
* 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 policy
import (
"encoding/json"
"reflect"
"testing"
)
func TestEffectIsAllowed(t *testing.T) {
testCases := []struct {
effect Effect
check bool
expectedResult bool
}{
{Allow, false, false},
{Allow, true, true},
{Deny, false, true},
{Deny, true, false},
}
for i, testCase := range testCases {
result := testCase.effect.IsAllowed(testCase.check)
if result != testCase.expectedResult {
t.Fatalf("case %v: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
}
}
}
func TestEffectIsValid(t *testing.T) {
testCases := []struct {
effect Effect
expectedResult bool
}{
{Allow, true},
{Deny, true},
{Effect(""), false},
{Effect("foo"), false},
}
for i, testCase := range testCases {
result := testCase.effect.IsValid()
if result != testCase.expectedResult {
t.Fatalf("case %v: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
}
}
}
func TestEffectMarshalJSON(t *testing.T) {
testCases := []struct {
effect Effect
expectedResult []byte
expectErr bool
}{
{Allow, []byte(`"Allow"`), false},
{Deny, []byte(`"Deny"`), false},
{Effect(""), nil, true},
{Effect("foo"), nil, true},
}
for i, testCase := range testCases {
result, err := json.Marshal(testCase.effect)
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, string(testCase.expectedResult), string(result))
}
}
}
}
func TestEffectUnmarshalJSON(t *testing.T) {
testCases := []struct {
data []byte
expectedResult Effect
expectErr bool
}{
{[]byte(`"Allow"`), Allow, false},
{[]byte(`"Deny"`), Deny, false},
{[]byte(`""`), Effect(""), true},
{[]byte(`"foo"`), Effect(""), true},
}
for i, testCase := range testCases {
var result Effect
err := json.Unmarshal(testCase.data, &result)
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 result != testCase.expectedResult {
t.Fatalf("case %v: result: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
}
}
}
}

64
pkg/policy/id.go Normal file
View File

@@ -0,0 +1,64 @@
/*
* 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 policy
import (
"encoding/json"
"fmt"
"regexp"
)
var idRegexp = regexp.MustCompile("^[[:alnum:]]+$")
// ID - policy ID.
type ID string
// IsValid - checks if ID is valid or not.
func (id ID) IsValid() bool {
// Allow empty string as ID.
if string(id) == "" {
return true
}
return idRegexp.MatchString(string(id))
}
// MarshalJSON - encodes ID to JSON data.
func (id ID) MarshalJSON() ([]byte, error) {
if !id.IsValid() {
return nil, fmt.Errorf("invalid ID %v", id)
}
return json.Marshal(string(id))
}
// UnmarshalJSON - decodes JSON data to ID.
func (id *ID) UnmarshalJSON(data []byte) error {
var s string
if err := json.Unmarshal(data, &s); err != nil {
return err
}
i := ID(s)
if !i.IsValid() {
return fmt.Errorf("invalid ID %v", s)
}
*id = i
return nil
}

99
pkg/policy/id_test.go Normal file
View File

@@ -0,0 +1,99 @@
/*
* 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 policy
import (
"encoding/json"
"reflect"
"testing"
)
func TestIDIsValid(t *testing.T) {
testCases := []struct {
id ID
expectedResult bool
}{
{ID("DenyEncryptionSt1"), true},
{ID(""), true},
}
for i, testCase := range testCases {
result := testCase.id.IsValid()
if result != testCase.expectedResult {
t.Fatalf("case %v: result: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
}
}
}
func TestIDMarshalJSON(t *testing.T) {
testCases := []struct {
id ID
expectedResult []byte
expectErr bool
}{
{ID("foo"), []byte(`"foo"`), false},
{ID("1234"), []byte(`"1234"`), false},
{ID("DenyEncryptionSt1"), []byte(`"DenyEncryptionSt1"`), false},
{ID(""), []byte(`""`), false},
}
for i, testCase := range testCases {
result, err := json.Marshal(testCase.id)
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, string(testCase.expectedResult), string(result))
}
}
}
}
func TestIDUnmarshalJSON(t *testing.T) {
testCases := []struct {
data []byte
expectedResult ID
expectErr bool
}{
{[]byte(`"foo"`), ID("foo"), false},
{[]byte(`"1234"`), ID("1234"), false},
{[]byte(`"DenyEncryptionSt1"`), ID("DenyEncryptionSt1"), false},
{[]byte(`""`), ID(""), false},
{[]byte(`"foo bar"`), ID(""), true},
}
for i, testCase := range testCases {
var result ID
err := json.Unmarshal(testCase.data, &result)
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 result != testCase.expectedResult {
t.Fatalf("case %v: result: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
}
}
}
}

176
pkg/policy/policy.go Normal file
View File

@@ -0,0 +1,176 @@
/*
* 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 policy
import (
"encoding/json"
"fmt"
"io"
)
// DefaultVersion - default policy version as per AWS S3 specification.
const DefaultVersion = "2012-10-17"
// Args - arguments to policy to check whether it is allowed
type Args struct {
AccountName string
Action Action
BucketName string
ConditionValues map[string][]string
IsOwner bool
ObjectName string
}
// Policy - bucket policy.
type Policy struct {
ID ID `json:"ID,omitempty"`
Version string
Statements []Statement `json:"Statement"`
}
// IsAllowed - checks given policy args is allowed to continue the Rest API.
func (policy Policy) IsAllowed(args Args) bool {
// Check all deny statements. If any one statement denies, return false.
for _, statement := range policy.Statements {
if statement.Effect == Deny {
if !statement.IsAllowed(args) {
return false
}
}
}
// For owner, its allowed by default.
if args.IsOwner {
return true
}
// Check all allow statements. If any one statement allows, return true.
for _, statement := range policy.Statements {
if statement.Effect == Allow {
if statement.IsAllowed(args) {
return true
}
}
}
return false
}
// IsEmpty - returns whether policy is empty or not.
func (policy Policy) IsEmpty() bool {
return len(policy.Statements) == 0
}
// isValid - checks if Policy is valid or not.
func (policy Policy) isValid() error {
if policy.Version != DefaultVersion {
return fmt.Errorf("invalid version '%v'", policy.Version)
}
for _, statement := range policy.Statements {
if err := statement.isValid(); err != nil {
return err
}
}
for i := range policy.Statements {
for _, statement := range policy.Statements[i+1:] {
principals := policy.Statements[i].Principal.Intersection(statement.Principal)
if principals.IsEmpty() {
continue
}
actions := policy.Statements[i].Actions.Intersection(statement.Actions)
if len(actions) == 0 {
continue
}
resources := policy.Statements[i].Resources.Intersection(statement.Resources)
if len(resources) == 0 {
continue
}
if policy.Statements[i].Conditions.String() != statement.Conditions.String() {
continue
}
return fmt.Errorf("duplicate principal %v, actions %v, resouces %v found in statements %v, %v",
principals, actions, resources, policy.Statements[i], statement)
}
}
return nil
}
// MarshalJSON - encodes Policy to JSON data.
func (policy Policy) MarshalJSON() ([]byte, error) {
if err := policy.isValid(); err != nil {
return nil, err
}
// subtype to avoid recursive call to MarshalJSON()
type subPolicy Policy
return json.Marshal(subPolicy(policy))
}
// UnmarshalJSON - decodes JSON data to Policy.
func (policy *Policy) UnmarshalJSON(data []byte) error {
// subtype to avoid recursive call to UnmarshalJSON()
type subPolicy Policy
var sp subPolicy
if err := json.Unmarshal(data, &sp); err != nil {
return err
}
p := Policy(sp)
if err := p.isValid(); err != nil {
return err
}
*policy = p
return nil
}
// Validate - validates all statements are for given bucket or not.
func (policy Policy) Validate(bucketName string) error {
if err := policy.isValid(); err != nil {
return err
}
for _, statement := range policy.Statements {
if err := statement.Validate(bucketName); err != nil {
return err
}
}
return nil
}
// ParseConfig - parses data in given reader to Policy.
func ParseConfig(reader io.Reader, bucketName string) (*Policy, error) {
var policy Policy
decoder := json.NewDecoder(reader)
decoder.DisallowUnknownFields()
if err := decoder.Decode(&policy); err != nil {
return nil, err
}
err := policy.Validate(bucketName)
return &policy, err
}

1089
pkg/policy/policy_test.go Normal file

File diff suppressed because it is too large Load Diff

92
pkg/policy/principal.go Normal file
View File

@@ -0,0 +1,92 @@
/*
* 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 policy
import (
"encoding/json"
"fmt"
"github.com/minio/minio-go/pkg/set"
"github.com/minio/minio/pkg/wildcard"
)
// Principal - policy principal.
type Principal struct {
AWS set.StringSet
}
// IsValid - checks whether Principal is valid or not.
func (p Principal) IsValid() bool {
return len(p.AWS) != 0
}
// Intersection - returns principals available in both Principal.
func (p Principal) Intersection(principal Principal) set.StringSet {
return p.AWS.Intersection(principal.AWS)
}
// MarshalJSON - encodes Principal to JSON data.
func (p Principal) MarshalJSON() ([]byte, error) {
if !p.IsValid() {
return nil, fmt.Errorf("invalid principal %v", p)
}
// subtype to avoid recursive call to MarshalJSON()
type subPrincipal Principal
sp := subPrincipal(p)
return json.Marshal(sp)
}
// Match - matches given principal is wildcard matching with Principal.
func (p Principal) Match(principal string) bool {
for _, pattern := range p.AWS.ToSlice() {
if wildcard.MatchSimple(pattern, principal) {
return true
}
}
return false
}
// UnmarshalJSON - decodes JSON data to Principal.
func (p *Principal) UnmarshalJSON(data []byte) error {
// subtype to avoid recursive call to UnmarshalJSON()
type subPrincipal Principal
var sp subPrincipal
if err := json.Unmarshal(data, &sp); err != nil {
var s string
if err = json.Unmarshal(data, &s); err != nil {
return err
}
if s != "*" {
return fmt.Errorf("invalid principal '%v'", s)
}
sp.AWS = set.CreateStringSet("*")
}
*p = Principal(sp)
return nil
}
// NewPrincipal - creates new Principal.
func NewPrincipal(principals ...string) Principal {
return Principal{AWS: set.CreateStringSet(principals...)}
}

View File

@@ -0,0 +1,141 @@
/*
* 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 policy
import (
"encoding/json"
"reflect"
"testing"
"github.com/minio/minio-go/pkg/set"
)
func TestPrincipalIsValid(t *testing.T) {
testCases := []struct {
principal Principal
expectedResult bool
}{
{NewPrincipal("*"), true},
{NewPrincipal("arn:aws:iam::AccountNumber:root"), true},
{NewPrincipal(), false},
}
for i, testCase := range testCases {
result := testCase.principal.IsValid()
if result != testCase.expectedResult {
t.Fatalf("case %v: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
}
}
}
func TestPrincipalIntersection(t *testing.T) {
testCases := []struct {
principal Principal
principalToIntersect Principal
expectedResult set.StringSet
}{
{NewPrincipal("*"), NewPrincipal("*"), set.CreateStringSet("*")},
{NewPrincipal("arn:aws:iam::AccountNumber:root"), NewPrincipal("arn:aws:iam::AccountNumber:myuser"), set.CreateStringSet()},
{NewPrincipal(), NewPrincipal("*"), set.CreateStringSet()},
}
for i, testCase := range testCases {
result := testCase.principal.Intersection(testCase.principalToIntersect)
if !reflect.DeepEqual(result, testCase.expectedResult) {
t.Fatalf("case %v: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
}
}
}
func TestPrincipalMarshalJSON(t *testing.T) {
testCases := []struct {
principal Principal
expectedResult []byte
expectErr bool
}{
{NewPrincipal("*"), []byte(`{"AWS":["*"]}`), false},
{NewPrincipal("arn:aws:iam::AccountNumber:*"), []byte(`{"AWS":["arn:aws:iam::AccountNumber:*"]}`), false},
{NewPrincipal(), nil, true},
}
for i, testCase := range testCases {
result, err := json.Marshal(testCase.principal)
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, string(testCase.expectedResult), string(result))
}
}
}
}
func TestPrincipalMatch(t *testing.T) {
testCases := []struct {
principals Principal
principal string
expectedResult bool
}{
{NewPrincipal("*"), "AccountNumber", true},
{NewPrincipal("arn:aws:iam::*"), "arn:aws:iam::AccountNumber:root", true},
{NewPrincipal("arn:aws:iam::AccountNumber:*"), "arn:aws:iam::TestAccountNumber:root", false},
}
for i, testCase := range testCases {
result := testCase.principals.Match(testCase.principal)
if result != testCase.expectedResult {
t.Fatalf("case %v: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
}
}
}
func TestPrincipalUnmarshalJSON(t *testing.T) {
testCases := []struct {
data []byte
expectedResult Principal
expectErr bool
}{
{[]byte(`"*"`), NewPrincipal("*"), false},
{[]byte(`{"AWS": "*"}`), NewPrincipal("*"), false},
{[]byte(`{"AWS": "arn:aws:iam::AccountNumber:*"}`), NewPrincipal("arn:aws:iam::AccountNumber:*"), false},
{[]byte(`"arn:aws:iam::AccountNumber:*"`), NewPrincipal(), true},
{[]byte(`["arn:aws:iam::AccountNumber:*", "arn:aws:iam:AnotherAccount:*"]`), NewPrincipal(), true},
}
for i, testCase := range testCases {
var result Principal
err := json.Unmarshal(testCase.data, &result)
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)
}
}
}
}

131
pkg/policy/resource.go Normal file
View File

@@ -0,0 +1,131 @@
/*
* 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 policy
import (
"encoding/json"
"fmt"
"strings"
"github.com/minio/minio/pkg/wildcard"
)
// ResourceARNPrefix - resource ARN prefix as per AWS S3 specification.
const ResourceARNPrefix = "arn:aws:s3:::"
// Resource - resource in policy statement.
type Resource struct {
bucketName string
pattern string
}
func (r Resource) isBucketPattern() bool {
return !strings.Contains(r.pattern, "/")
}
func (r Resource) isObjectPattern() bool {
return strings.Contains(r.pattern, "/") || strings.Contains(r.bucketName, "*")
}
// IsValid - checks whether Resource is valid or not.
func (r Resource) IsValid() bool {
return r.bucketName != "" && r.pattern != ""
}
// Match - matches object name with resource pattern.
func (r Resource) Match(resource string) bool {
return wildcard.Match(r.pattern, resource)
}
// MarshalJSON - encodes Resource to JSON data.
func (r Resource) MarshalJSON() ([]byte, error) {
if !r.IsValid() {
return nil, fmt.Errorf("invalid resource %v", r)
}
return json.Marshal(r.String())
}
func (r Resource) String() string {
return ResourceARNPrefix + r.pattern
}
// UnmarshalJSON - decodes JSON data to Resource.
func (r *Resource) UnmarshalJSON(data []byte) error {
var s string
if err := json.Unmarshal(data, &s); err != nil {
return err
}
parsedResource, err := parseResource(s)
if err != nil {
return err
}
*r = parsedResource
return nil
}
// Validate - validates Resource is for given bucket or not.
func (r Resource) Validate(bucketName string) error {
if !r.IsValid() {
return fmt.Errorf("invalid resource")
}
if !wildcard.Match(r.bucketName, bucketName) {
return fmt.Errorf("bucket name does not match")
}
return nil
}
// parseResource - parses string to Resource.
func parseResource(s string) (Resource, error) {
if !strings.HasPrefix(s, ResourceARNPrefix) {
return Resource{}, fmt.Errorf("invalid resource '%v'", s)
}
pattern := strings.TrimPrefix(s, ResourceARNPrefix)
tokens := strings.SplitN(pattern, "/", 2)
bucketName := tokens[0]
if bucketName == "" {
return Resource{}, fmt.Errorf("invalid resource format '%v'", s)
}
return Resource{
bucketName: bucketName,
pattern: pattern,
}, nil
}
// NewResource - creates new resource.
func NewResource(bucketName, keyName string) Resource {
pattern := bucketName
if keyName != "" {
if !strings.HasPrefix(keyName, "/") {
pattern += "/"
}
pattern += keyName
}
return Resource{
bucketName: bucketName,
pattern: pattern,
}
}

221
pkg/policy/resource_test.go Normal file
View File

@@ -0,0 +1,221 @@
/*
* 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 policy
import (
"encoding/json"
"reflect"
"testing"
)
func TestResourceIsBucketPattern(t *testing.T) {
testCases := []struct {
resource Resource
expectedResult bool
}{
{NewResource("*", ""), true},
{NewResource("mybucket", ""), true},
{NewResource("mybucket*", ""), true},
{NewResource("mybucket?0", ""), true},
{NewResource("", "*"), false},
{NewResource("*", "*"), false},
{NewResource("mybucket", "*"), false},
{NewResource("mybucket*", "/myobject"), false},
{NewResource("mybucket?0", "/2010/photos/*"), false},
}
for i, testCase := range testCases {
result := testCase.resource.isBucketPattern()
if result != testCase.expectedResult {
t.Fatalf("case %v: expected: %v, got: %v", i+1, testCase.expectedResult, result)
}
}
}
func TestResourceIsObjectPattern(t *testing.T) {
testCases := []struct {
resource Resource
expectedResult bool
}{
{NewResource("*", ""), true},
{NewResource("mybucket*", ""), true},
{NewResource("", "*"), true},
{NewResource("*", "*"), true},
{NewResource("mybucket", "*"), true},
{NewResource("mybucket*", "/myobject"), true},
{NewResource("mybucket?0", "/2010/photos/*"), true},
{NewResource("mybucket", ""), false},
{NewResource("mybucket?0", ""), false},
}
for i, testCase := range testCases {
result := testCase.resource.isObjectPattern()
if result != testCase.expectedResult {
t.Fatalf("case %v: expected: %v, got: %v", i+1, testCase.expectedResult, result)
}
}
}
func TestResourceIsValid(t *testing.T) {
testCases := []struct {
resource Resource
expectedResult bool
}{
{NewResource("*", ""), true},
{NewResource("mybucket*", ""), true},
{NewResource("*", "*"), true},
{NewResource("mybucket", "*"), true},
{NewResource("mybucket*", "/myobject"), true},
{NewResource("mybucket?0", "/2010/photos/*"), true},
{NewResource("mybucket", ""), true},
{NewResource("mybucket?0", ""), true},
{NewResource("", ""), false},
{NewResource("", "*"), false},
}
for i, testCase := range testCases {
result := testCase.resource.IsValid()
if result != testCase.expectedResult {
t.Fatalf("case %v: expected: %v, got: %v", i+1, testCase.expectedResult, result)
}
}
}
func TestResourceMatch(t *testing.T) {
testCases := []struct {
resource Resource
objectName string
expectedResult bool
}{
{NewResource("*", ""), "mybucket", true},
{NewResource("*", ""), "mybucket/myobject", true},
{NewResource("mybucket*", ""), "mybucket", true},
{NewResource("mybucket*", ""), "mybucket/myobject", true},
{NewResource("", "*"), "/myobject", true},
{NewResource("*", "*"), "mybucket/myobject", true},
{NewResource("mybucket", "*"), "mybucket/myobject", true},
{NewResource("mybucket*", "/myobject"), "mybucket/myobject", true},
{NewResource("mybucket*", "/myobject"), "mybucket100/myobject", true},
{NewResource("mybucket?0", "/2010/photos/*"), "mybucket20/2010/photos/1.jpg", true},
{NewResource("mybucket", ""), "mybucket", true},
{NewResource("mybucket?0", ""), "mybucket30", true},
{NewResource("", "*"), "mybucket/myobject", false},
{NewResource("*", "*"), "mybucket", false},
{NewResource("mybucket", "*"), "mybucket10/myobject", false},
{NewResource("mybucket?0", "/2010/photos/*"), "mybucket0/2010/photos/1.jpg", false},
{NewResource("mybucket", ""), "mybucket/myobject", false},
}
for i, testCase := range testCases {
result := testCase.resource.Match(testCase.objectName)
if result != testCase.expectedResult {
t.Fatalf("case %v: expected: %v, got: %v", i+1, testCase.expectedResult, result)
}
}
}
func TestResourceMarshalJSON(t *testing.T) {
testCases := []struct {
resource Resource
expectedResult []byte
expectErr bool
}{
{NewResource("*", ""), []byte(`"arn:aws:s3:::*"`), false},
{NewResource("mybucket*", ""), []byte(`"arn:aws:s3:::mybucket*"`), false},
{NewResource("mybucket", ""), []byte(`"arn:aws:s3:::mybucket"`), false},
{NewResource("*", "*"), []byte(`"arn:aws:s3:::*/*"`), false},
{NewResource("mybucket", "*"), []byte(`"arn:aws:s3:::mybucket/*"`), false},
{NewResource("mybucket*", "myobject"), []byte(`"arn:aws:s3:::mybucket*/myobject"`), false},
{NewResource("mybucket?0", "/2010/photos/*"), []byte(`"arn:aws:s3:::mybucket?0/2010/photos/*"`), false},
{Resource{}, nil, true},
{NewResource("", "*"), nil, true},
}
for i, testCase := range testCases {
result, err := json.Marshal(testCase.resource)
expectErr := (err != nil)
if expectErr != testCase.expectErr {
t.Fatalf("case %v: error: expected: %v, got: %v", i+1, testCase.expectErr, expectErr)
}
if !testCase.expectErr {
if !reflect.DeepEqual(result, testCase.expectedResult) {
t.Fatalf("case %v: result: expected: %v, got: %v", i+1, string(testCase.expectedResult), string(result))
}
}
}
}
func TestResourceUnmarshalJSON(t *testing.T) {
testCases := []struct {
data []byte
expectedResult Resource
expectErr bool
}{
{[]byte(`"arn:aws:s3:::*"`), NewResource("*", ""), false},
{[]byte(`"arn:aws:s3:::mybucket*"`), NewResource("mybucket*", ""), false},
{[]byte(`"arn:aws:s3:::mybucket"`), NewResource("mybucket", ""), false},
{[]byte(`"arn:aws:s3:::*/*"`), NewResource("*", "*"), false},
{[]byte(`"arn:aws:s3:::mybucket/*"`), NewResource("mybucket", "*"), false},
{[]byte(`"arn:aws:s3:::mybucket*/myobject"`), NewResource("mybucket*", "myobject"), false},
{[]byte(`"arn:aws:s3:::mybucket?0/2010/photos/*"`), NewResource("mybucket?0", "/2010/photos/*"), false},
{[]byte(`"mybucket/myobject*"`), Resource{}, true},
{[]byte(`"arn:aws:s3:::/*"`), Resource{}, true},
}
for i, testCase := range testCases {
var result Resource
err := json.Unmarshal(testCase.data, &result)
expectErr := (err != nil)
if expectErr != testCase.expectErr {
t.Fatalf("case %v: error: expected: %v, got: %v", i+1, testCase.expectErr, expectErr)
}
if !testCase.expectErr {
if !reflect.DeepEqual(result, testCase.expectedResult) {
t.Fatalf("case %v: result: expected: %v, got: %v", i+1, testCase.expectedResult, result)
}
}
}
}
func TestResourceValidate(t *testing.T) {
testCases := []struct {
resource Resource
bucketName string
expectErr bool
}{
{NewResource("mybucket", "/myobject*"), "mybucket", false},
{NewResource("", "/myobject*"), "yourbucket", true},
{NewResource("mybucket", "/myobject*"), "yourbucket", true},
}
for i, testCase := range testCases {
err := testCase.resource.Validate(testCase.bucketName)
expectErr := (err != nil)
if expectErr != testCase.expectErr {
t.Fatalf("case %v: error: expected: %v, got: %v", i+1, testCase.expectErr, expectErr)
}
}
}

147
pkg/policy/resourceset.go Normal file
View File

@@ -0,0 +1,147 @@
/*
* 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 policy
import (
"encoding/json"
"fmt"
"sort"
"github.com/minio/minio-go/pkg/set"
)
// ResourceSet - set of resources in policy statement.
type ResourceSet map[Resource]struct{}
// bucketResourceExists - checks if at least one bucket resource exists in the set.
func (resourceSet ResourceSet) bucketResourceExists() bool {
for resource := range resourceSet {
if resource.isBucketPattern() {
return true
}
}
return false
}
// objectResourceExists - checks if at least one object resource exists in the set.
func (resourceSet ResourceSet) objectResourceExists() bool {
for resource := range resourceSet {
if resource.isObjectPattern() {
return true
}
}
return false
}
// Add - adds resource to resource set.
func (resourceSet ResourceSet) Add(resource Resource) {
resourceSet[resource] = struct{}{}
}
// Intersection - returns resouces available in both ResourcsSet.
func (resourceSet ResourceSet) Intersection(sset ResourceSet) ResourceSet {
nset := NewResourceSet()
for k := range resourceSet {
if _, ok := sset[k]; ok {
nset.Add(k)
}
}
return nset
}
// MarshalJSON - encodes ResourceSet to JSON data.
func (resourceSet ResourceSet) MarshalJSON() ([]byte, error) {
if len(resourceSet) == 0 {
return nil, fmt.Errorf("empty resource set")
}
resources := []Resource{}
for resource := range resourceSet {
resources = append(resources, resource)
}
return json.Marshal(resources)
}
// Match - matches object name with anyone of resource pattern in resource set.
func (resourceSet ResourceSet) Match(resource string) bool {
for r := range resourceSet {
if r.Match(resource) {
return true
}
}
return false
}
func (resourceSet ResourceSet) String() string {
resources := []string{}
for resource := range resourceSet {
resources = append(resources, resource.String())
}
sort.Strings(resources)
return fmt.Sprintf("%v", resources)
}
// UnmarshalJSON - decodes JSON data to ResourceSet.
func (resourceSet *ResourceSet) UnmarshalJSON(data []byte) error {
var sset set.StringSet
if err := json.Unmarshal(data, &sset); err != nil {
return err
}
*resourceSet = make(ResourceSet)
for _, s := range sset.ToSlice() {
resource, err := parseResource(s)
if err != nil {
return err
}
if _, found := (*resourceSet)[resource]; found {
return fmt.Errorf("duplicate resource '%v' found", s)
}
resourceSet.Add(resource)
}
return nil
}
// Validate - validates ResourceSet is for given bucket or not.
func (resourceSet ResourceSet) Validate(bucketName string) error {
for resource := range resourceSet {
if err := resource.Validate(bucketName); err != nil {
return err
}
}
return nil
}
// NewResourceSet - creates new resource set.
func NewResourceSet(resources ...Resource) ResourceSet {
resourceSet := make(ResourceSet)
for _, resource := range resources {
resourceSet.Add(resource)
}
return resourceSet
}

View File

@@ -0,0 +1,240 @@
/*
* 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 policy
import (
"encoding/json"
"reflect"
"testing"
)
func TestResourceSetBucketResourceExists(t *testing.T) {
testCases := []struct {
resourceSet ResourceSet
expectedResult bool
}{
{NewResourceSet(NewResource("*", "")), true},
{NewResourceSet(NewResource("mybucket", "")), true},
{NewResourceSet(NewResource("mybucket*", "")), true},
{NewResourceSet(NewResource("mybucket?0", "")), true},
{NewResourceSet(NewResource("mybucket", "/2010/photos/*"), NewResource("mybucket", "")), true},
{NewResourceSet(NewResource("", "*")), false},
{NewResourceSet(NewResource("*", "*")), false},
{NewResourceSet(NewResource("mybucket", "*")), false},
{NewResourceSet(NewResource("mybucket*", "/myobject")), false},
{NewResourceSet(NewResource("mybucket?0", "/2010/photos/*")), false},
}
for i, testCase := range testCases {
result := testCase.resourceSet.bucketResourceExists()
if result != testCase.expectedResult {
t.Fatalf("case %v: expected: %v, got: %v", i+1, testCase.expectedResult, result)
}
}
}
func TestResourceSetObjectResourceExists(t *testing.T) {
testCases := []struct {
resourceSet ResourceSet
expectedResult bool
}{
{NewResourceSet(NewResource("*", "")), true},
{NewResourceSet(NewResource("mybucket*", "")), true},
{NewResourceSet(NewResource("", "*")), true},
{NewResourceSet(NewResource("*", "*")), true},
{NewResourceSet(NewResource("mybucket", "*")), true},
{NewResourceSet(NewResource("mybucket*", "/myobject")), true},
{NewResourceSet(NewResource("mybucket?0", "/2010/photos/*")), true},
{NewResourceSet(NewResource("mybucket", ""), NewResource("mybucket", "/2910/photos/*")), true},
{NewResourceSet(NewResource("mybucket", "")), false},
{NewResourceSet(NewResource("mybucket?0", "")), false},
}
for i, testCase := range testCases {
result := testCase.resourceSet.objectResourceExists()
if result != testCase.expectedResult {
t.Fatalf("case %v: expected: %v, got: %v", i+1, testCase.expectedResult, result)
}
}
}
func TestResourceSetAdd(t *testing.T) {
testCases := []struct {
resourceSet ResourceSet
resource Resource
expectedResult ResourceSet
}{
{NewResourceSet(), NewResource("mybucket", "/myobject*"),
NewResourceSet(NewResource("mybucket", "/myobject*"))},
{NewResourceSet(NewResource("mybucket", "/myobject*")),
NewResource("mybucket", "/yourobject*"),
NewResourceSet(NewResource("mybucket", "/myobject*"),
NewResource("mybucket", "/yourobject*"))},
{NewResourceSet(NewResource("mybucket", "/myobject*")),
NewResource("mybucket", "/myobject*"),
NewResourceSet(NewResource("mybucket", "/myobject*"))},
}
for i, testCase := range testCases {
testCase.resourceSet.Add(testCase.resource)
if !reflect.DeepEqual(testCase.resourceSet, testCase.expectedResult) {
t.Fatalf("case %v: expected: %v, got: %v", i+1, testCase.expectedResult, testCase.resourceSet)
}
}
}
func TestResourceSetIntersection(t *testing.T) {
testCases := []struct {
set ResourceSet
setToIntersect ResourceSet
expectedResult ResourceSet
}{
{NewResourceSet(), NewResourceSet(NewResource("mybucket", "/myobject*")), NewResourceSet()},
{NewResourceSet(NewResource("mybucket", "/myobject*")), NewResourceSet(), NewResourceSet()},
{NewResourceSet(NewResource("mybucket", "/myobject*")),
NewResourceSet(NewResource("mybucket", "/myobject*"), NewResource("mybucket", "/yourobject*")),
NewResourceSet(NewResource("mybucket", "/myobject*"))},
}
for i, testCase := range testCases {
result := testCase.set.Intersection(testCase.setToIntersect)
if !reflect.DeepEqual(result, testCase.expectedResult) {
t.Fatalf("case %v: expected: %v, got: %v\n", i+1, testCase.expectedResult, testCase.set)
}
}
}
func TestResourceSetMarshalJSON(t *testing.T) {
testCases := []struct {
resoruceSet ResourceSet
expectedResult []byte
expectErr bool
}{
{NewResourceSet(NewResource("mybucket", "/myobject*")),
[]byte(`["arn:aws:s3:::mybucket/myobject*"]`), false},
{NewResourceSet(NewResource("mybucket", "/photos/myobject*")),
[]byte(`["arn:aws:s3:::mybucket/photos/myobject*"]`), false},
{NewResourceSet(), nil, true},
}
for i, testCase := range testCases {
result, err := json.Marshal(testCase.resoruceSet)
expectErr := (err != nil)
if expectErr != testCase.expectErr {
t.Fatalf("case %v: error: expected: %v, got: %v", i+1, testCase.expectErr, expectErr)
}
if !testCase.expectErr {
if !reflect.DeepEqual(result, testCase.expectedResult) {
t.Fatalf("case %v: result: expected: %v, got: %v", i+1, string(testCase.expectedResult), string(result))
}
}
}
}
func TestResourceSetMatch(t *testing.T) {
testCases := []struct {
resourceSet ResourceSet
resource string
expectedResult bool
}{
{NewResourceSet(NewResource("*", "")), "mybucket", true},
{NewResourceSet(NewResource("*", "")), "mybucket/myobject", true},
{NewResourceSet(NewResource("mybucket*", "")), "mybucket", true},
{NewResourceSet(NewResource("mybucket*", "")), "mybucket/myobject", true},
{NewResourceSet(NewResource("", "*")), "/myobject", true},
{NewResourceSet(NewResource("*", "*")), "mybucket/myobject", true},
{NewResourceSet(NewResource("mybucket", "*")), "mybucket/myobject", true},
{NewResourceSet(NewResource("mybucket*", "/myobject")), "mybucket/myobject", true},
{NewResourceSet(NewResource("mybucket*", "/myobject")), "mybucket100/myobject", true},
{NewResourceSet(NewResource("mybucket?0", "/2010/photos/*")), "mybucket20/2010/photos/1.jpg", true},
{NewResourceSet(NewResource("mybucket", "")), "mybucket", true},
{NewResourceSet(NewResource("mybucket?0", "")), "mybucket30", true},
{NewResourceSet(NewResource("mybucket?0", "/2010/photos/*"),
NewResource("mybucket", "/2010/photos/*")), "mybucket/2010/photos/1.jpg", true},
{NewResourceSet(NewResource("", "*")), "mybucket/myobject", false},
{NewResourceSet(NewResource("*", "*")), "mybucket", false},
{NewResourceSet(NewResource("mybucket", "*")), "mybucket10/myobject", false},
{NewResourceSet(NewResource("mybucket", "")), "mybucket/myobject", false},
{NewResourceSet(), "mybucket/myobject", false},
}
for i, testCase := range testCases {
result := testCase.resourceSet.Match(testCase.resource)
if result != testCase.expectedResult {
t.Fatalf("case %v: expected: %v, got: %v", i+1, testCase.expectedResult, result)
}
}
}
func TestResourceSetUnmarshalJSON(t *testing.T) {
testCases := []struct {
data []byte
expectedResult ResourceSet
expectErr bool
}{
{[]byte(`"arn:aws:s3:::mybucket/myobject*"`),
NewResourceSet(NewResource("mybucket", "/myobject*")), false},
{[]byte(`"arn:aws:s3:::mybucket/photos/myobject*"`),
NewResourceSet(NewResource("mybucket", "/photos/myobject*")), false},
{[]byte(`"arn:aws:s3:::mybucket"`), NewResourceSet(NewResource("mybucket", "")), false},
{[]byte(`"mybucket/myobject*"`), nil, true},
}
for i, testCase := range testCases {
var result ResourceSet
err := json.Unmarshal(testCase.data, &result)
expectErr := (err != nil)
if expectErr != testCase.expectErr {
t.Fatalf("case %v: error: expected: %v, got: %v", i+1, testCase.expectErr, expectErr)
}
if !testCase.expectErr {
if !reflect.DeepEqual(result, testCase.expectedResult) {
t.Fatalf("case %v: result: expected: %v, got: %v", i+1, testCase.expectedResult, result)
}
}
}
}
func TestResourceSetValidate(t *testing.T) {
testCases := []struct {
resourceSet ResourceSet
bucketName string
expectErr bool
}{
{NewResourceSet(NewResource("mybucket", "/myobject*")), "mybucket", false},
{NewResourceSet(NewResource("", "/myobject*")), "yourbucket", true},
{NewResourceSet(NewResource("mybucket", "/myobject*")), "yourbucket", true},
}
for i, testCase := range testCases {
err := testCase.resourceSet.Validate(testCase.bucketName)
expectErr := (err != nil)
if expectErr != testCase.expectErr {
t.Fatalf("case %v: error: expected: %v, got: %v", i+1, testCase.expectErr, expectErr)
}
}
}

156
pkg/policy/statement.go Normal file
View File

@@ -0,0 +1,156 @@
/*
* 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 policy
import (
"encoding/json"
"fmt"
"strings"
"github.com/minio/minio/pkg/policy/condition"
)
// Statement - policy statement.
type Statement struct {
SID ID `json:"Sid,omitempty"`
Effect Effect `json:"Effect"`
Principal Principal `json:"Principal"`
Actions ActionSet `json:"Action"`
Resources ResourceSet `json:"Resource"`
Conditions condition.Functions `json:"Condition,omitempty"`
}
// IsAllowed - checks given policy args is allowed to continue the Rest API.
func (statement Statement) IsAllowed(args Args) bool {
check := func() bool {
if !statement.Principal.Match(args.AccountName) {
return false
}
if !statement.Actions.Contains(args.Action) {
return false
}
resource := args.BucketName
if args.ObjectName != "" {
if !strings.HasPrefix(args.ObjectName, "/") {
resource += "/"
}
resource += args.ObjectName
}
if !statement.Resources.Match(resource) {
return false
}
return statement.Conditions.Evaluate(args.ConditionValues)
}
return statement.Effect.IsAllowed(check())
}
// isValid - checks whether statement is valid or not.
func (statement Statement) isValid() error {
if !statement.Effect.IsValid() {
return fmt.Errorf("invalid Effect %v", statement.Effect)
}
if !statement.Principal.IsValid() {
return fmt.Errorf("invalid Principal %v", statement.Principal)
}
if len(statement.Actions) == 0 {
return fmt.Errorf("Action must not be empty")
}
if len(statement.Resources) == 0 {
return fmt.Errorf("Resource must not be empty")
}
for action := range statement.Actions {
if action.isObjectAction() {
if !statement.Resources.objectResourceExists() {
return fmt.Errorf("unsupported Resource found %v for action %v", statement.Resources, action)
}
} else {
if !statement.Resources.bucketResourceExists() {
return fmt.Errorf("unsupported Resource found %v for action %v", statement.Resources, action)
}
}
keys := statement.Conditions.Keys()
keyDiff := keys.Difference(actionConditionKeyMap[action])
if !keyDiff.IsEmpty() {
return fmt.Errorf("unsupported condition keys '%v' used for action '%v'", keyDiff, action)
}
}
return nil
}
// MarshalJSON - encodes JSON data to Statement.
func (statement Statement) MarshalJSON() ([]byte, error) {
if err := statement.isValid(); err != nil {
return nil, err
}
// subtype to avoid recursive call to MarshalJSON()
type subStatement Statement
ss := subStatement(statement)
return json.Marshal(ss)
}
// UnmarshalJSON - decodes JSON data to Statement.
func (statement *Statement) UnmarshalJSON(data []byte) error {
// subtype to avoid recursive call to UnmarshalJSON()
type subStatement Statement
var ss subStatement
if err := json.Unmarshal(data, &ss); err != nil {
return err
}
s := Statement(ss)
if err := s.isValid(); err != nil {
return err
}
*statement = s
return nil
}
// Validate - validates Statement is for given bucket or not.
func (statement Statement) Validate(bucketName string) error {
if err := statement.isValid(); err != nil {
return err
}
return statement.Resources.Validate(bucketName)
}
// NewStatement - creates new statement.
func NewStatement(effect Effect, principal Principal, actionSet ActionSet, resourceSet ResourceSet, conditions condition.Functions) Statement {
return Statement{
Effect: effect,
Principal: principal,
Actions: actionSet,
Resources: resourceSet,
Conditions: conditions,
}
}

View File

@@ -0,0 +1,571 @@
/*
* 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 policy
import (
"encoding/json"
"net"
"reflect"
"testing"
"github.com/minio/minio/pkg/policy/condition"
)
func TestStatementIsAllowed(t *testing.T) {
case1Statement := NewStatement(
Allow,
NewPrincipal("*"),
NewActionSet(GetBucketLocationAction, PutObjectAction),
NewResourceSet(NewResource("*", "")),
condition.NewFunctions(),
)
case2Statement := NewStatement(
Allow,
NewPrincipal("*"),
NewActionSet(GetObjectAction, PutObjectAction),
NewResourceSet(NewResource("mybucket", "/myobject*")),
condition.NewFunctions(),
)
_, IPNet1, err := net.ParseCIDR("192.168.1.0/24")
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
func1, err := condition.NewIPAddressFunc(
condition.AWSSourceIP,
IPNet1,
)
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case3Statement := NewStatement(
Allow,
NewPrincipal("*"),
NewActionSet(GetObjectAction, PutObjectAction),
NewResourceSet(NewResource("mybucket", "/myobject*")),
condition.NewFunctions(func1),
)
case4Statement := NewStatement(
Deny,
NewPrincipal("*"),
NewActionSet(GetObjectAction, PutObjectAction),
NewResourceSet(NewResource("mybucket", "/myobject*")),
condition.NewFunctions(func1),
)
anonGetBucketLocationArgs := Args{
AccountName: "Q3AM3UQ867SPQQA43P2F",
Action: GetBucketLocationAction,
BucketName: "mybucket",
ConditionValues: map[string][]string{},
}
anonPutObjectActionArgs := Args{
AccountName: "Q3AM3UQ867SPQQA43P2F",
Action: PutObjectAction,
BucketName: "mybucket",
ConditionValues: map[string][]string{
"x-amz-copy-source": {"mybucket/myobject"},
"SourceIp": {"192.168.1.10"},
},
ObjectName: "myobject",
}
anonGetObjectActionArgs := Args{
AccountName: "Q3AM3UQ867SPQQA43P2F",
Action: GetObjectAction,
BucketName: "mybucket",
ConditionValues: map[string][]string{},
ObjectName: "myobject",
}
getBucketLocationArgs := Args{
AccountName: "Q3AM3UQ867SPQQA43P2F",
Action: GetBucketLocationAction,
BucketName: "mybucket",
ConditionValues: map[string][]string{},
IsOwner: true,
}
putObjectActionArgs := Args{
AccountName: "Q3AM3UQ867SPQQA43P2F",
Action: PutObjectAction,
BucketName: "mybucket",
ConditionValues: map[string][]string{
"x-amz-copy-source": {"mybucket/myobject"},
"SourceIp": {"192.168.1.10"},
},
IsOwner: true,
ObjectName: "myobject",
}
getObjectActionArgs := Args{
AccountName: "Q3AM3UQ867SPQQA43P2F",
Action: GetObjectAction,
BucketName: "mybucket",
ConditionValues: map[string][]string{},
IsOwner: true,
ObjectName: "myobject",
}
testCases := []struct {
statement Statement
args Args
expectedResult bool
}{
{case1Statement, anonGetBucketLocationArgs, true},
{case1Statement, anonPutObjectActionArgs, true},
{case1Statement, anonGetObjectActionArgs, false},
{case1Statement, getBucketLocationArgs, true},
{case1Statement, putObjectActionArgs, true},
{case1Statement, getObjectActionArgs, false},
{case2Statement, anonGetBucketLocationArgs, false},
{case2Statement, anonPutObjectActionArgs, true},
{case2Statement, anonGetObjectActionArgs, true},
{case2Statement, getBucketLocationArgs, false},
{case2Statement, putObjectActionArgs, true},
{case2Statement, getObjectActionArgs, true},
{case3Statement, anonGetBucketLocationArgs, false},
{case3Statement, anonPutObjectActionArgs, true},
{case3Statement, anonGetObjectActionArgs, false},
{case3Statement, getBucketLocationArgs, false},
{case3Statement, putObjectActionArgs, true},
{case3Statement, getObjectActionArgs, false},
{case4Statement, anonGetBucketLocationArgs, true},
{case4Statement, anonPutObjectActionArgs, false},
{case4Statement, anonGetObjectActionArgs, true},
{case4Statement, getBucketLocationArgs, true},
{case4Statement, putObjectActionArgs, false},
{case4Statement, getObjectActionArgs, true},
}
for i, testCase := range testCases {
result := testCase.statement.IsAllowed(testCase.args)
if result != testCase.expectedResult {
t.Fatalf("case %v: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
}
}
}
func TestStatementIsValid(t *testing.T) {
_, IPNet1, err := net.ParseCIDR("192.168.1.0/24")
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
func1, err := condition.NewIPAddressFunc(
condition.AWSSourceIP,
IPNet1,
)
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
func2, err := condition.NewStringEqualsFunc(
condition.S3XAmzCopySource,
"mybucket/myobject",
)
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
testCases := []struct {
statement Statement
expectErr bool
}{
// Invalid effect error.
{NewStatement(
Effect("foo"),
NewPrincipal("*"),
NewActionSet(GetBucketLocationAction, PutObjectAction),
NewResourceSet(NewResource("*", "")),
condition.NewFunctions(),
), true},
// Invalid principal error.
{NewStatement(
Allow,
NewPrincipal(),
NewActionSet(GetBucketLocationAction, PutObjectAction),
NewResourceSet(NewResource("*", "")),
condition.NewFunctions(),
), true},
// Empty actions error.
{NewStatement(
Allow,
NewPrincipal("*"),
NewActionSet(),
NewResourceSet(NewResource("*", "")),
condition.NewFunctions(),
), true},
// Empty resources error.
{NewStatement(
Allow,
NewPrincipal("*"),
NewActionSet(GetBucketLocationAction, PutObjectAction),
NewResourceSet(),
condition.NewFunctions(),
), true},
// Unsupported resource found for object action.
{NewStatement(
Allow,
NewPrincipal("*"),
NewActionSet(GetBucketLocationAction, PutObjectAction),
NewResourceSet(NewResource("mybucket", "")),
condition.NewFunctions(),
), true},
// Unsupported resource found for bucket action.
{NewStatement(
Allow,
NewPrincipal("*"),
NewActionSet(GetBucketLocationAction, PutObjectAction),
NewResourceSet(NewResource("mybucket", "myobject*")),
condition.NewFunctions(),
), true},
// Unsupported condition key for action.
{NewStatement(
Allow,
NewPrincipal("*"),
NewActionSet(GetObjectAction, PutObjectAction),
NewResourceSet(NewResource("mybucket", "myobject*")),
condition.NewFunctions(func1, func2),
), true},
{NewStatement(
Deny,
NewPrincipal("*"),
NewActionSet(GetObjectAction, PutObjectAction),
NewResourceSet(NewResource("mybucket", "myobject*")),
condition.NewFunctions(func1),
), false},
}
for i, testCase := range testCases {
err := testCase.statement.isValid()
expectErr := (err != nil)
if expectErr != testCase.expectErr {
t.Fatalf("case %v: error: expected: %v, got: %v", i+1, testCase.expectErr, expectErr)
}
}
}
func TestStatementMarshalJSON(t *testing.T) {
case1Statement := NewStatement(
Allow,
NewPrincipal("*"),
NewActionSet(PutObjectAction),
NewResourceSet(NewResource("mybucket", "/myobject*")),
condition.NewFunctions(),
)
case1Statement.SID = "SomeId1"
case1Data := []byte(`{"Sid":"SomeId1","Effect":"Allow","Principal":{"AWS":["*"]},"Action":["s3:PutObject"],"Resource":["arn:aws:s3:::mybucket/myobject*"]}`)
func1, err := condition.NewNullFunc(
condition.S3XAmzCopySource,
true,
)
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case2Statement := NewStatement(
Allow,
NewPrincipal("*"),
NewActionSet(PutObjectAction),
NewResourceSet(NewResource("mybucket", "/myobject*")),
condition.NewFunctions(func1),
)
case2Data := []byte(`{"Effect":"Allow","Principal":{"AWS":["*"]},"Action":["s3:PutObject"],"Resource":["arn:aws:s3:::mybucket/myobject*"],"Condition":{"Null":{"s3:x-amz-copy-source":[true]}}}`)
func2, err := condition.NewNullFunc(
condition.S3XAmzServerSideEncryption,
false,
)
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case3Statement := NewStatement(
Deny,
NewPrincipal("*"),
NewActionSet(GetObjectAction),
NewResourceSet(NewResource("mybucket", "/myobject*")),
condition.NewFunctions(func2),
)
case3Data := []byte(`{"Effect":"Deny","Principal":{"AWS":["*"]},"Action":["s3:GetObject"],"Resource":["arn:aws:s3:::mybucket/myobject*"],"Condition":{"Null":{"s3:x-amz-server-side-encryption":[false]}}}`)
case4Statement := NewStatement(
Allow,
NewPrincipal("*"),
NewActionSet(GetObjectAction, PutObjectAction),
NewResourceSet(NewResource("mybucket", "myobject*")),
condition.NewFunctions(func1, func2),
)
testCases := []struct {
statement Statement
expectedResult []byte
expectErr bool
}{
{case1Statement, case1Data, false},
{case2Statement, case2Data, false},
{case3Statement, case3Data, false},
// Invalid statement error.
{case4Statement, nil, true},
}
for i, testCase := range testCases {
result, err := json.Marshal(testCase.statement)
expectErr := (err != nil)
if expectErr != testCase.expectErr {
t.Fatalf("case %v: error: expected: %v, got: %v", i+1, testCase.expectErr, expectErr)
}
if !testCase.expectErr {
if !reflect.DeepEqual(result, testCase.expectedResult) {
t.Fatalf("case %v: result: expected: %v, got: %v", i+1, string(testCase.expectedResult), string(result))
}
}
}
}
func TestStatementUnmarshalJSON(t *testing.T) {
case1Data := []byte(`{
"Sid": "SomeId1",
"Effect": "Allow",
"Principal": "*",
"Action": "s3:PutObject",
"Resource": "arn:aws:s3:::mybucket/myobject*"
}`)
case1Statement := NewStatement(
Allow,
NewPrincipal("*"),
NewActionSet(PutObjectAction),
NewResourceSet(NewResource("mybucket", "/myobject*")),
condition.NewFunctions(),
)
case1Statement.SID = "SomeId1"
case2Data := []byte(`{
"Effect": "Allow",
"Principal": "*",
"Action": "s3:PutObject",
"Resource": "arn:aws:s3:::mybucket/myobject*",
"Condition": {
"Null": {
"s3:x-amz-copy-source": true
}
}
}`)
func1, err := condition.NewNullFunc(
condition.S3XAmzCopySource,
true,
)
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case2Statement := NewStatement(
Allow,
NewPrincipal("*"),
NewActionSet(PutObjectAction),
NewResourceSet(NewResource("mybucket", "/myobject*")),
condition.NewFunctions(func1),
)
case3Data := []byte(`{
"Effect": "Deny",
"Principal": {
"AWS": "*"
},
"Action": [
"s3:PutObject",
"s3:GetObject"
],
"Resource": "arn:aws:s3:::mybucket/myobject*",
"Condition": {
"Null": {
"s3:x-amz-server-side-encryption": "false"
}
}
}`)
func2, err := condition.NewNullFunc(
condition.S3XAmzServerSideEncryption,
false,
)
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case3Statement := NewStatement(
Deny,
NewPrincipal("*"),
NewActionSet(PutObjectAction, GetObjectAction),
NewResourceSet(NewResource("mybucket", "/myobject*")),
condition.NewFunctions(func2),
)
case4Data := []byte(`{
"Effect": "Allow",
"Principal": "Q3AM3UQ867SPQQA43P2F",
"Action": "s3:PutObject",
"Resource": "arn:aws:s3:::mybucket/myobject*"
}`)
case5Data := []byte(`{
"Principal": "*",
"Action": "s3:PutObject",
"Resource": "arn:aws:s3:::mybucket/myobject*"
}`)
case6Data := []byte(`{
"Effect": "Allow",
"Action": "s3:PutObject",
"Resource": "arn:aws:s3:::mybucket/myobject*"
}`)
case7Data := []byte(`{
"Effect": "Allow",
"Principal": "*",
"Resource": "arn:aws:s3:::mybucket/myobject*"
}`)
case8Data := []byte(`{
"Effect": "Allow",
"Principal": "*",
"Action": "s3:PutObject"
}`)
case9Data := []byte(`{
"Effect": "Allow",
"Principal": "*",
"Action": "s3:PutObject",
"Resource": "arn:aws:s3:::mybucket/myobject*",
"Condition": {
}
}`)
case10Data := []byte(`{
"Effect": "Deny",
"Principal": {
"AWS": "*"
},
"Action": [
"s3:PutObject",
"s3:GetObject"
],
"Resource": "arn:aws:s3:::mybucket/myobject*",
"Condition": {
"StringEquals": {
"s3:x-amz-copy-source": "yourbucket/myobject*"
}
}
}`)
testCases := []struct {
data []byte
expectedResult Statement
expectErr bool
}{
{case1Data, case1Statement, false},
{case2Data, case2Statement, false},
{case3Data, case3Statement, false},
// JSON unmarshaling error.
{case4Data, Statement{}, true},
// Invalid effect error.
{case5Data, Statement{}, true},
// empty principal error.
{case6Data, Statement{}, true},
// Empty action error.
{case7Data, Statement{}, true},
// Empty resource error.
{case8Data, Statement{}, true},
// Empty condition error.
{case9Data, Statement{}, true},
// Unsupported condition key error.
{case10Data, Statement{}, true},
}
for i, testCase := range testCases {
var result Statement
err := json.Unmarshal(testCase.data, &result)
expectErr := (err != nil)
if expectErr != testCase.expectErr {
t.Fatalf("case %v: error: expected: %v, got: %v", i+1, testCase.expectErr, expectErr)
}
if !testCase.expectErr {
if !reflect.DeepEqual(result, testCase.expectedResult) {
t.Fatalf("case %v: result: expected: %v, got: %v", i+1, testCase.expectedResult, result)
}
}
}
}
func TestStatementValidate(t *testing.T) {
case1Statement := NewStatement(
Allow,
NewPrincipal("*"),
NewActionSet(PutObjectAction),
NewResourceSet(NewResource("mybucket", "/myobject*")),
condition.NewFunctions(),
)
func1, err := condition.NewNullFunc(
condition.S3XAmzCopySource,
true,
)
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
func2, err := condition.NewNullFunc(
condition.S3XAmzServerSideEncryption,
false,
)
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case2Statement := NewStatement(
Allow,
NewPrincipal("*"),
NewActionSet(GetObjectAction, PutObjectAction),
NewResourceSet(NewResource("mybucket", "myobject*")),
condition.NewFunctions(func1, func2),
)
testCases := []struct {
statement Statement
bucketName string
expectErr bool
}{
{case1Statement, "mybucket", false},
{case2Statement, "mybucket", true},
{case1Statement, "yourbucket", true},
}
for i, testCase := range testCases {
err := testCase.statement.Validate(testCase.bucketName)
expectErr := (err != nil)
if expectErr != testCase.expectErr {
t.Fatalf("case %v: error: expected: %v, got: %v", i+1, testCase.expectErr, expectErr)
}
}
}