diff --git a/CHANGELOG.md b/CHANGELOG.md index 65a054d4420..9b0f70f3074 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,18 +5,22 @@ Changelog for NeoFS Node ### Added - SN eACL processing of NULL and numeric operators (#2742) +- CLI now allows to create and print eACL with numeric filters (#2742) ### Fixed - Inability to deploy contract with non-standard zone via neofs-adm ### Changed - IR now checks format of NULL and numeric eACL filters specified in the protocol (#2742) +- Empty filter value is now treated as `NOT_PRESENT` op by CLI `acl extended create` cmd (#2742) ### Removed ### Updated ### Updating from v0.40.0 +CLI command `acl extended create` changed and extended input format for filters. +For example, `attr>=100` or `attr=` are now processed differently. See `-h` for details. ## [0.40.0] - 2024-02-09 - Maldo diff --git a/cmd/neofs-cli/modules/acl/extended/create.go b/cmd/neofs-cli/modules/acl/extended/create.go index d67a7a6b20c..b46c43cfbae 100644 --- a/cmd/neofs-cli/modules/acl/extended/create.go +++ b/cmd/neofs-cli/modules/acl/extended/create.go @@ -31,8 +31,11 @@ Filter consists of : Well-known system object headers start with '$Object:' prefix. User defined headers start without prefix. Read more about filter keys at github.com/nspcc-dev/neofs-api/blob/master/proto-docs/acl.md#message-eaclrecordfilter - Match is '=' for matching and '!=' for non-matching filter. - Value is a valid unicode string corresponding to object or request header value. + Match is: + '=' for string equality or, if no value, attribute absence; + '!=' for string inequality; + '>' | '>=' | '<' | '<=' for integer comparison. + Value is a valid unicode string corresponding to object or request header value. Numeric filters must have base-10 integer values. Target is 'user' for container owner, @@ -43,7 +46,7 @@ Target is When both '--rule' and '--file' arguments are used, '--rule' records will be placed higher in resulting extended ACL table. `, Example: `neofs-cli acl extended create --cid EutHBsdT1YCzHxjCfQHnLPL1vFrkSyLSio4vkphfnEk -f rules.txt --out table.json -neofs-cli acl extended create --cid EutHBsdT1YCzHxjCfQHnLPL1vFrkSyLSio4vkphfnEk -r 'allow get obj:Key=Value others' -r 'deny put others'`, +neofs-cli acl extended create --cid EutHBsdT1YCzHxjCfQHnLPL1vFrkSyLSio4vkphfnEk -r 'allow get obj:Key=Value others' -r 'deny put others' -r 'deny put obj:$Object:payloadLength<4096 others' -r 'deny get obj:Quality>=100 others'`, Args: cobra.NoArgs, Run: createEACL, } diff --git a/cmd/neofs-cli/modules/util/acl.go b/cmd/neofs-cli/modules/util/acl.go index 94bb76270ea..9cc95d6d1ee 100644 --- a/cmd/neofs-cli/modules/util/acl.go +++ b/cmd/neofs-cli/modules/util/acl.go @@ -6,6 +6,7 @@ import ( "encoding/hex" "errors" "fmt" + "math/big" "strings" "text/tabwriter" @@ -140,6 +141,16 @@ func eaclFiltersToString(fs []eacl.Filter) string { _, _ = tw.Write([]byte("\t==\t")) case eacl.MatchStringNotEqual: _, _ = tw.Write([]byte("\t!=\t")) + case eacl.MatchNumGT: + _, _ = tw.Write([]byte("\t>\t")) + case eacl.MatchNumGE: + _, _ = tw.Write([]byte("\t>=\t")) + case eacl.MatchNumLT: + _, _ = tw.Write([]byte("\t<\t")) + case eacl.MatchNumLE: + _, _ = tw.Write([]byte("\t<=\t")) + case eacl.MatchNotPresent: + _, _ = tw.Write([]byte("\tNULL\t")) case eacl.MatchUnknown: } @@ -278,23 +289,52 @@ func parseEACLRecord(args []string) (*eacl.Record, error) { func parseKVWithOp(s string) (string, string, eacl.Match, error) { i := strings.Index(s, "=") if i < 0 { + if i = strings.Index(s, "<"); i >= 0 { + if !validateDecimal(s[i+1:]) { + return "", "", 0, fmt.Errorf("invalid base-10 integer value %q for attribute %q", s[i+1:], s[:i]) + } + return s[:i], s[i+1:], eacl.MatchNumLT, nil + } else if i = strings.Index(s, ">"); i >= 0 { + if !validateDecimal(s[i+1:]) { + return "", "", 0, fmt.Errorf("invalid base-10 integer value %q for attribute %q", s[i+1:], s[:i]) + } + return s[:i], s[i+1:], eacl.MatchNumGT, nil + } + return "", "", 0, errors.New("missing op") } - var key, value string - var op eacl.Match + if len(s[i+1:]) == 0 { + return s[:i], "", eacl.MatchNotPresent, nil + } + + value := s[i+1:] - if 0 < i && s[i-1] == '!' { - key = s[:i-1] - op = eacl.MatchStringNotEqual - } else { - key = s[:i] - op = eacl.MatchStringEqual + if i == 0 { + return "", value, eacl.MatchStringEqual, nil } - value = s[i+1:] + switch s[i-1] { + case '!': + return s[:i-1], value, eacl.MatchStringNotEqual, nil + case '<': + if !validateDecimal(value) { + return "", "", 0, fmt.Errorf("invalid base-10 integer value %q for attribute %q", value, s[:i-1]) + } + return s[:i-1], value, eacl.MatchNumLE, nil + case '>': + if !validateDecimal(value) { + return "", "", 0, fmt.Errorf("invalid base-10 integer value %q for attribute %q", value, s[:i-1]) + } + return s[:i-1], value, eacl.MatchNumGE, nil + default: + return s[:i], value, eacl.MatchStringEqual, nil + } +} - return key, value, op, nil +func validateDecimal(s string) bool { + _, ok := new(big.Int).SetString(s, 10) + return ok } // eaclRoleFromString parses eacl.Role from string. @@ -341,12 +381,27 @@ func eaclOperationsFromString(s string) ([]eacl.Operation, error) { // ValidateEACLTable validates eACL table: // - eACL table must not modify [eacl.RoleSystem] access. func ValidateEACLTable(t *eacl.Table) error { + var b big.Int for _, record := range t.Records() { for _, target := range record.Targets() { if target.Role() == eacl.RoleSystem { return errors.New("it is prohibited to modify system access") } } + for _, f := range record.Filters() { + //nolint:exhaustive + switch f.Matcher() { + case eacl.MatchNotPresent: + if len(f.Value()) != 0 { + return errors.New("non-empty value in absence filter") + } + case eacl.MatchNumGT, eacl.MatchNumGE, eacl.MatchNumLT, eacl.MatchNumLE: + _, ok := b.SetString(f.Value(), 10) + if !ok { + return errors.New("numeric filter with non-decimal value") + } + } + } } return nil diff --git a/cmd/neofs-cli/modules/util/acl_test.go b/cmd/neofs-cli/modules/util/acl_test.go index 92bdcefa6ff..00adad1f7b9 100644 --- a/cmd/neofs-cli/modules/util/acl_test.go +++ b/cmd/neofs-cli/modules/util/acl_test.go @@ -14,22 +14,25 @@ func TestParseKVWithOp(t *testing.T) { op eacl.Match v string }{ - {"=", "", eacl.MatchStringEqual, ""}, - {"!=", "", eacl.MatchStringNotEqual, ""}, - {">=", ">", eacl.MatchStringEqual, ""}, + {"=", "", eacl.MatchNotPresent, ""}, + {"!=", "!", eacl.MatchNotPresent, ""}, + {">1234567890", "", eacl.MatchNumGT, "1234567890"}, + {"<1234567890", "", eacl.MatchNumLT, "1234567890"}, + {">=1234567890", "", eacl.MatchNumGE, "1234567890"}, {"=>", "", eacl.MatchStringEqual, ">"}, - {"<=", "<", eacl.MatchStringEqual, ""}, {"=<", "", eacl.MatchStringEqual, "<"}, - {"key=", "key", eacl.MatchStringEqual, ""}, - {"key>=", "key>", eacl.MatchStringEqual, ""}, - {"key<=", "key<", eacl.MatchStringEqual, ""}, + {"key=", "key", eacl.MatchNotPresent, ""}, + {"key>=", "key>", eacl.MatchNotPresent, ""}, + {"key<=", "key<", eacl.MatchNotPresent, ""}, {"=value", "", eacl.MatchStringEqual, "value"}, {"!=value", "", eacl.MatchStringNotEqual, "value"}, {"key=value", "key", eacl.MatchStringEqual, "value"}, + {"key>1234567890", "key", eacl.MatchNumGT, "1234567890"}, + {"key<1234567890", "key", eacl.MatchNumLT, "1234567890"}, {"key==value", "key", eacl.MatchStringEqual, "=value"}, {"key=>value", "key", eacl.MatchStringEqual, ">value"}, - {"key>=value", "key>", eacl.MatchStringEqual, "value"}, - {"key<=value", "key<", eacl.MatchStringEqual, "value"}, + {"key>=1234567890", "key", eacl.MatchNumGE, "1234567890"}, + {"key<=1234567890", "key", eacl.MatchNumLE, "1234567890"}, {"key=", "missing op"}, - {"<", "missing op"}, + {">", "invalid base-10 integer value \"\" for attribute \"\""}, + {">1.2", "invalid base-10 integer value \"1.2\" for attribute \"\""}, + {">=1.2", "invalid base-10 integer value \"1.2\" for attribute \"\""}, + {"<", "invalid base-10 integer value \"\" for attribute \"\""}, + {"<1.2", "invalid base-10 integer value \"1.2\" for attribute \"\""}, + {"<=1.2", "invalid base-10 integer value \"1.2\" for attribute \"\""}, {"k", "missing op"}, {"k!", "missing op"}, - {"k>", "missing op"}, - {"k<", "missing op"}, + {"k>", "invalid base-10 integer value \"\" for attribute \"k\""}, + {"k<", "invalid base-10 integer value \"\" for attribute \"k\""}, {"k!v", "missing op"}, - {"kv", "missing op"}, + {"k=v", "invalid base-10 integer value \"v\" for attribute \"k\""}, } { _, _, _, err := parseKVWithOp(tc.s) require.ErrorContains(t, err, tc.e, tc) } } + +var allNumMatchers = []eacl.Match{eacl.MatchNumGT, eacl.MatchNumGE, eacl.MatchNumLT, eacl.MatchNumLE} + +func anyValidEACL() eacl.Table { + return eacl.Table{} +} + +func TestValidateEACL(t *testing.T) { + t.Run("absence matcher", func(t *testing.T) { + var r eacl.Record + r.AddObjectAttributeFilter(eacl.MatchNotPresent, "any_key", "any_value") + tb := anyValidEACL() + tb.AddRecord(&r) + + err := ValidateEACLTable(&tb) + require.ErrorContains(t, err, "non-empty value in absence filter") + + r = eacl.Record{} + r.AddObjectAttributeFilter(eacl.MatchNotPresent, "any_key", "") + tb = anyValidEACL() + tb.AddRecord(&r) + + err = ValidateEACLTable(&tb) + require.NoError(t, err) + }) + + t.Run("numeric matchers", func(t *testing.T) { + for _, tc := range []struct { + ok bool + v string + }{ + {false, "not a base-10 integer"}, + {false, "1.2"}, + {false, ""}, + {true, "01"}, + {true, "0"}, + {true, "01"}, + {true, "-0"}, + {true, "-01"}, + {true, "1111111111111111111111111111111111111111111111"}, + {true, "-1111111111111111111111111111111111111111111111"}, + } { + for _, m := range allNumMatchers { + var r eacl.Record + r.AddObjectAttributeFilter(m, "any_key", tc.v) + tb := anyValidEACL() + tb.AddRecord(&r) + + err := ValidateEACLTable(&tb) + if tc.ok { + require.NoError(t, err, [2]any{m, tc}) + } else { + require.ErrorContains(t, err, "numeric filter with non-decimal value", [2]any{m, tc}) + } + } + } + }) +}