Skip to content

Commit

Permalink
CLI: Support new eACL operators
Browse files Browse the repository at this point in the history
NeoFS protocol was recently extended with NULL and numeric eACL
operators. Now `neofs-cli acl extended create` command:
 * treats input like `attr>=value` as numeric filter;
 * treats input like `obj:attr=` (empty value) as missing attribute filter.

The `print` command now also supports these ops. `NOT_PRESENT` matcher
is printed as `attr NULL`.

Refs #2730.

Signed-off-by: Leonard Lyubich <leonard@morphbits.io>
  • Loading branch information
cthulhu-rider committed Feb 22, 2024
1 parent cdd02e2 commit df54b5b
Show file tree
Hide file tree
Showing 4 changed files with 157 additions and 28 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@ 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 (#2740)
- Container session token's `wildcard` field support (#2741)

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

Expand All @@ -24,6 +26,9 @@ raw binaries. All binaries have OS in their names as well now, following
regular naming used throughout NSPCC, so instead of neofs-cli-amd64 you get
neofs-cli-linux-amd64 now.

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

### Added
Expand Down
9 changes: 6 additions & 3 deletions cmd/neofs-cli/modules/acl/extended/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,11 @@ Filter consists of <typ>:<key><match><value>
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,
Expand All @@ -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,
}
Expand Down
75 changes: 65 additions & 10 deletions cmd/neofs-cli/modules/util/acl.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"encoding/hex"
"errors"
"fmt"
"math/big"
"strings"
"text/tabwriter"

Expand Down Expand Up @@ -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"))

Check warning on line 153 in cmd/neofs-cli/modules/util/acl.go

View check run for this annotation

Codecov / codecov/patch

cmd/neofs-cli/modules/util/acl.go#L144-L153

Added lines #L144 - L153 were not covered by tests
case eacl.MatchUnknown:
}

Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down
96 changes: 81 additions & 15 deletions cmd/neofs-cli/modules/util/acl_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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=<value", "key", eacl.MatchStringEqual, "<value"},
{"key!=value", "key", eacl.MatchStringNotEqual, "value"},
{"key=!value", "key", eacl.MatchStringEqual, "!value"},
Expand All @@ -54,17 +57,80 @@ func TestParseKVWithOp(t *testing.T) {
}{
{"", "missing op"},
{"!", "missing op"},
{">", "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"},
{"k<v", "missing op"},
{"k>v", "missing op"},
{"k<v", "invalid base-10 integer value \"v\" for attribute \"k\""},
{"k<=v", "invalid base-10 integer value \"v\" for attribute \"k\""},
{"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})
}
}
}
})
}

0 comments on commit df54b5b

Please sign in to comment.