/* * Minio Cloud Storage, (C) 2015, 2016 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 main import ( "encoding/json" "errors" "fmt" "reflect" "testing" ) var ( readWriteBucketActions = []string{ "s3:GetBucketLocation", "s3:ListBucket", "s3:ListBucketMultipartUploads", // Add more bucket level read-write actions here. } readWriteObjectActions = []string{ "s3:AbortMultipartUpload", "s3:DeleteObject", "s3:GetObject", "s3:ListMultipartUploadParts", "s3:PutObject", // Add more object level read-write actions here. } ) // Write only actions. var ( writeOnlyBucketActions = []string{ "s3:GetBucketLocation", "s3:ListBucketMultipartUploads", // Add more bucket level write actions here. } writeOnlyObjectActions = []string{ "s3:AbortMultipartUpload", "s3:DeleteObject", "s3:ListMultipartUploadParts", "s3:PutObject", // Add more object level write actions here. } ) // Read only actions. var ( readOnlyBucketActions = []string{ "s3:GetBucketLocation", "s3:ListBucket", // Add more bucket level read actions here. } readOnlyObjectActions = []string{ "s3:GetObject", // Add more object level read actions here. } ) // Obtain statements for read-write BucketPolicy. func setReadWriteStatement(bucketName, objectPrefix string) []policyStatement { bucketResourceStatement := policyStatement{} objectResourceStatement := policyStatement{} statements := []policyStatement{} bucketResourceStatement.Effect = "Allow" bucketResourceStatement.Principal.AWS = []string{"*"} bucketResourceStatement.Resources = []string{fmt.Sprintf("%s%s", AWSResourcePrefix, bucketName)} bucketResourceStatement.Actions = readWriteBucketActions objectResourceStatement.Effect = "Allow" objectResourceStatement.Principal.AWS = []string{"*"} objectResourceStatement.Resources = []string{fmt.Sprintf("%s%s", AWSResourcePrefix, bucketName+"/"+objectPrefix+"*")} objectResourceStatement.Actions = readWriteObjectActions // Save the read write policy. statements = append(statements, bucketResourceStatement, objectResourceStatement) return statements } // Obtain statements for read only BucketPolicy. func setReadOnlyStatement(bucketName, objectPrefix string) []policyStatement { bucketResourceStatement := policyStatement{} objectResourceStatement := policyStatement{} statements := []policyStatement{} bucketResourceStatement.Effect = "Allow" bucketResourceStatement.Principal.AWS = []string{"*"} bucketResourceStatement.Resources = []string{fmt.Sprintf("%s%s", AWSResourcePrefix, bucketName)} bucketResourceStatement.Actions = readOnlyBucketActions objectResourceStatement.Effect = "Allow" objectResourceStatement.Principal.AWS = []string{"*"} objectResourceStatement.Resources = []string{fmt.Sprintf("%s%s", AWSResourcePrefix, bucketName+"/"+objectPrefix+"*")} objectResourceStatement.Actions = readOnlyObjectActions // Save the read only policy. statements = append(statements, bucketResourceStatement, objectResourceStatement) return statements } // Obtain statements for write only BucketPolicy. func setWriteOnlyStatement(bucketName, objectPrefix string) []policyStatement { bucketResourceStatement := policyStatement{} objectResourceStatement := policyStatement{} statements := []policyStatement{} // Write only policy. bucketResourceStatement.Effect = "Allow" bucketResourceStatement.Principal.AWS = []string{"*"} bucketResourceStatement.Resources = []string{fmt.Sprintf("%s%s", AWSResourcePrefix, bucketName)} bucketResourceStatement.Actions = writeOnlyBucketActions objectResourceStatement.Effect = "Allow" objectResourceStatement.Principal.AWS = []string{"*"} objectResourceStatement.Resources = []string{fmt.Sprintf("%s%s", AWSResourcePrefix, bucketName+"/"+objectPrefix+"*")} objectResourceStatement.Actions = writeOnlyObjectActions // Save the write only policy. statements = append(statements, bucketResourceStatement, objectResourceStatement) return statements } // Tests validate Action validator. func TestIsValidActions(t *testing.T) { testCases := []struct { // input. actions []string // 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. {[]string{"s3:GetObject", "s3:ListObject", "s3:RemoveObject"}, errors.New("Unsupported action found: ‘s3:ListObject’, please validate your policy document."), false}, // Test case - 2. // Empty Actions. {[]string{}, errors.New("Action list cannot be empty."), false}, // Test case - 3. // "s3:DeleteEverything"" is an invalid Action. {[]string{"s3:GetObject", "s3:ListBucket", "s3:PutObject", "s3:DeleteEverything"}, errors.New("Unsupported action found: ‘s3:DeleteEverything’, please validate your policy document."), false}, // Inputs with valid Action. // Test Case - 4. {[]string{"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: %s", i+1, err.Error()) } if err == nil && !testCase.shouldPass { t.Errorf("Test %d: Expected to fail with \"%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()) } } } } // Tests validate Effect validator. func TestIsValidEffect(t *testing.T) { testCases := []struct { // input. effect string // expected output. err error // flag indicating whether the test should pass. shouldPass bool }{ // Inputs with unsupported Effect. // Test case - 1. {"DontAllow", errors.New("Unsupported Effect found: ‘DontAllow’, please validate your policy document."), false}, // Test case - 2. {"NeverAllow", errors.New("Unsupported Effect found: ‘NeverAllow’, please validate your policy document."), false}, // Test case - 3. {"AllowAlways", errors.New("Unsupported Effect found: ‘AllowAlways’, please validate your policy document."), false}, // Inputs with valid Effect. // Test Case - 4. {"Allow", nil, true}, // Test Case - 5. {"Deny", nil, true}, } for i, testCase := range testCases { err := isValidEffect(testCase.effect) if err != nil && testCase.shouldPass { t.Errorf("Test %d: Expected to pass, but failed with: %s", i+1, err.Error()) } if err == nil && !testCase.shouldPass { t.Errorf("Test %d: Expected to fail with \"%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()) } } } } // 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(testCase.resources) if err != nil && testCase.shouldPass { t.Errorf("Test %d: Expected to pass, but failed with: %s", i+1, err.Error()) } if err == nil && !testCase.shouldPass { t.Errorf("Test %d: Expected to fail with \"%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()) } } } } // 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 principal style found: ‘my-principal’, please validate your policy document."), false}, // Test case - 3. {[]string{"*", "111122233"}, errors.New("Unsupported principal style found: ‘111122233’, 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(testCase.principals) if err != nil && testCase.shouldPass { t.Errorf("Test %d: Expected to pass, but failed with: %s", i+1, err.Error()) } if err == nil && !testCase.shouldPass { t.Errorf("Test %d: Expected to fail with \"%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()) } } } } // Tests validate policyStatement condition validator. func TestIsValidConditions(t *testing.T) { // returns empty conditions map. setEmptyConditions := func() map[string]map[string]string { return make(map[string]map[string]string) } // returns map with the "StringEquals" set to empty map. setEmptyStringEquals := func() map[string]map[string]string { emptyMap := make(map[string]string) conditions := make(map[string]map[string]string) conditions["StringEquals"] = emptyMap return conditions } // returns map with the "StringNotEquals" set to empty map. setEmptyStringNotEquals := func() map[string]map[string]string { emptyMap := make(map[string]string) conditions := make(map[string]map[string]string) conditions["StringNotEquals"] = emptyMap return conditions } // Generate conditions. generateConditions := func(key1, key2, value string) map[string]map[string]string { innerMap := make(map[string]string) innerMap[key2] = value conditions := make(map[string]map[string]string) conditions[key1] = innerMap return conditions } // generate ambigious conditions. generateAmbigiousConditions := func() map[string]map[string]string { innerMap := make(map[string]string) innerMap["s3:prefix"] = "Asia/" conditions := make(map[string]map[string]string) conditions["StringEquals"] = innerMap conditions["StringNotEquals"] = innerMap return conditions } // generate valid and non valid type in the condition map. generateValidInvalidConditions := func() map[string]map[string]string { innerMap := make(map[string]string) innerMap["s3:prefix"] = "Asia/" conditions := make(map[string]map[string]string) 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]string { innerMapValid := make(map[string]string) innerMapValid["s3:prefix"] = "Asia/" innerMapInValid := make(map[string]string) innerMapInValid["s3:invalid"] = "Asia/" conditions := make(map[string]map[string]string) conditions["StringEquals"] = innerMapValid conditions["StringEquals"] = innerMapInValid return conditions } // List of Conditions used for test cases. testConditions := []map[string]map[string]string{ 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]string // 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: %s", i+1, actualErr.Error()) } if actualErr == nil && !testCase.shouldPass { t.Errorf("Test %d: Expected to fail with \"%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()) } } } } // 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 = []string{"s3:DeleteObject", "s3:PutObject"} return statements } // contructing policy statement with recursive resources. // should result in ErrMalformedPolicy setRecurseResource := func(statements []policyStatement) []policyStatement { statements[0].Resources = []string{"arn:aws:s3:::minio-bucket/Asia/*", "arn:aws:s3:::minio-bucket/Asia/India/*"} return statements } // List of BucketPolicy used for tests. bucketAccessPolicies := []BucketPolicy{ // BucketPolicy - 0. // Contains valid read only policy statement. {Version: "1.0", Statements: setReadOnlyStatement("minio-bucket", "")}, // BucketPolicy - 1. // Contains valid read-write only policy statement. {Version: "1.0", Statements: setReadWriteStatement("minio-bucket", "Asia/")}, // BucketPolicy - 2. // Contains valid write only policy statement. {Version: "1.0", Statements: setWriteOnlyStatement("minio-bucket", "Asia/India/")}, // BucketPolicy - 3. // Contains invalidPrefixActions. // Since resourcePrefix is not to the bucket-name, it return ErrMalformedPolicy. {Version: "1.0", Statements: setReadOnlyStatement("minio-bucket-fail", "Asia/India/")}, // BucketPolicy - 4. // 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(setWriteOnlyStatement("minio-bucket-fail", "Asia/India/"))}, // BucketPolicy - 5. // constructing policy statement without invalidPrefixActions (check bucket-policy-parser.go). // contructing policy statement with recursive resources. // should result in ErrMalformedPolicy {Version: "1.0", Statements: setRecurseResource(setValidPrefixActions(setWriteOnlyStatement("minio-bucket", "")))}, } 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. // contructing policy statement with recursive resources. // should result in ErrMalformedPolicy. {bucketAccessPolicies[5], ErrMalformedPolicy, false}, } 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 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 = []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 = []string{"*", "User1111"} return statements } // set unsupported Resources. setUnsupportedResources := func(statements []policyStatement) []policyStatement { // "s3:DeleteEverything"" is an Unsupported Action. statements[0].Resources = []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: setReadOnlyStatement("minio-bucket", "")}, // BucketPolicy - 3. // Read-Write bucket policy. {Version: "1.0", Statements: setReadWriteStatement("minio-bucket", "Asia/")}, // BucketPolicy - 4. // Write only bucket policy. {Version: "1.0", Statements: setWriteOnlyStatement("minio-bucket", "Asia/India/")}, // BucketPolicy - 5. // BucketPolicy statement contains unsupported action. {Version: "1.0", Statements: setUnsupportedActions(setReadOnlyStatement("minio-bucket", ""))}, // BucketPolicy - 6. // BucketPolicy statement contains unsupported Effect. {Version: "1.0", Statements: setUnsupportedEffect(setReadWriteStatement("minio-bucket", "Asia/"))}, // BucketPolicy - 7. // BucketPolicy statement contains unsupported Principal. {Version: "1.0", Statements: setUnsupportedPrincipals(setWriteOnlyStatement("minio-bucket", "Asia/India/"))}, // BucketPolicy - 8. // BucketPolicy statement contains unsupported Resource. {Version: "1.0", Statements: setUnsupportedResources(setWriteOnlyStatement("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 action found: ‘s3:DeleteEverything’, 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 principal style found: ‘User1111’, 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 { inputPolicyBytes, e := json.Marshal(testCase.inputPolicy) if e != nil { t.Fatalf("Test %d: Couldn't Marshal bucket policy", i+1) } actualAccessPolicy, err := parseBucketPolicy(inputPolicyBytes) if err != nil && testCase.shouldPass { t.Errorf("Test %d: Expected to pass, but failed with: %s", i+1, err.Error()) } if err == nil && !testCase.shouldPass { t.Errorf("Test %d: Expected to fail with \"%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 !reflect.DeepEqual(testCase.expectedPolicy, actualAccessPolicy) { t.Errorf("Test %d: The expected statements from resource statement generator doesn't match the actual statements", i+1) } } } }