diff --git a/eacl/enums.go b/eacl/enums.go index 3465c2fc..6ced5a07 100644 --- a/eacl/enums.go +++ b/eacl/enums.go @@ -81,6 +81,21 @@ const ( // MatchStringNotEqual is a Match of string inequality. MatchStringNotEqual + + // MatchNotPresent is an operator for attribute absence. + MatchNotPresent + + // MatchNumGT is a numeric "greater than" operator. + MatchNumGT + + // MatchNumGE is a numeric "greater or equal than" operator. + MatchNumGE + + // MatchNumLT is a numeric "less than" operator. + MatchNumLT + + // MatchNumLE is a numeric "less or equal than" operator. + MatchNumLE ) // FilterHeaderType indicates source of headers to make matches. @@ -317,27 +332,35 @@ func (r *Role) DecodeString(s string) bool { // ToV2 converts Match to v2 MatchType enum value. func (m Match) ToV2() v2acl.MatchType { switch m { - case MatchStringEqual: - return v2acl.MatchTypeStringEqual - case MatchStringNotEqual: - return v2acl.MatchTypeStringNotEqual + case + MatchStringEqual, + MatchStringNotEqual, + MatchNotPresent, + MatchNumGT, + MatchNumGE, + MatchNumLT, + MatchNumLE: + return v2acl.MatchType(m) default: return v2acl.MatchTypeUnknown } } // MatchFromV2 converts v2 MatchType enum value to Match. -func MatchFromV2(match v2acl.MatchType) (m Match) { +func MatchFromV2(match v2acl.MatchType) Match { switch match { - case v2acl.MatchTypeStringEqual: - m = MatchStringEqual - case v2acl.MatchTypeStringNotEqual: - m = MatchStringNotEqual + case + v2acl.MatchTypeStringEqual, + v2acl.MatchTypeStringNotEqual, + v2acl.MatchTypeNotPresent, + v2acl.MatchTypeNumGT, + v2acl.MatchTypeNumGE, + v2acl.MatchTypeNumLT, + v2acl.MatchTypeNumLE: + return Match(match) default: - m = MatchUnknown + return MatchUnknown } - - return m } // EncodeToString returns string representation of Match. @@ -345,6 +368,11 @@ func MatchFromV2(match v2acl.MatchType) (m Match) { // String mapping: // - MatchStringEqual: STRING_EQUAL; // - MatchStringNotEqual: STRING_NOT_EQUAL; +// - MatchNotPresent: NOT_PRESENT; +// - MatchNumGT: NUM_GT; +// - MatchNumGE: NUM_GE; +// - MatchNumLT: NUM_LT; +// - MatchNumLE: NUM_LE; // - MatchUnknown, default: MATCH_TYPE_UNSPECIFIED. func (m Match) EncodeToString() string { return m.ToV2().String() diff --git a/eacl/enums_test.go b/eacl/enums_test.go index 23ea5ead..d5d95086 100644 --- a/eacl/enums_test.go +++ b/eacl/enums_test.go @@ -37,6 +37,11 @@ var ( eacl.MatchUnknown: v2acl.MatchTypeUnknown, eacl.MatchStringEqual: v2acl.MatchTypeStringEqual, eacl.MatchStringNotEqual: v2acl.MatchTypeStringNotEqual, + eacl.MatchNotPresent: v2acl.MatchTypeNotPresent, + eacl.MatchNumGT: v2acl.MatchTypeNumGT, + eacl.MatchNumGE: v2acl.MatchTypeNumGE, + eacl.MatchNumLT: v2acl.MatchTypeNumLT, + eacl.MatchNumLE: v2acl.MatchTypeNumLE, } eqV2HeaderTypes = map[eacl.FilterHeaderType]v2acl.HeaderType{ @@ -98,8 +103,8 @@ func TestMatch(t *testing.T) { }) t.Run("unknown matches", func(t *testing.T) { - require.Equal(t, (eacl.MatchStringNotEqual + 1).ToV2(), v2acl.MatchTypeUnknown) - require.Equal(t, eacl.MatchFromV2(v2acl.MatchTypeStringNotEqual+1), eacl.MatchUnknown) + require.Equal(t, (eacl.MatchNumLE + 1).ToV2(), v2acl.MatchTypeUnknown) + require.Equal(t, eacl.MatchFromV2(v2acl.MatchTypeNumLE+1), eacl.MatchUnknown) }) } @@ -198,6 +203,11 @@ func TestMatch_String(t *testing.T) { {val: toPtr(eacl.MatchStringEqual), str: "STRING_EQUAL"}, {val: toPtr(eacl.MatchStringNotEqual), str: "STRING_NOT_EQUAL"}, {val: toPtr(eacl.MatchUnknown), str: "MATCH_TYPE_UNSPECIFIED"}, + {val: toPtr(eacl.MatchNotPresent), str: "NOT_PRESENT"}, + {val: toPtr(eacl.MatchNumGT), str: "NUM_GT"}, + {val: toPtr(eacl.MatchNumGE), str: "NUM_GE"}, + {val: toPtr(eacl.MatchNumLT), str: "NUM_LT"}, + {val: toPtr(eacl.MatchNumLE), str: "NUM_LE"}, }) } diff --git a/eacl/record.go b/eacl/record.go index 111266dd..030003d2 100644 --- a/eacl/record.go +++ b/eacl/record.go @@ -123,56 +123,80 @@ func (r *Record) addObjectReservedFilter(m Match, typ filterKeyType, val stringE } // AddFilter adds generic filter. +// +// If matcher is [MatchNotPresent], the value must be empty. If matcher is +// numeric (e.g. [MatchNumGT]), value must be a base-10 integer. func (r *Record) AddFilter(from FilterHeaderType, matcher Match, name, value string) { r.addFilter(from, matcher, 0, name, staticStringer(value)) } // AddObjectAttributeFilter adds filter by object attribute. +// +// If m is [MatchNotPresent], the value must be empty. If matcher is numeric +// (e.g. [MatchNumGT]), value must be a base-10 integer. func (r *Record) AddObjectAttributeFilter(m Match, key, value string) { r.addObjectFilter(m, 0, key, staticStringer(value)) } // AddObjectVersionFilter adds filter by object version. +// +// The m must not be [MatchNotPresent] or numeric (e.g. [MatchNumGT]). func (r *Record) AddObjectVersionFilter(m Match, v *version.Version) { r.addObjectReservedFilter(m, fKeyObjVersion, staticStringer(version.EncodeToString(*v))) } // AddObjectIDFilter adds filter by object ID. +// +// The m must not be [MatchNotPresent] or numeric (e.g. [MatchNumGT]). func (r *Record) AddObjectIDFilter(m Match, id oid.ID) { r.addObjectReservedFilter(m, fKeyObjID, id) } // AddObjectContainerIDFilter adds filter by object container ID. +// +// The m must not be [MatchNotPresent] or numeric (e.g. [MatchNumGT]). func (r *Record) AddObjectContainerIDFilter(m Match, id cid.ID) { r.addObjectReservedFilter(m, fKeyObjContainerID, id) } // AddObjectOwnerIDFilter adds filter by object owner ID. +// +// The m must not be [MatchNotPresent] or numeric (e.g. [MatchNumGT]). func (r *Record) AddObjectOwnerIDFilter(m Match, id *user.ID) { r.addObjectReservedFilter(m, fKeyObjOwnerID, id) } // AddObjectCreationEpoch adds filter by object creation epoch. +// +// The m must not be [MatchNotPresent]. func (r *Record) AddObjectCreationEpoch(m Match, epoch uint64) { r.addObjectReservedFilter(m, fKeyObjCreationEpoch, u64Stringer(epoch)) } // AddObjectPayloadLengthFilter adds filter by object payload length. +// +// The m must not be [MatchNotPresent]. func (r *Record) AddObjectPayloadLengthFilter(m Match, size uint64) { r.addObjectReservedFilter(m, fKeyObjPayloadLength, u64Stringer(size)) } // AddObjectPayloadHashFilter adds filter by object payload hash value. +// +// The m must not be [MatchNotPresent] or numeric (e.g. [MatchNumGT]). func (r *Record) AddObjectPayloadHashFilter(m Match, h checksum.Checksum) { r.addObjectReservedFilter(m, fKeyObjPayloadHash, staticStringer(h.String())) } // AddObjectTypeFilter adds filter by object type. +// +// The m must not be [MatchNotPresent] or numeric (e.g. [MatchNumGT]). func (r *Record) AddObjectTypeFilter(m Match, t object.Type) { r.addObjectReservedFilter(m, fKeyObjType, staticStringer(t.EncodeToString())) } // AddObjectHomomorphicHashFilter adds filter by object payload homomorphic hash value. +// +// The m must not be [MatchNotPresent] or numeric (e.g. [MatchNumGT]). func (r *Record) AddObjectHomomorphicHashFilter(m Match, h checksum.Checksum) { r.addObjectReservedFilter(m, fKeyObjHomomorphicHash, staticStringer(h.String())) } diff --git a/eacl/validator.go b/eacl/validator.go index 86a2cdff..e99c6d71 100644 --- a/eacl/validator.go +++ b/eacl/validator.go @@ -2,6 +2,7 @@ package eacl import ( "bytes" + "math/big" ) // Validator is a tool that calculates @@ -25,6 +26,9 @@ func NewValidator() *Validator { // // If no matching table entry is found or some filters are missing, // ActionAllow is returned and the second return value is false. +// +// Note that if some rule imposes requirements on the format of values (like +// numeric), but they do not comply with it - such a rule does not match. func (v *Validator) CalculateAction(unit *ValidationUnit) (Action, bool) { for _, record := range unit.table.Records() { // check type of operation @@ -56,13 +60,22 @@ func (v *Validator) CalculateAction(unit *ValidationUnit) (Action, bool) { // - negative value if the headers of at least one filter cannot be obtained. func matchFilters(hdrSrc TypedHeaderSource, filters []Filter) int { matched := 0 + var nv, nf big.Int +nextFilter: for _, filter := range filters { headers, ok := hdrSrc.HeadersOfType(filter.From()) if !ok { return -1 } + m := filter.Matcher() + if m == MatchNumGT || m == MatchNumGE || m == MatchNumLT || m == MatchNumLE { + if _, ok = nf.SetString(filter.Value(), 10); !ok { + continue + } + } + // get headers of filtering type for _, header := range headers { // prevent NPE @@ -75,15 +88,42 @@ func matchFilters(hdrSrc TypedHeaderSource, filters []Filter) int { continue } - // get match function - matchFn, ok := mMatchFns[filter.Matcher()] - if !ok { - continue - } - // check match - if !matchFn(header, &filter) { + switch m { + default: continue + case MatchNotPresent: + continue nextFilter + case MatchStringEqual: + if header.Value() != filter.Value() { + continue + } + case MatchStringNotEqual: + if header.Value() == filter.Value() { + continue + } + case MatchNumGT, MatchNumGE, MatchNumLT, MatchNumLE: + // TODO: big math simplifies coding but almost always not efficient + // enough, try to optimize + if _, ok = nv.SetString(header.Value(), 10); !ok { + continue + } + switch nf.Cmp(&nv) { + default: + continue // should never happen but just in case + case -1: + if m == MatchNumGT || m == MatchNumGE { + continue + } + case 0: + if m == MatchNumGT || m == MatchNumLT { + continue + } + case 1: + if m == MatchNumLT || m == MatchNumLE { + continue + } + } } // increment match counter @@ -91,6 +131,10 @@ func matchFilters(hdrSrc TypedHeaderSource, filters []Filter) int { break } + + if m == MatchNotPresent { + matched++ + } } return len(filters) - matched @@ -123,14 +167,3 @@ func targetMatches(unit *ValidationUnit, record *Record) bool { return false } - -// Maps match type to corresponding function. -var mMatchFns = map[Match]func(Header, *Filter) bool{ - MatchStringEqual: func(header Header, filter *Filter) bool { - return header.Value() == filter.Value() - }, - - MatchStringNotEqual: func(header Header, filter *Filter) bool { - return header.Value() != filter.Value() - }, -} diff --git a/eacl/validator_test.go b/eacl/validator_test.go index 98a74dc2..de6de30d 100644 --- a/eacl/validator_test.go +++ b/eacl/validator_test.go @@ -19,10 +19,10 @@ func checkAction(t *testing.T, expected Action, v *Validator, vu *ValidationUnit require.Equal(t, expected, action) } -func checkDefaultAction(t *testing.T, v *Validator, vu *ValidationUnit) { +func checkDefaultAction(t *testing.T, v *Validator, vu *ValidationUnit, msgAndArgs ...any) { action, ok := v.CalculateAction(vu) - require.False(t, ok) - require.Equal(t, ActionAllow, action) + require.False(t, ok, msgAndArgs) + require.Equal(t, ActionAllow, action, msgAndArgs...) } func TestFilterMatch(t *testing.T) { @@ -283,3 +283,111 @@ func newValidationUnit(role Role, key []byte, table *Table) *ValidationUnit { WithSenderKey(key). WithEACLTable(table) } + +func TestNumericRules(t *testing.T) { + for _, tc := range []struct { + m Match + f string + h string + exp bool + }{ + // > + {MatchNumGT, "non-decimal", "0", false}, + {MatchNumGT, "0", "non-decimal", false}, + {MatchNumGT, "-1", "-2", true}, + {MatchNumGT, "0", "0", false}, + {MatchNumGT, "0", "-1", true}, + {MatchNumGT, "1", "0", true}, + {MatchNumGT, "111111111111111111111111111111", "111111111111111111111111111110", true}, // more than 64-bit + {MatchNumGT, "111111111111111111111111111111", "111111111111111111111111111111", false}, + {MatchNumGT, "-111111111111111111111111111110", "-111111111111111111111111111111", true}, + {MatchNumGT, "-2", "-1", false}, + {MatchNumGT, "-1", "0", false}, + {MatchNumGT, "0", "1", false}, + {MatchNumGT, "111111111111111111111111111110", "111111111111111111111111111111", false}, + {MatchNumGT, "-111111111111111111111111111111", "-111111111111111111111111111110", false}, + // >= + {MatchNumGE, "non-decimal", "0", false}, + {MatchNumGE, "0", "non-decimal", false}, + {MatchNumGE, "-1", "-2", true}, + {MatchNumGE, "0", "0", true}, + {MatchNumGE, "0", "-1", true}, + {MatchNumGE, "1", "0", true}, + {MatchNumGE, "111111111111111111111111111111", "111111111111111111111111111110", true}, + {MatchNumGE, "111111111111111111111111111111", "111111111111111111111111111111", true}, + {MatchNumGE, "-111111111111111111111111111110", "-111111111111111111111111111111", true}, + {MatchNumGE, "-2", "-1", false}, + {MatchNumGE, "-1", "0", false}, + {MatchNumGE, "0", "1", false}, + {MatchNumGE, "111111111111111111111111111110", "111111111111111111111111111111", false}, + {MatchNumGE, "-111111111111111111111111111111", "-111111111111111111111111111110", false}, + // < + {MatchNumLT, "non-decimal", "0", false}, + {MatchNumLT, "0", "non-decimal", false}, + {MatchNumLT, "-1", "-2", false}, + {MatchNumLT, "0", "0", false}, + {MatchNumLT, "0", "-1", false}, + {MatchNumLT, "1", "0", false}, + {MatchNumLT, "111111111111111111111111111111", "111111111111111111111111111110", false}, + {MatchNumLT, "111111111111111111111111111111", "111111111111111111111111111111", false}, + {MatchNumLT, "-111111111111111111111111111110", "-111111111111111111111111111111", false}, + {MatchNumLT, "-2", "-1", true}, + {MatchNumLT, "-1", "0", true}, + {MatchNumLT, "0", "1", true}, + {MatchNumLT, "111111111111111111111111111110", "111111111111111111111111111111", true}, + {MatchNumLT, "-111111111111111111111111111111", "-111111111111111111111111111110", true}, + // <= + {MatchNumLE, "non-decimal", "0", false}, + {MatchNumLE, "0", "non-decimal", false}, + {MatchNumLE, "-1", "-2", false}, + {MatchNumLE, "0", "0", true}, + {MatchNumLE, "0", "-1", false}, + {MatchNumLE, "1", "0", false}, + {MatchNumLE, "111111111111111111111111111111", "111111111111111111111111111110", false}, + {MatchNumLE, "111111111111111111111111111111", "111111111111111111111111111111", true}, + {MatchNumLE, "-111111111111111111111111111110", "-111111111111111111111111111111", false}, + {MatchNumLE, "-2", "-1", true}, + {MatchNumLE, "-1", "0", true}, + {MatchNumLE, "0", "1", true}, + {MatchNumLE, "111111111111111111111111111110", "111111111111111111111111111111", true}, + {MatchNumLE, "-111111111111111111111111111111", "-111111111111111111111111111110", true}, + } { + var rec Record + rec.AddObjectAttributeFilter(tc.m, "any_key", tc.f) + hs := headers{obj: makeHeaders("any_key", tc.h)} + + v := matchFilters(hs, rec.filters) + if tc.exp { + require.Zero(t, v, tc) + } else { + require.Positive(t, v, tc) + } + } +} + +func TestAbsenceRules(t *testing.T) { + hs := headers{obj: makeHeaders( + "key1", "val1", + "key2", "val2", + )} + + var r Record + + r.AddObjectAttributeFilter(MatchStringEqual, "key2", "val2") + r.AddObjectAttributeFilter(MatchNotPresent, "key1", "") + v := matchFilters(hs, r.filters) + require.Positive(t, v) + + r.filters = r.filters[:0] + r.AddObjectAttributeFilter(MatchStringEqual, "key1", "val1") + r.AddObjectAttributeFilter(MatchNotPresent, "key2", "") + v = matchFilters(hs, r.filters) + require.Positive(t, v) + + r.filters = r.filters[:0] + r.AddObjectAttributeFilter(MatchStringEqual, "key1", "val1") + r.AddObjectAttributeFilter(MatchStringEqual, "key2", "val2") + r.AddObjectAttributeFilter(MatchNotPresent, "key3", "") + v = matchFilters(hs, r.filters) + require.Zero(t, v) +}