From 0edc002441e4edec0918b1d0e891b07214eb6c91 Mon Sep 17 00:00:00 2001 From: Denis Kirillov Date: Thu, 1 Feb 2024 16:57:07 +0300 Subject: [PATCH] [#46] iam: Handle s3 complex actions Signed-off-by: Denis Kirillov --- iam/converter_s3.go | 97 +++++++++++++++++++++++++++++++++++++++---- iam/converter_test.go | 22 +++++----- 2 files changed, 102 insertions(+), 17 deletions(-) diff --git a/iam/converter_s3.go b/iam/converter_s3.go index f4ef7eb..2874c34 100644 --- a/iam/converter_s3.go +++ b/iam/converter_s3.go @@ -7,6 +7,76 @@ import ( "git.frostfs.info/TrueCloudLab/policy-engine/schema/s3" ) +var specialActionToS3OpMap = map[string][]string{ + specialS3ActionsListAllMyBuckets: {"s3:ListBuckets"}, + specialS3ActionsListBucket: {"s3:HeadBucket", "s3:GetBucketLocation", "s3:ListObjectsV1", "s3:ListObjectsV2"}, + specialS3ActionsListBucketVersions: {"s3:ListBucketObjectVersions"}, + specialS3ActionsListBucketMultipartUploads: {"s3:ListMultipartUploads"}, + specialS3ActionsGetBucketObjectLockConfiguration: {"s3:GetBucketObjectLockConfig"}, + specialS3ActionsGetEncryptionConfiguration: {"s3:GetBucketEncryption"}, + specialS3ActionsGetLifecycleConfiguration: {"s3:GetBucketLifecycle"}, + specialS3ActionsGetBucketACL: {"s3:GetBucketACL"}, + specialS3ActionsGetBucketCORS: {"s3:GetBucketCors"}, + specialS3ActionsPutBucketTagging: {"s3:PutBucketTagging", "s3:DeleteBucketTagging"}, + specialS3ActionsPutBucketObjectLockConfiguration: {"s3:PutBucketObjectLockConfig"}, + specialS3ActionsPutEncryptionConfiguration: {"s3:PutBucketEncryption", "s3:DeleteBucketEncryption"}, + specialS3ActionsPutLifecycleConfiguration: {"s3:PutBucketLifecycle", "s3:DeleteBucketLifecycle"}, + specialS3ActionsPutBucketACL: {"s3:PutBucketACL"}, + specialS3ActionsPutBucketCORS: {"s3:PutBucketCors", "s3:DeleteBucketCors"}, + specialS3ActionsDeleteBucketCORS: {"s3:DeleteBucketCors"}, + + specialS3ActionsListMultipartUploadParts: {"s3:ListParts"}, + specialS3ActionsGetObjectACL: {"s3:GetObjectACL"}, + specialS3ActionsGetObject: {"s3:GetObject", "s3:HeadObject"}, + specialS3ActionsGetObjectVersion: {"s3:GetObject", "s3:HeadObject"}, + specialS3ActionsGetObjectVersionACL: {"s3:GetObjectACL"}, + specialS3ActionsGetObjectVersionAttributes: {"s3:GetObjectAttributes"}, + specialS3ActionsGetObjectVersionTagging: {"s3:GetObjectTagging"}, + specialS3ActionsPutObjectACL: {"s3:PutObjectACL"}, + specialS3ActionsPutObjectVersionACL: {"s3:PutObjectACL"}, + specialS3ActionsPutObjectVersionTagging: {"s3:PutObjectTagging"}, + specialS3ActionsPutObject: { + "s3:PutObject", "s3:PostObject", "s3:CopyObject", + "s3:UploadPart", "s3:UploadPartCopy", "s3:CreateMultipartUpload", "s3:CompleteMultipartUpload", + }, + specialS3ActionsDeleteObjectVersionTagging: {"s3:DeleteObjectTagging"}, + specialS3ActionsDeleteObject: {"s3:DeleteObject", "s3:DeleteMultipleObjects"}, + specialS3ActionsDeleteObjectVersion: {"s3:DeleteObject", "s3:DeleteMultipleObjects"}, +} + +const ( + specialS3ActionsListAllMyBuckets = "s3:ListAllMyBuckets" + specialS3ActionsListBucket = "s3:ListBucket" + specialS3ActionsListBucketVersions = "s3:ListBucketVersions" + specialS3ActionsListBucketMultipartUploads = "s3:ListBucketMultipartUploads" + specialS3ActionsGetBucketObjectLockConfiguration = "s3:GetBucketObjectLockConfiguration" + specialS3ActionsGetEncryptionConfiguration = "s3:GetEncryptionConfiguration" + specialS3ActionsGetLifecycleConfiguration = "s3:GetLifecycleConfiguration" + specialS3ActionsGetBucketACL = "s3:GetBucketAcl" + specialS3ActionsGetBucketCORS = "s3:GetBucketCORS" + specialS3ActionsPutBucketTagging = "s3:PutBucketTagging" + specialS3ActionsPutBucketObjectLockConfiguration = "s3:PutBucketObjectLockConfiguration" + specialS3ActionsPutEncryptionConfiguration = "s3:PutEncryptionConfiguration" + specialS3ActionsPutLifecycleConfiguration = "s3:PutLifecycleConfiguration" + specialS3ActionsPutBucketACL = "s3:PutBucketAcl" + specialS3ActionsPutBucketCORS = "s3:PutBucketCORS" + specialS3ActionsDeleteBucketCORS = "s3:DeleteBucketCORS" + specialS3ActionsListMultipartUploadParts = "s3:ListMultipartUploadParts" + specialS3ActionsGetObjectACL = "s3:GetObjectAcl" + specialS3ActionsGetObject = "s3:GetObject" + specialS3ActionsGetObjectVersion = "s3:GetObjectVersion" + specialS3ActionsGetObjectVersionACL = "s3:GetObjectVersionAcl" + specialS3ActionsGetObjectVersionAttributes = "s3:GetObjectVersionAttributes" + specialS3ActionsGetObjectVersionTagging = "s3:GetObjectVersionTagging" + specialS3ActionsPutObjectACL = "s3:PutObjectAcl" + specialS3ActionsPutObjectVersionACL = "s3:PutObjectVersionAcl" + specialS3ActionsPutObjectVersionTagging = "s3:PutObjectVersionTagging" + specialS3ActionsPutObject = "s3:PutObject" + specialS3ActionsDeleteObjectVersionTagging = "s3:DeleteObjectVersionTagging" + specialS3ActionsDeleteObject = "s3:DeleteObject" + specialS3ActionsDeleteObjectVersion = "s3:DeleteObjectVersion" +) + type S3Resolver interface { GetUserAddress(account, user string) (string, error) } @@ -22,10 +92,11 @@ func ConvertToS3Chain(p Policy, resolver S3Resolver) (*chain.Chain, error) { status := formStatus(statement) actions, actionInverted := statement.action() - if err := validateS3ActionNames(actions); err != nil { + s3Actions, err := formS3ActionNames(actions) + if err != nil { return nil, err } - ruleAction := chain.Actions{Inverted: actionInverted, Names: actions} + ruleAction := chain.Actions{Inverted: actionInverted, Names: s3Actions} if len(ruleAction.Names) == 0 { continue } @@ -161,12 +232,24 @@ func validateS3ResourceNames(names []string) error { return nil } -func validateS3ActionNames(names []string) error { - for i := range names { - if err := validateAction(names[i]); err != nil { - return err +func formS3ActionNames(names []string) ([]string, error) { + res := make([]string, 0, len(names)) + + for _, action := range names { + if err := validateAction(action); err != nil { + return nil, err + } + + if action == Wildcard { + return []string{Wildcard}, nil + } + + if actions, ok := specialActionToS3OpMap[action]; ok { + res = append(res, actions...) + } else { + res = append(res, action) } } - return nil + return res, nil } diff --git a/iam/converter_test.go b/iam/converter_test.go index f5c5ef4..9591e02 100644 --- a/iam/converter_test.go +++ b/iam/converter_test.go @@ -71,7 +71,8 @@ func TestConverters(t *testing.T) { bktName := "DOC-EXAMPLE-BUCKET" objName := "object-name" resource := fmt.Sprintf(s3.ResourceFormatS3BucketObjects, bktName) - s3action := "s3:PutObject" + s3GetObjectAction := "s3:GetObject" + s3HeadObjectAction := "s3:HeadObject" mockResolver := newMockUserResolver([]string{user}, []string{bktName}, namespace) @@ -83,7 +84,7 @@ func TestConverters(t *testing.T) { AWSPrincipalType: {principal}, }, Effect: AllowEffect, - Action: []string{"s3:PutObject"}, + Action: []string{s3GetObjectAction}, Resource: []string{resource}, Conditions: map[string]Condition{ CondStringEquals: { @@ -96,7 +97,7 @@ func TestConverters(t *testing.T) { expected := &chain.Chain{Rules: []chain.Rule{ { Status: chain.Allow, - Actions: chain.Actions{Names: []string{s3action}}, + Actions: chain.Actions{Names: []string{s3GetObjectAction, s3HeadObjectAction}}, Resources: chain.Resources{Names: []string{resource}}, Condition: []chain.Condition{ { @@ -162,7 +163,7 @@ func TestConverters(t *testing.T) { AWSPrincipalType: {principal}, }, Effect: DenyEffect, - NotAction: []string{"s3:PutObject"}, + NotAction: []string{s3GetObjectAction}, NotResource: []string{resource}, }}, } @@ -170,7 +171,7 @@ func TestConverters(t *testing.T) { expected := &chain.Chain{Rules: []chain.Rule{ { Status: chain.AccessDenied, - Actions: chain.Actions{Inverted: true, Names: []string{s3action}}, + Actions: chain.Actions{Inverted: true, Names: []string{s3GetObjectAction, s3HeadObjectAction}}, Resources: chain.Resources{Inverted: true, Names: []string{resource}}, Condition: []chain.Condition{ { @@ -317,14 +318,14 @@ func TestConverters(t *testing.T) { Statement: []Statement{{ Principal: map[PrincipalType][]string{AWSPrincipalType: {principal}}, Effect: AllowEffect, - Action: []string{"s3:PutObject", "iam:*"}, + Action: []string{"s3:DeleteObject", "iam:*"}, Resource: []string{"*"}, }}, } s3Expected := &chain.Chain{Rules: []chain.Rule{{ Status: chain.Allow, - Actions: chain.Actions{Names: []string{"s3:PutObject", "iam:*"}}, + Actions: chain.Actions{Names: []string{"s3:DeleteObject", "s3:DeleteMultipleObjects", "iam:*"}}, Resources: chain.Resources{Names: []string{"*"}}, Condition: []chain.Condition{{ Op: chain.CondStringEquals, @@ -340,7 +341,7 @@ func TestConverters(t *testing.T) { nativeExpected := &chain.Chain{Rules: []chain.Rule{{ Status: chain.Allow, - Actions: chain.Actions{Names: []string{native.MethodPutObject}}, + Actions: chain.Actions{Names: []string{native.MethodDeleteObject}}, Resources: chain.Resources{Names: []string{native.ResourceFormatAllObjects}}, Condition: []chain.Condition{{ Op: chain.CondStringEquals, @@ -928,7 +929,8 @@ func TestComplexS3Conditions(t *testing.T) { resource1 := fmt.Sprintf(s3.ResourceFormatS3BucketObject, bktName1, objName1) resource2 := fmt.Sprintf(s3.ResourceFormatS3BucketObjects, bktName2) resource3 := fmt.Sprintf(s3.ResourceFormatS3BucketObjects, bktName3) - action := "s3:PutObject" + action := "s3:DeleteObject" + action2 := "s3:DeleteMultipleObjects" key1, key2 := "key1", "key2" val0, val1, val2 := "val0", "val1", "val2" @@ -952,7 +954,7 @@ func TestComplexS3Conditions(t *testing.T) { } expectedStatus := chain.AccessDenied - expectedActions := chain.Actions{Names: []string{action}} + expectedActions := chain.Actions{Names: []string{action, action2}} expectedResources := chain.Resources{Names: []string{resource1, resource2, resource3}} user1Condition := chain.Condition{Op: chain.CondStringEquals, Object: chain.ObjectRequest, Key: s3.PropertyKeyOwner, Value: mockResolver.users[user1]}