package iam import ( "errors" "fmt" "strings" "git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain" "git.frostfs.info/TrueCloudLab/policy-engine/schema/native" ) const PropertyKeyFilePath = "FilePath" var actionToNativeOpMap = map[string][]string{ s3ActionAbortMultipartUpload: {native.MethodGetContainer, native.MethodDeleteObject, native.MethodHeadObject, native.MethodGetObject, native.MethodPutObject}, s3ActionCreateBucket: {native.MethodGetContainer, native.MethodPutContainer, native.MethodSetContainerEACL, native.MethodGetObject, native.MethodPutObject}, s3ActionDeleteBucket: {native.MethodGetContainer, native.MethodDeleteContainer, native.MethodSearchObject, native.MethodHeadObject, native.MethodGetObject}, s3ActionDeleteBucketPolicy: {native.MethodGetContainer}, s3ActionDeleteObject: {native.MethodGetContainer, native.MethodDeleteObject, native.MethodPutObject, native.MethodHeadObject, native.MethodGetObject, native.MethodRangeObject}, s3ActionDeleteObjectTagging: {native.MethodGetContainer, native.MethodHeadObject, native.MethodGetObject, native.MethodPutObject}, s3ActionDeleteObjectVersion: {native.MethodGetContainer, native.MethodDeleteObject, native.MethodPutObject, native.MethodHeadObject, native.MethodGetObject, native.MethodRangeObject}, s3ActionDeleteObjectVersionTagging: {native.MethodGetContainer, native.MethodHeadObject, native.MethodGetObject, native.MethodPutObject}, s3ActionGetBucketACL: {native.MethodGetContainer, native.MethodGetContainerEACL, native.MethodGetObject}, s3ActionGetBucketCORS: {native.MethodGetContainer, native.MethodGetObject, native.MethodHeadObject}, s3ActionGetBucketLocation: {native.MethodGetContainer}, s3ActionGetBucketNotification: {native.MethodGetContainer, native.MethodGetObject, native.MethodHeadObject}, s3ActionGetBucketObjectLockConfiguration: {native.MethodGetContainer, native.MethodGetObject}, s3ActionGetBucketPolicy: {native.MethodGetContainer}, s3ActionGetBucketPolicyStatus: {native.MethodGetContainer}, s3ActionGetBucketTagging: {native.MethodGetContainer, native.MethodGetObject}, s3ActionGetBucketVersioning: {native.MethodGetContainer, native.MethodGetObject}, 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, native.MethodGetObject}, s3ActionGetObjectRetention: {native.MethodGetContainer, native.MethodHeadObject, native.MethodGetObject}, s3ActionGetObjectTagging: {native.MethodGetContainer, native.MethodHeadObject, native.MethodGetObject}, 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, native.MethodGetObject}, s3ActionListAllMyBuckets: {native.MethodListContainers, native.MethodGetContainer}, s3ActionListBucket: {native.MethodGetContainer, native.MethodGetObject, native.MethodHeadObject, native.MethodSearchObject, native.MethodRangeObject, native.MethodHashObject}, s3ActionListBucketMultipartUploads: {native.MethodGetContainer, native.MethodGetObject}, s3ActionListBucketVersions: {native.MethodGetContainer, native.MethodGetObject, native.MethodHeadObject, native.MethodSearchObject, native.MethodRangeObject, native.MethodHashObject}, s3ActionListMultipartUploadParts: {native.MethodGetContainer, native.MethodGetObject}, s3ActionPutBucketACL: {native.MethodGetContainer, native.MethodSetContainerEACL, native.MethodGetObject, native.MethodPutObject}, s3ActionPutBucketCORS: {native.MethodGetContainer, native.MethodGetObject, native.MethodPutObject}, s3ActionPutBucketNotification: {native.MethodGetContainer, native.MethodHeadObject, native.MethodDeleteObject, native.MethodGetObject, native.MethodPutObject}, s3ActionPutBucketObjectLockConfiguration: {native.MethodGetContainer, native.MethodGetObject, native.MethodPutObject}, s3ActionPutBucketPolicy: {native.MethodGetContainer}, s3ActionPutBucketTagging: {native.MethodGetContainer, native.MethodGetObject, native.MethodPutObject}, s3ActionPutBucketVersioning: {native.MethodGetContainer, native.MethodGetObject, native.MethodPutObject}, s3ActionPutLifecycleConfiguration: { /*not implemented yet*/ }, s3ActionPutObject: {native.MethodGetContainer, native.MethodPutObject, native.MethodGetObject, native.MethodHeadObject, native.MethodRangeObject}, s3ActionPutObjectACL: {native.MethodGetContainer, native.MethodGetContainerEACL, native.MethodSetContainerEACL, native.MethodGetObject, native.MethodHeadObject}, s3ActionPutObjectLegalHold: {native.MethodGetContainer, native.MethodHeadObject, native.MethodGetObject, native.MethodPutObject}, s3ActionPutObjectRetention: {native.MethodGetContainer, native.MethodHeadObject, native.MethodGetObject, native.MethodPutObject}, s3ActionPutObjectTagging: {native.MethodGetContainer, native.MethodHeadObject, native.MethodGetObject, native.MethodPutObject}, s3ActionPutObjectVersionACL: {native.MethodGetContainer, native.MethodGetContainerEACL, native.MethodSetContainerEACL, native.MethodGetObject, native.MethodHeadObject}, s3ActionPutObjectVersionTagging: {native.MethodGetContainer, native.MethodHeadObject, native.MethodGetObject, native.MethodPutObject}, } 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: {}, } var errConditionKeyNotApplicable = errors.New("condition key is not applicable") 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) 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) 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 { if errors.Is(err, errConditionKeyNotApplicable) { continue } 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 = res.Object || isObj || action == Wildcard res.Container = 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, Kind: chain.KindRequest, Key: native.PropertyKeyActorPublicKey, Value: principal, } }, nil } func convertToNativeChainCondition(c Conditions, resolver NativeResolver) ([]GroupedConditions, error) { return convertToChainConditions(c, func(gr GroupedConditions) (GroupedConditions, error) { res := GroupedConditions{ Conditions: make([]chain.Condition, 0, len(gr.Conditions)), Any: gr.Any, } for i := range gr.Conditions { switch { case gr.Conditions[i].Key == condKeyAWSMFAPresent: return GroupedConditions{}, errConditionKeyNotApplicable case 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 res.Conditions = append(res.Conditions, gr.Conditions[i]) case strings.HasPrefix(gr.Conditions[i].Key, condKeyAWSRequestTagPrefix) || strings.HasPrefix(gr.Conditions[i].Key, condKeyAWSResourceTagPrefix): // Tags exist only in S3 requests, so native protocol should not process such conditions. continue default: res.Conditions = append(res.Conditions, gr.Conditions[i]) } } return res, 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)) combined := make(map[string]struct{}) 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[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[fmt.Sprintf(native.ResourceFormatNamespaceContainer, bktInfo.Namespace, bktInfo.Container)] = struct{}{} continue } res = append(res, GroupedResources{ Names: []string{ fmt.Sprintf(native.ResourceFormatNamespaceContainerObjects, bktInfo.Namespace, bktInfo.Container), fmt.Sprintf(native.ResourceFormatNamespaceContainer, bktInfo.Namespace, bktInfo.Container), }, Conditions: []chain.Condition{ { Op: chain.CondStringLike, Kind: chain.KindResource, Key: PropertyKeyFilePath, Value: obj, }, }, }) } if len(combined) != 0 { 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 } 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) { 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 { continue } if action[len(s3ActionPrefix):] == Wildcard { return []string{Wildcard}, nil } 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 }