package iam import ( "fmt" "strings" "git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain" "git.frostfs.info/TrueCloudLab/policy-engine/schema/s3" ) const condKeyAWSMFAPresent = "aws:MultiFactorAuthPresent" 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}, 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", }, s3ActionDeleteObjectVersionTagging: {"s3:DeleteObjectTagging"}, s3ActionDeleteObject: {"s3:DeleteObject", "s3:DeleteMultipleObjects"}, s3ActionDeleteObjectVersion: {"s3:DeleteObject", "s3:DeleteMultipleObjects"}, } type S3Resolver interface { GetUserAddress(account, user string) (string, error) } func ConvertToS3Chain(p Policy, resolver S3Resolver) (*chain.Chain, error) { if err := p.Validate(ResourceBasedPolicyType); err != nil { return nil, err } var engineChain chain.Chain for _, statement := range p.Statement { status := formStatus(statement) actions, actionInverted := statement.action() s3Actions, err := formS3ActionNames(actions) if err != nil { return nil, err } ruleAction := chain.Actions{Inverted: actionInverted, Names: s3Actions} if len(ruleAction.Names) == 0 { continue } resources, resourceInverted := statement.resource() if err := validateS3ResourceNames(resources); err != nil { return nil, err } ruleResource := chain.Resources{Inverted: resourceInverted, Names: resources} groupedConditions, err := convertToS3ChainCondition(statement.Conditions, resolver) if err != nil { return nil, err } splitConditions := splitGroupedConditions(groupedConditions) principals, principalCondFn, err := getS3PrincipalsAndConditionFunc(statement, resolver) if err != nil { return nil, err } for _, principal := range principals { for _, conditions := range splitConditions { var principalCondition []chain.Condition if principal != Wildcard { principalCondition = []chain.Condition{principalCondFn(principal)} } r := chain.Rule{ Status: status, Actions: ruleAction, Resources: ruleResource, Condition: append(principalCondition, conditions...), } engineChain.Rules = append(engineChain.Rules, r) } } } if len(engineChain.Rules) == 0 { return nil, ErrActionsNotApplicable } return &engineChain, nil } func getS3PrincipalsAndConditionFunc(statement Statement, resolver S3Resolver) ([]string, formPrincipalConditionFunc, error) { var principals []string var op chain.ConditionType statementPrincipal, inverted := statement.principal() if _, ok := statementPrincipal[Wildcard]; ok { // this can be true only if 'inverted' false principals = []string{Wildcard} op = chain.CondStringLike } else { for principalType, principal := range statementPrincipal { if principalType != AWSPrincipalType { return nil, nil, fmt.Errorf("unsupported principal type '%s'", principalType) } parsedPrincipal, err := formS3Principal(principal, resolver) if err != nil { return nil, nil, fmt.Errorf("parse principal: %w", err) } principals = append(principals, parsedPrincipal...) } op = chain.CondStringEquals if inverted { op = chain.CondStringNotEquals } } return principals, func(principal string) chain.Condition { return chain.Condition{ Op: op, Kind: chain.KindRequest, Key: s3.PropertyKeyOwner, Value: principal, } }, nil } func convertToS3ChainCondition(c Conditions, resolver S3Resolver) ([]GroupedConditions, error) { return convertToChainConditions(c, func(gr GroupedConditions) (GroupedConditions, error) { for i := range gr.Conditions { switch { case gr.Conditions[i].Key == condKeyAWSPrincipalARN: gr.Conditions[i].Key = s3.PropertyKeyOwner val, err := formPrincipalOwner(gr.Conditions[i].Value, resolver) if err != nil { return GroupedConditions{}, err } gr.Conditions[i].Value = val case gr.Conditions[i].Key == condKeyAWSMFAPresent: gr.Conditions[i].Key = s3.PropertyKeyAccessBoxAttrMFA case strings.HasPrefix(gr.Conditions[i].Key, condKeyAWSResourceTagPrefix): gr.Conditions[i].Kind = chain.KindResource } } return gr, nil }) } func formS3Principal(principal []string, resolver S3Resolver) ([]string, error) { res := make([]string, len(principal)) var err error for i := range principal { if res[i], err = formPrincipalOwner(principal[i], resolver); err != nil { return nil, err } } return res, nil } func formPrincipalOwner(principal string, resolver S3Resolver) (string, error) { account, user, err := parsePrincipalAsIAMUser(principal) if err != nil { return "", err } address, err := resolver.GetUserAddress(account, user) if err != nil { return "", fmt.Errorf("get user address: %w", err) } return address, nil } func validateS3ResourceNames(names []string) error { for i := range names { if err := validateResource(names[i]); err != nil { return err } } return nil } func formS3ActionNames(names []string) ([]string, error) { uniqueActions := make(map[string]struct{}, len(names)) for _, action := range names { if action == Wildcard { return []string{Wildcard}, nil } 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 }