From 8cc5173d737781eb07a01222b12dd6caf18f7317 Mon Sep 17 00:00:00 2001 From: Denis Kirillov Date: Fri, 26 Jan 2024 12:26:20 +0300 Subject: [PATCH] [#46] iam: Support namespaces when forming native rules Signed-off-by: Denis Kirillov --- iam/converter_native.go | 93 ++++++++++++++++++++++++++++++++++++----- iam/converter_test.go | 63 ++++++++++++++-------------- iam/policy_test.go | 4 +- 3 files changed, 116 insertions(+), 44 deletions(-) diff --git a/iam/converter_native.go b/iam/converter_native.go index e28289b..a28d443 100644 --- a/iam/converter_native.go +++ b/iam/converter_native.go @@ -15,7 +15,29 @@ var actionToOpMap = map[string][]string{ supportedS3ActionGetObject: {native.MethodGetObject, native.MethodHeadObject, native.MethodSearchObject, native.MethodRangeObject, native.MethodHashObject}, supportedS3ActionHeadObject: {native.MethodHeadObject, native.MethodSearchObject, native.MethodRangeObject, native.MethodHashObject}, supportedS3ActionPutObject: {native.MethodPutObject}, - supportedS3ActionListBucket: {native.MethodGetObject, native.MethodHeadObject, native.MethodSearchObject, native.MethodRangeObject, native.MethodHashObject}, + supportedS3ActionListBucket: {native.MethodGetContainer, native.MethodGetObject, native.MethodHeadObject, native.MethodSearchObject, native.MethodRangeObject, native.MethodHashObject}, + + supportedS3ActionCreateBucket: {native.MethodPutContainer}, + supportedS3ActionDeleteBucket: {native.MethodDeleteContainer}, +} + +var containerNativeOperations = map[string]struct{}{ + native.MethodPutContainer: {}, + native.MethodDeleteContainer: {}, + native.MethodGetContainer: {}, + native.MethodListContainers: {}, + native.MethodSetContainerEACL: {}, + native.MethodGetContainerEACL: {}, +} + +var objectNativeOperations = map[string]struct{}{ + native.MethodGetObject: {}, + native.MethodPutObject: {}, + native.MethodHeadObject: {}, + native.MethodDeleteObject: {}, + native.MethodSearchObject: {}, + native.MethodRangeObject: {}, + native.MethodHashObject: {}, } const ( @@ -24,11 +46,19 @@ const ( supportedS3ActionHeadObject = "s3:HeadObject" supportedS3ActionPutObject = "s3:PutObject" supportedS3ActionListBucket = "s3:ListBucket" + + supportedS3ActionCreateBucket = "s3:CreateBucket" + supportedS3ActionDeleteBucket = "s3:DeleteBucket" ) type NativeResolver interface { GetUserKey(account, name string) (string, error) - GetBucketCID(bucket string) (string, error) + GetBucketInfo(bucket string) (*BucketInfo, error) +} + +type BucketInfo struct { + Namespace string + Container string } func ConvertToNativeChain(p Policy, resolver NativeResolver) (*chain.Chain, error) { @@ -52,7 +82,7 @@ func ConvertToNativeChain(p Policy, resolver NativeResolver) (*chain.Chain, erro } resource, resourceInverted := statement.resource() - groupedResources, err := formNativeResourceNamesAndConditions(resource, resolver) + groupedResources, err := formNativeResourceNamesAndConditions(resource, resolver, getActionTypes(nativeActions)) if err != nil { return nil, err } @@ -95,6 +125,23 @@ func ConvertToNativeChain(p Policy, resolver NativeResolver) (*chain.Chain, erro return &engineChain, nil } +func getActionTypes(nativeActions []string) ActionTypes { + var res ActionTypes + for _, action := range nativeActions { + if res.Object && res.Container { + break + } + + _, isObj := objectNativeOperations[action] + _, isCnr := containerNativeOperations[action] + + res.Object = isObj || action == Wildcard + res.Container = isCnr || action == Wildcard + } + + return res +} + func getNativePrincipalsAndConditionFunc(statement Statement, resolver NativeResolver) ([]string, formPrincipalConditionFunc, error) { var principals []string var op chain.ConditionType @@ -152,7 +199,16 @@ type GroupedResources struct { Conditions []chain.Condition } -func formNativeResourceNamesAndConditions(names []string, resolver NativeResolver) ([]GroupedResources, error) { +type ActionTypes struct { + Object bool + Container bool +} + +func formNativeResourceNamesAndConditions(names []string, resolver NativeResolver, actionTypes ActionTypes) ([]GroupedResources, error) { + if !actionTypes.Object && !actionTypes.Container { + return nil, ErrActionsNotApplicable + } + res := make([]GroupedResources, 0, len(names)) var combined []string @@ -164,7 +220,13 @@ func formNativeResourceNamesAndConditions(names []string, resolver NativeResolve if resource == Wildcard { res = res[:0] - return append(res, GroupedResources{Names: []string{native.ResourceFormatAllObjects}}), nil + if actionTypes.Object { + res = append(res, GroupedResources{Names: []string{native.ResourceFormatAllObjects}}) + } + if actionTypes.Container { + res = append(res, GroupedResources{Names: []string{native.ResourceFormatAllContainers}}) + } + return res, nil } if !strings.HasPrefix(resource, s3ResourcePrefix) { @@ -175,7 +237,13 @@ func formNativeResourceNamesAndConditions(names []string, resolver NativeResolve s3Resource := strings.TrimPrefix(resource, s3ResourcePrefix) if s3Resource == Wildcard { res = res[:0] - return append(res, GroupedResources{Names: []string{native.ResourceFormatAllObjects}}), nil + if actionTypes.Object { + res = append(res, GroupedResources{Names: []string{native.ResourceFormatAllObjects}}) + } + if actionTypes.Container { + res = append(res, GroupedResources{Names: []string{native.ResourceFormatAllContainers}}) + } + return res, nil } if sepIndex := strings.Index(s3Resource, "/"); sepIndex < 0 { @@ -188,19 +256,22 @@ func formNativeResourceNamesAndConditions(names []string, resolver NativeResolve } } - cnrID, err := resolver.GetBucketCID(bkt) + bktInfo, err := resolver.GetBucketInfo(bkt) if err != nil { return nil, err } - nativeResource := fmt.Sprintf(native.ResourceFormatRootContainerObjects, cnrID) - if obj == Wildcard || obj == "" { - combined = append(combined, nativeResource) + if obj == Wildcard && actionTypes.Object { // this corresponds to arn:aws:s3:::BUCKET/ or arn:aws:s3:::BUCKET/* + combined = append(combined, fmt.Sprintf(native.ResourceFormatNamespaceContainerObjects, bktInfo.Namespace, bktInfo.Container)) + continue + } + if obj == "" && actionTypes.Container { // this corresponds to arn:aws:s3:::BUCKET + combined = append(combined, fmt.Sprintf(native.ResourceFormatNamespaceContainer, bktInfo.Namespace, bktInfo.Container)) continue } res = append(res, GroupedResources{ - Names: []string{nativeResource}, + Names: []string{fmt.Sprintf(native.ResourceFormatNamespaceContainerObjects, bktInfo.Namespace, bktInfo.Container)}, Conditions: []chain.Condition{ { Op: chain.CondStringLike, diff --git a/iam/converter_test.go b/iam/converter_test.go index ba918eb..0bf917c 100644 --- a/iam/converter_test.go +++ b/iam/converter_test.go @@ -17,22 +17,23 @@ import ( ) type mockUserResolver struct { - users map[string]string - buckets map[string]string + users map[string]string + containers map[string]string + namespace string } -func newMockUserResolver(accountUsers []string, buckets []string) *mockUserResolver { +func newMockUserResolver(accountUsers []string, buckets []string, namespace string) *mockUserResolver { userMap := make(map[string]string, len(accountUsers)) for _, user := range accountUsers { userMap[user] = user + "/resolvedValue" } - bucketMap := make(map[string]string, len(buckets)) + containerMap := make(map[string]string, len(buckets)) for _, bkt := range buckets { - bucketMap[bkt] = bkt + "/resolvedValues" + containerMap[bkt] = bkt + "/resolvedValues" } - return &mockUserResolver{users: userMap, buckets: bucketMap} + return &mockUserResolver{users: userMap, containers: containerMap, namespace: namespace} } func (m *mockUserResolver) GetUserAddress(account, user string) (string, error) { @@ -53,13 +54,13 @@ func (m *mockUserResolver) GetUserKey(account, user string) (string, error) { return key, nil } -func (m *mockUserResolver) GetBucketCID(bkt string) (string, error) { - cnrID, ok := m.buckets[bkt] +func (m *mockUserResolver) GetBucketInfo(bkt string) (*BucketInfo, error) { + cnr, ok := m.containers[bkt] if !ok { - return "", errors.New("not found") + return nil, errors.New("not found") } - return cnrID, nil + return &BucketInfo{Container: cnr, Namespace: m.namespace}, nil } func TestConverters(t *testing.T) { @@ -72,7 +73,7 @@ func TestConverters(t *testing.T) { resource := fmt.Sprintf(s3.ResourceFormatS3BucketObjects, bktName) s3action := "s3:PutObject" - mockResolver := newMockUserResolver([]string{user}, []string{bktName}) + mockResolver := newMockUserResolver([]string{user}, []string{bktName}, namespace) t.Run("valid policy", func(t *testing.T) { p := Policy{ @@ -136,7 +137,7 @@ func TestConverters(t *testing.T) { { Status: chain.Allow, Actions: chain.Actions{Names: []string{native.MethodPutObject}}, - Resources: chain.Resources{Names: []string{fmt.Sprintf(native.ResourceFormatRootContainerObjects, mockResolver.buckets[bktName])}}, + Resources: chain.Resources{Names: []string{fmt.Sprintf(native.ResourceFormatNamespaceContainerObjects, namespace, mockResolver.containers[bktName])}}, Condition: []chain.Condition{ { Op: chain.CondStringEquals, @@ -205,7 +206,7 @@ func TestConverters(t *testing.T) { Status: chain.AccessDenied, Actions: chain.Actions{Inverted: true, Names: actionToOpMap["s3:GetObject"]}, Resources: chain.Resources{Inverted: true, Names: []string{ - fmt.Sprintf(native.ResourceFormatRootContainerObjects, mockResolver.buckets[bktName]), + fmt.Sprintf(native.ResourceFormatNamespaceContainerObjects, namespace, mockResolver.containers[bktName]), }}, Condition: []chain.Condition{ { @@ -603,10 +604,10 @@ func TestComplexNativeConditions(t *testing.T) { key1, key2 := "key1", "key2" val0, val1, val2 := "val0", "val1", "val2" - mockResolver := newMockUserResolver([]string{user1, user2}, []string{bktName1, bktName2, bktName3}) - nativeResource1 := fmt.Sprintf(native.ResourceFormatRootContainerObjects, mockResolver.buckets[bktName1]) - nativeResource2 := fmt.Sprintf(native.ResourceFormatRootContainerObjects, mockResolver.buckets[bktName2]) - nativeResource3 := fmt.Sprintf(native.ResourceFormatRootContainerObjects, mockResolver.buckets[bktName3]) + mockResolver := newMockUserResolver([]string{user1, user2}, []string{bktName1, bktName2, bktName3}, "") + nativeResource1 := fmt.Sprintf(native.ResourceFormatRootContainerObjects, mockResolver.containers[bktName1]) + nativeResource2 := fmt.Sprintf(native.ResourceFormatRootContainerObjects, mockResolver.containers[bktName2]) + nativeResource3 := fmt.Sprintf(native.ResourceFormatRootContainerObjects, mockResolver.containers[bktName3]) p := Policy{ Version: "2012-10-17", @@ -742,7 +743,7 @@ func TestComplexNativeConditions(t *testing.T) { { name: "bucket resource1, all conditions matched", action: action, - resource: fmt.Sprintf(native.ResourceFormatRootContainerObject, mockResolver.buckets[bktName2], "some-oid"), + resource: fmt.Sprintf(native.ResourceFormatRootContainerObject, mockResolver.containers[bktName2], "some-oid"), resourceMap: map[string]string{ PropertyKeyFilePath: "any-object-name", }, @@ -756,7 +757,7 @@ func TestComplexNativeConditions(t *testing.T) { { name: "bucket resource3, all conditions matched", action: action, - resource: fmt.Sprintf(native.ResourceFormatRootContainerObject, mockResolver.buckets[bktName3], "some-oid"), + resource: fmt.Sprintf(native.ResourceFormatRootContainerObject, mockResolver.containers[bktName3], "some-oid"), resourceMap: map[string]string{ PropertyKeyFilePath: "any-object-name", }, @@ -770,7 +771,7 @@ func TestComplexNativeConditions(t *testing.T) { { name: "bucket resource, user condition mismatched", action: action, - resource: fmt.Sprintf(native.ResourceFormatRootContainerObject, mockResolver.buckets[bktName2], "some-oid"), + resource: fmt.Sprintf(native.ResourceFormatRootContainerObject, mockResolver.containers[bktName2], "some-oid"), resourceMap: map[string]string{ PropertyKeyFilePath: "any-object-name", }, @@ -783,7 +784,7 @@ func TestComplexNativeConditions(t *testing.T) { { name: "bucket resource, key2 condition mismatched", action: action, - resource: fmt.Sprintf(native.ResourceFormatRootContainerObject, mockResolver.buckets[bktName3], "some-oid"), + resource: fmt.Sprintf(native.ResourceFormatRootContainerObject, mockResolver.containers[bktName3], "some-oid"), resourceMap: map[string]string{ PropertyKeyFilePath: "any-object-name", }, @@ -797,7 +798,7 @@ func TestComplexNativeConditions(t *testing.T) { { name: "bucket resource, key1 condition mismatched", action: action, - resource: fmt.Sprintf(native.ResourceFormatRootContainerObject, mockResolver.buckets[bktName2], "some-oid"), + resource: fmt.Sprintf(native.ResourceFormatRootContainerObject, mockResolver.containers[bktName2], "some-oid"), resourceMap: map[string]string{ PropertyKeyFilePath: "any-object-name", }, @@ -810,7 +811,7 @@ func TestComplexNativeConditions(t *testing.T) { { name: "bucket/object resource, all conditions matched", action: action, - resource: fmt.Sprintf(native.ResourceFormatRootContainerObject, mockResolver.buckets[bktName1], "some-oid"), + resource: fmt.Sprintf(native.ResourceFormatRootContainerObject, mockResolver.containers[bktName1], "some-oid"), resourceMap: map[string]string{ PropertyKeyFilePath: objName1, }, @@ -824,7 +825,7 @@ func TestComplexNativeConditions(t *testing.T) { { name: "bucket/object resource, user condition mismatched", action: action, - resource: fmt.Sprintf(native.ResourceFormatRootContainerObject, mockResolver.buckets[bktName1], "some-oid"), + resource: fmt.Sprintf(native.ResourceFormatRootContainerObject, mockResolver.containers[bktName1], "some-oid"), resourceMap: map[string]string{ PropertyKeyFilePath: objName1, }, @@ -838,7 +839,7 @@ func TestComplexNativeConditions(t *testing.T) { { name: "bucket/object resource, key1 condition mismatched", action: action, - resource: fmt.Sprintf(native.ResourceFormatRootContainerObject, mockResolver.buckets[bktName1], "some-oid"), + resource: fmt.Sprintf(native.ResourceFormatRootContainerObject, mockResolver.containers[bktName1], "some-oid"), resourceMap: map[string]string{ PropertyKeyFilePath: objName1, }, @@ -851,7 +852,7 @@ func TestComplexNativeConditions(t *testing.T) { { name: "bucket/object resource, key2 condition mismatched", action: action, - resource: fmt.Sprintf(native.ResourceFormatRootContainerObject, mockResolver.buckets[bktName1], "some-oid"), + resource: fmt.Sprintf(native.ResourceFormatRootContainerObject, mockResolver.containers[bktName1], "some-oid"), resourceMap: map[string]string{ PropertyKeyFilePath: objName1, }, @@ -865,7 +866,7 @@ func TestComplexNativeConditions(t *testing.T) { { name: "bucket/object resource, object filepath condition mismatched", action: action, - resource: fmt.Sprintf(native.ResourceFormatRootContainerObject, mockResolver.buckets[bktName1], "some-oid"), + resource: fmt.Sprintf(native.ResourceFormatRootContainerObject, mockResolver.containers[bktName1], "some-oid"), resourceMap: map[string]string{ PropertyKeyFilePath: "any-object-name", }, @@ -916,7 +917,7 @@ func TestComplexS3Conditions(t *testing.T) { key1, key2 := "key1", "key2" val0, val1, val2 := "val0", "val1", "val2" - mockResolver := newMockUserResolver([]string{user1, user2}, []string{bktName1, bktName2, bktName3}) + mockResolver := newMockUserResolver([]string{user1, user2}, []string{bktName1, bktName2, bktName3}, "") p := Policy{ Version: "2012-10-17", @@ -1136,7 +1137,7 @@ func TestS3BucketResource(t *testing.T) { bktName1, bktName2 := "bucket1", "bucket2" chainName := chain.Name("name") - mockResolver := newMockUserResolver([]string{}, []string{}) + mockResolver := newMockUserResolver([]string{}, []string{}, "") p := Policy{ Version: "2012-10-17", @@ -1189,10 +1190,10 @@ func TestWildcardConverters(t *testing.T) { err := json.Unmarshal([]byte(policy), &p) require.NoError(t, err) - _, err = ConvertToS3Chain(p, newMockUserResolver(nil, nil)) + _, err = ConvertToS3Chain(p, newMockUserResolver(nil, nil, "")) require.NoError(t, err) - _, err = ConvertToNativeChain(p, newMockUserResolver(nil, nil)) + _, err = ConvertToNativeChain(p, newMockUserResolver(nil, nil, "")) require.NoError(t, err) } diff --git a/iam/policy_test.go b/iam/policy_test.go index c397f33..20aed3b 100644 --- a/iam/policy_test.go +++ b/iam/policy_test.go @@ -474,7 +474,7 @@ func TestProcessDenyFirst(t *testing.T) { err = json.Unmarshal([]byte(resourceBasedPolicyStr), &resourcePolicy) require.NoError(t, err) - mockResolver := newMockUserResolver([]string{"root/user-name"}, []string{"test-bucket"}) + mockResolver := newMockUserResolver([]string{"root/user-name"}, []string{"test-bucket"}, "") identityNativePolicy, err := ConvertToNativeChain(identityPolicy, mockResolver) require.NoError(t, err) @@ -493,7 +493,7 @@ func TestProcessDenyFirst(t *testing.T) { _, _, err = s.MorphRuleChainStorage().AddMorphRuleChain(chain.Ingress, target, resourceNativePolicy) require.NoError(t, err) - resource := testutil.NewResource(fmt.Sprintf(native.ResourceFormatRootContainerObjects, mockResolver.buckets["test-bucket"]), nil) + resource := testutil.NewResource(fmt.Sprintf(native.ResourceFormatRootContainerObjects, mockResolver.containers["test-bucket"]), nil) request := testutil.NewRequest("PutObject", resource, map[string]string{native.PropertyKeyActorPublicKey: mockResolver.users["root/user-name"]}) status, found, err := s.IsAllowed(chain.Ingress, engine.NewRequestTarget("ns", ""), request)