From 84c6be01de1697425a5e346b9c68a7a403410994 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 | 31 ++++++++- iam/converter_test.go | 71 +++++++++++++++++++++ pkg/chain/chain.go | 33 ++++++++++ pkg/chain/chain_test.go | 136 ++++++++++++++++++++++++++++++++++++++++ schema/s3/consts.go | 1 + 5 files changed, 270 insertions(+), 2 deletions(-) diff --git a/iam/converter.go b/iam/converter.go index 61124ed..c81be05 100644 --- a/iam/converter.go +++ b/iam/converter.go @@ -10,6 +10,7 @@ import ( "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 ( @@ -233,8 +234,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: @@ -269,12 +269,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 := fixedn.Fixed8FromString(val); 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 3e8a86e..dab1a07 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,76 @@ 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) + + invalidConditions := []Condition{ + {"key1": {"invalid"}}, + {"key2": {"1 2"}}, + {"key3": {"0x12f"}}, + {"key4": {"0b1010"}}, + {"key5": {"+Inf"}}, + {"key6": {"-Inf"}}, + {"key7": {"inf"}}, + {"key8": {"NaN"}}, + {"key9": {"nan"}}, + } + + for _, cond := range invalidConditions { + _, err = convertToChainCondition(Conditions{CondNumericEquals: cond}) + require.Error(t, err) + } } func TestParsePrincipalARN(t *testing.T) { diff --git a/pkg/chain/chain.go b/pkg/chain/chain.go index aa857c9..87685e5 100644 --- a/pkg/chain/chain.go +++ b/pkg/chain/chain.go @@ -6,6 +6,7 @@ import ( "git.frostfs.info/TrueCloudLab/policy-engine/pkg/resource" "git.frostfs.info/TrueCloudLab/policy-engine/util" + "github.com/nspcc-dev/neo-go/pkg/encoding/fixedn" "golang.org/x/exp/slices" ) @@ -186,6 +187,38 @@ 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 { + valDecimal, err := fixedn.Fixed8FromString(val) + if err != nil { + return c.Op == CondNumericNotEquals + } + + condVal, err := fixedn.Fixed8FromString(c.Value) + if err != nil { + return c.Op == CondNumericNotEquals + } + + switch c.Op { + default: + panic(fmt.Sprintf("unimplemented: %d", c.Op)) + case CondNumericEquals: + return valDecimal.Equal(condVal) + case CondNumericNotEquals: + return !valDecimal.Equal(condVal) + case CondNumericLessThan: + return valDecimal.LessThan(condVal) + case CondNumericLessThanEquals: + return valDecimal.LessThan(condVal) || valDecimal.Equal(condVal) + case CondNumericGreaterThan: + return valDecimal.GreaterThan(condVal) + case CondNumericGreaterThanEquals: + return valDecimal.GreaterThan(condVal) || valDecimal.Equal(condVal) } } 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"