From 1d51f2121d7e2d13a3c12365a820b9d9a29b6a40 Mon Sep 17 00:00:00 2001 From: Denis Kirillov Date: Fri, 29 Mar 2024 15:47:49 +0300 Subject: [PATCH] [#58] iam: Support more s3 actions Signed-off-by: Denis Kirillov --- iam/converter.go | 66 ++++++-- iam/converter_native.go | 132 +++++++++++----- iam/converter_s3.go | 151 +++++++++--------- iam/converter_test.go | 333 ++++++++++++++++++++++++++++++---------- iam/policy_test.go | 17 +- 5 files changed, 485 insertions(+), 214 deletions(-) diff --git a/iam/converter.go b/iam/converter.go index ffa2352..bd3483e 100644 --- a/iam/converter.go +++ b/iam/converter.go @@ -11,6 +11,57 @@ import ( "git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain" ) +const ( + s3ActionAbortMultipartUpload = "s3:AbortMultipartUpload" + s3ActionCreateBucket = "s3:CreateBucket" + s3ActionDeleteBucket = "s3:DeleteBucket" + s3ActionDeleteBucketPolicy = "s3:DeleteBucketPolicy" + s3ActionDeleteObject = "s3:DeleteObject" + s3ActionDeleteObjectTagging = "s3:DeleteObjectTagging" + s3ActionDeleteObjectVersion = "s3:DeleteObjectVersion" + s3ActionDeleteObjectVersionTagging = "s3:DeleteObjectVersionTagging" + s3ActionGetBucketACL = "s3:GetBucketAcl" + s3ActionGetBucketCORS = "s3:GetBucketCORS" + s3ActionGetBucketLocation = "s3:GetBucketLocation" + s3ActionGetBucketNotification = "s3:GetBucketNotification" + s3ActionGetBucketObjectLockConfiguration = "s3:GetBucketObjectLockConfiguration" + s3ActionGetBucketPolicy = "s3:GetBucketPolicy" + s3ActionGetBucketPolicyStatus = "s3:GetBucketPolicyStatus" + s3ActionGetBucketTagging = "s3:GetBucketTagging" + s3ActionGetBucketVersioning = "s3:GetBucketVersioning" + s3ActionGetLifecycleConfiguration = "s3:GetLifecycleConfiguration" + s3ActionGetObject = "s3:GetObject" + s3ActionGetObjectACL = "s3:GetObjectAcl" + s3ActionGetObjectAttributes = "s3:GetObjectAttributes" + s3ActionGetObjectLegalHold = "s3:GetObjectLegalHold" + s3ActionGetObjectRetention = "s3:GetObjectRetention" + s3ActionGetObjectTagging = "s3:GetObjectTagging" + s3ActionGetObjectVersion = "s3:GetObjectVersion" + s3ActionGetObjectVersionACL = "s3:GetObjectVersionAcl" + s3ActionGetObjectVersionAttributes = "s3:GetObjectVersionAttributes" + s3ActionGetObjectVersionTagging = "s3:GetObjectVersionTagging" + s3ActionListAllMyBuckets = "s3:ListAllMyBuckets" + s3ActionListBucket = "s3:ListBucket" + s3ActionListBucketMultipartUploads = "s3:ListBucketMultipartUploads" + s3ActionListBucketVersions = "s3:ListBucketVersions" + s3ActionListMultipartUploadParts = "s3:ListMultipartUploadParts" + s3ActionPutBucketACL = "s3:PutBucketAcl" + s3ActionPutBucketCORS = "s3:PutBucketCORS" + s3ActionPutBucketNotification = "s3:PutBucketNotification" + s3ActionPutBucketObjectLockConfiguration = "s3:PutBucketObjectLockConfiguration" + s3ActionPutBucketPolicy = "s3:PutBucketPolicy" + s3ActionPutBucketTagging = "s3:PutBucketTagging" + s3ActionPutBucketVersioning = "s3:PutBucketVersioning" + s3ActionPutLifecycleConfiguration = "s3:PutLifecycleConfiguration" + s3ActionPutObject = "s3:PutObject" + s3ActionPutObjectACL = "s3:PutObjectAcl" + s3ActionPutObjectLegalHold = "s3:PutObjectLegalHold" + s3ActionPutObjectRetention = "s3:PutObjectRetention" + s3ActionPutObjectTagging = "s3:PutObjectTagging" + s3ActionPutObjectVersionACL = "s3:PutObjectVersionAcl" + s3ActionPutObjectVersionTagging = "s3:PutObjectVersionTagging" +) + const condKeyAWSPrincipalARN = "aws:PrincipalArn" const ( @@ -269,21 +320,18 @@ func validateResource(resource string) error { return nil } -func validateAction(action string) error { - if action == Wildcard { - return nil - } - - if !strings.HasPrefix(action, s3ActionPrefix) && !strings.HasPrefix(action, iamActionPrefix) { - return ErrInvalidActionFormat +func validateAction(action string) (bool, error) { + isIAM := strings.HasPrefix(action, iamActionPrefix) + if !strings.HasPrefix(action, s3ActionPrefix) && !isIAM { + return false, ErrInvalidActionFormat } index := strings.IndexByte(action, Wildcard[0]) if index != -1 && index != utf8.RuneCountInString(action)-1 { - return ErrInvalidActionFormat + return false, ErrInvalidActionFormat } - return nil + return isIAM, nil } func splitGroupedConditions(groupedConditions []GroupedConditions) [][]chain.Condition { diff --git a/iam/converter_native.go b/iam/converter_native.go index b205638..dfc3bf0 100644 --- a/iam/converter_native.go +++ b/iam/converter_native.go @@ -10,18 +10,55 @@ import ( const PropertyKeyFilePath = "FilePath" -var supportedActionToNativeOpMap = map[string][]string{ - supportedS3NativeActionDeleteObject: {native.MethodDeleteObject, native.MethodHeadObject}, - supportedS3NativeActionHeadObject: {native.MethodHeadObject}, - supportedS3NativeActionGetObject: {native.MethodGetObject, native.MethodHeadObject, native.MethodSearchObject, native.MethodRangeObject, native.MethodHashObject}, - supportedS3NativeActionPutObject: {native.MethodPutObject}, - supportedS3NativeActionListBucket: {native.MethodGetContainer, native.MethodGetObject, native.MethodHeadObject, native.MethodSearchObject, native.MethodRangeObject, native.MethodHashObject}, - - supportedS3NativeActionCreateBucket: {native.MethodPutContainer}, - supportedS3NativeActionDeleteBucket: {native.MethodDeleteContainer}, - supportedS3NativeActionListAllMyBucket: {native.MethodListContainers}, - supportedS3NativeActionPutBucketACL: {native.MethodSetContainerEACL}, - supportedS3NativeActionGetBucketACL: {native.MethodGetContainerEACL}, +var actionToNativeOpMap = map[string][]string{ + s3ActionAbortMultipartUpload: {native.MethodGetContainer, native.MethodDeleteObject, native.MethodHeadObject}, + s3ActionCreateBucket: {native.MethodGetContainer, native.MethodPutContainer, native.MethodSetContainerEACL}, + s3ActionDeleteBucket: {native.MethodGetContainer, native.MethodDeleteContainer, native.MethodSearchObject, native.MethodHeadObject}, + s3ActionDeleteBucketPolicy: {native.MethodGetContainer}, + s3ActionDeleteObject: {native.MethodGetContainer, native.MethodDeleteObject, native.MethodHeadObject}, + s3ActionDeleteObjectTagging: {native.MethodGetContainer, native.MethodHeadObject}, + s3ActionDeleteObjectVersion: {native.MethodGetContainer, native.MethodDeleteObject, native.MethodHeadObject}, + s3ActionDeleteObjectVersionTagging: {native.MethodGetContainer, native.MethodDeleteObject, native.MethodHeadObject}, + s3ActionGetBucketACL: {native.MethodGetContainer, native.MethodGetContainerEACL}, + s3ActionGetBucketCORS: {native.MethodGetContainer, native.MethodGetObject, native.MethodHeadObject}, + s3ActionGetBucketLocation: {native.MethodGetContainer}, + s3ActionGetBucketNotification: {native.MethodGetContainer, native.MethodGetObject, native.MethodHeadObject}, + s3ActionGetBucketObjectLockConfiguration: {native.MethodGetContainer}, + s3ActionGetBucketPolicy: {native.MethodGetContainer}, + s3ActionGetBucketPolicyStatus: {native.MethodGetContainer}, + s3ActionGetBucketTagging: {native.MethodGetContainer}, + s3ActionGetBucketVersioning: {native.MethodGetContainer}, + s3ActionGetLifecycleConfiguration: { /*not implemented yet*/ }, + s3ActionGetObject: {native.MethodGetContainer, native.MethodGetObject, native.MethodHeadObject, native.MethodSearchObject, native.MethodRangeObject, native.MethodHashObject}, + s3ActionGetObjectACL: {native.MethodGetContainer, native.MethodGetContainerEACL, native.MethodGetObject, native.MethodHeadObject}, + s3ActionGetObjectAttributes: {native.MethodGetContainer, native.MethodGetObject, native.MethodHeadObject}, + s3ActionGetObjectLegalHold: {native.MethodGetContainer, native.MethodHeadObject}, + s3ActionGetObjectRetention: {native.MethodGetContainer, native.MethodHeadObject}, + s3ActionGetObjectTagging: {native.MethodGetContainer, native.MethodHeadObject}, + s3ActionGetObjectVersion: {native.MethodGetContainer, native.MethodGetObject, native.MethodHeadObject, native.MethodSearchObject, native.MethodRangeObject, native.MethodHashObject}, + s3ActionGetObjectVersionACL: {native.MethodGetContainer, native.MethodGetContainerEACL, native.MethodGetObject, native.MethodHeadObject}, + s3ActionGetObjectVersionAttributes: {native.MethodGetContainer, native.MethodGetObject, native.MethodHeadObject}, + s3ActionGetObjectVersionTagging: {native.MethodGetContainer, native.MethodHeadObject}, + s3ActionListAllMyBuckets: {native.MethodListContainers, native.MethodGetContainer}, + s3ActionListBucket: {native.MethodGetContainer, native.MethodGetObject, native.MethodHeadObject, native.MethodSearchObject, native.MethodRangeObject, native.MethodHashObject}, + s3ActionListBucketMultipartUploads: {native.MethodGetContainer}, + s3ActionListBucketVersions: {native.MethodGetContainer, native.MethodGetObject, native.MethodHeadObject, native.MethodSearchObject, native.MethodRangeObject, native.MethodHashObject}, + s3ActionListMultipartUploadParts: {native.MethodGetContainer}, + s3ActionPutBucketACL: {native.MethodGetContainer, native.MethodSetContainerEACL}, + s3ActionPutBucketCORS: {native.MethodGetContainer}, + s3ActionPutBucketNotification: {native.MethodGetContainer, native.MethodHeadObject, native.MethodDeleteObject, native.MethodHeadObject}, + s3ActionPutBucketObjectLockConfiguration: {native.MethodGetContainer}, + s3ActionPutBucketPolicy: {native.MethodGetContainer}, + s3ActionPutBucketTagging: {native.MethodGetContainer}, + s3ActionPutBucketVersioning: {native.MethodGetContainer}, + s3ActionPutLifecycleConfiguration: { /*not implemented yet*/ }, + s3ActionPutObject: {native.MethodGetContainer, native.MethodPutObject}, + s3ActionPutObjectACL: {native.MethodGetContainer, native.MethodGetContainerEACL, native.MethodSetContainerEACL, native.MethodGetObject, native.MethodHeadObject}, + s3ActionPutObjectLegalHold: {native.MethodGetContainer, native.MethodHeadObject}, + s3ActionPutObjectRetention: {native.MethodGetContainer, native.MethodHeadObject}, + s3ActionPutObjectTagging: {native.MethodGetContainer, native.MethodHeadObject}, + s3ActionPutObjectVersionACL: {native.MethodGetContainer, native.MethodGetContainerEACL, native.MethodSetContainerEACL, native.MethodGetObject, native.MethodHeadObject}, + s3ActionPutObjectVersionTagging: {native.MethodGetContainer, native.MethodHeadObject}, } var containerNativeOperations = map[string]struct{}{ @@ -43,20 +80,6 @@ var objectNativeOperations = map[string]struct{}{ native.MethodHashObject: {}, } -const ( - supportedS3NativeActionDeleteObject = "s3:DeleteObject" - supportedS3NativeActionGetObject = "s3:GetObject" - supportedS3NativeActionHeadObject = "s3:HeadObject" - supportedS3NativeActionPutObject = "s3:PutObject" - supportedS3NativeActionListBucket = "s3:ListBucket" - - supportedS3NativeActionCreateBucket = "s3:CreateBucket" - supportedS3NativeActionDeleteBucket = "s3:DeleteBucket" - supportedS3NativeActionListAllMyBucket = "s3:ListAllMyBuckets" - supportedS3NativeActionPutBucketACL = "s3:PutBucketAcl" - supportedS3NativeActionGetBucketACL = "s3:GetBucketAcl" -) - type NativeResolver interface { GetUserKey(account, name string) (string, error) GetBucketInfo(bucket string) (*BucketInfo, error) @@ -76,6 +99,11 @@ func ConvertToNativeChain(p Policy, resolver NativeResolver) (*chain.Chain, erro for _, statement := range p.Statement { status := formStatus(statement) + 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. + continue + } action, actionInverted := statement.action() nativeActions, err := formNativeActionNames(action) @@ -146,8 +174,8 @@ func getActionTypes(nativeActions []string) ActionTypes { _, isObj := objectNativeOperations[action] _, isCnr := containerNativeOperations[action] - res.Object = isObj || action == Wildcard - res.Container = isCnr || action == Wildcard + res.Object = res.Object || isObj || action == Wildcard + res.Container = res.Container || isCnr || action == Wildcard } return res @@ -222,7 +250,7 @@ func formNativeResourceNamesAndConditions(names []string, resolver NativeResolve res := make([]GroupedResources, 0, len(names)) - var combined []string + combined := make(map[string]struct{}) for _, resource := range names { if err := validateResource(resource); err != nil { @@ -261,16 +289,20 @@ func formNativeResourceNamesAndConditions(names []string, resolver NativeResolve } 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)) + combined[fmt.Sprintf(native.ResourceFormatNamespaceContainerObjects, bktInfo.Namespace, bktInfo.Container)] = struct{}{} + combined[fmt.Sprintf(native.ResourceFormatNamespaceContainer, bktInfo.Namespace, bktInfo.Container)] = struct{}{} continue } if obj == "" && actionTypes.Container { // this corresponds to arn:aws:s3:::BUCKET - combined = append(combined, fmt.Sprintf(native.ResourceFormatNamespaceContainer, bktInfo.Namespace, bktInfo.Container)) + combined[fmt.Sprintf(native.ResourceFormatNamespaceContainer, bktInfo.Namespace, bktInfo.Container)] = struct{}{} continue } res = append(res, GroupedResources{ - Names: []string{fmt.Sprintf(native.ResourceFormatNamespaceContainerObjects, bktInfo.Namespace, bktInfo.Container)}, + Names: []string{ + fmt.Sprintf(native.ResourceFormatNamespaceContainerObjects, bktInfo.Namespace, bktInfo.Container), + fmt.Sprintf(native.ResourceFormatNamespaceContainer, bktInfo.Namespace, bktInfo.Container), + }, Conditions: []chain.Condition{ { Op: chain.CondStringLike, @@ -283,7 +315,12 @@ func formNativeResourceNamesAndConditions(names []string, resolver NativeResolve } if len(combined) != 0 { - res = append(res, GroupedResources{Names: combined}) + gr := GroupedResources{Names: make([]string, 0, len(combined))} + for key := range combined { + gr.Names = append(gr.Names, key) + } + + res = append(res, gr) } return res, nil @@ -329,26 +366,39 @@ func formPrincipalKey(principal string, resolver NativeResolver) (string, error) } func formNativeActionNames(names []string) ([]string, error) { - res := make([]string, 0, len(names)) + uniqueActions := make(map[string]struct{}, len(names)) for _, action := range names { - if err := validateAction(action); err != nil { - return nil, err - } - if action == Wildcard { return []string{Wildcard}, nil } - if !strings.HasPrefix(action, s3ActionPrefix) { + isIAM, err := validateAction(action) + if err != nil { + return nil, err + } + + if isIAM { continue } - if strings.TrimPrefix(action, s3ActionPrefix) == Wildcard { + if action[len(s3ActionPrefix):] == Wildcard { return []string{Wildcard}, nil } - res = append(res, supportedActionToNativeOpMap[action]...) + nativeActions := actionToNativeOpMap[action] + if len(nativeActions) == 0 { + return nil, ErrActionsNotApplicable + } + + for _, nativeAction := range nativeActions { + uniqueActions[nativeAction] = struct{}{} + } + } + + res := make([]string, 0, len(uniqueActions)) + for key := range uniqueActions { + res = append(res, key) } return res, nil diff --git a/iam/converter_s3.go b/iam/converter_s3.go index 2874c34..add55b3 100644 --- a/iam/converter_s3.go +++ b/iam/converter_s3.go @@ -7,76 +7,62 @@ 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"}, +var actionToS3OpMap = map[string][]string{ + s3ActionAbortMultipartUpload: {s3ActionAbortMultipartUpload}, + s3ActionCreateBucket: {s3ActionCreateBucket}, + s3ActionDeleteBucket: {s3ActionDeleteBucket}, + s3ActionDeleteBucketPolicy: {s3ActionDeleteBucketPolicy}, + s3ActionDeleteObjectTagging: {s3ActionDeleteObjectTagging}, + s3ActionGetBucketLocation: {s3ActionGetBucketLocation}, + s3ActionGetBucketNotification: {s3ActionGetBucketNotification}, + s3ActionGetBucketPolicy: {s3ActionGetBucketPolicy}, + s3ActionGetBucketPolicyStatus: {s3ActionGetBucketPolicyStatus}, + s3ActionGetBucketTagging: {s3ActionGetBucketTagging}, + s3ActionGetBucketVersioning: {s3ActionGetBucketVersioning}, + s3ActionGetObjectAttributes: {s3ActionGetObjectAttributes}, + s3ActionGetObjectLegalHold: {s3ActionGetObjectLegalHold}, + s3ActionGetObjectRetention: {s3ActionGetObjectRetention}, + s3ActionGetObjectTagging: {s3ActionGetObjectTagging}, + s3ActionPutBucketNotification: {s3ActionPutBucketNotification}, + s3ActionPutBucketPolicy: {s3ActionPutBucketPolicy}, + s3ActionPutBucketVersioning: {s3ActionPutBucketVersioning}, + s3ActionPutObjectLegalHold: {s3ActionPutObjectLegalHold}, + s3ActionPutObjectRetention: {s3ActionPutObjectRetention}, + s3ActionPutObjectTagging: {s3ActionPutObjectTagging}, - 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: { + s3ActionListAllMyBuckets: {"s3:ListBuckets"}, + s3ActionListBucket: {"s3:HeadBucket", "s3:GetBucketLocation", "s3:ListObjectsV1", "s3:ListObjectsV2"}, + s3ActionListBucketVersions: {"s3:ListBucketObjectVersions"}, + s3ActionListBucketMultipartUploads: {"s3:ListMultipartUploads"}, + s3ActionGetBucketObjectLockConfiguration: {"s3:GetBucketObjectLockConfig"}, + s3ActionGetLifecycleConfiguration: {"s3:GetBucketLifecycle"}, + s3ActionGetBucketACL: {"s3:GetBucketACL"}, + s3ActionGetBucketCORS: {"s3:GetBucketCors"}, + s3ActionPutBucketTagging: {"s3:PutBucketTagging", "s3:DeleteBucketTagging"}, + s3ActionPutBucketObjectLockConfiguration: {"s3:PutBucketObjectLockConfig"}, + s3ActionPutLifecycleConfiguration: {"s3:PutBucketLifecycle", "s3:DeleteBucketLifecycle"}, + s3ActionPutBucketACL: {"s3:PutBucketACL"}, + s3ActionPutBucketCORS: {"s3:PutBucketCors", "s3:DeleteBucketCors"}, + + s3ActionListMultipartUploadParts: {"s3:ListParts"}, + s3ActionGetObjectACL: {"s3:GetObjectACL"}, + s3ActionGetObject: {"s3:GetObject", "s3:HeadObject"}, + s3ActionGetObjectVersion: {"s3:GetObject", "s3:HeadObject"}, + s3ActionGetObjectVersionACL: {"s3:GetObjectACL"}, + s3ActionGetObjectVersionAttributes: {"s3:GetObjectAttributes"}, + s3ActionGetObjectVersionTagging: {"s3:GetObjectTagging"}, + s3ActionPutObjectACL: {"s3:PutObjectACL"}, + s3ActionPutObjectVersionACL: {"s3:PutObjectACL"}, + s3ActionPutObjectVersionTagging: {"s3:PutObjectTagging"}, + s3ActionPutObject: { "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"}, + s3ActionDeleteObjectVersionTagging: {"s3:DeleteObjectTagging"}, + s3ActionDeleteObject: {"s3:DeleteObject", "s3:DeleteMultipleObjects"}, + s3ActionDeleteObjectVersion: {"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) } @@ -233,22 +219,41 @@ func validateS3ResourceNames(names []string) error { } func formS3ActionNames(names []string) ([]string, error) { - res := make([]string, 0, len(names)) + uniqueActions := make(map[string]struct{}, 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) + isIAM, err := validateAction(action) + if err != nil { + return nil, err } + + if isIAM { + uniqueActions[action] = struct{}{} + continue + } + + if action[len(s3ActionPrefix):] == Wildcard { + uniqueActions[action] = struct{}{} + continue + } + + s3Actions := actionToS3OpMap[action] + if len(s3Actions) == 0 { + return nil, ErrActionsNotApplicable + } + + for _, s3Action := range s3Actions { + uniqueActions[s3Action] = struct{}{} + } + } + + res := make([]string, 0, len(uniqueActions)) + for key := range uniqueActions { + res = append(res, key) } return res, nil diff --git a/iam/converter_test.go b/iam/converter_test.go index 350e218..97e1cd7 100644 --- a/iam/converter_test.go +++ b/iam/converter_test.go @@ -118,7 +118,7 @@ func TestConverters(t *testing.T) { s3Chain, err := ConvertToS3Chain(p, mockResolver) require.NoError(t, err) - require.Equal(t, expected, s3Chain) + assertChainsEqual(t, expected, s3Chain) }) t.Run("valid native policy", func(t *testing.T) { @@ -136,9 +136,12 @@ func TestConverters(t *testing.T) { expected := &chain.Chain{Rules: []chain.Rule{ { - Status: chain.Allow, - Actions: chain.Actions{Names: []string{native.MethodPutObject}}, - Resources: chain.Resources{Names: []string{fmt.Sprintf(native.ResourceFormatNamespaceContainerObjects, namespace, mockResolver.containers[bktName])}}, + Status: chain.Allow, + Actions: chain.Actions{Names: []string{native.MethodGetContainer, native.MethodPutObject}}, + Resources: chain.Resources{Names: []string{ + fmt.Sprintf(native.ResourceFormatNamespaceContainerObjects, namespace, mockResolver.containers[bktName]), + fmt.Sprintf(native.ResourceFormatNamespaceContainer, namespace, mockResolver.containers[bktName])}, + }, Condition: []chain.Condition{ { Op: chain.CondStringEquals, @@ -152,7 +155,7 @@ func TestConverters(t *testing.T) { nativeChain, err := ConvertToNativeChain(p, mockResolver) require.NoError(t, err) - require.Equal(t, expected, nativeChain) + assertChainsEqual(t, expected, nativeChain) }) t.Run("valid inverted policy", func(t *testing.T) { @@ -186,7 +189,7 @@ func TestConverters(t *testing.T) { s3Chain, err := ConvertToS3Chain(p, mockResolver) require.NoError(t, err) - require.Equal(t, expected, s3Chain) + assertChainsEqual(t, expected, s3Chain) }) t.Run("valid native policy map action", func(t *testing.T) { @@ -196,7 +199,7 @@ func TestConverters(t *testing.T) { Principal: map[PrincipalType][]string{ AWSPrincipalType: {principal}, }, - Effect: DenyEffect, + Effect: AllowEffect, Action: []string{"s3:DeleteObject", "s3:DeleteBucket"}, Resource: []string{ fmt.Sprintf(s3.ResourceFormatS3BucketObject, bktName, objName), @@ -207,10 +210,11 @@ func TestConverters(t *testing.T) { expected := &chain.Chain{Rules: []chain.Rule{ { - Status: chain.AccessDenied, - Actions: chain.Actions{Names: []string{native.MethodDeleteObject, native.MethodHeadObject, native.MethodDeleteContainer}}, + Status: chain.Allow, + Actions: chain.Actions{Names: []string{native.MethodGetContainer, native.MethodDeleteContainer, native.MethodSearchObject, native.MethodHeadObject, native.MethodDeleteObject}}, Resources: chain.Resources{Names: []string{ fmt.Sprintf(native.ResourceFormatNamespaceContainerObjects, namespace, mockResolver.containers[bktName]), + fmt.Sprintf(native.ResourceFormatNamespaceContainer, namespace, mockResolver.containers[bktName]), }}, Condition: []chain.Condition{ { @@ -228,8 +232,8 @@ func TestConverters(t *testing.T) { }, }, { - Status: chain.AccessDenied, - Actions: chain.Actions{Names: []string{native.MethodDeleteObject, native.MethodHeadObject, native.MethodDeleteContainer}}, + Status: chain.Allow, + Actions: chain.Actions{Names: []string{native.MethodGetContainer, native.MethodDeleteContainer, native.MethodSearchObject, native.MethodHeadObject, native.MethodDeleteObject}}, Resources: chain.Resources{Names: []string{ fmt.Sprintf(native.ResourceFormatNamespaceContainer, namespace, mockResolver.containers[bktName]), }}, @@ -244,7 +248,7 @@ func TestConverters(t *testing.T) { nativeChain, err := ConvertToNativeChain(p, mockResolver) require.NoError(t, err) - require.Equal(t, expected, nativeChain) + assertChainsEqual(t, expected, nativeChain) }) t.Run("invalid policy (unsupported principal type)", func(t *testing.T) { @@ -338,12 +342,12 @@ func TestConverters(t *testing.T) { s3Chain, err := ConvertToS3Chain(p, mockResolver) require.NoError(t, err) - require.Equal(t, s3Expected, s3Chain) + assertChainsEqual(t, s3Expected, s3Chain) nativeExpected := &chain.Chain{Rules: []chain.Rule{{ Status: chain.Allow, - Actions: chain.Actions{Names: []string{native.MethodDeleteObject, native.MethodHeadObject}}, - Resources: chain.Resources{Names: []string{native.ResourceFormatAllObjects}}, + Actions: chain.Actions{Names: []string{native.MethodGetContainer, native.MethodDeleteObject, native.MethodHeadObject}}, + Resources: chain.Resources{Names: []string{native.ResourceFormatAllObjects, native.ResourceFormatAllContainers}}, Condition: []chain.Condition{{ Op: chain.CondStringEquals, Object: chain.ObjectRequest, @@ -354,7 +358,7 @@ func TestConverters(t *testing.T) { nativeChain, err := ConvertToNativeChain(p, mockResolver) require.NoError(t, err) - require.Equal(t, nativeExpected, nativeChain) + assertChainsEqual(t, nativeExpected, nativeChain) }) } @@ -624,8 +628,11 @@ func TestComplexNativeConditions(t *testing.T) { mockResolver := newMockUserResolver([]string{user1, user2}, []string{bktName1, bktName2, bktName3}, "") nativeResource1 := fmt.Sprintf(native.ResourceFormatRootContainerObjects, mockResolver.containers[bktName1]) + nativeResource1cnr := fmt.Sprintf(native.ResourceFormatRootContainer, mockResolver.containers[bktName1]) nativeResource2 := fmt.Sprintf(native.ResourceFormatRootContainerObjects, mockResolver.containers[bktName2]) + nativeResource2cnr := fmt.Sprintf(native.ResourceFormatRootContainer, mockResolver.containers[bktName2]) nativeResource3 := fmt.Sprintf(native.ResourceFormatRootContainerObjects, mockResolver.containers[bktName3]) + nativeResource3cnr := fmt.Sprintf(native.ResourceFormatRootContainer, mockResolver.containers[bktName3]) p := Policy{ Version: "2012-10-17", @@ -633,7 +640,7 @@ func TestComplexNativeConditions(t *testing.T) { Principal: map[PrincipalType][]string{ AWSPrincipalType: {principal1, principal2}, }, - Effect: DenyEffect, + Effect: AllowEffect, Action: []string{"s3:" + action}, Resource: []string{"arn:aws:s3:::" + resource1, "arn:aws:s3:::" + resource2, "arn:aws:s3:::" + resource3}, Conditions: map[string]Condition{ @@ -643,10 +650,10 @@ func TestComplexNativeConditions(t *testing.T) { }}, } - expectedStatus := chain.AccessDenied - expectedActions := chain.Actions{Names: supportedActionToNativeOpMap["s3:"+action]} - expectedResource1 := chain.Resources{Names: []string{nativeResource1}} - expectedResource23 := chain.Resources{Names: []string{nativeResource2, nativeResource3}} + expectedStatus := chain.Allow + expectedActions := chain.Actions{Names: actionToNativeOpMap["s3:"+action]} + expectedResource1 := chain.Resources{Names: []string{nativeResource1, nativeResource1cnr}} + expectedResource23 := chain.Resources{Names: []string{nativeResource2, nativeResource2cnr, nativeResource3, nativeResource3cnr}} user1Condition := chain.Condition{Op: chain.CondStringEquals, Object: chain.ObjectRequest, Key: native.PropertyKeyActorPublicKey, Value: mockResolver.users[user1]} user2Condition := chain.Condition{Op: chain.CondStringEquals, Object: chain.ObjectRequest, Key: native.PropertyKeyActorPublicKey, Value: mockResolver.users[user2]} @@ -770,7 +777,7 @@ func TestComplexNativeConditions(t *testing.T) { key1: val0, key2: val2, }, - status: chain.AccessDenied, + status: chain.Allow, }, { name: "bucket resource3, all conditions matched", @@ -784,7 +791,7 @@ func TestComplexNativeConditions(t *testing.T) { key1: val0, key2: val2, }, - status: chain.AccessDenied, + status: chain.Allow, }, { name: "bucket resource, user condition mismatched", @@ -838,7 +845,7 @@ func TestComplexNativeConditions(t *testing.T) { key1: val0, key2: val2, }, - status: chain.AccessDenied, + status: chain.Allow, }, { name: "bucket/object resource, user condition mismatched", @@ -1164,7 +1171,7 @@ func TestS3BucketResource(t *testing.T) { { Principal: map[PrincipalType][]string{Wildcard: nil}, Effect: DenyEffect, - Action: []string{"s3:HeadBucket"}, + Action: []string{"s3:ListBucket"}, Resource: []string{fmt.Sprintf(s3.ResourceFormatS3Bucket, bktName1)}, }, { @@ -1203,7 +1210,7 @@ func TestS3BucketResource(t *testing.T) { } func TestWildcardConverters(t *testing.T) { - policy := `{"Version":"2012-10-17","Statement":{"Effect":"Allow", "Principal": "*", "Action":"*","Resource":"*"}}` + policy := `{"Version":"2012-10-17","Statement":{"Effect":"Allow", "Principal": "*", "Action":"s3:*","Resource":"*"}}` var p Policy err := json.Unmarshal([]byte(policy), &p) @@ -1212,7 +1219,7 @@ func TestWildcardConverters(t *testing.T) { s3Expected := &chain.Chain{ Rules: []chain.Rule{{ Status: chain.Allow, - Actions: chain.Actions{Names: []string{Wildcard}}, + Actions: chain.Actions{Names: []string{"s3:*"}}, Resources: chain.Resources{Names: []string{Wildcard}}, }}, } @@ -1234,50 +1241,176 @@ func TestWildcardConverters(t *testing.T) { require.Equal(t, nativeExpected, nativeChain) } -func TestActionParsing(t *testing.T) { - for _, tc := range []struct { - action string - err bool - }{ - { - action: "withoutPrefix", - err: true, - }, - { - action: "s3:*Object", - err: true, - }, - { - action: "*", - }, - { - action: "s3:PutObject", - }, - { - action: "s3:Put*", - }, - { - action: "s3:*", - }, - { - action: "s3:", - }, - { - action: "iam:ListAccessKeys", - }, - { - action: "iam:*", - }, - } { - t.Run("", func(t *testing.T) { - err := validateAction(tc.action) - if tc.err { - require.Error(t, err) - } else { - require.NoError(t, err) - } - }) +func TestWildcardObjectsConverters(t *testing.T) { + policy := `{"Version":"2012-10-17","Statement":{"Effect":"Allow", "Principal": "*", "Action":"s3:*","Resource":"arn:aws:s3:::bucket/*"}}` + + var p Policy + err := json.Unmarshal([]byte(policy), &p) + require.NoError(t, err) + + s3Expected := &chain.Chain{ + Rules: []chain.Rule{{ + Status: chain.Allow, + Actions: chain.Actions{Names: []string{"s3:*"}}, + Resources: chain.Resources{Names: []string{"arn:aws:s3:::bucket/*"}}, + }}, } + + s3Chain, err := ConvertToS3Chain(p, newMockUserResolver(nil, nil, "")) + require.NoError(t, err) + require.Equal(t, s3Expected, s3Chain) + + mockResolver := newMockUserResolver(nil, []string{"bucket"}, "") + + nativeExpected := &chain.Chain{ + Rules: []chain.Rule{{ + Status: chain.Allow, + Actions: chain.Actions{Names: []string{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) + require.NoError(t, err) + assertChainsEqual(t, nativeExpected, nativeChain) +} + +func TestDisableNativeDeny(t *testing.T) { + policy := ` +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Deny", + "Principal": "*", + "Action": "s3:*", + "Resource": [ "arn:aws:s3:::test-bucket/*" ] + } + ] +} +` + var p Policy + err := json.Unmarshal([]byte(policy), &p) + require.NoError(t, err) + + _, err = ConvertToNativeChain(p, newMockUserResolver(nil, nil, "")) + require.ErrorIs(t, err, ErrActionsNotApplicable) +} + +func TestFromActions(t *testing.T) { + t.Run("s3 actions", func(t *testing.T) { + for _, tc := range []struct { + action string + res []string + err bool + }{ + { + action: "withoutPrefix", + err: true, + }, + { + action: "s3:*Object", + err: true, + }, + { + action: "*", + res: []string{Wildcard}, + }, + { + action: "s3:PutObject", + res: []string{"s3:PutObject", "s3:PostObject", "s3:CopyObject", + "s3:UploadPart", "s3:UploadPartCopy", "s3:CreateMultipartUpload", "s3:CompleteMultipartUpload"}, + }, + { + action: "s3:Put*", + err: true, + }, + { + action: "s3:*", + res: []string{"s3:*"}, + }, + { + action: "s3:", + err: true, + }, + { + action: "iam:ListAccessKeys", + res: []string{"iam:ListAccessKeys"}, + }, + { + action: "iam:*", + res: []string{"iam:*"}, + }, + } { + t.Run("", func(t *testing.T) { + actions, err := formS3ActionNames([]string{tc.action}) + if tc.err { + require.Error(t, err) + } else { + require.NoError(t, err) + require.ElementsMatch(t, tc.res, actions) + } + }) + } + }) + + t.Run("native actions", func(t *testing.T) { + for _, tc := range []struct { + action string + res []string + err bool + }{ + { + action: "withoutPrefix", + err: true, + }, + { + action: "s3:*Object", + err: true, + }, + { + action: "*", + res: []string{Wildcard}, + }, + { + action: "s3:PutObject", + res: []string{native.MethodGetContainer, native.MethodPutObject}, + }, + { + action: "s3:Put*", + err: true, + }, + { + action: "s3:*", + res: []string{Wildcard}, + }, + { + action: "s3:", + err: true, + }, + { + action: "iam:ListAccessKeys", + res: []string{}, + }, + { + action: "iam:*", + res: []string{}, + }, + } { + t.Run("", func(t *testing.T) { + actions, err := formNativeActionNames([]string{tc.action}) + if tc.err { + require.Error(t, err) + } else { + require.NoError(t, err) + require.ElementsMatch(t, tc.res, actions) + } + }) + } + }) } func TestPrincipalParsing(t *testing.T) { @@ -1397,19 +1530,39 @@ func areRulesMatched(rule1, rule2 chain.Rule) bool { return false } - for i, name := range rule1.Resources.Names { - if name != rule2.Resources.Names[i] { - return false - } - } - - for i, name := range rule1.Actions.Names { - if name != rule2.Actions.Names[i] { - return false - } - } - seen := make(map[int]struct{}) + for _, name1 := range rule1.Resources.Names { + for j, name2 := range rule2.Resources.Names { + if _, ok := seen[j]; ok { + continue + } + if name1 == name2 { + seen[j] = struct{}{} + break + } + } + } + if len(seen) != len(rule1.Resources.Names) { + return false + } + + seen = make(map[int]struct{}) + for _, name1 := range rule1.Actions.Names { + for j, name2 := range rule2.Actions.Names { + if _, ok := seen[j]; ok { + continue + } + if name1 == name2 { + seen[j] = struct{}{} + break + } + } + } + if len(seen) != len(rule1.Actions.Names) { + return false + } + + seen = make(map[int]struct{}) for _, cond1 := range rule1.Condition { for j, cond2 := range rule2.Condition { if _, ok := seen[j]; ok { @@ -1424,3 +1577,19 @@ func areRulesMatched(rule1, rule2 chain.Rule) bool { return len(seen) == len(rule1.Condition) } + +func assertChainsEqual(t *testing.T, chain1, chain2 *chain.Chain) { + require.Equal(t, string(chain1.ID), string(chain2.ID)) + require.Equal(t, chain1.MatchType, chain2.MatchType) + require.Equal(t, len(chain1.Rules), len(chain2.Rules)) + + for i, rule := range chain1.Rules { + require.Equal(t, rule.Any, chain2.Rules[i].Any) + require.Equal(t, rule.Resources.Inverted, chain2.Rules[i].Resources.Inverted) + require.ElementsMatch(t, rule.Resources.Names, chain2.Rules[i].Resources.Names) + require.Equal(t, rule.Status, chain2.Rules[i].Status) + require.ElementsMatch(t, rule.Condition, chain2.Rules[i].Condition) + require.Equal(t, rule.Actions.Inverted, chain2.Rules[i].Actions.Inverted) + require.ElementsMatch(t, rule.Actions.Names, chain2.Rules[i].Actions.Names) + } +} diff --git a/iam/policy_test.go b/iam/policy_test.go index ef66d68..79d93c6 100644 --- a/iam/policy_test.go +++ b/iam/policy_test.go @@ -2,14 +2,13 @@ package iam import ( "encoding/json" - "fmt" "testing" "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" + "git.frostfs.info/TrueCloudLab/policy-engine/schema/s3" "github.com/stretchr/testify/require" ) @@ -525,27 +524,27 @@ func TestProcessDenyFirst(t *testing.T) { mockResolver := newMockUserResolver([]string{"root/user-name"}, []string{"test-bucket"}, "") - identityNativePolicy, err := ConvertToNativeChain(identityPolicy, mockResolver) + identityNativePolicy, err := ConvertToS3Chain(identityPolicy, mockResolver) require.NoError(t, err) identityNativePolicy.MatchType = chain.MatchTypeFirstMatch - resourceNativePolicy, err := ConvertToNativeChain(resourcePolicy, mockResolver) + resourceNativePolicy, err := ConvertToS3Chain(resourcePolicy, mockResolver) require.NoError(t, err) s := inmemory.NewInMemory() target := engine.NamespaceTarget("ns") - _, _, err = s.MorphRuleChainStorage().AddMorphRuleChain(chain.Ingress, target, identityNativePolicy) + _, _, err = s.MorphRuleChainStorage().AddMorphRuleChain(chain.S3, target, identityNativePolicy) require.NoError(t, err) - _, _, err = s.MorphRuleChainStorage().AddMorphRuleChain(chain.Ingress, target, resourceNativePolicy) + _, _, err = s.MorphRuleChainStorage().AddMorphRuleChain(chain.S3, target, resourceNativePolicy) require.NoError(t, err) - 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"]}) + resource := testutil.NewResource("arn:aws:s3:::test-bucket/object", nil) + request := testutil.NewRequest("s3:PutObject", resource, map[string]string{s3.PropertyKeyOwner: mockResolver.users["root/user-name"]}) - status, found, err := s.IsAllowed(chain.Ingress, engine.NewRequestTarget("ns", ""), request) + status, found, err := s.IsAllowed(chain.S3, engine.NewRequestTarget("ns", ""), request) require.NoError(t, err) require.True(t, found) require.Equal(t, chain.AccessDenied, status)