Repository navigation
Run plugin owned index access as the plugin subject - #5838
DarshitChanpura wants to merge 7 commits into
Conversation
The client wraps the node client and routes doExecute through the subject assigned by IdentityAwarePlugin, so the plugin can reach the system indices it registers without running as the authenticated user. Lives in the opensearch module, which is the module that owns the OpenSearch-server-facing client code and is already on the compile classpath of every call site that needs it. Signed-off-by: Darshit Chanpura <dchanp@amazon.com>
SQLPlugin implements IdentityAwarePlugin and hands the assigned subject to a PluginClient built around the node client. The three places that reached their own indices by stashing the thread context now take that client instead: - OpenSearchDataSourceMetadataStorage, for .ql-datasources - StateStore, for .query_execution_request_<datasource> - IndexCleanup, for the retention purge driven by FlintIndexRetention Their client fields are typed PluginClient so a raw node client cannot be wired in by mistake. The query paths that serve user data keep using the node client, since they are meant to run with the caller's privileges. Signed-off-by: Darshit Chanpura <dchanp@amazon.com>
A plugin subject is granted every index action on the system indices the plugin registers, so the single document reads and writes need nothing declared. Two things fall outside that and are only granted through this file. Delete-by-query drives itself with a scroll, and security classifies scroll and clear-scroll as cluster permissions even though they read an index. The async query result index is written by the Spark job and read back by the user, so it is not a system index. FlintIndexRetention purges it on the cluster manager where there is no user to act for, and outside its own system indices the plugin subject is evaluated like any other role. Signed-off-by: Darshit Chanpura <dchanp@amazon.com>
PR Code Analyzer ❗AI-powered 'Code-Diff-Analyzer' found issues on commit dd2a5f4.
The table above displays the top 10 most important findings. Pull Requests Author(s): Please update your Pull Request according to the report above. Repository Maintainer(s): You can Thanks. |
PR Reviewer Guide 🔍(Review updated until commit ca64ad2)Here are some key observations to aid the review process:
|
PR Code Suggestions ✨Latest suggestions up to ca64ad2
Previous suggestionsSuggestions up to commit dd2a5f4
|
Codecov Report❌ Patch coverage is
❌ Your project check has failed because the head coverage (75.28%) is below the target coverage (99.00%). You can increase the head coverage or adjust the target coverage. Additional details and impacted files@@ Coverage Diff @@
## main #5838 +/- ##
============================================
+ Coverage 75.19% 75.28% +0.08%
- Complexity 10362 10383 +21
============================================
Files 962 963 +1
Lines 41098 41107 +9
Branches 4683 4687 +4
============================================
+ Hits 30905 30948 +43
+ Misses 8896 8862 -34
Partials 1297 1297
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Resolves the SQLPlugin conflict by keeping both the plugin client and the remote cluster service provider. Signed-off-by: Darshit Chanpura <dchanp@amazon.com>
The listener is now wrapped with ContextPreservingActionListener, so it runs in the caller's context, keeps the response headers the action added, and leaves the responding thread's own context in place. The previous newStoredContext and runBefore form left the caller's context on the responding thread and dropped response headers. A synchronous failure is no longer caught and sent to the listener. It is thrown to the caller as the wrapped client throws it, so a listener that throws is not notified a second time. The subject is typed PluginSubject, can be assigned once, and rejects null and a different subject. getRemoteClusterClient is unsupported, since a plugin subject has no meaning on another cluster; nothing in the plugin calls it. The class is final. The test helper now uses the subject core assigns when security is not installed, which stashes the context around each action, instead of a subject that ran the action inline. Signed-off-by: Darshit Chanpura <dchanp@amazon.com>
Security checks a shard bulk against the action of each item in it, so the delete-by-query on query_execution_result* also needs indices:data/write/delete. Its refresh runs the shard level indices:admin/refresh[s] through the action filters, so the refresh grant becomes indices:admin/refresh*. Delete-by-query completes normally when searches or deletes in it were rejected and lists them in the response. IndexCleanup now fails the listener when it lists any, so a rejected purge is logged as an error by FlintIndexRetention instead of reading as zero deleted docs. A version conflict is not counted, because the doc was written after the search found it and is not stale. Signed-off-by: Darshit Chanpura <dchanp@amazon.com>
The separate ActionFuture variables only existed to keep the call inside the stashed context's scope. The new imports are moved into sorted position. Signed-off-by: Darshit Chanpura <dchanp@amazon.com>
|
Persistent review updated to latest commit ca64ad2 |
Description
Replaces
ThreadContext.stashContextwith the plugin subject assigned throughIdentityAwarePlugin, so sql reaches the indices it owns as its plugin subject rather than from a stashed context that skips privilege evaluation.All 12 production stash calls were in three classes, each wrapping a call on the node client.
SQLPluginnow builds aPluginClientaround that client, hands it the subject inassignSubject, and passes it to the three:OpenSearchDataSourceMetadataStorage, 5 sites, reads and writes.ql-datasourcesStateStore, 6 sites, reads and writes.query_execution_request_<datasource>IndexCleanup, 1 site, runs the delete-by-query behindFlintIndexRetentionPluginClientis a finalFilterClientthat runs each action insidePluginSubject.runAs:ContextPreservingActionListener.wrapPreservingContext, so it runs in the caller's thread context, keeps the response headers the action added, and leaves the responding thread's own context in place afterwards.getRemoteClusterClientis not supported, because a plugin subject has no meaning on another cluster. Nothing calls it on this client;RemoteMappingsReaderuses the node client.PluginClientlives in theopensearchmodule, which already owns the server facing client code and is already on the compile classpath of all three call sites and of the plugin entry point, so this adds no module dependency. It is in its own commit so that moving to a shared class incommon-utils, when that publishes, is a delete plus an import change.The three client fields are typed
PluginClientrather thanClient, so a plain node client cannot be wired in by mistake. Nothing else moves. sql queries user indices by design, and the paths that do that, includingOpenSearchJobExecutionResponseReaderreading a query's results andOpenSearchAsyncQuerySchedulerwriting a job document, keep the node client and keep running with the caller's privileges.Ordering:
assignSubjectruns aftercreateComponents, so the client is built first and the subject arrives second. Nothing calls through it in between. The only index workcreateComponentsdoes iscreateDataSourceon the default datasource, which returns before it reaches storage, and if that ever changed the client would throw rather than run without a subject.Index coverage and the permissions file
.ql-datasourcesis registered throughgetSystemIndexDescriptors, and.query_execution_request_<datasource>is covered by the registered.query_execution_request*pattern. A plugin subject is allowed every index action on its own registered system indices, so the gets, searches, indexes, updates, deletes and index creations need nothing declared.IndexCleanupis also driven againstquery_execution_result*, which is not registered and should not be: the Spark job writes that index and the user reads their own results back from it, so making it a system index would hide it from both. On that pattern the subject is evaluated like any other role, soplugin-additional-permissions.ymlgrants what the retention purge runs there:indices:data/write/delete/byquery, the request itselfindices:data/read/search, the search that opens its scroll. Its shard level phases are not evaluated again.indices:data/write/bulk*andindices:data/write/delete, for the shard level bulk that carries the deletes. Security checks a shard bulk against the action of each item in it as well as againstbulk[s].indices:admin/refresh*, forsetRefresh(true). The refresh runs the shard levelindices:admin/refresh[s]through the action filters, so the literalindices:admin/refreshalone would not cover it.indices:data/read/scrollandindices:data/read/scroll/clearare cluster permissions in security's classification even though they read an index, so they are listed undercluster_permissions. The top level bulk is cluster classified too, but security grants it to every plugin already.Every entry is a literal action or a wildcard. Action group names such as
readare not used, because they match no action in this file.Delete-by-query failures are now reported
Delete-by-query completes normally when searches or deletes inside it were rejected, and lists them in the response.
IndexCleanupread only the deleted count, so a rejected purge looked like a purge with nothing to delete. Under a stashed context privileges were not evaluated and this could not happen; under the plugin subject it can.IndexCleanupnow fails its listener when the response lists a search or delete failure, andFlintIndexRetentionlogs it as an error and continues with the next index, as it already does for other failures. A version conflict is not counted: it means the document was written after the search found it, so it is not stale.Other behaviour checked for this change
StateStorecreates and updates session, statement, job metadata and Flint index state documents and later reads them back by id, for exampleOpenSearchSessionStorageService.getSession,OpenSearchStatementStorageService.getStatement,OpenSearchAsyncQueryJobMetadataStorageService.getJobMetadataandOpenSearchFlintIndexStateModelService.getFlintIndexStateModel, all throughStateStore.get. Under the plugin subject a get on a system index is evaluated and made non realtime.StateStore.getalready setsrefresh(true), and a non realtime get withrefresh(true)refreshes the shard before reading, so it still sees the write. EveryStateStorewrite also usesWAIT_UNTIL.OpenSearchDataSourceMetadataStoragereads only through search, after writes that useIMMEDIATE. So no read path changes, and no refresh workaround is added..ql-datasources,.query_execution_request*orquery_execution_result*is a resource index, so resource sharing filtering does not apply to any migrated search whether resource sharing is on or off. The counts behind the session and refresh job limits inDefaultLeaseManagerand the active session and statement gauges stay cluster wide.Testing
spotlessCheck, andbuildforopensearch,datasourcesandasync-query(compile, unit tests,jacocoTestCoverageVerification), plustestforplugin, pass. Unit test counts from the JUnit XML, against a baseline onmain:The delta is
PluginClientTest(12 tests) andIndexCleanupTest(6 tests).PluginClientTestcovers running inside the subject, theindex()andadmin()paths the stores use, the caller's context in the listener on success and on failure, the responding thread's context afterwards, response headers, a synchronous failure thrown and not sent to the listener, a throwing listener notified once, use before a subject is assigned, set once, null, and the remote cluster client. Each test was confirmed to fail on its own when the client is changed to the variant it guards against: action outsiderunAs, raw listener,newStoredContext(false)withrunBefore,newRestorableContext(false), a catch that sends failures to the listener, an uncheckedsetSubject, a forwardinggetRemoteClusterClient, no null check, no unassigned check, and anindex()that forwards straight to the wrapped client.IndexCleanupTestcovers the request shape, the deleted count, a rejected delete, a rejected search, an ignored version conflict and a transport failure. Its failure tests fail when the failure lists are ignored, and its conflict test fails when conflicts are not excluded.Not verified by execution: the permissions file. Unit tests do not go through the security plugin. This repository runs
integTestWithSecurityon every pull request, against a cluster with the security plugin installed and system indices enabled, but every test in that task is underorg.opensearch.sql.securityand none of them touch.ql-datasources, the async query state index or the retention cron. So nothing in CI exercises a migrated call site under security, and the entries in the permissions file are reasoned from security's evaluator rather than observed passing.The async-query tests that build a real
StateStore(StatementTest,InteractiveSessionTest,OpenSearchAsyncQueryJobMetadataStorageServiceTestand theAsyncQueryExecutorServiceSpecsubclasses) are JUnit 4. Thetesttask includes only the jupiter engine, and thejunit4task resolves to NO-SOURCE, so the 15 JUnit 4 classes in async-query do not run in CI, onmaineither. Run locally with the task temporarily pointed at the test classes, and not committed:StatementTest,InteractiveSessionTestandOpenSearchAsyncQueryJobMetadataStorageServiceTestpass, 27 of 27, against an in-memory cluster with the plugin client and core's no-op subject. TheSpecclasses and the house keeper tests fail with the same 76 failures on this branch and onmain, from a settings registration error in their setup. Wiring that task back up and fixing those classes is left for a separate change.Related
Tracking issue: opensearch-project/opensearch-plugins#238
RFC: opensearch-project/security#4439
Check List
--signoff.By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.