package iam import ( "fmt" "strings" "git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain" "git.frostfs.info/TrueCloudLab/policy-engine/schema/native" ) const PropertyKeyFilePath = "FilePath" var supportedActionToNativeOpMap = map[string][]string{ supportedS3NativeActionDeleteObject: {native.MethodDeleteObject}, 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 containerNativeOperations = map[string]struct{}{ native.MethodPutContainer: {}, native.MethodDeleteContainer: {}, native.MethodGetContainer: {}, native.MethodListContainers: {}, native.MethodSetContainerEACL: {}, native.MethodGetContainerEACL: {}, } var objectNativeOperations = map[string]struct{}{ native.MethodGetObject: {}, native.MethodPutObject: {}, native.MethodHeadObject: {}, native.MethodDeleteObject: {}, native.MethodSearchObject: {}, native.MethodRangeObject: {}, native.MethodHashObject: {}, } const ( supportedS3NativeActionDeleteObject = "s3:DeleteObject" supportedS3NativeActionGetObject = "s3:GetObject" 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) } type BucketInfo struct { Namespace string Container string } func ConvertToNativeChain(p Policy, resolver NativeResolver) (*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) action, actionInverted := statement.action() nativeActions, err := formNativeActionNames(action) if err != nil { return nil, err } ruleAction := chain.Actions{Inverted: actionInverted, Names: nativeActions} if len(ruleAction.Names) == 0 { continue } resource, resourceInverted := statement.resource() groupedResources, err := formNativeResourceNamesAndConditions(resource, resolver, getActionTypes(nativeActions)) if err != nil { return nil, err } groupedConditions, err := convertToNativeChainCondition(statement.Conditions, resolver) if err != nil { return nil, err } splitConditions := splitGroupedConditions(groupedConditions) principals, principalCondFn, err := getNativePrincipalsAndConditionFunc(statement, resolver) if err != nil { return nil, err } for _, groupedResource := range groupedResources { for _, principal := range principals { for _, conditions := range splitConditions { var principalCondition []chain.Condition if principal != Wildcard { principalCondition = []chain.Condition{principalCondFn(principal)} } ruleConditions := append(principalCondition, groupedResource.Conditions...) r := chain.Rule{ Status: status, Actions: ruleAction, Resources: chain.Resources{ Inverted: resourceInverted, Names: groupedResource.Names, }, Condition: append(ruleConditions, conditions...), } engineChain.Rules = append(engineChain.Rules, r) } } } } if len(engineChain.Rules) == 0 { return nil, ErrActionsNotApplicable } return &engineChain, nil } func getActionTypes(nativeActions []string) ActionTypes { var res ActionTypes for _, action := range nativeActions { if res.Object && res.Container { break } _, isObj := objectNativeOperations[action] _, isCnr := containerNativeOperations[action] res.Object = isObj || action == Wildcard res.Container = isCnr || action == Wildcard } return res } func getNativePrincipalsAndConditionFunc(statement Statement, resolver NativeResolver) ([]string, formPrincipalConditionFunc, error) { var principals []string var op chain.ConditionType 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 := formNativePrincipal(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, Object: chain.ObjectRequest, Key: native.PropertyKeyActorPublicKey, Value: principal, } }, nil } func convertToNativeChainCondition(c Conditions, resolver NativeResolver) ([]GroupedConditions, error) { return convertToChainConditions(c, func(gr GroupedConditions) (GroupedConditions, error) { for i := range gr.Conditions { if gr.Conditions[i].Key == condKeyAWSPrincipalARN { gr.Conditions[i].Key = native.PropertyKeyActorPublicKey val, err := formPrincipalKey(gr.Conditions[i].Value, resolver) if err != nil { return GroupedConditions{}, err } gr.Conditions[i].Value = val } } return gr, nil }) } type GroupedResources struct { Names []string Conditions []chain.Condition } type ActionTypes struct { Object bool Container bool } func formNativeResourceNamesAndConditions(names []string, resolver NativeResolver, actionTypes ActionTypes) ([]GroupedResources, error) { if !actionTypes.Object && !actionTypes.Container { return nil, ErrActionsNotApplicable } res := make([]GroupedResources, 0, len(names)) var combined []string for _, resource := range names { if err := validateResource(resource); err != nil { return nil, err } if resource == Wildcard { res = res[:0] return append(res, formWildcardNativeResource(actionTypes)), nil } if !strings.HasPrefix(resource, s3ResourcePrefix) { continue } var bkt, obj string s3Resource := strings.TrimPrefix(resource, s3ResourcePrefix) if s3Resource == Wildcard { res = res[:0] return append(res, formWildcardNativeResource(actionTypes)), nil } if sepIndex := strings.Index(s3Resource, "/"); sepIndex < 0 { bkt = s3Resource } else { bkt = s3Resource[:sepIndex] obj = s3Resource[sepIndex+1:] if len(obj) == 0 { obj = Wildcard } } bktInfo, err := resolver.GetBucketInfo(bkt) if err != nil { return nil, err } if obj == Wildcard && actionTypes.Object { // this corresponds to arn:aws:s3:::BUCKET/ or arn:aws:s3:::BUCKET/* combined = append(combined, fmt.Sprintf(native.ResourceFormatNamespaceContainerObjects, bktInfo.Namespace, bktInfo.Container)) continue } if obj == "" && actionTypes.Container { // this corresponds to arn:aws:s3:::BUCKET combined = append(combined, fmt.Sprintf(native.ResourceFormatNamespaceContainer, bktInfo.Namespace, bktInfo.Container)) continue } res = append(res, GroupedResources{ Names: []string{fmt.Sprintf(native.ResourceFormatNamespaceContainerObjects, bktInfo.Namespace, bktInfo.Container)}, Conditions: []chain.Condition{ { Op: chain.CondStringLike, Object: chain.ObjectResource, Key: PropertyKeyFilePath, Value: obj, }, }, }) } if len(combined) != 0 { res = append(res, GroupedResources{Names: combined}) } return res, nil } func formWildcardNativeResource(actionTypes ActionTypes) GroupedResources { groupedNames := make([]string, 0, 2) if actionTypes.Object { groupedNames = append(groupedNames, native.ResourceFormatAllObjects) } if actionTypes.Container { groupedNames = append(groupedNames, native.ResourceFormatAllContainers) } return GroupedResources{Names: groupedNames} } func formNativePrincipal(principal []string, resolver NativeResolver) ([]string, error) { res := make([]string, len(principal)) var err error for i := range principal { if res[i], err = formPrincipalKey(principal[i], resolver); err != nil { return nil, err } } return res, nil } func formPrincipalKey(principal string, resolver NativeResolver) (string, error) { account, user, err := parsePrincipalAsIAMUser(principal) if err != nil { return "", err } key, err := resolver.GetUserKey(account, user) if err != nil { return "", fmt.Errorf("get user key: %w", err) } return key, nil } func formNativeActionNames(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 !strings.HasPrefix(action, s3ActionPrefix) { continue } if strings.TrimPrefix(action, s3ActionPrefix) == Wildcard { return []string{Wildcard}, nil } res = append(res, supportedActionToNativeOpMap[action]...) } return res, nil }