…r FGAC
The legacy (V1) SQL cursor continuation path built its SearchRequest with
`new SearchRequest()` (no indices). Under OpenSearch Security Fine-Grained
Access Control, an indices-less SearchRequest resolves to a wildcard during
authorization, so users who only have permissions on the queried index are
denied `indices:data/read/search` on the second and subsequent pages. Page 1
worked because the initial SearchRequest was built via SearchRequestBuilder
and carried the concrete indices.
Persist the resolved indices into DefaultCursor at page-1 time and pass them
to the continuation SearchRequest so Security authorizes against the same
concrete indices across all pages. The cursor payload uses a new short key
'x' and falls back to an empty array when absent, so cursor tokens issued by
pre-fix nodes continue to deserialize cleanly.
Regression coverage:
- Unit test for DefaultCursor round-trip including the legacy-without-x case.
- Integration test SQLCursorPermissionsIT that exercises V1 cursor paging
under an FGAC role scoped to a single index. The new test fails on HEAD
with 403 on page 2 and passes after the fix.
Signed-off-by: Eric Wei <mengwei.eric@gmail.com>
Summary
SearchRequestwithnew SearchRequest()(no indices). Under OpenSearch Security Fine-Grained Access Control, an indices-lessSearchRequestresolves to a wildcard during authorization, so users who only have permissions on the queried index get403 no permissions for [indices:data/read/search]on page 2 and later. Page 1 worked because the initialSearchRequestwas built viaSearchRequestBuilderand carried the concrete indices.DefaultCursorat page-1 time and scope the continuationSearchRequestwith them. Security now authorizes against the same concrete indices across every page.Root cause
PrettyFormatRestExecutor.buildProtocolWithPaginationcreates the page-1SearchRequestviaqueryAction.getRequestBuilder(), which carriesqueryAction.getSelect().getIndexArr(). Security sees concrete indices and grants.CursorResultExecutor.handleDefaultCursorRequestbuiltnew SearchRequest()with no indices and only aSearchSourceBuilder+ PIT id. Security resolves the empty indices array to the cluster-wide wildcard, evaluates the user's role against*, and denies.The PIT is correctly scoped on both page 1 and continuation; the denial happens in Security's pre-PIT authorization of the outer
SearchRequest.Fix
DefaultCursor: newString[] indicesfield, serialized under a new short cursor-payload key"x". Deserialization usesoptJSONArrayand defaults tonew String[0]when the key is absent, so cursor tokens issued by pre-fix nodes continue to deserialize cleanly after a rolling upgrade.PrettyFormatRestExecutor.createCursorWithPit: populatescursor.setIndices(queryAction.getSelect().getIndexArr())at page-1 time.CursorResultExecutor.handleDefaultCursorRequest: continuationSearchRequestnownew SearchRequest(cursor.getIndices()).Scope: only the legacy V1 cursor path changes. The V2 cursor path (
buildProtocolWithPaginationgoing throughSearchRequestBuilder.get()) already carried indices end-to-end and is untouched;PaginationITstays green.Back-compat
"x"field) deserialize withindices=[]. Those continuations still hit the original bug for FGAC users but behave exactly as they did before the fix, i.e. nothing gets worse during a rolling upgrade."x"and authorize correctly on every page.Test plan
./gradlew :legacy:spotlessCheck :integ-test:spotlessCheck./gradlew :legacy:test(all pass, including 4 newDefaultCursorTestcases: serialization of indices, null-indices default, round-trip, legacy-cursor-without-x back-compat)./gradlew build -x integTest -x integTestWithSecurity -x yamlRestTest(all module unit tests)./gradlew :integ-test:integTestWithSecurity --tests org.opensearch.sql.security.SQLCursorPermissionsIT(both tests pass)cursorPaginationUnderFgacSucceedsAcrossPagesfails with the exact customer symptom (403 no permissions for [indices:data/read/search]), then passes once the fix is restored../gradlew :integ-test:integTest --tests org.opensearch.sql.legacy.CursorIT(54/54 pass; serialization change is back-compat with existing V1 cursor flows)./gradlew :integ-test:integTest --tests org.opensearch.sql.sql.PaginationIT(24/24 pass; V2 cursor path unaffected)Files changed
legacy/src/main/java/org/opensearch/sql/legacy/cursor/DefaultCursor.javalegacy/src/main/java/org/opensearch/sql/legacy/executor/cursor/CursorResultExecutor.javalegacy/src/main/java/org/opensearch/sql/legacy/executor/format/PrettyFormatRestExecutor.javalegacy/src/test/java/org/opensearch/sql/legacy/unittest/cursor/DefaultCursorTest.javainteg-test/src/test/java/org/opensearch/sql/security/SQLCursorPermissionsIT.java(new)