From ab67b9028eb625191c8e65d6c50687e5c884095e Mon Sep 17 00:00:00 2001 From: Airat Arifullin Date: Wed, 2 Oct 2024 13:30:23 +0300 Subject: [PATCH] [#93] chain: Introduce new condition operations * Introduce new operation with suffix "IfExists" for all string and numeric condition operations; * An operation with "IfExists" suffix specifies the following: if the condition key is present in the context of the request, process the key as specified in the policy. If the key is not present, evaluate the condition element as true; * Change the signature for interface method `Property()`: since it returns two values - the second indicates whether the property exists; * This also means that original condition operations for string and number comparison is slightly changed: if property doesn't exist, then the condition is not evaluated. Signed-off-by: Airat Arifullin --- pkg/chain/chain.go | 80 ++++++-- pkg/chain/chain_test.go | 331 ++++++++++++++++++++++++++++++ pkg/resource/resource.go | 8 +- pkg/resource/testutil/resource.go | 10 +- 4 files changed, 405 insertions(+), 24 deletions(-) diff --git a/pkg/chain/chain.go b/pkg/chain/chain.go index 5e94940..dfeca6b 100644 --- a/pkg/chain/chain.go +++ b/pkg/chain/chain.go @@ -111,6 +111,20 @@ const ( CondIPAddress CondNotIPAddress + + CondStringEqualsIfExists + CondStringEqualsIgnoreCaseIfExists + CondStringLikeIfExists + CondStringLessThanIfExists + CondStringLessThanEqualsIfExists + CondStringGreaterThanIfExists + CondStringGreaterThanEqualsIfExists + + CondNumericEqualsIfExists + CondNumericLessThanIfExists + CondNumericLessThanEqualsIfExists + CondNumericGreaterThanIfExists + CondNumericGreaterThanEqualsIfExists ) var condToStr = []struct { @@ -127,12 +141,24 @@ var condToStr = []struct { {CondStringLessThanEquals, "StringLessThanEquals"}, {CondStringGreaterThan, "StringGreaterThan"}, {CondStringGreaterThanEquals, "StringGreaterThanEquals"}, + {CondStringEqualsIfExists, "StringEqualsIfExists"}, + {CondStringEqualsIgnoreCaseIfExists, "StringEqualsIgnoreCaseIfExists"}, + {CondStringLikeIfExists, "StringLikeIfExists"}, + {CondStringLessThanIfExists, "StringLessThanIfExists"}, + {CondStringLessThanEqualsIfExists, "StringLessThanEqualsIfExists"}, + {CondStringGreaterThanIfExists, "StringGreaterThanIfExists"}, + {CondStringGreaterThanEqualsIfExists, "StringGreaterThanEqualsIfExists"}, {CondNumericEquals, "NumericEquals"}, {CondNumericNotEquals, "NumericNotEquals"}, {CondNumericLessThan, "NumericLessThan"}, {CondNumericLessThanEquals, "NumericLessThanEquals"}, {CondNumericGreaterThan, "NumericGreaterThan"}, {CondNumericGreaterThanEquals, "NumericGreaterThanEquals"}, + {CondNumericEqualsIfExists, "NumericEqualsIfExists"}, + {CondNumericLessThanIfExists, "NumericLessThanIfExists"}, + {CondNumericLessThanEqualsIfExists, "NumericLessThanEqualsIfExists"}, + {CondNumericGreaterThanIfExists, "NumericGreaterThanIfExists"}, + {CondNumericGreaterThanEqualsIfExists, "NumericGreaterThanEqualsIfExists"}, {CondSliceContains, "SliceContains"}, {CondIPAddress, "IPAddress"}, {CondNotIPAddress, "NotIPAddress"}, @@ -157,11 +183,12 @@ func FormCondSliceContainsValue(values []string) string { func (c *Condition) Match(req resource.Request) bool { var val string + var exists bool switch c.Kind { case KindResource: - val = req.Resource().Property(c.Key) + val, exists = req.Resource().Property(c.Key) case KindRequest: - val = req.Property(c.Key) + val, exists = req.Property(c.Key) default: panic(fmt.Sprintf("unknown condition type: %d", c.Kind)) } @@ -170,30 +197,47 @@ func (c *Condition) Match(req resource.Request) bool { default: panic(fmt.Sprintf("unimplemented: %d", c.Op)) case CondStringEquals: - return val == c.Value + return exists && val == c.Value + case CondStringEqualsIfExists: + return !exists || val == c.Value case CondStringNotEquals: - return val != c.Value + return exists && val != c.Value case CondStringEqualsIgnoreCase: - return strings.EqualFold(val, c.Value) + return exists && strings.EqualFold(val, c.Value) + case CondStringEqualsIgnoreCaseIfExists: + return !exists || strings.EqualFold(val, c.Value) case CondStringNotEqualsIgnoreCase: - return !strings.EqualFold(val, c.Value) + return exists && !strings.EqualFold(val, c.Value) case CondStringLike: - return util.GlobMatch(val, c.Value) + return exists && util.GlobMatch(val, c.Value) + case CondStringLikeIfExists: + return !exists || util.GlobMatch(val, c.Value) case CondStringNotLike: - return !util.GlobMatch(val, c.Value) + return exists && !util.GlobMatch(val, c.Value) case CondStringLessThan: - return val < c.Value + return exists && val < c.Value + case CondStringLessThanIfExists: + return !exists || val < c.Value case CondStringLessThanEquals: - return val <= c.Value + return exists && val <= c.Value + case CondStringLessThanEqualsIfExists: + return !exists || val <= c.Value case CondStringGreaterThan: - return val > c.Value + return exists && val > c.Value + case CondStringGreaterThanIfExists: + return !exists || val > c.Value case CondStringGreaterThanEquals: - return val >= c.Value + return exists && val >= c.Value + case CondStringGreaterThanEqualsIfExists: + return !exists || 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) + return exists && c.matchNumeric(val) + case CondNumericEqualsIfExists, CondNumericLessThanIfExists, CondNumericLessThanEqualsIfExists, CondNumericGreaterThanIfExists, + CondNumericGreaterThanEqualsIfExists: + return !exists || c.matchNumeric(val) case CondIPAddress, CondNotIPAddress: return c.matchIP(val) } @@ -213,17 +257,17 @@ func (c *Condition) matchNumeric(val string) bool { switch c.Op { default: panic(fmt.Sprintf("unimplemented: %d", c.Op)) - case CondNumericEquals: + case CondNumericEquals, CondNumericEqualsIfExists: return valDecimal.Equal(condVal) case CondNumericNotEquals: return !valDecimal.Equal(condVal) - case CondNumericLessThan: + case CondNumericLessThan, CondNumericLessThanIfExists: return valDecimal.LessThan(condVal) - case CondNumericLessThanEquals: + case CondNumericLessThanEquals, CondNumericLessThanEqualsIfExists: return valDecimal.LessThan(condVal) || valDecimal.Equal(condVal) - case CondNumericGreaterThan: + case CondNumericGreaterThan, CondNumericGreaterThanIfExists: return valDecimal.GreaterThan(condVal) - case CondNumericGreaterThanEquals: + case CondNumericGreaterThanEquals, CondNumericGreaterThanEqualsIfExists: return valDecimal.GreaterThan(condVal) || valDecimal.Equal(condVal) } } diff --git a/pkg/chain/chain_test.go b/pkg/chain/chain_test.go index a5154eb..7c5542a 100644 --- a/pkg/chain/chain_test.go +++ b/pkg/chain/chain_test.go @@ -398,6 +398,25 @@ func testNumericConditionsMatch(t *testing.T) { value: "50", status: Allow, }, + { + name: "value if exists from interval", + conditions: []Condition{ + { + Op: CondNumericLessThanIfExists, + Kind: KindRequest, + Key: propKey, + Value: "100", + }, + { + Op: CondNumericGreaterThanIfExists, + Kind: KindRequest, + Key: propKey, + Value: "80", + }, + }, + value: "90", + status: Allow, + }, } { t.Run(tc.name, func(t *testing.T) { resource := testutil.NewResource(native.ResourceFormatRootContainers, nil) @@ -411,6 +430,16 @@ func testNumericConditionsMatch(t *testing.T) { }}} st, _ := ch.Match(request) require.Equal(t, tc.status.String(), st.String()) + + emptyPropsRequest := testutil.NewRequest(native.MethodPutObject, resource, map[string]string{}) + st, found := ch.Match(emptyPropsRequest) + if strings.HasSuffix(tc.conditions[0].Op.String(), "IfExists") { + require.True(t, found) + require.Equal(t, tc.status.String(), st.String()) + } else { + require.False(t, found) + require.Equal(t, st, NoRuleFound) + } }) } } @@ -448,6 +477,47 @@ func testStringConiditionsMatch(t *testing.T) { st, found = ch.Match(request) require.False(t, found) require.Equal(t, NoRuleFound, st) + + emptyPropsRequest := testutil.NewRequest(native.MethodPutObject, resource, map[string]string{}) + _, found = ch.Match(emptyPropsRequest) + require.False(t, found) + require.Equal(t, NoRuleFound, st) + }) + + t.Run(CondStringEqualsIfExists.String(), func(t *testing.T) { + ch := Chain{Rules: []Rule{{ + Status: Allow, + Actions: Actions{Names: []string{native.MethodPutObject}}, + Resources: Resources{Names: []string{native.ResourceFormatRootContainers}}, + Condition: []Condition{{ + Op: CondStringEqualsIfExists, + Kind: KindRequest, + Key: propKey, + Value: val, + }}, + }}} + + resource := testutil.NewResource(native.ResourceFormatRootContainers, nil) + request := testutil.NewRequest(native.MethodPutObject, resource, map[string]string{ + propKey: val, + }) + + st, found := ch.Match(request) + require.True(t, found) + require.Equal(t, Allow, st) + + request = testutil.NewRequest(native.MethodPutObject, resource, map[string]string{ + propKey: "distort_tag_value" + val, + }) + + st, found = ch.Match(request) + require.False(t, found) + require.Equal(t, NoRuleFound, st) + + emptyPropsRequest := testutil.NewRequest(native.MethodPutObject, resource, map[string]string{}) + st, found = ch.Match(emptyPropsRequest) + require.True(t, found) + require.Equal(t, Allow, st) }) t.Run(CondStringNotEquals.String(), func(t *testing.T) { @@ -479,6 +549,11 @@ func testStringConiditionsMatch(t *testing.T) { st, found = ch.Match(request) require.False(t, found) require.Equal(t, NoRuleFound, st) + + emptyPropsRequest := testutil.NewRequest(native.MethodPutObject, resource, map[string]string{}) + _, found = ch.Match(emptyPropsRequest) + require.False(t, found) + require.Equal(t, NoRuleFound, st) }) t.Run(CondStringEqualsIgnoreCase.String(), func(t *testing.T) { @@ -510,6 +585,47 @@ func testStringConiditionsMatch(t *testing.T) { st, found = ch.Match(request) require.False(t, found) require.Equal(t, NoRuleFound, st) + + emptyPropsRequest := testutil.NewRequest(native.MethodPutObject, resource, map[string]string{}) + _, found = ch.Match(emptyPropsRequest) + require.False(t, found) + require.Equal(t, NoRuleFound, st) + }) + + t.Run(CondStringEqualsIgnoreCaseIfExists.String(), func(t *testing.T) { + ch := Chain{Rules: []Rule{{ + Status: Allow, + Actions: Actions{Names: []string{native.MethodPutObject}}, + Resources: Resources{Names: []string{native.ResourceFormatRootContainers}}, + Condition: []Condition{{ + Op: CondStringEqualsIgnoreCaseIfExists, + Kind: KindRequest, + Key: propKey, + Value: val, + }}, + }}} + + resource := testutil.NewResource(native.ResourceFormatRootContainers, nil) + request := testutil.NewRequest(native.MethodPutObject, resource, map[string]string{ + propKey: strings.ToUpper(val), + }) + + st, found := ch.Match(request) + require.True(t, found) + require.Equal(t, Allow, st) + + request = testutil.NewRequest(native.MethodPutObject, resource, map[string]string{ + propKey: strings.ToUpper("distort_tag_value" + val), + }) + + st, found = ch.Match(request) + require.False(t, found) + require.Equal(t, NoRuleFound, st) + + emptyPropsRequest := testutil.NewRequest(native.MethodPutObject, resource, map[string]string{}) + st, found = ch.Match(emptyPropsRequest) + require.True(t, found) + require.Equal(t, Allow, st) }) t.Run(CondStringNotEqualsIgnoreCase.String(), func(t *testing.T) { @@ -541,6 +657,11 @@ func testStringConiditionsMatch(t *testing.T) { st, found = ch.Match(request) require.False(t, found) require.Equal(t, NoRuleFound, st) + + emptyPropsRequest := testutil.NewRequest(native.MethodPutObject, resource, map[string]string{}) + st, found = ch.Match(emptyPropsRequest) + require.False(t, found) + require.Equal(t, NoRuleFound, st) }) t.Run(CondStringLike.String(), func(t *testing.T) { @@ -572,6 +693,47 @@ func testStringConiditionsMatch(t *testing.T) { st, found = ch.Match(request) require.False(t, found) require.Equal(t, NoRuleFound, st) + + emptyPropsRequest := testutil.NewRequest(native.MethodPutObject, resource, map[string]string{}) + st, found = ch.Match(emptyPropsRequest) + require.False(t, found) + require.Equal(t, NoRuleFound, st) + }) + + t.Run(CondStringLikeIfExists.String(), func(t *testing.T) { + ch := Chain{Rules: []Rule{{ + Status: Allow, + Actions: Actions{Names: []string{native.MethodPutObject}}, + Resources: Resources{Names: []string{native.ResourceFormatRootContainers}}, + Condition: []Condition{{ + Op: CondStringLikeIfExists, + Kind: KindRequest, + Key: propKey, + Value: val + "*", + }}, + }}} + + resource := testutil.NewResource(native.ResourceFormatRootContainers, nil) + request := testutil.NewRequest(native.MethodPutObject, resource, map[string]string{ + propKey: val + "suffix", + }) + + st, found := ch.Match(request) + require.True(t, found) + require.Equal(t, Allow, st) + + request = testutil.NewRequest(native.MethodPutObject, resource, map[string]string{ + propKey: string([]byte(val)[:len(val)-1]), //cut last letter + }) + + st, found = ch.Match(request) + require.False(t, found) + require.Equal(t, NoRuleFound, st) + + emptyPropsRequest := testutil.NewRequest(native.MethodPutObject, resource, map[string]string{}) + st, found = ch.Match(emptyPropsRequest) + require.True(t, found) + require.Equal(t, Allow, st) }) t.Run(CondStringNotLike.String(), func(t *testing.T) { @@ -603,6 +765,11 @@ func testStringConiditionsMatch(t *testing.T) { st, found = ch.Match(request) require.False(t, found) require.Equal(t, NoRuleFound, st) + + emptyPropsRequest := testutil.NewRequest(native.MethodPutObject, resource, map[string]string{}) + st, found = ch.Match(emptyPropsRequest) + require.False(t, found) + require.Equal(t, NoRuleFound, st) }) t.Run(CondStringLessThan.String(), func(t *testing.T) { @@ -634,6 +801,47 @@ func testStringConiditionsMatch(t *testing.T) { st, found = ch.Match(request) require.False(t, found) require.Equal(t, NoRuleFound, st) + + emptyPropsRequest := testutil.NewRequest(native.MethodPutObject, resource, map[string]string{}) + st, found = ch.Match(emptyPropsRequest) + require.False(t, found) + require.Equal(t, NoRuleFound, st) + }) + + t.Run(CondStringLessThanIfExists.String(), func(t *testing.T) { + ch := Chain{Rules: []Rule{{ + Status: Allow, + Actions: Actions{Names: []string{native.MethodPutObject}}, + Resources: Resources{Names: []string{native.ResourceFormatRootContainers}}, + Condition: []Condition{{ + Op: CondStringLessThanIfExists, + Kind: KindRequest, + Key: propKey, + Value: val + "b", + }}, + }}} + + resource := testutil.NewResource(native.ResourceFormatRootContainers, nil) + request := testutil.NewRequest(native.MethodPutObject, resource, map[string]string{ + propKey: val + "a", + }) + + st, found := ch.Match(request) + require.True(t, found) + require.Equal(t, Allow, st) + + request = testutil.NewRequest(native.MethodPutObject, resource, map[string]string{ + propKey: val + "c", + }) + + st, found = ch.Match(request) + require.False(t, found) + require.Equal(t, NoRuleFound, st) + + emptyPropsRequest := testutil.NewRequest(native.MethodPutObject, resource, map[string]string{}) + st, found = ch.Match(emptyPropsRequest) + require.True(t, found) + require.Equal(t, Allow, st) }) t.Run(CondStringLessThanEquals.String(), func(t *testing.T) { @@ -665,6 +873,47 @@ func testStringConiditionsMatch(t *testing.T) { st, found = ch.Match(request) require.True(t, found) require.Equal(t, Allow, st) + + emptyPropsRequest := testutil.NewRequest(native.MethodPutObject, resource, map[string]string{}) + st, found = ch.Match(emptyPropsRequest) + require.False(t, found) + require.Equal(t, NoRuleFound, st) + }) + + t.Run(CondStringLessThanEqualsIfExists.String(), func(t *testing.T) { + ch := Chain{Rules: []Rule{{ + Status: Allow, + Actions: Actions{Names: []string{native.MethodPutObject}}, + Resources: Resources{Names: []string{native.ResourceFormatRootContainers}}, + Condition: []Condition{{ + Op: CondStringLessThanEqualsIfExists, + Kind: KindRequest, + Key: propKey, + Value: val + "b", + }}, + }}} + + resource := testutil.NewResource(native.ResourceFormatRootContainers, nil) + request := testutil.NewRequest(native.MethodPutObject, resource, map[string]string{ + propKey: val + "a", + }) + + st, found := ch.Match(request) + require.True(t, found) + require.Equal(t, Allow, st) + + request = testutil.NewRequest(native.MethodPutObject, resource, map[string]string{ + propKey: val + "b", + }) + + st, found = ch.Match(request) + require.True(t, found) + require.Equal(t, Allow, st) + + emptyPropsRequest := testutil.NewRequest(native.MethodPutObject, resource, map[string]string{}) + st, found = ch.Match(emptyPropsRequest) + require.True(t, found) + require.Equal(t, Allow, st) }) t.Run(CondStringGreaterThan.String(), func(t *testing.T) { @@ -696,6 +945,47 @@ func testStringConiditionsMatch(t *testing.T) { st, found = ch.Match(request) require.False(t, found) require.Equal(t, NoRuleFound, st) + + emptyPropsRequest := testutil.NewRequest(native.MethodPutObject, resource, map[string]string{}) + st, found = ch.Match(emptyPropsRequest) + require.False(t, found) + require.Equal(t, NoRuleFound, st) + }) + + t.Run(CondStringGreaterThanIfExists.String(), func(t *testing.T) { + ch := Chain{Rules: []Rule{{ + Status: Allow, + Actions: Actions{Names: []string{native.MethodPutObject}}, + Resources: Resources{Names: []string{native.ResourceFormatRootContainers}}, + Condition: []Condition{{ + Op: CondStringGreaterThanIfExists, + Kind: KindRequest, + Key: propKey, + Value: val + "b", + }}, + }}} + + resource := testutil.NewResource(native.ResourceFormatRootContainers, nil) + request := testutil.NewRequest(native.MethodPutObject, resource, map[string]string{ + propKey: val + "c", + }) + + st, found := ch.Match(request) + require.True(t, found) + require.Equal(t, Allow, st) + + request = testutil.NewRequest(native.MethodPutObject, resource, map[string]string{ + propKey: val + "b", + }) + + st, found = ch.Match(request) + require.False(t, found) + require.Equal(t, NoRuleFound, st) + + emptyPropsRequest := testutil.NewRequest(native.MethodPutObject, resource, map[string]string{}) + st, found = ch.Match(emptyPropsRequest) + require.True(t, found) + require.Equal(t, Allow, st) }) t.Run(CondStringGreaterThanEquals.String(), func(t *testing.T) { @@ -727,6 +1017,47 @@ func testStringConiditionsMatch(t *testing.T) { st, found = ch.Match(request) require.True(t, found) require.Equal(t, Allow, st) + + emptyPropsRequest := testutil.NewRequest(native.MethodPutObject, resource, map[string]string{}) + st, found = ch.Match(emptyPropsRequest) + require.False(t, found) + require.Equal(t, NoRuleFound, st) + }) + + t.Run(CondStringGreaterThanEqualsIfExists.String(), func(t *testing.T) { + ch := Chain{Rules: []Rule{{ + Status: Allow, + Actions: Actions{Names: []string{native.MethodPutObject}}, + Resources: Resources{Names: []string{native.ResourceFormatRootContainers}}, + Condition: []Condition{{ + Op: CondStringGreaterThanEqualsIfExists, + Kind: KindRequest, + Key: propKey, + Value: val + "b", + }}, + }}} + + resource := testutil.NewResource(native.ResourceFormatRootContainers, nil) + request := testutil.NewRequest(native.MethodPutObject, resource, map[string]string{ + propKey: val + "c", + }) + + st, found := ch.Match(request) + require.True(t, found) + require.Equal(t, Allow, st) + + request = testutil.NewRequest(native.MethodPutObject, resource, map[string]string{ + propKey: val + "b", + }) + + st, found = ch.Match(request) + require.True(t, found) + require.Equal(t, Allow, st) + + emptyPropsRequest := testutil.NewRequest(native.MethodPutObject, resource, map[string]string{}) + st, found = ch.Match(emptyPropsRequest) + require.True(t, found) + require.Equal(t, Allow, st) }) } diff --git a/pkg/resource/resource.go b/pkg/resource/resource.go index 7180f20..e258eb7 100644 --- a/pkg/resource/resource.go +++ b/pkg/resource/resource.go @@ -7,13 +7,17 @@ type Request interface { // Name is the operation name, such as Object.Put. Must not include wildcards. Operation() string // Property returns request properties, such as IP address of the origin. - Property(string) string + // The second return boolean value determines if the specified value exists within the properties. + Property(string) (string, bool) // Resource returns resource the operation is applied to. Resource() Resource } // Resource represents the resource operation is applied to. type Resource interface { + // Name is the resource name. Name() string - Property(string) string + // Property returns resource properties, such as object type etc. + // The second return boolean value determines if the specified value exists within the properties. + Property(string) (string, bool) } diff --git a/pkg/resource/testutil/resource.go b/pkg/resource/testutil/resource.go index 02e6394..5b0ce85 100644 --- a/pkg/resource/testutil/resource.go +++ b/pkg/resource/testutil/resource.go @@ -13,8 +13,9 @@ func (r *Resource) Name() string { return r.name } -func (r *Resource) Property(name string) string { - return r.properties[name] +func (r *Resource) Property(name string) (val string, exists bool) { + val, exists = r.properties[name] + return } func NewResource(name string, properties map[string]string) *Resource { @@ -40,8 +41,9 @@ func (r *Request) Resource() resourcepkg.Resource { return r.resource } -func (r *Request) Property(name string) string { - return r.properties[name] +func (r *Request) Property(name string) (val string, exists bool) { + val, exists = r.properties[name] + return } func NewRequest(op string, r *Resource, properties map[string]string) *Request { -- 2.45.2