[#60] chain: Support numeric conditions
DCO action / DCO (pull_request) Successful in 1m41s Details
Tests and linters / Tests (1.20) (pull_request) Successful in 2m2s Details
Tests and linters / Tests (1.21) (pull_request) Successful in 2m12s Details
Tests and linters / Staticcheck (pull_request) Successful in 2m15s Details
Tests and linters / Tests with -race (pull_request) Successful in 2m26s Details
Tests and linters / Lint (pull_request) Successful in 3m0s Details

Signed-off-by: Marina Biryukova <m.biryukova@yadro.com>
Marina Biryukova 2024-04-01 17:27:45 +03:00
parent 42497ad242
commit d06de615ad
5 changed files with 266 additions and 2 deletions

View File

@ -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

View File

@ -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) {

View File

@ -2,6 +2,8 @@ package chain
import (
"fmt"
"math"
"strconv"
"strings"
"git.frostfs.info/TrueCloudLab/policy-engine/pkg/resource"
@ -186,9 +188,45 @@ 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 {
return c.Op == CondNumericNotEquals
}
condVal, err := strconv.ParseFloat(c.Value, 64)
if err != nil {
return c.Op == CondNumericNotEquals
}
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 {

View File

@ -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)
}
})
}
}

View File

@ -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"