From ed63a7bfb14101546bf32bf89f5c73e7f202b295 Mon Sep 17 00:00:00 2001 From: Leonard Lyubich Date: Tue, 18 Feb 2025 19:06:38 +0300 Subject: [PATCH 1/4] object: Deprecate CID and OID search filters Follow https://github.com/nspcc-dev/neofs-api/commit/819d651e66f8f5cf604cc678fa1921b1ec286b06. Signed-off-by: Leonard Lyubich --- client/object_search.go | 23 +++++++++++++++++++++-- client/object_search_test.go | 13 +++++++++++++ object/search.go | 6 ++++-- 3 files changed, 38 insertions(+), 4 deletions(-) diff --git a/client/object_search.go b/client/object_search.go index 62460499..e23ee29f 100644 --- a/client/object_search.go +++ b/client/object_search.go @@ -74,7 +74,8 @@ func (x *SearchObjectsOptions) SetCount(count uint32) { x.count = count } // operation from. To start the search anew, pass an empty cursor. // // Max number of filters is 8. Max number of attributes is 8. If attributes are -// specified, filters must include the 1st of them. +// specified, filters must include the 1st of them. Neither filters nor +// attributes can contain [object.FilterContainerID] or [object.FilterID]. // // Note that if requested attribute is missing in the matching object, // corresponding element in its [SearchResultItem.Attributes] is empty. @@ -106,9 +107,13 @@ func (c *Client) SearchObjects(ctx context.Context, cnr cid.ID, filters object.S return nil, "", err } for i := range attrs { - if attrs[i] == "" { + switch attrs[i] { + case "": err = fmt.Errorf("empty attribute #%d", i) return nil, "", err + case object.FilterContainerID, object.FilterID: + err = fmt.Errorf("prohibited attribute %s", attrs[i]) + return nil, "", err } for j := i + 1; j < len(attrs); j++ { if attrs[i] == attrs[j] { @@ -122,6 +127,12 @@ func (c *Client) SearchObjects(ctx context.Context, cnr cid.ID, filters object.S return nil, "", err } } + for i := range filters { + if err = verifySearchFilter(filters[i]); err != nil { + err = fmt.Errorf("invalid filter #%d: %w", i, err) + return nil, "", err + } + } if opts.count == 0 { opts.count = maxSearchObjectsCount @@ -234,6 +245,14 @@ func (c *Client) SearchObjects(ctx context.Context, cnr cid.ID, filters object.S return res, resp.Body.Cursor, nil } +func verifySearchFilter(f object.SearchFilter) error { + switch attr := f.Header(); attr { + case object.FilterContainerID, object.FilterID: + return fmt.Errorf("prohibited attribute %s", attr) + } + return nil +} + // PrmObjectSearch groups optional parameters of ObjectSearch operation. type PrmObjectSearch struct { sessionContainer diff --git a/client/object_search_test.go b/client/object_search_test.go index a7cced5d..2a89ce3f 100644 --- a/client/object_search_test.go +++ b/client/object_search_test.go @@ -1076,6 +1076,17 @@ func TestClient_SearchObjects(t *testing.T) { _, _, err := okConn.SearchObjects(ctx, anyCID, fs, as, anyRequestCursor, anyValidSigner, anyValidOpts) require.EqualError(t, err, `attribute "a1" is requested but not filtered`) }) + t.Run("prohibited", func(t *testing.T) { + var fs object.SearchFilters + fs.AddFilter("attr", "val", object.MatchStringEqual) + fs.AddObjectContainerIDFilter(object.SearchMatchType(rand.Int31()), cidtest.ID()) + _, _, err := okConn.SearchObjects(ctx, anyCID, fs, anyValidAttrs, anyRequestCursor, anyValidSigner, anyValidOpts) + require.EqualError(t, err, "invalid filter #1: prohibited attribute $Object:containerID") + fs = fs[:1] + fs.AddObjectIDFilter(object.SearchMatchType(rand.Int31()), oidtest.ID()) + _, _, err = okConn.SearchObjects(ctx, anyCID, fs, anyValidAttrs, anyRequestCursor, anyValidSigner, anyValidOpts) + require.EqualError(t, err, "invalid filter #1: prohibited attribute $Object:objectID") + }) }) t.Run("attributes", func(t *testing.T) { for _, tc := range []struct { @@ -1085,6 +1096,8 @@ func TestClient_SearchObjects(t *testing.T) { {name: "empty", err: "empty attribute #1", as: []string{"a1", "", "a3"}}, {name: "duplicated", err: `duplicated attribute "a2"`, as: []string{"a1", "a2", "a3", "a2"}}, {name: "limit exceeded", err: "more than 8 attributes", as: []string{"a1", "a2", "a3", "a4", "a5", "a6", "a7", "a8", "a9"}}, + {name: "prohibited/CID", err: "prohibited attribute $Object:containerID", as: []string{object.FilterContainerID}}, + {name: "prohibited/OID", err: "prohibited attribute $Object:objectID", as: []string{object.FilterID}}, } { t.Run(tc.name, func(t *testing.T) { _, _, err := okConn.SearchObjects(ctx, anyCID, anyValidFilters, tc.as, anyRequestCursor, anyValidSigner, anyValidOpts) diff --git a/object/search.go b/object/search.go index 46abe426..83131617 100644 --- a/object/search.go +++ b/object/search.go @@ -155,8 +155,8 @@ type SearchFilters []SearchFilter const ( reservedFilterPrefix = "$Object:" FilterVersion = reservedFilterPrefix + "version" - FilterID = reservedFilterPrefix + "objectID" - FilterContainerID = reservedFilterPrefix + "containerID" + FilterID = reservedFilterPrefix + "objectID" // Deprecated. + FilterContainerID = reservedFilterPrefix + "containerID" // Deprecated. FilterOwnerID = reservedFilterPrefix + "ownerID" FilterPayloadChecksum = reservedFilterPrefix + "payloadHash" FilterType = reservedFilterPrefix + "objectType" @@ -252,6 +252,7 @@ func (f *SearchFilters) AddObjectVersionFilter(op SearchMatchType, v version.Ver // AddObjectContainerIDFilter adds a filter by container id. // // The m must not be numeric (like [MatchNumGT]). +// Deprecated: must not be used. func (f *SearchFilters) AddObjectContainerIDFilter(m SearchMatchType, id cid.ID) { f.addFilter(m, FilterContainerID, id.EncodeToString()) } @@ -321,6 +322,7 @@ func (f *SearchFilters) AddParentIDFilter(m SearchMatchType, id oid.ID) { // AddObjectIDFilter adds filter by object identifier. // // The m must not be numeric (like [MatchNumGT]). +// Deprecated: must not be used. func (f *SearchFilters) AddObjectIDFilter(m SearchMatchType, id oid.ID) { f.addFilter(m, FilterID, id.EncodeToString()) } From caa1acbfdc88a4970faad817e82df7ac17133176 Mon Sep 17 00:00:00 2001 From: Leonard Lyubich Date: Tue, 18 Feb 2025 19:18:23 +0300 Subject: [PATCH 2/4] client/object: Require 1st attribute to be the 1st filter in SearchV2 Follow https://github.com/nspcc-dev/neofs-api/commit/b5fdf47b106752686b1a3f8781a0bcf6706d8b25. Signed-off-by: Leonard Lyubich --- client/object_search.go | 7 +++---- client/object_search_test.go | 14 ++++++++------ 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/client/object_search.go b/client/object_search.go index e23ee29f..1090e02c 100644 --- a/client/object_search.go +++ b/client/object_search.go @@ -5,7 +5,6 @@ import ( "errors" "fmt" "io" - "slices" "time" "github.com/nspcc-dev/neofs-sdk-go/bearer" @@ -74,7 +73,7 @@ func (x *SearchObjectsOptions) SetCount(count uint32) { x.count = count } // operation from. To start the search anew, pass an empty cursor. // // Max number of filters is 8. Max number of attributes is 8. If attributes are -// specified, filters must include the 1st of them. Neither filters nor +// specified, the 1st filter must be about it. Neither filters nor // attributes can contain [object.FilterContainerID] or [object.FilterID]. // // Note that if requested attribute is missing in the matching object, @@ -122,8 +121,8 @@ func (c *Client) SearchObjects(ctx context.Context, cnr cid.ID, filters object.S } } } - if !slices.ContainsFunc(filters, func(f object.SearchFilter) bool { return f.Header() == attrs[0] }) { - err = fmt.Errorf("attribute %q is requested but not filtered", attrs[0]) + if len(filters) == 0 || filters[0].Header() != attrs[0] { + err = fmt.Errorf("1st attribute %q is requested but not filtered 1st", attrs[0]) return nil, "", err } } diff --git a/client/object_search_test.go b/client/object_search_test.go index 2a89ce3f..59c17ae4 100644 --- a/client/object_search_test.go +++ b/client/object_search_test.go @@ -1069,22 +1069,24 @@ func TestClient_SearchObjects(t *testing.T) { t.Run("missing 1st requested attribute", func(t *testing.T) { as := []string{"a1", "a2", "a3"} var fs object.SearchFilters - for i := range as[1:] { - fs.AddFilter(as[i+1], "any_val", 100) + for i := range as { + fs.AddFilter(as[i], "any_val", 100) } - _, _, err := okConn.SearchObjects(ctx, anyCID, fs, as, anyRequestCursor, anyValidSigner, anyValidOpts) - require.EqualError(t, err, `attribute "a1" is requested but not filtered`) + _, _, err := okConn.SearchObjects(ctx, anyCID, fs, as[1:], anyRequestCursor, anyValidSigner, anyValidOpts) + require.EqualError(t, err, `1st attribute "a2" is requested but not filtered 1st`) + _, _, err = okConn.SearchObjects(ctx, anyCID, nil, as[2:], anyRequestCursor, anyValidSigner, anyValidOpts) + require.EqualError(t, err, `1st attribute "a3" is requested but not filtered 1st`) }) t.Run("prohibited", func(t *testing.T) { var fs object.SearchFilters fs.AddFilter("attr", "val", object.MatchStringEqual) fs.AddObjectContainerIDFilter(object.SearchMatchType(rand.Int31()), cidtest.ID()) - _, _, err := okConn.SearchObjects(ctx, anyCID, fs, anyValidAttrs, anyRequestCursor, anyValidSigner, anyValidOpts) + _, _, err := okConn.SearchObjects(ctx, anyCID, fs, []string{"attr"}, anyRequestCursor, anyValidSigner, anyValidOpts) require.EqualError(t, err, "invalid filter #1: prohibited attribute $Object:containerID") fs = fs[:1] fs.AddObjectIDFilter(object.SearchMatchType(rand.Int31()), oidtest.ID()) - _, _, err = okConn.SearchObjects(ctx, anyCID, fs, anyValidAttrs, anyRequestCursor, anyValidSigner, anyValidOpts) + _, _, err = okConn.SearchObjects(ctx, anyCID, fs, []string{"attr"}, anyRequestCursor, anyValidSigner, anyValidOpts) require.EqualError(t, err, "invalid filter #1: prohibited attribute $Object:objectID") }) }) From 66489a1edd1f90dbf523c81d68d5fd8c0e2742a1 Mon Sep 17 00:00:00 2001 From: Leonard Lyubich Date: Tue, 18 Feb 2025 19:21:25 +0300 Subject: [PATCH 3/4] client/object: Prohibit empty filter attributes in SearchV2 They are invalid according to the protocol, so no sense to pass them. Signed-off-by: Leonard Lyubich --- client/object_search.go | 2 ++ client/object_search_test.go | 7 +++++++ 2 files changed, 9 insertions(+) diff --git a/client/object_search.go b/client/object_search.go index 1090e02c..7b1e45d7 100644 --- a/client/object_search.go +++ b/client/object_search.go @@ -246,6 +246,8 @@ func (c *Client) SearchObjects(ctx context.Context, cnr cid.ID, filters object.S func verifySearchFilter(f object.SearchFilter) error { switch attr := f.Header(); attr { + case "": + return errors.New("missing attribute") case object.FilterContainerID, object.FilterID: return fmt.Errorf("prohibited attribute %s", attr) } diff --git a/client/object_search_test.go b/client/object_search_test.go index 59c17ae4..da0c9428 100644 --- a/client/object_search_test.go +++ b/client/object_search_test.go @@ -1089,6 +1089,13 @@ func TestClient_SearchObjects(t *testing.T) { _, _, err = okConn.SearchObjects(ctx, anyCID, fs, []string{"attr"}, anyRequestCursor, anyValidSigner, anyValidOpts) require.EqualError(t, err, "invalid filter #1: prohibited attribute $Object:objectID") }) + t.Run("empty", func(t *testing.T) { + var fs object.SearchFilters + fs.AddFilter("attr", "val", object.MatchStringEqual) + fs.AddFilter("", "val", object.SearchMatchType(rand.Int31())) + _, _, err := okConn.SearchObjects(ctx, anyCID, fs, []string{"attr"}, anyRequestCursor, anyValidSigner, anyValidOpts) + require.EqualError(t, err, "invalid filter #1: missing attribute") + }) }) t.Run("attributes", func(t *testing.T) { for _, tc := range []struct { From 2e7585315319b6b40c7d4f70fc9797c655a846bf Mon Sep 17 00:00:00 2001 From: Leonard Lyubich Date: Tue, 18 Feb 2025 19:32:34 +0300 Subject: [PATCH 4/4] client/object: Add checks for ROOT and PHY props in SearchV2 These filters are key-only, no sense to pass them otherwise. Signed-off-by: Leonard Lyubich --- client/object_search.go | 9 +++++++++ client/object_search_test.go | 19 +++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/client/object_search.go b/client/object_search.go index 7b1e45d7..8a98dc4b 100644 --- a/client/object_search.go +++ b/client/object_search.go @@ -75,6 +75,8 @@ func (x *SearchObjectsOptions) SetCount(count uint32) { x.count = count } // Max number of filters is 8. Max number of attributes is 8. If attributes are // specified, the 1st filter must be about it. Neither filters nor // attributes can contain [object.FilterContainerID] or [object.FilterID]. +// Filters by [object.FilterRoot] and [object.FilterPhysical] properties must +// have zero value and matcher. // // Note that if requested attribute is missing in the matching object, // corresponding element in its [SearchResultItem.Attributes] is empty. @@ -250,6 +252,13 @@ func verifySearchFilter(f object.SearchFilter) error { return errors.New("missing attribute") case object.FilterContainerID, object.FilterID: return fmt.Errorf("prohibited attribute %s", attr) + case object.FilterRoot, object.FilterPhysical: + if m := f.Operation(); m != 0 { + return fmt.Errorf("non-zero matcher %s for attribute %s", m, attr) + } + if val := f.Value(); val != "" { + return fmt.Errorf("value for attribute %s is prohibited", attr) + } } return nil } diff --git a/client/object_search_test.go b/client/object_search_test.go index da0c9428..60ad914f 100644 --- a/client/object_search_test.go +++ b/client/object_search_test.go @@ -1096,6 +1096,25 @@ func TestClient_SearchObjects(t *testing.T) { _, _, err := okConn.SearchObjects(ctx, anyCID, fs, []string{"attr"}, anyRequestCursor, anyValidSigner, anyValidOpts) require.EqualError(t, err, "invalid filter #1: missing attribute") }) + t.Run("key-only", func(t *testing.T) { + var fs object.SearchFilters + fs.AddFilter("attr", "val", object.MatchStringEqual) + fs.AddFilter(object.FilterRoot, "", 123) + _, _, err := okConn.SearchObjects(ctx, anyCID, fs, []string{"attr"}, anyRequestCursor, anyValidSigner, anyValidOpts) + require.EqualError(t, err, "invalid filter #1: non-zero matcher 123 for attribute $Object:ROOT") + fs = fs[:1] + fs.AddFilter(object.FilterRoot, "val", 0) + _, _, err = okConn.SearchObjects(ctx, anyCID, fs, []string{"attr"}, anyRequestCursor, anyValidSigner, anyValidOpts) + require.EqualError(t, err, "invalid filter #1: value for attribute $Object:ROOT is prohibited") + fs = fs[:1] + fs.AddFilter(object.FilterPhysical, "", 123) + _, _, err = okConn.SearchObjects(ctx, anyCID, fs, []string{"attr"}, anyRequestCursor, anyValidSigner, anyValidOpts) + require.EqualError(t, err, "invalid filter #1: non-zero matcher 123 for attribute $Object:PHY") + fs = fs[:1] + fs.AddFilter(object.FilterPhysical, "val", 0) + _, _, err = okConn.SearchObjects(ctx, anyCID, fs, []string{"attr"}, anyRequestCursor, anyValidSigner, anyValidOpts) + require.EqualError(t, err, "invalid filter #1: value for attribute $Object:PHY is prohibited") + }) }) t.Run("attributes", func(t *testing.T) { for _, tc := range []struct {