frostfs-s3-gw/pkg/policy-engine/common/converter.go
Denis Kirillov 0ba6989197 [#680] Move policy engine converter to s3-gw
Signed-off-by: Denis Kirillov <d.kirillov@yadro.com>
2025-04-14 12:11:54 +00:00

427 lines
14 KiB
Go

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::<account>:user/<user-name-with-path>
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
}