From 18aa3a34420757d20293598be0f0aee9c75d6f41 Mon Sep 17 00:00:00 2001 From: Marina Biryukova Date: Mon, 1 Apr 2024 17:27:45 +0300 Subject: [PATCH] [#60] chain: Support numeric conditions Signed-off-by: Marina Biryukova --- iam/converter.go | 30 ++++++++- iam/converter_test.go | 63 +++++++++++++++++++ pkg/chain/chain.go | 44 +++++++++++++ pkg/chain/chain_test.go | 136 ++++++++++++++++++++++++++++++++++++++++ schema/s3/consts.go | 1 + 5 files changed, 272 insertions(+), 2 deletions(-) diff --git a/iam/converter.go b/iam/converter.go index bd3483e..534abe0 100644 --- a/iam/converter.go +++ b/iam/converter.go @@ -219,8 +219,7 @@ func getConditionTypeAndConverter(op string) (chain.ConditionType, convertFuncti return 0, nil, fmt.Errorf("unsupported condition operator: '%s'", op) } case strings.HasPrefix(op, "Numeric"): - // TODO - return 0, nil, fmt.Errorf("currently nummeric conditions unsupported: '%s'", op) + return numericConditionTypeAndConverter(op) case strings.HasPrefix(op, "Date"): switch op { case CondDateEquals: @@ -255,12 +254,39 @@ func getConditionTypeAndConverter(op string) (chain.ConditionType, convertFuncti } } +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 := strconv.ParseFloat(val, 64); err == nil { + return val, nil + } + + return "", fmt.Errorf("invalid numeric value: '%s'", val) +} + func dateConvertFunction(val string) (string, error) { if _, err := strconv.ParseInt(val, 10, 64); err == nil { return val, nil diff --git a/iam/converter_test.go b/iam/converter_test.go index 97e1cd7..0f5e8e8 100644 --- a/iam/converter_test.go +++ b/iam/converter_test.go @@ -385,6 +385,12 @@ func TestConvertToChainCondition(t *testing.T) { CondArnLike: {condKeyAWSPrincipalARN: {principal}}, CondArnNotEquals: {"key18": {"val18"}}, CondArnNotLike: {"key19": {"val19"}}, + CondNumericEquals: {"key20": {"-20"}}, + CondNumericNotEquals: {"key21": {"+21"}}, + CondNumericLessThan: {"key22": {"0"}}, + CondNumericLessThanEquals: {"key23": {"23.23"}}, + CondNumericGreaterThan: {"key24": {"-24.24"}}, + CondNumericGreaterThanEquals: {"key25": {"+25.25"}}, } expectedCondition := []GroupedConditions{ @@ -549,11 +555,68 @@ func TestConvertToChainCondition(t *testing.T) { Value: "val19", }}, }, + { + Conditions: []chain.Condition{{ + Op: chain.CondNumericEquals, + Object: chain.ObjectRequest, + Key: "key20", + Value: "-20", + }}, + }, + { + Conditions: []chain.Condition{{ + Op: chain.CondNumericNotEquals, + Object: chain.ObjectRequest, + Key: "key21", + Value: "+21", + }}, + }, + { + Conditions: []chain.Condition{{ + Op: chain.CondNumericLessThan, + Object: chain.ObjectRequest, + Key: "key22", + Value: "0", + }}, + }, + { + Conditions: []chain.Condition{{ + Op: chain.CondNumericLessThanEquals, + Object: chain.ObjectRequest, + Key: "key23", + Value: "23.23", + }}, + }, + { + Conditions: []chain.Condition{{ + Op: chain.CondNumericGreaterThan, + Object: chain.ObjectRequest, + Key: "key24", + Value: "-24.24", + }}, + }, + { + Conditions: []chain.Condition{{ + Op: chain.CondNumericGreaterThanEquals, + Object: chain.ObjectRequest, + Key: "key25", + Value: "+25.25", + }}, + }, } actualCondition, err := convertToChainCondition(conditions) require.NoError(t, err) require.ElementsMatch(t, expectedCondition, actualCondition) + + invalidCondition := Conditions{ + CondNumericEquals: {"key1": {"invalid"}}, + CondNumericNotEquals: {"key2": {"1 2"}}, + CondNumericLessThan: {"key3": {"0x12f"}}, + CondNumericLessThanEquals: {"key4": {"0b1010"}}, + } + _, err = convertToChainCondition(invalidCondition) + require.Error(t, err) } func TestParsePrincipalARN(t *testing.T) { diff --git a/pkg/chain/chain.go b/pkg/chain/chain.go index aa857c9..d904ff2 100644 --- a/pkg/chain/chain.go +++ b/pkg/chain/chain.go @@ -2,6 +2,8 @@ package chain import ( "fmt" + "math" + "strconv" "strings" "git.frostfs.info/TrueCloudLab/policy-engine/pkg/resource" @@ -186,9 +188,51 @@ func (c *Condition) Match(req resource.Request) bool { return val >= c.Value case CondSliceContains: return slices.Contains(strings.Split(val, condSliceContainsDelimiter), c.Value) + case CondNumericEquals, CondNumericNotEquals, CondNumericLessThan, CondNumericLessThanEquals, CondNumericGreaterThan, + CondNumericGreaterThanEquals: + return c.matchNumeric(val) } } +func (c *Condition) matchNumeric(val string) bool { + valFloat, err := strconv.ParseFloat(val, 64) + if err != nil { + if c.Op == CondNumericNotEquals { + return true + } + return false + } + + condVal, err := strconv.ParseFloat(c.Value, 64) + if err != nil { + if c.Op == CondNumericNotEquals { + return true + } + return false + } + + switch c.Op { + default: + panic(fmt.Sprintf("unimplemented: %d", c.Op)) + case CondNumericEquals: + return equalsFloat(valFloat, condVal) + case CondNumericNotEquals: + return !equalsFloat(valFloat, condVal) + case CondNumericLessThan: + return valFloat < condVal + case CondNumericLessThanEquals: + return valFloat <= condVal + case CondNumericGreaterThan: + return valFloat > condVal + case CondNumericGreaterThanEquals: + return valFloat >= condVal + } +} + +func equalsFloat(a, b float64) bool { + return math.Abs(a-b) < 1e-8 +} + func (r *Rule) Match(req resource.Request) (status Status, matched bool) { found := len(r.Resources.Names) == 0 for i := range r.Resources.Names { diff --git a/pkg/chain/chain_test.go b/pkg/chain/chain_test.go index f050574..ee7d7d7 100644 --- a/pkg/chain/chain_test.go +++ b/pkg/chain/chain_test.go @@ -6,6 +6,7 @@ import ( "git.frostfs.info/TrueCloudLab/policy-engine/pkg/resource/testutil" "git.frostfs.info/TrueCloudLab/policy-engine/schema/common" "git.frostfs.info/TrueCloudLab/policy-engine/schema/native" + "git.frostfs.info/TrueCloudLab/policy-engine/schema/s3" "github.com/stretchr/testify/require" ) @@ -149,3 +150,138 @@ func TestCondSliceContainsMatch(t *testing.T) { }) } } + +func TestNumericConditionsMatch(t *testing.T) { + propKey := s3.PropertyKeyMaxKeys + + for _, tc := range []struct { + name string + conditions []Condition + value string + status Status + }{ + { + name: "value from interval", + conditions: []Condition{ + { + Op: CondNumericLessThan, + Object: ObjectRequest, + Key: propKey, + Value: "100", + }, + { + Op: CondNumericGreaterThan, + Object: ObjectRequest, + Key: propKey, + Value: "80", + }, + { + Op: CondNumericNotEquals, + Object: ObjectRequest, + Key: propKey, + Value: "91", + }, + }, + value: "90", + status: Allow, + }, + { + name: "border value", + conditions: []Condition{ + { + Op: CondNumericEquals, + Object: ObjectRequest, + Key: propKey, + Value: "50", + }, + { + Op: CondNumericLessThanEquals, + Object: ObjectRequest, + Key: propKey, + Value: "50", + }, + { + Op: CondNumericGreaterThanEquals, + Object: ObjectRequest, + Key: propKey, + Value: "50", + }, + }, + value: "50", + status: Allow, + }, + } { + t.Run(tc.name, func(t *testing.T) { + resource := testutil.NewResource(native.ResourceFormatRootContainers, nil) + request := testutil.NewRequest(native.MethodPutObject, resource, map[string]string{propKey: tc.value}) + + ch := Chain{Rules: []Rule{{ + Status: Allow, + Actions: Actions{Names: []string{native.MethodPutObject}}, + Resources: Resources{Names: []string{native.ResourceFormatRootContainers}}, + Condition: tc.conditions, + }}} + st, _ := ch.Match(request) + require.Equal(t, tc.status.String(), st.String()) + }) + } +} + +func TestInvalidNumericValues(t *testing.T) { + propKey := s3.PropertyKeyMaxKeys + propValues := []string{"", "invalid"} + + for _, tc := range []struct { + name string + conditionType ConditionType + match bool + }{ + { + name: "NumericEquals condition", + conditionType: CondNumericEquals, + match: false, + }, + { + name: "NumericNotEquals condition", + conditionType: CondNumericNotEquals, + match: true, + }, + { + name: "NumericLessThan condition", + conditionType: CondNumericLessThan, + match: false, + }, + { + name: "NumericLessThanEquals condition", + conditionType: CondNumericLessThanEquals, + match: false, + }, + { + name: "NumericGreaterThan condition", + conditionType: CondNumericGreaterThan, + match: false, + }, + { + name: "NumericGreaterThanEquals condition", + conditionType: CondNumericGreaterThanEquals, + match: false, + }, + } { + t.Run(tc.name, func(t *testing.T) { + resource := testutil.NewResource(native.ResourceFormatRootContainers, nil) + condition := Condition{ + Op: tc.conditionType, + Object: ObjectRequest, + Key: propKey, + Value: "50", + } + + for _, propValue := range propValues { + request := testutil.NewRequest(native.MethodPutObject, resource, map[string]string{propKey: propValue}) + + match := condition.Match(request) + require.Equal(t, tc.match, match) + } + }) + } +} diff --git a/schema/s3/consts.go b/schema/s3/consts.go index 7159a32..ab8021e 100644 --- a/schema/s3/consts.go +++ b/schema/s3/consts.go @@ -6,6 +6,7 @@ const ( PropertyKeyDelimiter = "s3:delimiter" PropertyKeyPrefix = "s3:prefix" PropertyKeyVersionID = "s3:VersionId" + PropertyKeyMaxKeys = "s3:max-keys" ResourceFormatS3All = "arn:aws:s3:::*" ResourceFormatS3Bucket = "arn:aws:s3:::%s"