package common import ( "errors" "fmt" "net/netip" "strconv" "strings" "time" "unicode/utf8" "git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain" "git.frostfs.info/TrueCloudLab/policy-engine/schema/common" "github.com/nspcc-dev/neo-go/pkg/encoding/fixedn" ) 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" S3ActionPatchObject = "s3:PatchObject" S3ActionPutBucketPublicAccessBlock = "s3:PutBucketPublicAccessBlock" S3ActionGetBucketPublicAccessBlock = "s3:GetBucketPublicAccessBlock" ) const ( CondKeyAWSPrincipalARN = "aws:PrincipalArn" CondKeyAWSSourceIP = "aws:SourceIp" CondKeyAWSPrincipalTagPrefix = "aws:PrincipalTag/" CondKeyAWSRequestTagPrefix = "aws:RequestTag/" CondKeyAWSResourceTagPrefix = "aws:ResourceTag/" UserClaimTagPrefix = "tag-" ) const ( // String condition operators. CondStringEquals string = "StringEquals" CondStringNotEquals string = "StringNotEquals" CondStringEqualsIgnoreCase string = "StringEqualsIgnoreCase" CondStringNotEqualsIgnoreCase string = "StringNotEqualsIgnoreCase" CondStringLike string = "StringLike" CondStringNotLike string = "StringNotLike" // Numeric condition operators. CondNumericEquals string = "NumericEquals" CondNumericNotEquals string = "NumericNotEquals" CondNumericLessThan string = "NumericLessThan" CondNumericLessThanEquals string = "NumericLessThanEquals" CondNumericGreaterThan string = "NumericGreaterThan" CondNumericGreaterThanEquals string = "NumericGreaterThanEquals" // Date condition operators. CondDateEquals string = "DateEquals" CondDateNotEquals string = "DateNotEquals" CondDateLessThan string = "DateLessThan" CondDateLessThanEquals string = "DateLessThanEquals" CondDateGreaterThan string = "DateGreaterThan" CondDateGreaterThanEquals string = "DateGreaterThanEquals" // Bolean condition operators. CondBool string = "Bool" // IP address condition operators. CondIPAddress string = "IpAddress" CondNotIPAddress string = "NotIpAddress" // ARN condition operators. CondArnEquals string = "ArnEquals" CondArnLike string = "ArnLike" CondArnNotEquals string = "ArnNotEquals" CondArnNotLike string = "ArnNotLike" // Custom condition operators. CondSliceContains string = "SliceContains" ) const ( ArnIAMPrefix = "arn:aws:iam::" S3ResourcePrefix = "arn:aws:s3:::" S3ActionPrefix = "s3:" IamActionPrefix = "iam:" ) var ( // ErrInvalidPrincipalFormat occurs when principal has unknown/unsupported format. ErrInvalidPrincipalFormat = errors.New("invalid principal format") // ErrInvalidResourceFormat occurs when resource has unknown/unsupported format. ErrInvalidResourceFormat = errors.New("invalid resource format") // ErrInvalidActionFormat occurs when action has unknown/unsupported format. ErrInvalidActionFormat = errors.New("invalid action format") // ErrActionsNotApplicable occurs when failed to convert any actions. ErrActionsNotApplicable = errors.New("actions not applicable") ) type FormPrincipalConditionFunc func(string) chain.Condition type TransformConditionFunc func(gr GroupedConditions) (GroupedConditions, error) func ConvertToChainConditions(c Conditions, transformer TransformConditionFunc) ([]GroupedConditions, error) { conditions, err := ConvertToChainCondition(c) if err != nil { return nil, err } for i := range conditions { if conditions[i], err = transformer(conditions[i]); err != nil { return nil, fmt.Errorf("transform condition: %w", err) } } return conditions, nil } type GroupedConditions struct { Conditions []chain.Condition Any bool } func ConvertToChainCondition(c Conditions) ([]GroupedConditions, error) { var grouped []GroupedConditions for op, KVs := range c { condType, convertValue, err := getConditionTypeAndConverter(op) if err != nil { return nil, err } for key, values := range KVs { group := GroupedConditions{ Conditions: make([]chain.Condition, len(values)), Any: len(values) > 1, } for i, val := range values { converted, err := convertValue(val) if err != nil { return nil, err } group.Conditions[i] = chain.Condition{ Op: condType, Kind: chain.KindRequest, Key: transformKey(key), Value: converted, } } grouped = append(grouped, group) } } return grouped, nil } func transformKey(key string) string { tagName, isTag := strings.CutPrefix(key, CondKeyAWSPrincipalTagPrefix) if isTag { return fmt.Sprintf(common.PropertyKeyFormatFrostFSIDUserClaim, UserClaimTagPrefix+tagName) } switch key { case CondKeyAWSSourceIP: return common.PropertyKeyFrostFSSourceIP } return key } func getConditionTypeAndConverter(op string) (chain.ConditionType, convertFunction, error) { switch { case strings.HasPrefix(op, "String"): switch op { case CondStringEquals: return chain.CondStringEquals, noConvertFunction, nil case CondStringNotEquals: return chain.CondStringNotEquals, noConvertFunction, nil case CondStringEqualsIgnoreCase: return chain.CondStringEqualsIgnoreCase, noConvertFunction, nil case CondStringNotEqualsIgnoreCase: return chain.CondStringNotEqualsIgnoreCase, noConvertFunction, nil case CondStringLike: return chain.CondStringLike, noConvertFunction, nil case CondStringNotLike: return chain.CondStringNotLike, noConvertFunction, nil default: return 0, nil, fmt.Errorf("unsupported condition operator: '%s'", op) } case strings.HasPrefix(op, "Arn"): switch op { case CondArnEquals: return chain.CondStringEquals, noConvertFunction, nil case CondArnNotEquals: return chain.CondStringNotEquals, noConvertFunction, nil case CondArnLike: return chain.CondStringLike, noConvertFunction, nil case CondArnNotLike: return chain.CondStringNotLike, noConvertFunction, nil default: return 0, nil, fmt.Errorf("unsupported condition operator: '%s'", op) } case strings.HasPrefix(op, "Numeric"): return numericConditionTypeAndConverter(op) case strings.HasPrefix(op, "Date"): switch op { case CondDateEquals: return chain.CondStringEquals, dateConvertFunction, nil case CondDateNotEquals: return chain.CondStringNotEquals, dateConvertFunction, nil case CondDateLessThan: return chain.CondStringLessThan, dateConvertFunction, nil case CondDateLessThanEquals: return chain.CondStringLessThanEquals, dateConvertFunction, nil case CondDateGreaterThan: return chain.CondStringGreaterThan, dateConvertFunction, nil case CondDateGreaterThanEquals: return chain.CondStringGreaterThanEquals, dateConvertFunction, nil default: return 0, nil, fmt.Errorf("unsupported condition operator: '%s'", op) } case op == CondBool: return chain.CondStringEqualsIgnoreCase, noConvertFunction, nil case op == CondIPAddress: return chain.CondIPAddress, IPConvertFunction, nil case op == CondNotIPAddress: return chain.CondNotIPAddress, IPConvertFunction, nil case op == CondSliceContains: return chain.CondSliceContains, noConvertFunction, nil default: return 0, nil, fmt.Errorf("unsupported condition operator: '%s'", op) } } func numericConditionTypeAndConverter(op string) (chain.ConditionType, convertFunction, error) { switch op { case CondNumericEquals: return chain.CondNumericEquals, numericConvertFunction, nil case CondNumericNotEquals: return chain.CondNumericNotEquals, numericConvertFunction, nil case CondNumericLessThan: return chain.CondNumericLessThan, numericConvertFunction, nil case CondNumericLessThanEquals: return chain.CondNumericLessThanEquals, numericConvertFunction, nil case CondNumericGreaterThan: return chain.CondNumericGreaterThan, numericConvertFunction, nil case CondNumericGreaterThanEquals: return chain.CondNumericGreaterThanEquals, numericConvertFunction, nil default: return 0, nil, fmt.Errorf("unsupported condition operator: '%s'", op) } } type convertFunction func(string) (string, error) func noConvertFunction(val string) (string, error) { return val, nil } func numericConvertFunction(val string) (string, error) { if _, err := fixedn.Fixed8FromString(val); err == nil { return val, nil } return "", fmt.Errorf("invalid numeric value: '%s'", val) } func IPConvertFunction(val string) (string, error) { if _, err := netip.ParsePrefix(val); err != nil { if _, err = netip.ParseAddr(val); err != nil { return "", err } val += "/32" } return val, nil } func dateConvertFunction(val string) (string, error) { if _, err := strconv.ParseInt(val, 10, 64); err == nil { return val, nil } parsed, err := time.Parse(time.RFC3339, val) if err != nil { return "", err } return strconv.FormatInt(parsed.UTC().Unix(), 10), nil } func ParsePrincipalAsIAMUser(principal string) (account string, user string, err error) { if !strings.HasPrefix(principal, ArnIAMPrefix) { return "", "", ErrInvalidPrincipalFormat } // iam arn format arn:aws:iam:::user/ iamResource := strings.TrimPrefix(principal, ArnIAMPrefix) sepIndex := strings.Index(iamResource, ":user/") if sepIndex < 0 { return "", "", ErrInvalidPrincipalFormat } account = iamResource[:sepIndex] user = iamResource[sepIndex+6:] if len(user) == 0 { return "", "", ErrInvalidPrincipalFormat } userNameIndex := strings.LastIndexByte(user, '/') if userNameIndex > -1 { user = user[userNameIndex+1:] if len(user) == 0 { return "", "", ErrInvalidPrincipalFormat } } return account, user, nil } func ValidateResource(resource string) error { if resource == Wildcard { return nil } if !strings.HasPrefix(resource, S3ResourcePrefix) && !strings.HasPrefix(resource, ArnIAMPrefix) { return ErrInvalidResourceFormat } index := strings.IndexByte(resource, Wildcard[0]) if index != -1 && index != utf8.RuneCountInString(resource)-1 { return ErrInvalidResourceFormat } return nil } 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 false, ErrInvalidActionFormat } return isIAM, nil } func SplitGroupedConditions(groupedConditions []GroupedConditions) [][]chain.Condition { var orConditions []chain.Condition commonConditions := make([]chain.Condition, 0, len(groupedConditions)) for _, grouped := range groupedConditions { if grouped.Any { orConditions = append(orConditions, grouped.Conditions...) } else { commonConditions = append(commonConditions, grouped.Conditions...) } } if len(orConditions) == 0 { return [][]chain.Condition{commonConditions} } res := make([][]chain.Condition, len(orConditions)) for i, condition := range orConditions { res[i] = append([]chain.Condition{condition}, commonConditions...) } return res } func FormStatus(statement Statement) chain.Status { status := chain.AccessDenied if statement.Effect == AllowEffect { status = chain.Allow } return status }