// ConditionKeyMap.Remove() is called and the result is validated. func TestConditionKeyMapRemove(t *testing.T) { condKeyMap := make(ConditionKeyMap) condKeyMap.Add("s3:prefix", set.CreateStringSet("hello", "world")) testCases := []struct { key string value set.StringSet expectedResult string }{ // Remove non-existent key and value. {"s3:myprefix", set.CreateStringSet("hello"), `{"s3:prefix":["hello","world"]}`}, // Remove existing key and value. {"s3:prefix", set.CreateStringSet("hello"), `{"s3:prefix":["world"]}`}, // Remove existing key to make the key also removed. {"s3:prefix", set.CreateStringSet("world"), `{}`}, } for _, testCase := range testCases { condKeyMap.Remove(testCase.key, testCase.value) if data, err := json.Marshal(condKeyMap); err != nil { t.Fatalf("Unable to marshal ConditionKeyMap to JSON, %s", err) } else { if string(data) != testCase.expectedResult { t.Fatalf("case: %+v: expected: %s, got: %s", testCase, testCase.expectedResult, string(data)) } } } }
// ConditionMap.Add() is called and the result is validated. func TestConditionMapAdd(t *testing.T) { condMap := make(ConditionMap) condKeyMap1 := make(ConditionKeyMap) condKeyMap1.Add("s3:prefix", set.CreateStringSet("hello")) condKeyMap2 := make(ConditionKeyMap) condKeyMap2.Add("s3:prefix", set.CreateStringSet("hello", "world")) testCases := []struct { key string value ConditionKeyMap expectedResult string }{ // Add new key and value. {"StringEquals", condKeyMap1, `{"StringEquals":{"s3:prefix":["hello"]}}`}, // Add existing key and value. {"StringEquals", condKeyMap1, `{"StringEquals":{"s3:prefix":["hello"]}}`}, // Add existing key and not value. {"StringEquals", condKeyMap2, `{"StringEquals":{"s3:prefix":["hello","world"]}}`}, } for _, testCase := range testCases { condMap.Add(testCase.key, testCase.value) if data, err := json.Marshal(condMap); err != nil { t.Fatalf("Unable to marshal ConditionKeyMap to JSON, %s", err) } else { if string(data) != testCase.expectedResult { t.Fatalf("case: %+v: expected: %s, got: %s", testCase, testCase.expectedResult, string(data)) } } } }
// Obtain object statements for write only bucketPolicy. func getWriteOnlyObjectStatement(bucketName, objectPrefix string) policyStatement { objectResourceStatement := policyStatement{} objectResourceStatement.Effect = "Allow" objectResourceStatement.Principal.AWS = set.CreateStringSet([]string{"*"}...) objectResourceStatement.Resources = set.CreateStringSet([]string{fmt.Sprintf("%s%s", AWSResourcePrefix, bucketName+"/"+objectPrefix+"*")}...) objectResourceStatement.Actions = set.CreateStringSet(writeOnlyObjectActions...) return objectResourceStatement }
// Obtain object statement for read-write bucketPolicy. func getReadWriteBucketStatement(bucketName, objectPrefix string) policyStatement { bucketResourceStatement := policyStatement{} bucketResourceStatement.Effect = "Allow" bucketResourceStatement.Principal.AWS = set.CreateStringSet([]string{"*"}...) bucketResourceStatement.Resources = set.CreateStringSet([]string{fmt.Sprintf("%s%s", AWSResourcePrefix, bucketName)}...) bucketResourceStatement.Actions = set.CreateStringSet(readWriteBucketActions...) return bucketResourceStatement }
// Obtain bucket statement for read-write bucketPolicy. func getReadWriteObjectStatement(bucketName, objectPrefix string) policyStatement { objectResourceStatement := policyStatement{} objectResourceStatement.Effect = "Allow" objectResourceStatement.Principal = map[string]interface{}{ "AWS": "*", } objectResourceStatement.Resources = set.CreateStringSet([]string{fmt.Sprintf("%s%s", AWSResourcePrefix, bucketName+"/"+objectPrefix+"*")}...) objectResourceStatement.Actions = set.CreateStringSet(readWriteObjectActions...) return objectResourceStatement }
// Obtain bucket statement for read only bucketPolicy. func getReadOnlyBucketStatement(bucketName, objectPrefix string) policyStatement { bucketResourceStatement := policyStatement{} bucketResourceStatement.Effect = "Allow" bucketResourceStatement.Principal = map[string]interface{}{ "AWS": "*", } bucketResourceStatement.Resources = set.CreateStringSet([]string{fmt.Sprintf("%s%s", AWSResourcePrefix, bucketName)}...) bucketResourceStatement.Actions = set.CreateStringSet(readOnlyBucketActions...) return bucketResourceStatement }
// Returns new statements with bucket actions for given policy. func newBucketStatement(policy BucketPolicy, bucketName string, prefix string) (statements []Statement) { statements = []Statement{} if policy == BucketPolicyNone || bucketName == "" { return statements } bucketResource := set.CreateStringSet(awsResourcePrefix + bucketName) statement := Statement{ Actions: commonBucketActions, Effect: "Allow", Principal: User{AWS: set.CreateStringSet("*")}, Resources: bucketResource, Sid: "", } statements = append(statements, statement) if policy == BucketPolicyReadOnly || policy == BucketPolicyReadWrite { statement = Statement{ Actions: readOnlyBucketActions, Effect: "Allow", Principal: User{AWS: set.CreateStringSet("*")}, Resources: bucketResource, Sid: "", } if prefix != "" { condKeyMap := make(ConditionKeyMap) condKeyMap.Add("s3:prefix", set.CreateStringSet(prefix)) condMap := make(ConditionMap) condMap.Add("StringEquals", condKeyMap) statement.Conditions = condMap } statements = append(statements, statement) } if policy == BucketPolicyWriteOnly || policy == BucketPolicyReadWrite { statement = Statement{ Actions: writeOnlyBucketActions, Effect: "Allow", Principal: User{AWS: set.CreateStringSet("*")}, Resources: bucketResource, Sid: "", } statements = append(statements, statement) } return statements }
// CopyConditionKeyMap() is called and the result is validated. func TestCopyConditionKeyMap(t *testing.T) { emptyCondKeyMap := make(ConditionKeyMap) nonEmptyCondKeyMap := make(ConditionKeyMap) nonEmptyCondKeyMap.Add("s3:prefix", set.CreateStringSet("hello", "world")) testCases := []struct { condKeyMap ConditionKeyMap expectedResult string }{ // To test empty ConditionKeyMap. {emptyCondKeyMap, `{}`}, // To test non-empty ConditionKeyMap. {nonEmptyCondKeyMap, `{"s3:prefix":["hello","world"]}`}, } for _, testCase := range testCases { condKeyMap := CopyConditionKeyMap(testCase.condKeyMap) if data, err := json.Marshal(condKeyMap); err != nil { t.Fatalf("Unable to marshal ConditionKeyMap to JSON, %s", err) } else { if string(data) != testCase.expectedResult { t.Fatalf("case: %+v: expected: %s, got: %s", testCase, testCase.expectedResult, string(data)) } } } }
// http://docs.aws.amazon.com/AmazonS3/latest/dev/using-with-s3-actions.html // Enforces bucket policies for a bucket for a given tatusaction. func enforceBucketPolicy(bucket string, action string, reqURL *url.URL) (s3Error APIErrorCode) { if !IsValidBucketName(bucket) { return ErrInvalidBucketName } // Fetch bucket policy, if policy is not set return access denied. policy := globalBucketPolicies.GetBucketPolicy(bucket) if policy == nil { return ErrAccessDenied } // Construct resource in 'arn:aws:s3:::examplebucket/object' format. resource := AWSResourcePrefix + strings.TrimSuffix(strings.TrimPrefix(reqURL.Path, "/"), "/") // Get conditions for policy verification. conditionKeyMap := make(map[string]set.StringSet) for queryParam := range reqURL.Query() { conditionKeyMap[queryParam] = set.CreateStringSet(reqURL.Query().Get(queryParam)) } // Validate action, resource and conditions with current policy statements. if !bucketPolicyEvalStatements(action, resource, conditionKeyMap, policy.Statements) { return ErrAccessDenied } return ErrNone }
// mergeConditionMap() is called and the result is validated. func TestMergeConditionMap(t *testing.T) { condKeyMap1 := make(ConditionKeyMap) condKeyMap1.Add("s3:prefix", set.CreateStringSet("hello")) condMap1 := make(ConditionMap) condMap1.Add("StringEquals", condKeyMap1) condKeyMap2 := make(ConditionKeyMap) condKeyMap2.Add("s3:prefix", set.CreateStringSet("world")) condMap2 := make(ConditionMap) condMap2.Add("StringEquals", condKeyMap2) condMap3 := make(ConditionMap) condMap3.Add("StringNotEquals", condKeyMap2) testCases := []struct { condMap1 ConditionMap condMap2 ConditionMap expectedResult string }{ // Both arguments are empty. {make(ConditionMap), make(ConditionMap), `{}`}, // First argument is empty. {make(ConditionMap), condMap1, `{"StringEquals":{"s3:prefix":["hello"]}}`}, // Second argument is empty. {condMap1, make(ConditionMap), `{"StringEquals":{"s3:prefix":["hello"]}}`}, // Both arguments are same value. {condMap1, condMap1, `{"StringEquals":{"s3:prefix":["hello"]}}`}, // Value of second argument will be merged. {condMap1, condMap2, `{"StringEquals":{"s3:prefix":["hello","world"]}}`}, // second argument will be added. {condMap1, condMap3, `{"StringEquals":{"s3:prefix":["hello"]},"StringNotEquals":{"s3:prefix":["world"]}}`}, } for _, testCase := range testCases { condMap := mergeConditionMap(testCase.condMap1, testCase.condMap2) if data, err := json.Marshal(condMap); err != nil { t.Fatalf("Unable to marshal ConditionKeyMap to JSON, %s", err) } else { if string(data) != testCase.expectedResult { t.Fatalf("case: %+v: expected: %s, got: %s", testCase, testCase.expectedResult, string(data)) } } } }
// Tests validate Bucket policy resource matcher. func TestBucketPolicyResourceMatch(t *testing.T) { // generates statement with given resource.. generateStatement := func(resource string) policyStatement { statement := policyStatement{} statement.Resources = set.CreateStringSet([]string{resource}...) return statement } // generates resource prefix. generateResource := func(bucketName, objectName string) string { return AWSResourcePrefix + bucketName + "/" + objectName } testCases := []struct { resourceToMatch string statement policyStatement expectedResourceMatch bool }{ // Test case 1-4. // Policy with resource ending with bucket/* allows access to all objects inside the given bucket. {generateResource("minio-bucket", ""), generateStatement(fmt.Sprintf("%s%s", AWSResourcePrefix, "minio-bucket"+"/*")), true}, {generateResource("minio-bucket", ""), generateStatement(fmt.Sprintf("%s%s", AWSResourcePrefix, "minio-bucket"+"/*")), true}, {generateResource("minio-bucket", ""), generateStatement(fmt.Sprintf("%s%s", AWSResourcePrefix, "minio-bucket"+"/*")), true}, {generateResource("minio-bucket", ""), generateStatement(fmt.Sprintf("%s%s", AWSResourcePrefix, "minio-bucket"+"/*")), true}, // Test case - 5. // Policy with resource ending with bucket/oo* should not allow access to bucket/output.txt. {generateResource("minio-bucket", "output.txt"), generateStatement(fmt.Sprintf("%s%s", AWSResourcePrefix, "minio-bucket"+"/oo*")), false}, // Test case - 6. // Policy with resource ending with bucket/oo* should allow access to bucket/ootput.txt. {generateResource("minio-bucket", "ootput.txt"), generateStatement(fmt.Sprintf("%s%s", AWSResourcePrefix, "minio-bucket"+"/oo*")), true}, // Test case - 7. // Policy with resource ending with bucket/oo* allows access to all sub-dirs starting with "oo" inside given bucket. {generateResource("minio-bucket", "oop-bucket/my-file"), generateStatement(fmt.Sprintf("%s%s", AWSResourcePrefix, "minio-bucket"+"/oo*")), true}, // Test case - 8. {generateResource("minio-bucket", "Asia/India/1.pjg"), generateStatement(fmt.Sprintf("%s%s", AWSResourcePrefix, "minio-bucket"+"/Asia/Japan/*")), false}, // Test case - 9. {generateResource("minio-bucket", "Asia/India/1.pjg"), generateStatement(fmt.Sprintf("%s%s", AWSResourcePrefix, "minio-bucket"+"/Asia/Japan/*")), false}, // Test case - 10. // Proves that the name space is flat. {generateResource("minio-bucket", "Africa/Bihar/India/design_info.doc/Bihar"), generateStatement(fmt.Sprintf("%s%s", AWSResourcePrefix, "minio-bucket"+"/*/India/*/Bihar")), true}, // Test case - 11. // Proves that the name space is flat. {generateResource("minio-bucket", "Asia/China/India/States/Bihar/output.txt"), generateStatement(fmt.Sprintf("%s%s", AWSResourcePrefix, "minio-bucket"+"/*/India/*/Bihar/*")), true}, } for i, testCase := range testCases { actualResourceMatch := bucketPolicyResourceMatch(testCase.resourceToMatch, testCase.statement) if testCase.expectedResourceMatch != actualResourceMatch { t.Errorf("Test %d: Expected Resource match to be `%v`, but instead found it to be `%v`", i+1, testCase.expectedResourceMatch, actualResourceMatch) } } }
// Tests validate Action validator. func TestIsValidActions(t *testing.T) { testCases := []struct { // input. actions set.StringSet // expected output. err error // flag indicating whether the test should pass. shouldPass bool }{ // Inputs with unsupported Action. // Test case - 1. // "s3:ListObject" is an invalid Action. {set.CreateStringSet([]string{"s3:GetObject", "s3:ListObject", "s3:RemoveObject"}...), errors.New("Unsupported actions found: ‘set.StringSet{\"s3:RemoveObject\":struct {}{}, \"s3:ListObject\":struct {}{}}’, please validate your policy document."), false}, // Test case - 2. // Empty Actions. {set.CreateStringSet([]string{}...), errors.New("Action list cannot be empty."), false}, // Test case - 3. // "s3:DeleteEverything"" is an invalid Action. {set.CreateStringSet([]string{"s3:GetObject", "s3:ListBucket", "s3:PutObject", "s3:DeleteEverything"}...), errors.New("Unsupported actions found: ‘set.StringSet{\"s3:DeleteEverything\":struct {}{}}’, please validate your policy document."), false}, // Inputs with valid Action. // Test Case - 4. {set.CreateStringSet([]string{ "s3:*", "*", "s3:GetObject", "s3:ListBucket", "s3:PutObject", "s3:GetBucketLocation", "s3:DeleteObject", "s3:AbortMultipartUpload", "s3:ListBucketMultipartUploads", "s3:ListMultipartUploadParts"}...), nil, true}, } for i, testCase := range testCases { err := isValidActions(testCase.actions) if err != nil && testCase.shouldPass { t.Errorf("Test %d: Expected to pass, but failed with: <ERROR> %s", i+1, err.Error()) } if err == nil && !testCase.shouldPass { t.Errorf("Test %d: Expected to fail with <ERROR> \"%s\", but passed instead", i+1, testCase.err.Error()) } } }
// isValidPrincipals - are valid principals. func isValidPrincipals(principals set.StringSet) (err error) { // Statement principal should have a value. if len(principals) == 0 { err = errors.New("Principal cannot be empty.") return err } if unsuppPrincipals := principals.Difference(set.CreateStringSet([]string{"*"}...)); !unsuppPrincipals.IsEmpty() { // Minio does not support or implement IAM, "*" is the only valid value. // Amazon s3 doc on principals: http://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements.html#Principal err = fmt.Errorf("Unsupported principals found: ‘%#v’, please validate your policy document.", unsuppPrincipals) return err } return nil }
// Returns new statements contains object actions for given policy. func newObjectStatement(policy BucketPolicy, bucketName string, prefix string) (statements []Statement) { statements = []Statement{} if policy == BucketPolicyNone || bucketName == "" { return statements } statement := Statement{ Effect: "Allow", Principal: User{AWS: set.CreateStringSet("*")}, Resources: set.CreateStringSet(awsResourcePrefix + bucketName + "/" + prefix + "*"), Sid: "", } if policy == BucketPolicyReadOnly { statement.Actions = readOnlyObjectActions } else if policy == BucketPolicyWriteOnly { statement.Actions = writeOnlyObjectActions } else if policy == BucketPolicyReadWrite { statement.Actions = readWriteObjectActions } statements = append(statements, statement) return statements }
// Parse principals parses a incoming json. Handles cases for // these three combinations. // - "Principal": "*", // - "Principal": { "AWS" : "*" } // - "Principal": { "AWS" : [ "*" ]} func parsePrincipals(principal interface{}) set.StringSet { principals, ok := principal.(map[string]interface{}) if !ok { var principalStr string principalStr, ok = principal.(string) if ok { return set.CreateStringSet(principalStr) } } // else { var principalStrs []string for _, p := range principals { principalStr, isStr := p.(string) if !isStr { principalsAdd, isInterface := p.([]interface{}) if !isInterface { principalStrsAddr, isStrs := p.([]string) if !isStrs { continue } principalStrs = append(principalStrs, principalStrsAddr...) } else { for _, pa := range principalsAdd { var pstr string pstr, isStr = pa.(string) if !isStr { continue } principalStrs = append(principalStrs, pstr) } } continue } // else { principalStrs = append(principalStrs, principalStr) } return set.CreateStringSet(principalStrs...) }
// Tests validate Resources validator. func TestIsValidResources(t *testing.T) { testCases := []struct { // input. resources []string // expected output. err error // flag indicating whether the test should pass. shouldPass bool }{ // Inputs with unsupported Action. // Test case - 1. // Empty Resources. {[]string{}, errors.New("Resource list cannot be empty."), false}, // Test case - 2. // A valid resource should have prefix "arn:aws:s3:::". {[]string{"my-resource"}, errors.New("Unsupported resource style found: ‘my-resource’, please validate your policy document."), false}, // Test case - 3. // A Valid resource should have bucket name followed by "arn:aws:s3:::". {[]string{"arn:aws:s3:::"}, errors.New("Invalid resource style found: ‘arn:aws:s3:::’, please validate your policy document."), false}, // Test Case - 4. // Valid resource shouldn't have slash('/') followed by "arn:aws:s3:::". {[]string{"arn:aws:s3:::/"}, errors.New("Invalid resource style found: ‘arn:aws:s3:::/’, please validate your policy document."), false}, // Test cases with valid Resources. {[]string{"arn:aws:s3:::my-bucket"}, nil, true}, {[]string{"arn:aws:s3:::my-bucket/Asia/*"}, nil, true}, {[]string{"arn:aws:s3:::my-bucket/Asia/India/*"}, nil, true}, } for i, testCase := range testCases { err := isValidResources(set.CreateStringSet(testCase.resources...)) if err != nil && testCase.shouldPass { t.Errorf("Test %d: Expected to pass, but failed with: <ERROR> %s", i+1, err.Error()) } if err == nil && !testCase.shouldPass { t.Errorf("Test %d: Expected to fail with <ERROR> \"%s\", but passed instead", i+1, testCase.err.Error()) } // Failed as expected, but does it fail for the expected reason. if err != nil && !testCase.shouldPass { if err.Error() != testCase.err.Error() { t.Errorf("Test %d: Expected to fail with error \"%s\", but instead failed with error \"%s\"", i+1, testCase.err.Error(), err.Error()) } } } }
func waitForFormattingDisks(disks, ignoredDisks []string) ([]StorageAPI, error) { // FS Setup if len(disks) == 1 { storage, err := newStorageAPI(disks[0]) if err != nil && err != errDiskNotFound { return nil, err } return []StorageAPI{storage}, nil } // XL Setup if err := checkSufficientDisks(disks); err != nil { return nil, err } disksSet := set.NewStringSet() if len(ignoredDisks) > 0 { disksSet = set.CreateStringSet(ignoredDisks...) } // Bootstrap disks. storageDisks := make([]StorageAPI, len(disks)) for index, disk := range disks { // Check if disk is ignored. if disksSet.Contains(disk) { // Set this situation as disk not found. storageDisks[index] = nil continue } // Intentionally ignore disk not found errors. XL is designed // to handle these errors internally. storage, err := newStorageAPI(disk) if err != nil && err != errDiskNotFound { return nil, err } storageDisks[index] = storage } // Start wait loop retrying formatting disks. return retryFormattingDisks(disks, storageDisks) }
// http://docs.aws.amazon.com/AmazonS3/latest/dev/using-with-s3-actions.html // Enforces bucket policies for a bucket for a given tatusaction. func enforceBucketPolicy(bucket string, action string, reqURL *url.URL) (s3Error APIErrorCode) { // Verify if bucket actually exists if err := isBucketExist(bucket, newObjectLayerFn()); err != nil { err = errorCause(err) switch err.(type) { case BucketNameInvalid: // Return error for invalid bucket name. return ErrInvalidBucketName case BucketNotFound: // For no bucket found we return NoSuchBucket instead. return ErrNoSuchBucket } errorIf(err, "Unable to read bucket policy.") // Return internal error for any other errors so that we can investigate. return ErrInternalError } // Fetch bucket policy, if policy is not set return access denied. policy := globalBucketPolicies.GetBucketPolicy(bucket) if policy == nil { return ErrAccessDenied } // Construct resource in 'arn:aws:s3:::examplebucket/object' format. resource := AWSResourcePrefix + strings.TrimSuffix(strings.TrimPrefix(reqURL.Path, "/"), "/") // Get conditions for policy verification. conditionKeyMap := make(map[string]set.StringSet) for queryParam := range reqURL.Query() { conditionKeyMap[queryParam] = set.CreateStringSet(reqURL.Query().Get(queryParam)) } // Validate action, resource and conditions with current policy statements. if !bucketPolicyEvalStatements(action, resource, conditionKeyMap, policy.Statements) { return ErrAccessDenied } return ErrNone }
// Tests validate principals validator. func TestIsValidPrincipals(t *testing.T) { testCases := []struct { // input. principals []string // expected output. err error // flag indicating whether the test should pass. shouldPass bool }{ // Inputs with unsupported Principals. // Test case - 1. // Empty Principals list. {[]string{}, errors.New("Principal cannot be empty."), false}, // Test case - 2. // "*" is the only valid principal. {[]string{"my-principal"}, errors.New("Unsupported principals found: ‘set.StringSet{\"my-principal\":struct {}{}}’, please validate your policy document."), false}, // Test case - 3. {[]string{"*", "111122233"}, errors.New("Unsupported principals found: ‘set.StringSet{\"111122233\":struct {}{}}’, please validate your policy document."), false}, // Test case - 4. // Test case with valid principal value. {[]string{"*"}, nil, true}, } for i, testCase := range testCases { err := isValidPrincipals(set.CreateStringSet(testCase.principals...)) if err != nil && testCase.shouldPass { t.Errorf("Test %d: Expected to pass, but failed with: <ERROR> %s", i+1, err.Error()) } if err == nil && !testCase.shouldPass { t.Errorf("Test %d: Expected to fail with <ERROR> \"%s\", but passed instead", i+1, testCase.err.Error()) } // Failed as expected, but does it fail for the expected reason. if err != nil && !testCase.shouldPass { if err.Error() != testCase.err.Error() { t.Errorf("Test %d: Expected to fail with error \"%s\", but instead failed with error \"%s\"", i+1, testCase.err.Error(), err.Error()) } } } }
"io" "path" "sort" "strings" "github.com/minio/minio-go/pkg/set" ) const ( // AWSResourcePrefix - bucket policy resource prefix. AWSResourcePrefix = "arn:aws:s3:::" ) // supportedActionMap - lists all the actions supported by minio. var supportedActionMap = set.CreateStringSet("*", "*", "s3:*", "s3:GetObject", "s3:ListBucket", "s3:PutObject", "s3:GetBucketLocation", "s3:DeleteObject", "s3:AbortMultipartUpload", "s3:ListBucketMultipartUploads", "s3:ListMultipartUploadParts") // supported Conditions type. var supportedConditionsType = set.CreateStringSet("StringEquals", "StringNotEquals") // Validate s3:prefix, s3:max-keys are present if not // supported keys for the conditions. var supportedConditionsKey = set.CreateStringSet("s3:prefix", "s3:max-keys") // supportedEffectMap - supported effects. var supportedEffectMap = set.CreateStringSet("Allow", "Deny") // policyUser - canonical users list. type policyUser struct { AWS set.StringSet `json:"AWS,omitempty"`
// Tests validate parsing of BucketAccessPolicy. func TestParseBucketPolicy(t *testing.T) { // set Unsupported Actions. setUnsupportedActions := func(statements []policyStatement) []policyStatement { // "s3:DeleteEverything"" is an Unsupported Action. statements[0].Actions = set.CreateStringSet([]string{"s3:GetObject", "s3:ListBucket", "s3:PutObject", "s3:DeleteEverything"}...) return statements } // set unsupported Effect. setUnsupportedEffect := func(statements []policyStatement) []policyStatement { // Effect "Don't allow" is Unsupported. statements[0].Effect = "DontAllow" return statements } // set unsupported principals. setUnsupportedPrincipals := func(statements []policyStatement) []policyStatement { // "User1111"" is an Unsupported Principal. statements[0].Principal.AWS = set.CreateStringSet([]string{"*", "User1111"}...) return statements } // set unsupported Resources. setUnsupportedResources := func(statements []policyStatement) []policyStatement { // "s3:DeleteEverything"" is an Unsupported Action. statements[0].Resources = set.CreateStringSet([]string{"my-resource"}...) return statements } // List of bucketPolicy used for test cases. bucketAccesPolicies := []bucketPolicy{ // bucketPolicy - 0. // bucketPolicy statement empty. {Version: "1.0"}, // bucketPolicy - 1. // bucketPolicy version empty. {Version: "", Statements: []policyStatement{}}, // bucketPolicy - 2. // Readonly bucketPolicy. {Version: "1.0", Statements: getReadOnlyStatement("minio-bucket", "")}, // bucketPolicy - 3. // Read-Write bucket policy. {Version: "1.0", Statements: getReadWriteStatement("minio-bucket", "Asia/")}, // bucketPolicy - 4. // Write only bucket policy. {Version: "1.0", Statements: getWriteOnlyStatement("minio-bucket", "Asia/India/")}, // bucketPolicy - 5. // bucketPolicy statement contains unsupported action. {Version: "1.0", Statements: setUnsupportedActions(getReadOnlyStatement("minio-bucket", ""))}, // bucketPolicy - 6. // bucketPolicy statement contains unsupported Effect. {Version: "1.0", Statements: setUnsupportedEffect(getReadWriteStatement("minio-bucket", "Asia/"))}, // bucketPolicy - 7. // bucketPolicy statement contains unsupported Principal. {Version: "1.0", Statements: setUnsupportedPrincipals(getWriteOnlyStatement("minio-bucket", "Asia/India/"))}, // bucketPolicy - 8. // bucketPolicy statement contains unsupported Resource. {Version: "1.0", Statements: setUnsupportedResources(getWriteOnlyStatement("minio-bucket", "Asia/India/"))}, } testCases := []struct { inputPolicy bucketPolicy // expected results. expectedPolicy bucketPolicy err error // Flag indicating whether the test should pass. shouldPass bool }{ // Test case - 1. // bucketPolicy statement empty. {bucketAccesPolicies[0], bucketPolicy{}, errors.New("Policy statement cannot be empty."), false}, // Test case - 2. // bucketPolicy version empty. {bucketAccesPolicies[1], bucketPolicy{}, errors.New("Policy version cannot be empty."), false}, // Test case - 3. // Readonly bucketPolicy. {bucketAccesPolicies[2], bucketAccesPolicies[2], nil, true}, // Test case - 4. // Read-Write bucket policy. {bucketAccesPolicies[3], bucketAccesPolicies[3], nil, true}, // Test case - 5. // Write only bucket policy. {bucketAccesPolicies[4], bucketAccesPolicies[4], nil, true}, // Test case - 6. // bucketPolicy statement contains unsupported action. {bucketAccesPolicies[5], bucketAccesPolicies[5], fmt.Errorf("Unsupported actions found: ‘set.StringSet{\"s3:DeleteEverything\":struct {}{}}’, please validate your policy document."), false}, // Test case - 7. // bucketPolicy statement contains unsupported Effect. {bucketAccesPolicies[6], bucketAccesPolicies[6], fmt.Errorf("Unsupported Effect found: ‘DontAllow’, please validate your policy document."), false}, // Test case - 8. // bucketPolicy statement contains unsupported Principal. {bucketAccesPolicies[7], bucketAccesPolicies[7], fmt.Errorf("Unsupported principals found: ‘set.StringSet{\"User1111\":struct {}{}}’, please validate your policy document."), false}, // Test case - 9. // bucketPolicy statement contains unsupported Resource. {bucketAccesPolicies[8], bucketAccesPolicies[8], fmt.Errorf("Unsupported resource style found: ‘my-resource’, please validate your policy document."), false}, } for i, testCase := range testCases { var buffer bytes.Buffer encoder := json.NewEncoder(&buffer) err := encoder.Encode(testCase.inputPolicy) if err != nil { t.Fatalf("Test %d: Couldn't Marshal bucket policy %s", i+1, err) } var actualAccessPolicy = &bucketPolicy{} err = parseBucketPolicy(&buffer, actualAccessPolicy) if err != nil && testCase.shouldPass { t.Errorf("Test %d: Expected to pass, but failed with: <ERROR> %s", i+1, err.Error()) } if err == nil && !testCase.shouldPass { t.Errorf("Test %d: Expected to fail with <ERROR> \"%s\", but passed instead", i+1, testCase.err.Error()) } // Failed as expected, but does it fail for the expected reason. if err != nil && !testCase.shouldPass { if err.Error() != testCase.err.Error() { t.Errorf("Test %d: Expected to fail with error \"%s\", but instead failed with error \"%s\"", i+1, testCase.err.Error(), err.Error()) } } // Test passes as expected, but the output values are verified for correctness here. if err == nil && testCase.shouldPass { if testCase.expectedPolicy.String() != actualAccessPolicy.String() { t.Errorf("Test %d: The expected statements from resource statement generator doesn't match the actual statements", i+1) } } } }
// Tests validate Policy Action and Resource fields. func TestCheckbucketPolicyResources(t *testing.T) { // constructing policy statement without invalidPrefixActions (check bucket-policy-parser.go). setValidPrefixActions := func(statements []policyStatement) []policyStatement { statements[0].Actions = set.CreateStringSet([]string{"s3:DeleteObject", "s3:PutObject"}...) return statements } // contracting policy statement with recursive resources. // should result in ErrMalformedPolicy setRecurseResource := func(statements []policyStatement) []policyStatement { statements[0].Resources = set.CreateStringSet([]string{"arn:aws:s3:::minio-bucket/Asia/*", "arn:aws:s3:::minio-bucket/Asia/India/*"}...) return statements } // constructing policy statement with lexically close characters. // should not result in ErrMalformedPolicy setResourceLexical := func(statements []policyStatement) []policyStatement { statements[0].Resources = set.CreateStringSet([]string{"arn:aws:s3:::minio-bucket/op*", "arn:aws:s3:::minio-bucket/oo*"}...) return statements } // List of bucketPolicy used for tests. bucketAccessPolicies := []bucketPolicy{ // bucketPolicy - 1. // Contains valid read only policy statement. {Version: "1.0", Statements: getReadOnlyStatement("minio-bucket", "")}, // bucketPolicy - 2. // Contains valid read-write only policy statement. {Version: "1.0", Statements: getReadWriteStatement("minio-bucket", "Asia/")}, // bucketPolicy - 3. // Contains valid write only policy statement. {Version: "1.0", Statements: getWriteOnlyStatement("minio-bucket", "Asia/India/")}, // bucketPolicy - 4. // Contains invalidPrefixActions. // Since resourcePrefix is not to the bucket-name, it return ErrMalformedPolicy. {Version: "1.0", Statements: getReadOnlyStatement("minio-bucket-fail", "Asia/India/")}, // bucketPolicy - 5. // constructing policy statement without invalidPrefixActions (check bucket-policy-parser.go). // but bucket part of the resource is not equal to the bucket name. // this results in return of ErrMalformedPolicy. {Version: "1.0", Statements: setValidPrefixActions(getWriteOnlyStatement("minio-bucket-fail", "Asia/India/"))}, // bucketPolicy - 6. // contracting policy statement with recursive resources. // should result in ErrMalformedPolicy {Version: "1.0", Statements: setRecurseResource(setValidPrefixActions(getWriteOnlyStatement("minio-bucket", "")))}, // BucketPolciy - 7. // constructing policy statement with non recursive but // lexically close resources. // should result in ErrNone. {Version: "1.0", Statements: setResourceLexical(setValidPrefixActions(getWriteOnlyStatement("minio-bucket", "oo")))}, } testCases := []struct { inputPolicy bucketPolicy // expected results. apiErrCode APIErrorCode // Flag indicating whether the test should pass. shouldPass bool }{ // Test case - 1. {bucketAccessPolicies[0], ErrNone, true}, // Test case - 2. {bucketAccessPolicies[1], ErrNone, true}, // Test case - 3. {bucketAccessPolicies[2], ErrNone, true}, // Test case - 4. // contains invalidPrefixActions (check bucket-policy-parser.go). // Resource prefix will not be equal to the bucket name in this case. {bucketAccessPolicies[3], ErrMalformedPolicy, false}, // Test case - 5. // actions contain invalidPrefixActions (check bucket-policy-parser.go). // Resource prefix bucket part is not equal to the bucket name in this case. {bucketAccessPolicies[4], ErrMalformedPolicy, false}, // Test case - 6. // contracting policy statement with recursive resources. // should result in ErrPolicyNesting. {bucketAccessPolicies[5], ErrPolicyNesting, false}, // Test case - 7. // constructing policy statement with lexically close // characters. // should result in ErrNone. {bucketAccessPolicies[6], ErrNone, true}, } for i, testCase := range testCases { apiErrCode := checkBucketPolicyResources("minio-bucket", &testCase.inputPolicy) if apiErrCode != ErrNone && testCase.shouldPass { t.Errorf("Test %d: Expected to pass, but failed with Errocode %v", i+1, apiErrCode) } if apiErrCode == ErrNone && !testCase.shouldPass { t.Errorf("Test %d: Expected to fail with ErrCode %v, but passed instead", i+1, testCase.apiErrCode) } // Failed as expected, but does it fail for the expected reason. if apiErrCode != ErrNone && !testCase.shouldPass { if testCase.apiErrCode != apiErrCode { t.Errorf("Test %d: Expected to fail with error code %v, but instead failed with error code %v", i+1, testCase.apiErrCode, apiErrCode) } } } }
// Tests validate policyStatement condition validator. func TestIsValidConditions(t *testing.T) { // returns empty conditions map. setEmptyConditions := func() map[string]map[string]set.StringSet { return make(map[string]map[string]set.StringSet) } // returns map with the "StringEquals" set to empty map. setEmptyStringEquals := func() map[string]map[string]set.StringSet { emptyMap := make(map[string]set.StringSet) conditions := make(map[string]map[string]set.StringSet) conditions["StringEquals"] = emptyMap return conditions } // returns map with the "StringNotEquals" set to empty map. setEmptyStringNotEquals := func() map[string]map[string]set.StringSet { emptyMap := make(map[string]set.StringSet) conditions := make(map[string]map[string]set.StringSet) conditions["StringNotEquals"] = emptyMap return conditions } // Generate conditions. generateConditions := func(key1, key2, value string) map[string]map[string]set.StringSet { innerMap := make(map[string]set.StringSet) innerMap[key2] = set.CreateStringSet(value) conditions := make(map[string]map[string]set.StringSet) conditions[key1] = innerMap return conditions } // generate ambigious conditions. generateAmbigiousConditions := func() map[string]map[string]set.StringSet { innerMap := make(map[string]set.StringSet) innerMap["s3:prefix"] = set.CreateStringSet("Asia/") conditions := make(map[string]map[string]set.StringSet) conditions["StringEquals"] = innerMap conditions["StringNotEquals"] = innerMap return conditions } // generate valid and non valid type in the condition map. generateValidInvalidConditions := func() map[string]map[string]set.StringSet { innerMap := make(map[string]set.StringSet) innerMap["s3:prefix"] = set.CreateStringSet("Asia/") conditions := make(map[string]map[string]set.StringSet) conditions["StringEquals"] = innerMap conditions["InvalidType"] = innerMap return conditions } // generate valid and invalid keys for valid types in the same condition map. generateValidInvalidConditionKeys := func() map[string]map[string]set.StringSet { innerMapValid := make(map[string]set.StringSet) innerMapValid["s3:prefix"] = set.CreateStringSet("Asia/") innerMapInValid := make(map[string]set.StringSet) innerMapInValid["s3:invalid"] = set.CreateStringSet("Asia/") conditions := make(map[string]map[string]set.StringSet) conditions["StringEquals"] = innerMapValid conditions["StringEquals"] = innerMapInValid return conditions } // List of Conditions used for test cases. testConditions := []map[string]map[string]set.StringSet{ generateConditions("StringValues", "s3:max-keys", "100"), generateConditions("StringEquals", "s3:Object", "100"), generateAmbigiousConditions(), generateValidInvalidConditions(), generateValidInvalidConditionKeys(), setEmptyConditions(), setEmptyStringEquals(), setEmptyStringNotEquals(), generateConditions("StringEquals", "s3:prefix", "Asia/"), generateConditions("StringEquals", "s3:max-keys", "100"), generateConditions("StringNotEquals", "s3:prefix", "Asia/"), generateConditions("StringNotEquals", "s3:max-keys", "100"), } testCases := []struct { inputCondition map[string]map[string]set.StringSet // expected result. expectedErr error // flag indicating whether test should pass. shouldPass bool }{ // Malformed conditions. // Test case - 1. // "StringValues" is an invalid type. {testConditions[0], fmt.Errorf("Unsupported condition type 'StringValues', " + "please validate your policy document."), false}, // Test case - 2. // "s3:Object" is an invalid key. {testConditions[1], fmt.Errorf("Unsupported condition key " + "'StringEquals', please validate your policy document."), false}, // Test case - 3. // Test case with Ambigious conditions set. {testConditions[2], fmt.Errorf("Ambigious condition values for key 's3:prefix', " + "please validate your policy document."), false}, // Test case - 4. // Test case with valid and invalid condition types. {testConditions[3], fmt.Errorf("Unsupported condition type 'InvalidType', " + "please validate your policy document."), false}, // Test case - 5. // Test case with valid and invalid condition keys. {testConditions[4], fmt.Errorf("Unsupported condition key 'StringEquals', " + "please validate your policy document."), false}, // Test cases with valid conditions. // Test case - 6. {testConditions[5], nil, true}, // Test case - 7. {testConditions[6], nil, true}, // Test case - 8. {testConditions[7], nil, true}, // Test case - 9. {testConditions[8], nil, true}, // Test case - 10. {testConditions[9], nil, true}, // Test case - 11. {testConditions[10], nil, true}, // Test case 10. {testConditions[11], nil, true}, } for i, testCase := range testCases { actualErr := isValidConditions(testCase.inputCondition) if actualErr != nil && testCase.shouldPass { t.Errorf("Test %d: Expected to pass, but failed with: <ERROR> %s", i+1, actualErr.Error()) } if actualErr == nil && !testCase.shouldPass { t.Errorf("Test %d: Expected to fail with <ERROR> \"%s\", but passed instead", i+1, testCase.expectedErr.Error()) } // Failed as expected, but does it fail for the expected reason. if actualErr != nil && !testCase.shouldPass { if actualErr.Error() != testCase.expectedErr.Error() { t.Errorf("Test %d: Expected to fail with error \"%s\", but instead failed with error \"%s\"", i+1, testCase.expectedErr.Error(), actualErr.Error()) } } } }
// newXLObjects - initialize new xl object layer. func newXLObjects(disks, ignoredDisks []string) (ObjectLayer, error) { if disks == nil { return nil, errInvalidArgument } disksSet := set.NewStringSet() if len(ignoredDisks) > 0 { disksSet = set.CreateStringSet(ignoredDisks...) } // Bootstrap disks. storageDisks := make([]StorageAPI, len(disks)) for index, disk := range disks { // Check if disk is ignored. if disksSet.Contains(disk) { storageDisks[index] = nil continue } var err error // Intentionally ignore disk not found errors. XL is designed // to handle these errors internally. storageDisks[index], err = newStorageAPI(disk) if err != nil && err != errDiskNotFound { switch diskType := storageDisks[index].(type) { case networkStorage: diskType.rpcClient.Close() } return nil, err } } // Fix format files in case of fresh or corrupted disks repairDiskMetadata(storageDisks) // Runs house keeping code, like t, cleaning up tmp files etc. if err := xlHouseKeeping(storageDisks); err != nil { return nil, err } // Load saved XL format.json and validate. newPosixDisks, err := loadFormatXL(storageDisks) if err != nil { // errCorruptedDisk - healing failed return nil, fmt.Errorf("Unable to recognize backend format, %s", err) } // Calculate data and parity blocks. dataBlocks, parityBlocks := len(newPosixDisks)/2, len(newPosixDisks)/2 // Initialize object cache. objCache := objcache.New(globalMaxCacheSize, globalCacheExpiry) // Initialize list pool. listPool := newTreeWalkPool(globalLookupTimeout) // Initialize xl objects. xl := xlObjects{ storageDisks: newPosixDisks, dataBlocks: dataBlocks, parityBlocks: parityBlocks, listPool: listPool, objCache: objCache, objCacheEnabled: globalMaxCacheSize > 0, } // Figure out read and write quorum based on number of storage disks. // READ and WRITE quorum is always set to (N/2) number of disks. xl.readQuorum = len(xl.storageDisks) / 2 xl.writeQuorum = len(xl.storageDisks)/2 + 1 // Return successfully initialized object layer. return xl, nil }
// TestBucketPolicyConditionMatch - Tests to validate whether bucket policy conditions match. func TestBucketPolicyConditionMatch(t *testing.T) { // obtain the inner map[string]set.StringSet for policyStatement.Conditions . getInnerMap := func(key2, value string) map[string]set.StringSet { innerMap := make(map[string]set.StringSet) innerMap[key2] = set.CreateStringSet(value) return innerMap } // obtain policyStatement with Conditions set. getStatementWithCondition := func(key1, key2, value string) policyStatement { innerMap := getInnerMap(key2, value) // to set policyStatment.Conditions . conditions := make(map[string]map[string]set.StringSet) conditions[key1] = innerMap // new policy statement. statement := policyStatement{} // set the condition. statement.Conditions = conditions return statement } testCases := []struct { statementCondition policyStatement condition map[string]set.StringSet expectedMatch bool }{ // Test case - 1. // StringEquals condition matches. { statementCondition: getStatementWithCondition("StringEquals", "s3:prefix", "Asia/"), condition: getInnerMap("prefix", "Asia/"), expectedMatch: true, }, // Test case - 2. // StringEquals condition doesn't match. { statementCondition: getStatementWithCondition("StringEquals", "s3:prefix", "Asia/"), condition: getInnerMap("prefix", "Africa/"), expectedMatch: false, }, // Test case - 3. // StringEquals condition matches. { statementCondition: getStatementWithCondition("StringEquals", "s3:max-keys", "Asia/"), condition: getInnerMap("max-keys", "Asia/"), expectedMatch: true, }, // Test case - 4. // StringEquals condition doesn't match. { statementCondition: getStatementWithCondition("StringEquals", "s3:max-keys", "Asia/"), condition: getInnerMap("max-keys", "Africa/"), expectedMatch: false, }, // Test case - 5. // StringNotEquals condition matches. { statementCondition: getStatementWithCondition("StringNotEquals", "s3:prefix", "Asia/"), condition: getInnerMap("prefix", "Asia/"), expectedMatch: true, }, // Test case - 6. // StringNotEquals condition doesn't match. { statementCondition: getStatementWithCondition("StringNotEquals", "s3:prefix", "Asia/"), condition: getInnerMap("prefix", "Africa/"), expectedMatch: false, }, // Test case - 7. // StringNotEquals condition matches. { statementCondition: getStatementWithCondition("StringNotEquals", "s3:max-keys", "Asia/"), condition: getInnerMap("max-keys", "Asia/"), expectedMatch: true, }, // Test case - 8. // StringNotEquals condition doesn't match. { statementCondition: getStatementWithCondition("StringNotEquals", "s3:max-keys", "Asia/"), condition: getInnerMap("max-keys", "Africa/"), expectedMatch: false, }, } for i, tc := range testCases { t.Run(fmt.Sprintf("Test case %d: Failed.", i+1), func(t *testing.T) { // call the function under test and assert the result with the expected result. doesMatch := bucketPolicyConditionMatch(tc.condition, tc.statementCondition) if tc.expectedMatch != doesMatch { t.Errorf("Expected the match to be `%v`; got `%v`.", tc.expectedMatch, doesMatch) } }) } }
// testWebListAllBucketPoliciesHandler - Test ListAllBucketPolicies web handler func testWebListAllBucketPoliciesHandler(obj ObjectLayer, instanceType string, t TestErrHandler) { // Register the API end points with XL/FS object layer. apiRouter := initTestWebRPCEndPoint(obj) // initialize the server and obtain the credentials and root. // credentials are necessary to sign the HTTP request. rootPath, err := newTestConfig("us-east-1") if err != nil { t.Fatalf("Init Test config failed") } // remove the root directory after the test ends. defer removeAll(rootPath) credentials := serverConfig.GetCredential() authorization, err := getWebRPCToken(apiRouter, credentials.AccessKeyID, credentials.SecretAccessKey) if err != nil { t.Fatal("Cannot authenticate") } rec := httptest.NewRecorder() bucketName := getRandomBucketName() if err := obj.MakeBucket(bucketName); err != nil { t.Fatal("Unexpected error: ", err) } policyVal := bucketPolicy{ Version: "2012-10-17", Statements: []policyStatement{ { Actions: set.CreateStringSet("s3:GetBucketLocation"), Effect: "Allow", Principal: map[string][]string{"AWS": {"*"}}, Resources: set.CreateStringSet("arn:aws:s3:::" + bucketName), Sid: "", }, { Actions: set.CreateStringSet("s3:ListBucket"), Conditions: map[string]map[string]set.StringSet{ "StringEquals": { "s3:prefix": set.CreateStringSet("hello"), }, }, Effect: "Allow", Principal: map[string][]string{"AWS": {"*"}}, Resources: set.CreateStringSet("arn:aws:s3:::" + bucketName), Sid: "", }, { Actions: set.CreateStringSet("s3:ListBucketMultipartUploads"), Effect: "Allow", Principal: map[string][]string{"AWS": {"*"}}, Resources: set.CreateStringSet("arn:aws:s3:::" + bucketName), Sid: "", }, { Actions: set.CreateStringSet("s3:AbortMultipartUpload", "s3:DeleteObject", "s3:GetObject", "s3:ListMultipartUploadParts", "s3:PutObject"), Effect: "Allow", Principal: map[string][]string{"AWS": {"*"}}, Resources: set.CreateStringSet("arn:aws:s3:::" + bucketName + "/hello*"), Sid: "", }, }, } if err := writeBucketPolicy(bucketName, obj, &policyVal); err != nil { t.Fatal("Unexpected error: ", err) } testCaseResult1 := []bucketAccessPolicy{{ Prefix: bucketName + "/hello*", Policy: policy.BucketPolicyReadWrite, }} testCases := []struct { bucketName string expectedResult []bucketAccessPolicy }{ {bucketName, testCaseResult1}, } for i, testCase := range testCases { args := &ListAllBucketPoliciesArgs{BucketName: testCase.bucketName} reply := &ListAllBucketPoliciesRep{} req, err := newTestWebRPCRequest("Web.ListAllBucketPolicies", authorization, args) if err != nil { t.Fatalf("Test %d: Failed to create HTTP request: <ERROR> %v", i+1, err) } apiRouter.ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Fatalf("Test %d: Expected the response status to be 200, but instead found `%d`", i+1, rec.Code) } if err = getTestWebRPCResponse(rec, &reply); err != nil { t.Fatalf("Test %d: Should succeed but it didn't, %v", i+1, err) } if !reflect.DeepEqual(testCase.expectedResult, reply.Policies) { t.Fatalf("Test %d: expected: %v, got: %v", i+1, testCase.expectedResult, reply.Policies) } } }
) // isValidBucketPolicy - Is provided policy value supported. func (p BucketPolicy) IsValidBucketPolicy() bool { switch p { case BucketPolicyNone, BucketPolicyReadOnly, BucketPolicyReadWrite, BucketPolicyWriteOnly: return true } return false } // Resource prefix for all aws resources. const awsResourcePrefix = "arn:aws:s3:::" // Common bucket actions for both read and write policies. var commonBucketActions = set.CreateStringSet("s3:GetBucketLocation") // Read only bucket actions. var readOnlyBucketActions = set.CreateStringSet("s3:ListBucket") // Write only bucket actions. var writeOnlyBucketActions = set.CreateStringSet("s3:ListBucketMultipartUploads") // Read only object actions. var readOnlyObjectActions = set.CreateStringSet("s3:GetObject") // Write only object actions. var writeOnlyObjectActions = set.CreateStringSet("s3:AbortMultipartUpload", "s3:DeleteObject", "s3:ListMultipartUploadParts", "s3:PutObject") // Read and write object actions. var readWriteObjectActions = readOnlyObjectActions.Union(writeOnlyObjectActions)