From 4d305d0d0f6fbf0273a8bca3fdc4150180c46d7c Mon Sep 17 00:00:00 2001 From: Denis Kirillov Date: Mon, 7 Apr 2025 11:02:08 +0300 Subject: [PATCH] [#680] Add new converter Signed-off-by: Denis Kirillov --- go.mod | 2 +- go.sum | 4 +- pkg/policy-engine/converter_test.go | 235 +++++++++++++++++++ pkg/policy-engine/v2/iam/converter_native.go | 202 ++++++++++------ pkg/policy-engine/v2/iam/converter_test.go | 213 +++++++++++++---- 5 files changed, 542 insertions(+), 114 deletions(-) create mode 100644 pkg/policy-engine/converter_test.go diff --git a/go.mod b/go.mod index 7269fe714..c53f53db2 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( git.frostfs.info/TrueCloudLab/frostfs-qos v0.0.0-20250227072915-25102d1e1aa3 git.frostfs.info/TrueCloudLab/frostfs-sdk-go v0.0.0-20250317082814-87bb55f992dc git.frostfs.info/TrueCloudLab/multinet v0.0.0-20241015075604-6cb0d80e0972 - git.frostfs.info/TrueCloudLab/policy-engine v0.0.0-20250317084311-594ac20859fc + git.frostfs.info/TrueCloudLab/policy-engine v0.0.0-20250402100642-acd94d200f88 git.frostfs.info/TrueCloudLab/zapjournald v0.0.0-20240124114243-cb2e66427d02 github.com/aws/aws-sdk-go-v2 v1.34.0 github.com/aws/aws-sdk-go-v2/config v1.27.32 diff --git a/go.sum b/go.sum index c5fddfccf..a44021060 100644 --- a/go.sum +++ b/go.sum @@ -52,8 +52,8 @@ git.frostfs.info/TrueCloudLab/hrw v1.2.1 h1:ccBRK21rFvY5R1WotI6LNoPlizk7qSvdfD8l git.frostfs.info/TrueCloudLab/hrw v1.2.1/go.mod h1:C1Ygde2n843yTZEQ0FP69jYiuaYV0kriLvP4zm8JuvM= git.frostfs.info/TrueCloudLab/multinet v0.0.0-20241015075604-6cb0d80e0972 h1:/960fWeyn2AFHwQUwDsWB3sbP6lTEnFnMzLMM6tx6N8= git.frostfs.info/TrueCloudLab/multinet v0.0.0-20241015075604-6cb0d80e0972/go.mod h1:2hM42MBrlhvN6XToaW6OWNk5ZLcu1FhaukGgxtfpDDI= -git.frostfs.info/TrueCloudLab/policy-engine v0.0.0-20250317084311-594ac20859fc h1:o56+epHNuC7o0NwLJylvT+jf/icpgTZCTQPr7ydanek= -git.frostfs.info/TrueCloudLab/policy-engine v0.0.0-20250317084311-594ac20859fc/go.mod h1:GZTk55RI4dKzsK6BCn5h2xxE28UHNfgoq/NJxW/LQ6A= +git.frostfs.info/TrueCloudLab/policy-engine v0.0.0-20250402100642-acd94d200f88 h1:V0a7ia84ZpSM2YxpJq1SKLQfeYmsqFWqcxwweBHJIzc= +git.frostfs.info/TrueCloudLab/policy-engine v0.0.0-20250402100642-acd94d200f88/go.mod h1:GZTk55RI4dKzsK6BCn5h2xxE28UHNfgoq/NJxW/LQ6A= git.frostfs.info/TrueCloudLab/rfc6979 v0.4.0 h1:M2KR3iBj7WpY3hP10IevfIB9MURr4O9mwVfJ+SjT3HA= git.frostfs.info/TrueCloudLab/rfc6979 v0.4.0/go.mod h1:okpbKfVYf/BpejtfFTfhZqFP+sZ8rsHrP8Rr/jYPNRc= git.frostfs.info/TrueCloudLab/tzhash v1.8.0 h1:UFMnUIk0Zh17m8rjGHJMqku2hCgaXDqjqZzS4gsb4UA= diff --git a/pkg/policy-engine/converter_test.go b/pkg/policy-engine/converter_test.go new file mode 100644 index 000000000..74f0a829a --- /dev/null +++ b/pkg/policy-engine/converter_test.go @@ -0,0 +1,235 @@ +package policyengine + +import ( + "encoding/json" + "errors" + "fmt" + "testing" + + s3common "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/pkg/policy-engine/common" + "git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain" + "git.frostfs.info/TrueCloudLab/policy-engine/pkg/engine" + "git.frostfs.info/TrueCloudLab/policy-engine/pkg/engine/inmemory" + "git.frostfs.info/TrueCloudLab/policy-engine/pkg/resource/testutil" + "git.frostfs.info/TrueCloudLab/policy-engine/schema/native" + "github.com/stretchr/testify/require" +) + +type apeConverterMock struct { + version ConverterVersion +} + +func (a apeConverterMock) ConverterVersion() ConverterVersion { + return a.version +} + +type resolverMock struct { + users map[string]string + containers map[string]string + namespace string +} + +func newMockUserResolver(accountUsers []string, buckets []string, namespace string) *resolverMock { + userMap := make(map[string]string, len(accountUsers)) + for _, user := range accountUsers { + userMap[user] = user + "/resolvedValue" + } + + containerMap := make(map[string]string, len(buckets)) + for _, bkt := range buckets { + containerMap[bkt] = bkt + "/resolvedValues" + } + + return &resolverMock{users: userMap, containers: containerMap, namespace: namespace} +} + +func (m *resolverMock) GetUserAddress(account, user string) (string, error) { + key, ok := m.users[account+"/"+user] + if !ok { + return "", errors.New("not found") + } + + return key, nil +} + +func (m *resolverMock) GetUserKey(account, user string) (string, error) { + key, ok := m.users[account+"/"+user] + if !ok { + return "", errors.New("not found") + } + + return key, nil +} + +func (m *resolverMock) GetBucketInfo(bkt string) (*s3common.BucketInfo, error) { + cnr, ok := m.containers[bkt] + if !ok { + return nil, errors.New("not found") + } + + return &s3common.BucketInfo{Container: cnr, Namespace: m.namespace}, nil +} + +func TestChainsIsAllowed(t *testing.T) { + t.Run("ListBucket Allow", func(t *testing.T) { + policy := ` +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": "*", + "Action": "s3:ListBucket", + "Resource": "arn:aws:s3:::bkt" + } + ] +} +` + var p s3common.Policy + err := json.Unmarshal([]byte(policy), &p) + require.NoError(t, err) + + resolver := newMockUserResolver(nil, []string{"bkt"}, "") + + for _, tc := range []struct { + version ConverterVersion + status chain.Status + }{ + { + version: V1, + status: chain.NoRuleFound, + }, + { + version: V2, + status: chain.Allow, + }, + } { + t.Run(string(tc.version), func(t *testing.T) { + converter := NewConverter(Config{VersionFetcher: apeConverterMock{version: tc.version}}) + nativeChain, err := converter.ToNativeChain(p, resolver) + require.NoError(t, err) + + s := inmemory.NewInMemory() + _, _, err = s.MorphRuleChainStorage().AddMorphRuleChain(chain.Ingress, engine.NamespaceTarget(""), nativeChain) + require.NoError(t, err) + + res := testutil.NewResource(fmt.Sprintf(native.ResourceFormatRootContainerObjects, resolver.containers["bkt"]), map[string]string{native.ProperyKeyTreeID: "system"}) + req := testutil.NewRequest(native.MethodGetObject, res, nil) + status, _, err := s.IsAllowed(chain.Ingress, engine.NewRequestTargetWithNamespace(""), req) + require.NoError(t, err) + require.Equal(t, tc.status.String(), status.String()) + }) + } + }) + + t.Run("PutObject Deny/Allow", func(t *testing.T) { + user1, user2 := "user1", "user2" + principal1, principal2 := "/"+user1, "/"+user2 + + policy := fmt.Sprintf(` +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Deny", + "Principal": {"AWS": ["arn:aws:iam:::user/%s"]}, + "Action": "s3:PutObject", + "Resource": "arn:aws:s3:::bkt/*" + }, + { + "Effect": "Allow", + "Principal": {"AWS": ["arn:aws:iam:::user/%s"]}, + "Action": "s3:PutObject", + "Resource": "arn:aws:s3:::bkt/*" + } + ] +} +`, user1, user2) + + var p s3common.Policy + err := json.Unmarshal([]byte(policy), &p) + require.NoError(t, err) + + resolver := newMockUserResolver([]string{principal1, principal2}, []string{"bkt"}, "") + + for _, tc := range []struct { + version ConverterVersion + status chain.Status + }{ + { + version: V1, + status: chain.Allow, // because we skip deny rules for native chains + }, + { + version: V2, + status: chain.Allow, + }, + } { + t.Run(string(tc.version), func(t *testing.T) { + converter := NewConverter(Config{VersionFetcher: apeConverterMock{version: tc.version}}) + nativeChain, err := converter.ToNativeChain(p, resolver) + require.NoError(t, err) + + s := inmemory.NewInMemory() + _, _, err = s.MorphRuleChainStorage().AddMorphRuleChain(chain.Ingress, engine.NamespaceTarget(""), nativeChain) + require.NoError(t, err) + + res := testutil.NewResource(fmt.Sprintf(native.ResourceFormatRootContainerObjects, resolver.containers["bkt"]), map[string]string{native.ProperyKeyTreeID: "system"}) + req := testutil.NewRequest(native.MethodPutObject, res, map[string]string{native.PropertyKeyActorPublicKey: resolver.users[principal2]}) + status, _, err := s.IsAllowed(chain.Ingress, engine.NewRequestTargetWithNamespace(""), req) + require.NoError(t, err) + require.Equal(t, tc.status.String(), status.String()) + }) + } + }) + + t.Run("GetObject Allow", func(t *testing.T) { + policy := ` +{ + "Version": "2012-10-17", + "Statement": [{ + "Effect": "Allow", + "Principal": "*", + "Action": "s3:GetObject", + "Resource": "arn:aws:s3:::bkt/object" + }] +} +` + + var p s3common.Policy + err := json.Unmarshal([]byte(policy), &p) + require.NoError(t, err) + + resolver := newMockUserResolver(nil, []string{"bkt"}, "") + + for _, tc := range []struct { + version ConverterVersion + status chain.Status + }{ + { + version: V1, + status: chain.NoRuleFound, + }, + { + version: V2, + status: chain.Allow, + }, + } { + t.Run(string(tc.version), func(t *testing.T) { + converter := NewConverter(Config{VersionFetcher: apeConverterMock{version: tc.version}}) + nativeChain, err := converter.ToNativeChain(p, resolver) + require.NoError(t, err) + + s := inmemory.NewInMemory() + _, _, err = s.MorphRuleChainStorage().AddMorphRuleChain(chain.Ingress, engine.NamespaceTarget(""), nativeChain) + require.NoError(t, err) + + res := testutil.NewResource(fmt.Sprintf(native.ResourceFormatRootContainerObjects, resolver.containers["bkt"]), map[string]string{native.ProperyKeyTreeID: "version"}) + req := testutil.NewRequest(native.MethodGetObject, res, nil) + status, _, err := s.IsAllowed(chain.Ingress, engine.NewRequestTargetWithNamespace(""), req) + require.NoError(t, err) + require.Equal(t, tc.status.String(), status.String()) + }) + } + }) +} diff --git a/pkg/policy-engine/v2/iam/converter_native.go b/pkg/policy-engine/v2/iam/converter_native.go index f286e0e1e..085f1009e 100644 --- a/pkg/policy-engine/v2/iam/converter_native.go +++ b/pkg/policy-engine/v2/iam/converter_native.go @@ -12,58 +12,63 @@ import ( const PropertyKeyFilePath = "FilePath" -var actionToNativeOpMap = map[string][]string{ - s3common.S3ActionAbortMultipartUpload: {native.MethodGetContainer, native.MethodDeleteObject, native.MethodHeadObject, native.MethodGetObject, native.MethodPutObject}, - s3common.S3ActionCreateBucket: {native.MethodGetContainer, native.MethodPutContainer, native.MethodSetContainerEACL, native.MethodGetObject, native.MethodPutObject}, - s3common.S3ActionDeleteBucket: {native.MethodGetContainer, native.MethodDeleteContainer, native.MethodSearchObject, native.MethodHeadObject, native.MethodGetObject}, - s3common.S3ActionDeleteBucketPolicy: {native.MethodGetContainer}, - s3common.S3ActionDeleteObject: {native.MethodGetContainer, native.MethodDeleteObject, native.MethodPutObject, native.MethodHeadObject, native.MethodGetObject, native.MethodRangeObject}, - s3common.S3ActionDeleteObjectTagging: {native.MethodGetContainer, native.MethodHeadObject, native.MethodGetObject, native.MethodPutObject}, - s3common.S3ActionDeleteObjectVersion: {native.MethodGetContainer, native.MethodDeleteObject, native.MethodPutObject, native.MethodHeadObject, native.MethodGetObject, native.MethodRangeObject}, - s3common.S3ActionDeleteObjectVersionTagging: {native.MethodGetContainer, native.MethodHeadObject, native.MethodGetObject, native.MethodPutObject}, - s3common.S3ActionGetBucketACL: {native.MethodGetContainer, native.MethodGetContainerEACL, native.MethodGetObject}, - s3common.S3ActionGetBucketCORS: {native.MethodGetContainer, native.MethodGetObject, native.MethodHeadObject}, - s3common.S3ActionGetBucketLocation: {native.MethodGetContainer}, - s3common.S3ActionGetBucketNotification: {native.MethodGetContainer, native.MethodGetObject, native.MethodHeadObject}, - s3common.S3ActionGetBucketObjectLockConfiguration: {native.MethodGetContainer, native.MethodGetObject}, - s3common.S3ActionGetBucketPolicy: {native.MethodGetContainer}, - s3common.S3ActionGetBucketPolicyStatus: {native.MethodGetContainer}, - s3common.S3ActionGetBucketTagging: {native.MethodGetContainer, native.MethodGetObject}, - s3common.S3ActionGetBucketVersioning: {native.MethodGetContainer, native.MethodGetObject}, - s3common.S3ActionGetLifecycleConfiguration: {native.MethodGetContainer, native.MethodGetObject, native.MethodHeadObject}, - s3common.S3ActionGetObject: {native.MethodGetContainer, native.MethodGetObject, native.MethodHeadObject, native.MethodSearchObject, native.MethodRangeObject, native.MethodHashObject}, - s3common.S3ActionGetObjectACL: {native.MethodGetContainer, native.MethodGetContainerEACL, native.MethodGetObject, native.MethodHeadObject}, - s3common.S3ActionGetObjectAttributes: {native.MethodGetContainer, native.MethodGetObject, native.MethodHeadObject}, - s3common.S3ActionGetObjectLegalHold: {native.MethodGetContainer, native.MethodHeadObject, native.MethodGetObject}, - s3common.S3ActionGetObjectRetention: {native.MethodGetContainer, native.MethodHeadObject, native.MethodGetObject}, - s3common.S3ActionGetObjectTagging: {native.MethodGetContainer, native.MethodHeadObject, native.MethodGetObject}, - s3common.S3ActionGetObjectVersion: {native.MethodGetContainer, native.MethodGetObject, native.MethodHeadObject, native.MethodSearchObject, native.MethodRangeObject, native.MethodHashObject}, - s3common.S3ActionGetObjectVersionACL: {native.MethodGetContainer, native.MethodGetContainerEACL, native.MethodGetObject, native.MethodHeadObject}, - s3common.S3ActionGetObjectVersionAttributes: {native.MethodGetContainer, native.MethodGetObject, native.MethodHeadObject}, - s3common.S3ActionGetObjectVersionTagging: {native.MethodGetContainer, native.MethodHeadObject, native.MethodGetObject}, - s3common.S3ActionListAllMyBuckets: {native.MethodListContainers, native.MethodGetContainer}, - s3common.S3ActionListBucket: {native.MethodGetContainer, native.MethodGetObject, native.MethodHeadObject, native.MethodSearchObject, native.MethodRangeObject, native.MethodHashObject}, - s3common.S3ActionListBucketMultipartUploads: {native.MethodGetContainer, native.MethodGetObject}, - s3common.S3ActionListBucketVersions: {native.MethodGetContainer, native.MethodGetObject, native.MethodHeadObject, native.MethodSearchObject, native.MethodRangeObject, native.MethodHashObject}, - s3common.S3ActionListMultipartUploadParts: {native.MethodGetContainer, native.MethodGetObject}, - s3common.S3ActionPutBucketACL: {native.MethodGetContainer, native.MethodSetContainerEACL, native.MethodGetObject, native.MethodPutObject}, - s3common.S3ActionPutBucketCORS: {native.MethodGetContainer, native.MethodGetObject, native.MethodPutObject}, - s3common.S3ActionPutBucketNotification: {native.MethodGetContainer, native.MethodHeadObject, native.MethodDeleteObject, native.MethodGetObject, native.MethodPutObject}, - s3common.S3ActionPutBucketObjectLockConfiguration: {native.MethodGetContainer, native.MethodGetObject, native.MethodPutObject}, - s3common.S3ActionPutBucketPolicy: {native.MethodGetContainer}, - s3common.S3ActionPutBucketTagging: {native.MethodGetContainer, native.MethodGetObject, native.MethodPutObject}, - s3common.S3ActionPutBucketVersioning: {native.MethodGetContainer, native.MethodGetObject, native.MethodPutObject}, - s3common.S3ActionPutLifecycleConfiguration: {native.MethodGetContainer, native.MethodGetObject, native.MethodHeadObject, native.MethodPutObject, native.MethodDeleteObject}, - s3common.S3ActionPutObject: {native.MethodGetContainer, native.MethodPutObject, native.MethodGetObject, native.MethodHeadObject, native.MethodRangeObject}, - s3common.S3ActionPutObjectACL: {native.MethodGetContainer, native.MethodGetContainerEACL, native.MethodSetContainerEACL, native.MethodGetObject, native.MethodHeadObject}, - s3common.S3ActionPutObjectLegalHold: {native.MethodGetContainer, native.MethodHeadObject, native.MethodGetObject, native.MethodPutObject}, - s3common.S3ActionPutObjectRetention: {native.MethodGetContainer, native.MethodHeadObject, native.MethodGetObject, native.MethodPutObject}, - s3common.S3ActionPutObjectTagging: {native.MethodGetContainer, native.MethodHeadObject, native.MethodGetObject, native.MethodPutObject}, - s3common.S3ActionPutObjectVersionACL: {native.MethodGetContainer, native.MethodGetContainerEACL, native.MethodSetContainerEACL, native.MethodGetObject, native.MethodHeadObject}, - s3common.S3ActionPutObjectVersionTagging: {native.MethodGetContainer, native.MethodHeadObject, native.MethodGetObject, native.MethodPutObject}, - s3common.S3ActionPatchObject: {native.MethodGetContainer, native.MethodGetObject, native.MethodHeadObject, native.MethodPatchObject, native.MethodPutObject, native.MethodRangeObject}, - s3common.S3ActionPutBucketPublicAccessBlock: {native.MethodGetContainer, native.MethodPutObject, native.MethodDeleteObject, native.MethodGetObject}, - s3common.S3ActionGetBucketPublicAccessBlock: {native.MethodGetContainer, native.MethodGetObject}, +type nativeOperationInfo struct { + operations []string + needTreeWrite bool +} + +var actionToNativeOpMap = map[string]nativeOperationInfo{ + s3common.S3ActionAbortMultipartUpload: {operations: []string{native.MethodGetContainer, native.MethodDeleteObject, native.MethodHeadObject, native.MethodGetObject, native.MethodPutObject}, needTreeWrite: true}, + s3common.S3ActionCreateBucket: {operations: []string{native.MethodGetContainer, native.MethodPutContainer, native.MethodSetContainerEACL}, needTreeWrite: true}, + s3common.S3ActionDeleteBucket: {operations: []string{native.MethodGetContainer, native.MethodDeleteContainer, native.MethodSearchObject, native.MethodHeadObject, native.MethodGetObject, native.MethodPutObject}, needTreeWrite: true}, + s3common.S3ActionDeleteBucketPolicy: {operations: []string{native.MethodGetContainer}}, + s3common.S3ActionDeleteObject: {operations: []string{native.MethodGetContainer, native.MethodDeleteObject, native.MethodPutObject, native.MethodHeadObject, native.MethodGetObject, native.MethodRangeObject}, needTreeWrite: true}, + s3common.S3ActionDeleteObjectTagging: {operations: []string{native.MethodGetContainer}, needTreeWrite: true}, + s3common.S3ActionDeleteObjectVersion: {operations: []string{native.MethodGetContainer, native.MethodDeleteObject, native.MethodPutObject, native.MethodHeadObject, native.MethodGetObject, native.MethodRangeObject}, needTreeWrite: true}, + s3common.S3ActionDeleteObjectVersionTagging: {operations: []string{native.MethodGetContainer}, needTreeWrite: true}, + s3common.S3ActionGetBucketACL: {operations: []string{native.MethodGetContainer}}, + s3common.S3ActionGetBucketCORS: {operations: []string{native.MethodGetContainer, native.MethodGetObject, native.MethodHeadObject}}, + s3common.S3ActionGetBucketLocation: {operations: []string{native.MethodGetContainer}}, + s3common.S3ActionGetBucketNotification: {operations: []string{native.MethodGetContainer, native.MethodGetObject, native.MethodHeadObject}}, // not supported + s3common.S3ActionGetBucketObjectLockConfiguration: {operations: []string{native.MethodGetContainer}}, + s3common.S3ActionGetBucketPolicy: {operations: []string{native.MethodGetContainer}}, + s3common.S3ActionGetBucketPolicyStatus: {operations: []string{native.MethodGetContainer}}, + s3common.S3ActionGetBucketTagging: {operations: []string{native.MethodGetContainer}}, + s3common.S3ActionGetBucketVersioning: {operations: []string{native.MethodGetContainer}}, + s3common.S3ActionGetLifecycleConfiguration: {operations: []string{native.MethodGetContainer, native.MethodGetObject, native.MethodHeadObject}}, + s3common.S3ActionGetObject: {operations: []string{native.MethodGetContainer, native.MethodGetObject, native.MethodHeadObject, native.MethodSearchObject, native.MethodRangeObject, native.MethodHashObject}}, + s3common.S3ActionGetObjectACL: {operations: []string{native.MethodGetContainer}}, + s3common.S3ActionGetObjectAttributes: {operations: []string{native.MethodGetContainer, native.MethodHeadObject}}, + s3common.S3ActionGetObjectLegalHold: {operations: []string{native.MethodGetContainer}}, + s3common.S3ActionGetObjectRetention: {operations: []string{native.MethodGetContainer}}, + s3common.S3ActionGetObjectTagging: {operations: []string{native.MethodGetContainer}}, + s3common.S3ActionGetObjectVersion: {operations: []string{native.MethodGetContainer, native.MethodGetObject, native.MethodHeadObject, native.MethodSearchObject, native.MethodRangeObject, native.MethodHashObject}}, + s3common.S3ActionGetObjectVersionACL: {operations: []string{native.MethodGetContainer}}, + s3common.S3ActionGetObjectVersionAttributes: {operations: []string{native.MethodGetContainer, native.MethodHeadObject}}, + s3common.S3ActionGetObjectVersionTagging: {operations: []string{native.MethodGetContainer}}, + s3common.S3ActionListAllMyBuckets: {operations: []string{native.MethodListContainers, native.MethodGetContainer}}, + s3common.S3ActionListBucket: {operations: []string{native.MethodGetContainer, native.MethodGetObject, native.MethodHeadObject, native.MethodSearchObject, native.MethodRangeObject, native.MethodHashObject}}, + s3common.S3ActionListBucketMultipartUploads: {operations: []string{native.MethodGetContainer}}, + s3common.S3ActionListBucketVersions: {operations: []string{native.MethodGetContainer, native.MethodGetObject, native.MethodHeadObject, native.MethodSearchObject, native.MethodRangeObject, native.MethodHashObject}}, + s3common.S3ActionListMultipartUploadParts: {operations: []string{native.MethodGetContainer}}, + s3common.S3ActionPutBucketACL: {operations: []string{native.MethodGetContainer}, needTreeWrite: true}, + s3common.S3ActionPutBucketCORS: {operations: []string{native.MethodGetContainer, native.MethodGetObject, native.MethodPutObject}, needTreeWrite: true}, + s3common.S3ActionPutBucketNotification: {operations: []string{native.MethodGetContainer, native.MethodHeadObject, native.MethodDeleteObject, native.MethodGetObject, native.MethodPutObject}, needTreeWrite: true}, // not supported + s3common.S3ActionPutBucketObjectLockConfiguration: {operations: []string{native.MethodGetContainer}, needTreeWrite: true}, + s3common.S3ActionPutBucketPolicy: {operations: []string{native.MethodGetContainer}}, + s3common.S3ActionPutBucketTagging: {operations: []string{native.MethodGetContainer}, needTreeWrite: true}, + s3common.S3ActionPutBucketVersioning: {operations: []string{native.MethodGetContainer}, needTreeWrite: true}, + s3common.S3ActionPutLifecycleConfiguration: {operations: []string{native.MethodGetContainer, native.MethodGetObject, native.MethodHeadObject, native.MethodPutObject, native.MethodDeleteObject}, needTreeWrite: true}, + s3common.S3ActionPutObject: {operations: []string{native.MethodGetContainer, native.MethodPutObject, native.MethodGetObject, native.MethodHeadObject, native.MethodRangeObject}, needTreeWrite: true}, + s3common.S3ActionPutObjectACL: {operations: []string{native.MethodGetContainer}}, // not supported + s3common.S3ActionPutObjectLegalHold: {operations: []string{native.MethodGetContainer, native.MethodHeadObject, native.MethodGetObject, native.MethodPutObject}, needTreeWrite: true}, + s3common.S3ActionPutObjectRetention: {operations: []string{native.MethodGetContainer, native.MethodHeadObject, native.MethodGetObject, native.MethodPutObject}, needTreeWrite: true}, + s3common.S3ActionPutObjectTagging: {operations: []string{native.MethodGetContainer}, needTreeWrite: true}, + s3common.S3ActionPutObjectVersionACL: {operations: []string{native.MethodGetContainer}}, // not supported + s3common.S3ActionPutObjectVersionTagging: {operations: []string{native.MethodGetContainer}, needTreeWrite: true}, + s3common.S3ActionPatchObject: {operations: []string{native.MethodGetContainer, native.MethodGetObject, native.MethodHeadObject, native.MethodPatchObject, native.MethodPutObject, native.MethodRangeObject}, needTreeWrite: true}, + s3common.S3ActionPutBucketPublicAccessBlock: {operations: []string{native.MethodGetContainer}, needTreeWrite: true}, + s3common.S3ActionGetBucketPublicAccessBlock: {operations: []string{native.MethodGetContainer}}, } var containerNativeOperations = map[string]struct{}{ @@ -99,11 +104,12 @@ func ConvertToNativeChain(p s3common.Policy, resolver s3common.NativeResolver) ( if status != chain.Allow { // Most s3 methods share the same native operations. Deny rules must not affect shared native operations, // therefore this code skips all deny rules for native protocol. Deny is applied for s3 protocol only, in this case. + // Consider adding new condition for object operations (so that $Tree:ID be empty) if we will stop skipping deny rules. continue } action, actionInverted := statement.GetAction() - nativeActions, err := formNativeActionNames(action) + nativeActions, treeWrite, err := formNativeActionNames(action) if err != nil { return nil, err } @@ -113,7 +119,7 @@ func ConvertToNativeChain(p s3common.Policy, resolver s3common.NativeResolver) ( } resource, resourceInverted := statement.GetResource() - groupedResources, err := formNativeResourceNamesAndConditions(resource, resolver, getActionTypes(nativeActions)) + groupedResources, treeRes, err := formNativeResourceNamesAndConditions(resource, resolver, getActionTypes(nativeActions)) if err != nil { return nil, err } @@ -132,6 +138,8 @@ func ConvertToNativeChain(p s3common.Policy, resolver s3common.NativeResolver) ( return nil, err } + engineChain.Rules = append(engineChain.Rules, getTreeRule(treeRes, principals, principalCondFn, treeWrite)...) + for _, groupedResource := range groupedResources { for _, principal := range principals { for _, conditions := range splitConditions { @@ -164,6 +172,50 @@ func ConvertToNativeChain(p s3common.Policy, resolver s3common.NativeResolver) ( return &engineChain, nil } +func getTreeRule(resources []string, principals []string, principalCondFn s3common.FormPrincipalConditionFunc, needWrite bool) []chain.Rule { + ops := []string{native.MethodGetObject} + if needWrite { + ops = append(ops, native.MethodPutObject) + } + + treeCondition := chain.Condition{ + Op: chain.CondStringNotEquals, + Kind: chain.KindResource, + Key: native.ProperyKeyTreeID, + Value: "", + } + + var principalTreeConditions []chain.Condition + for _, principal := range principals { + if principal == s3common.Wildcard { + principalTreeConditions = principalTreeConditions[:0] + break + } + principalTreeConditions = append(principalTreeConditions, principalCondFn(principal)) + } + + if len(principalTreeConditions) == 0 { + return []chain.Rule{{ + Status: chain.Allow, + Actions: chain.Actions{Names: ops}, + Resources: chain.Resources{Names: resources}, + Condition: []chain.Condition{treeCondition}, + }} + } + + res := make([]chain.Rule, len(principalTreeConditions)) + for i, condition := range principalTreeConditions { + res[i] = chain.Rule{ + Status: chain.Allow, + Actions: chain.Actions{Names: ops}, + Resources: chain.Resources{Names: resources}, + Condition: []chain.Condition{treeCondition, condition}, + } + } + + return res +} + func getActionTypes(nativeActions []string) ActionTypes { var res ActionTypes for _, action := range nativeActions { @@ -258,23 +310,24 @@ type ActionTypes struct { Container bool } -func formNativeResourceNamesAndConditions(names []string, resolver s3common.NativeResolver, actionTypes ActionTypes) ([]GroupedResources, error) { +func formNativeResourceNamesAndConditions(names []string, resolver s3common.NativeResolver, actionTypes ActionTypes) ([]GroupedResources, []string, error) { if !actionTypes.Object && !actionTypes.Container { - return nil, s3common.ErrActionsNotApplicable + return nil, nil, s3common.ErrActionsNotApplicable } res := make([]GroupedResources, 0, len(names)) + treeResMap := make(map[string]struct{}, len(names)) combined := make(map[string]struct{}) for _, resource := range names { if err := s3common.ValidateResource(resource); err != nil { - return nil, err + return nil, nil, err } if resource == s3common.Wildcard { res = res[:0] - return append(res, formWildcardNativeResource(actionTypes)), nil + return append(res, formWildcardNativeResource(actionTypes)), []string{native.ResourceFormatAllObjects}, nil } if !strings.HasPrefix(resource, s3common.S3ResourcePrefix) { @@ -285,7 +338,7 @@ func formNativeResourceNamesAndConditions(names []string, resolver s3common.Nati s3Resource := strings.TrimPrefix(resource, s3common.S3ResourcePrefix) if s3Resource == s3common.Wildcard { res = res[:0] - return append(res, formWildcardNativeResource(actionTypes)), nil + return append(res, formWildcardNativeResource(actionTypes)), []string{native.ResourceFormatAllObjects}, nil } if sepIndex := strings.Index(s3Resource, "/"); sepIndex < 0 { @@ -300,9 +353,11 @@ func formNativeResourceNamesAndConditions(names []string, resolver s3common.Nati bktInfo, err := resolver.GetBucketInfo(bkt) if err != nil { - return nil, err + return nil, nil, err } + treeResMap[fmt.Sprintf(native.ResourceFormatNamespaceContainerObjects, bktInfo.Namespace, bktInfo.Container)] = struct{}{} + if obj == s3common.Wildcard && actionTypes.Object { // this corresponds to arn:aws:s3:::BUCKET/ or arn:aws:s3:::BUCKET/* combined[fmt.Sprintf(native.ResourceFormatNamespaceContainerObjects, bktInfo.Namespace, bktInfo.Container)] = struct{}{} combined[fmt.Sprintf(native.ResourceFormatNamespaceContainer, bktInfo.Namespace, bktInfo.Container)] = struct{}{} @@ -338,7 +393,12 @@ func formNativeResourceNamesAndConditions(names []string, resolver s3common.Nati res = append(res, gr) } - return res, nil + treeRes := make([]string, 0, len(treeResMap)) + for k := range treeResMap { + treeRes = append(treeRes, k) + } + + return res, treeRes, nil } func formWildcardNativeResource(actionTypes ActionTypes) GroupedResources { @@ -380,17 +440,18 @@ func formPrincipalKey(principal string, resolver s3common.NativeResolver) (strin return key, nil } -func formNativeActionNames(names []string) ([]string, error) { +func formNativeActionNames(names []string) ([]string, bool, error) { uniqueActions := make(map[string]struct{}, len(names)) + var treeWrite bool for _, action := range names { if action == s3common.Wildcard { - return []string{s3common.Wildcard}, nil + return []string{s3common.Wildcard}, true, nil } isIAM, err := s3common.ValidateAction(action) if err != nil { - return nil, err + return nil, false, err } if isIAM { @@ -398,15 +459,18 @@ func formNativeActionNames(names []string) ([]string, error) { } if action[len(s3common.S3ActionPrefix):] == s3common.Wildcard { - return []string{s3common.Wildcard}, nil + return []string{s3common.Wildcard}, true, nil } nativeActions := actionToNativeOpMap[action] - if len(nativeActions) == 0 { - return nil, s3common.ErrActionsNotApplicable + if nativeActions.needTreeWrite { + treeWrite = true + } + if len(nativeActions.operations) == 0 { + return nil, false, s3common.ErrActionsNotApplicable } - for _, nativeAction := range nativeActions { + for _, nativeAction := range nativeActions.operations { uniqueActions[nativeAction] = struct{}{} } } @@ -416,5 +480,5 @@ func formNativeActionNames(names []string) ([]string, error) { res = append(res, key) } - return res, nil + return res, treeWrite, nil } diff --git a/pkg/policy-engine/v2/iam/converter_test.go b/pkg/policy-engine/v2/iam/converter_test.go index 5366fa158..2bf671ea4 100644 --- a/pkg/policy-engine/v2/iam/converter_test.go +++ b/pkg/policy-engine/v2/iam/converter_test.go @@ -137,6 +137,27 @@ func TestConverters(t *testing.T) { } expected := &chain.Chain{Rules: []chain.Rule{ + { + Status: chain.Allow, + Actions: chain.Actions{Names: []string{native.MethodGetObject, native.MethodPutObject}}, + Resources: chain.Resources{Names: []string{ + fmt.Sprintf(native.ResourceFormatNamespaceContainerObjects, namespace, mockResolver.containers[bktName]), + }}, + Condition: []chain.Condition{ + { + Op: chain.CondStringEquals, + Kind: chain.KindRequest, + Key: native.PropertyKeyActorPublicKey, + Value: mockResolver.users[user], + }, + { + Op: chain.CondStringNotEquals, + Kind: chain.KindResource, + Key: native.ProperyKeyTreeID, + Value: "", + }, + }, + }, { Status: chain.Allow, Actions: chain.Actions{Names: []string{native.MethodGetContainer, native.MethodPutObject, @@ -212,6 +233,27 @@ func TestConverters(t *testing.T) { } expected := &chain.Chain{Rules: []chain.Rule{ + { + Status: chain.Allow, + Actions: chain.Actions{Names: []string{native.MethodGetObject, native.MethodPutObject}}, + Resources: chain.Resources{Names: []string{ + fmt.Sprintf(native.ResourceFormatNamespaceContainerObjects, namespace, mockResolver.containers[bktName]), + }}, + Condition: []chain.Condition{ + { + Op: chain.CondStringEquals, + Kind: chain.KindRequest, + Key: native.PropertyKeyActorPublicKey, + Value: mockResolver.users[user], + }, + { + Op: chain.CondStringNotEquals, + Kind: chain.KindResource, + Key: native.ProperyKeyTreeID, + Value: "", + }, + }, + }, { Status: chain.Allow, Actions: chain.Actions{Names: []string{ @@ -357,17 +399,38 @@ func TestConverters(t *testing.T) { require.NoError(t, err) assertChainsEqual(t, s3Expected, s3Chain) - nativeExpected := &chain.Chain{Rules: []chain.Rule{{ - Status: chain.Allow, - Actions: chain.Actions{Names: []string{native.MethodGetContainer, native.MethodDeleteObject, native.MethodPutObject, native.MethodHeadObject, native.MethodGetObject, native.MethodRangeObject}}, - Resources: chain.Resources{Names: []string{native.ResourceFormatAllObjects, native.ResourceFormatAllContainers}}, - Condition: []chain.Condition{{ - Op: chain.CondStringEquals, - Kind: chain.KindRequest, - Key: native.PropertyKeyActorPublicKey, - Value: mockResolver.users[user], - }}, - }}} + nativeExpected := &chain.Chain{Rules: []chain.Rule{ + { + Status: chain.Allow, + Actions: chain.Actions{Names: []string{native.MethodGetObject, native.MethodPutObject}}, + Resources: chain.Resources{Names: []string{native.ResourceFormatAllObjects}}, + Condition: []chain.Condition{ + { + Op: chain.CondStringEquals, + Kind: chain.KindRequest, + Key: native.PropertyKeyActorPublicKey, + Value: mockResolver.users[user], + }, + { + Op: chain.CondStringNotEquals, + Kind: chain.KindResource, + Key: native.ProperyKeyTreeID, + Value: "", + }, + }, + }, + { + Status: chain.Allow, + Actions: chain.Actions{Names: []string{native.MethodGetContainer, native.MethodDeleteObject, native.MethodPutObject, native.MethodHeadObject, native.MethodGetObject, native.MethodRangeObject}}, + Resources: chain.Resources{Names: []string{native.ResourceFormatAllObjects, native.ResourceFormatAllContainers}}, + Condition: []chain.Condition{{ + Op: chain.CondStringEquals, + Kind: chain.KindRequest, + Key: native.PropertyKeyActorPublicKey, + Value: mockResolver.users[user], + }}, + }, + }} nativeChain, err := ConvertToNativeChain(p, mockResolver) require.NoError(t, err) @@ -695,17 +758,30 @@ func TestIPConditions(t *testing.T) { require.Equal(t, s3Expected, s3Chain) nativeExpected := &chain.Chain{ - Rules: []chain.Rule{{ - Status: chain.Allow, - Actions: chain.Actions{Names: []string{s3common.Wildcard}}, - Resources: chain.Resources{Names: []string{native.ResourceFormatAllObjects, native.ResourceFormatAllContainers}}, - Condition: []chain.Condition{{ - Op: chain.CondIPAddress, - Kind: chain.KindRequest, - Key: common.PropertyKeyFrostFSSourceIP, - Value: "203.0.113.0/24", - }}, - }}, + Rules: []chain.Rule{ + { + Status: chain.Allow, + Actions: chain.Actions{Names: []string{native.MethodGetObject, native.MethodPutObject}}, + Resources: chain.Resources{Names: []string{native.ResourceFormatAllObjects}}, + Condition: []chain.Condition{{ + Op: chain.CondStringNotEquals, + Kind: chain.KindResource, + Key: native.ProperyKeyTreeID, + Value: "", + }}, + }, + { + Status: chain.Allow, + Actions: chain.Actions{Names: []string{s3common.Wildcard}}, + Resources: chain.Resources{Names: []string{native.ResourceFormatAllObjects, native.ResourceFormatAllContainers}}, + Condition: []chain.Condition{{ + Op: chain.CondIPAddress, + Kind: chain.KindRequest, + Key: common.PropertyKeyFrostFSSourceIP, + Value: "203.0.113.0/24", + }}, + }, + }, } nativeChain, err := ConvertToNativeChain(p, newMockUserResolver(nil, nil, "")) @@ -853,10 +929,12 @@ func TestComplexNativeConditions(t *testing.T) { } expectedStatus := chain.Allow - expectedActions := chain.Actions{Names: actionToNativeOpMap["s3:"+action]} + expectedActions := chain.Actions{Names: actionToNativeOpMap["s3:"+action].operations} expectedResource1 := chain.Resources{Names: []string{nativeResource1, nativeResource1cnr}} expectedResource23 := chain.Resources{Names: []string{nativeResource2, nativeResource2cnr, nativeResource3, nativeResource3cnr}} + treeCondition := chain.Condition{Op: chain.CondStringNotEquals, Kind: chain.KindResource, Key: native.ProperyKeyTreeID, Value: ""} + //notTreeCondition := chain.Condition{Op: chain.CondStringEquals, Kind: chain.KindResource, Key: native.ProperyKeyTreeID, Value: ""} // todo user1Condition := chain.Condition{Op: chain.CondStringEquals, Kind: chain.KindRequest, Key: native.PropertyKeyActorPublicKey, Value: mockResolver.users[user1]} user2Condition := chain.Condition{Op: chain.CondStringEquals, Kind: chain.KindRequest, Key: native.PropertyKeyActorPublicKey, Value: mockResolver.users[user2]} objectName1Condition := chain.Condition{Op: chain.CondStringLike, Kind: chain.KindResource, Key: PropertyKeyFilePath, Value: objName1} @@ -865,6 +943,24 @@ func TestComplexNativeConditions(t *testing.T) { key2val2Condition := chain.Condition{Op: chain.CondStringLike, Kind: chain.KindRequest, Key: key2, Value: val2} expected := &chain.Chain{Rules: []chain.Rule{ + { + Status: expectedStatus, + Actions: chain.Actions{Names: []string{native.MethodGetObject, native.MethodPutObject}}, + Resources: chain.Resources{Names: []string{nativeResource1, nativeResource2, nativeResource3}}, + Condition: []chain.Condition{ + user1Condition, + treeCondition, + }, + }, + { + Status: expectedStatus, + Actions: chain.Actions{Names: []string{native.MethodGetObject, native.MethodPutObject}}, + Resources: chain.Resources{Names: []string{nativeResource1, nativeResource2, nativeResource3}}, + Condition: []chain.Condition{ + user2Condition, + treeCondition, + }, + }, { Status: expectedStatus, Actions: expectedActions, @@ -1443,11 +1539,24 @@ func TestWildcardConverters(t *testing.T) { require.Equal(t, s3Expected, s3Chain) nativeExpected := &chain.Chain{ - Rules: []chain.Rule{{ - Status: chain.Allow, - Actions: chain.Actions{Names: []string{s3common.Wildcard}}, - Resources: chain.Resources{Names: []string{native.ResourceFormatAllObjects, native.ResourceFormatAllContainers}}, - }}, + Rules: []chain.Rule{ + { + Status: chain.Allow, + Actions: chain.Actions{Names: []string{native.MethodGetObject, native.MethodPutObject}}, + Resources: chain.Resources{Names: []string{native.ResourceFormatAllObjects}}, + Condition: []chain.Condition{{ + Op: chain.CondStringNotEquals, + Kind: chain.KindResource, + Key: native.ProperyKeyTreeID, + Value: "", + }}, + }, + { + Status: chain.Allow, + Actions: chain.Actions{Names: []string{s3common.Wildcard}}, + Resources: chain.Resources{Names: []string{native.ResourceFormatAllObjects, native.ResourceFormatAllContainers}}, + }, + }, } nativeChain, err := ConvertToNativeChain(p, newMockUserResolver(nil, nil, "")) @@ -1490,14 +1599,27 @@ func TestWildcardObjectsConverters(t *testing.T) { mockResolver := newMockUserResolver(nil, []string{"bucket"}, "") nativeExpected := &chain.Chain{ - Rules: []chain.Rule{{ - Status: chain.Allow, - Actions: chain.Actions{Names: []string{s3common.Wildcard}}, - Resources: chain.Resources{Names: []string{ - fmt.Sprintf(native.ResourceFormatRootContainer, mockResolver.containers["bucket"]), - fmt.Sprintf(native.ResourceFormatRootContainerObjects, mockResolver.containers["bucket"]), - }}, - }}, + Rules: []chain.Rule{ + { + Status: chain.Allow, + Actions: chain.Actions{Names: []string{native.MethodGetObject, native.MethodPutObject}}, + Resources: chain.Resources{Names: []string{fmt.Sprintf(native.ResourceFormatRootContainerObjects, mockResolver.containers["bucket"])}}, + Condition: []chain.Condition{{ + Op: chain.CondStringNotEquals, + Kind: chain.KindResource, + Key: native.ProperyKeyTreeID, + Value: "", + }}, + }, + { + Status: chain.Allow, + Actions: chain.Actions{Names: []string{s3common.Wildcard}}, + Resources: chain.Resources{Names: []string{ + fmt.Sprintf(native.ResourceFormatRootContainer, mockResolver.containers["bucket"]), + fmt.Sprintf(native.ResourceFormatRootContainerObjects, mockResolver.containers["bucket"]), + }}, + }, + }, } nativeChain, err := ConvertToNativeChain(p, mockResolver) @@ -1655,7 +1777,7 @@ func TestFromActions(t *testing.T) { }, } { t.Run("", func(t *testing.T) { - actions, err := formNativeActionNames([]string{tc.action}) + actions, _, err := formNativeActionNames([]string{tc.action}) if tc.err { require.Error(t, err) } else { @@ -1776,13 +1898,19 @@ func TestTagsConditions(t *testing.T) { }, } - expectedNativeConditions := []chain.Condition{ - { + expectedNativeConditions := [][]chain.Condition{ + {{ + Op: chain.CondStringNotEquals, + Kind: chain.KindResource, + Key: native.ProperyKeyTreeID, + Value: "", + }}, + {{ Op: chain.CondStringEquals, Kind: chain.KindRequest, Key: fmt.Sprintf(common.PropertyKeyFormatFrostFSIDUserClaim, "tag-department"), Value: "hr", - }, + }}, } for _, tc := range []struct { @@ -1847,8 +1975,9 @@ func TestTagsConditions(t *testing.T) { nativeChain, err := ConvertToNativeChain(p, newMockUserResolver(nil, nil, "")) require.NoError(t, err) - require.Len(t, nativeChain.Rules, 1) - require.ElementsMatch(t, expectedNativeConditions, nativeChain.Rules[0].Condition) + require.Len(t, nativeChain.Rules, 2) + require.ElementsMatch(t, expectedNativeConditions[0], nativeChain.Rules[0].Condition) + require.ElementsMatch(t, expectedNativeConditions[1], nativeChain.Rules[1].Condition) } }