[ 
https://issues.apache.org/jira/browse/DRILL-8558?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=18130548#comment-18130548
 ] 

ASF GitHub Bot commented on DRILL-8558:
---------------------------------------

cgivre opened a new pull request, #3094:
URL: https://github.com/apache/drill/pull/3094

   # [DRILL-8558](https://issues.apache.org/jira/browse/DRILL-8558): Fix 
credential leak and cross-user OAuth token reuse
   
   ## Description
   
   Two related problems in how Drill stores and serves the credentials a 
storage plugin uses to authenticate at its data source.
   
   ### 1. `/credentials.json` disclosed every user's credentials
   
   `CredentialResources` is `@RolesAllowed(AUTHENTICATED_ROLE)`, deliberately: 
supplying your own data source credentials is not an admin action. But `GET 
/credentials.json` serialized whole `StoragePluginConfig` objects via 
`PluginConfigWrapper`, and a config publishes its `credentialsProvider`. For 
`PlainCredentialsProvider` that is the entire `userCredentials` map — **every** 
user's stored username and password in cleartext — plus shared secrets such as 
an OAuth `clientSecret` and `tokenURI`.
   
   The equivalent admin-only `/storage/{name}.json` refused the same caller, so 
a non-admin could read through the credentials endpoint what the storage 
endpoint denied them.
   
   Only `USER_TRANSLATION` plugins were exposed, since that is the endpoint's 
filter. The sibling `/credentials{group}-plugins.json` looks like it would 
serve every plugin via `group=all`, but its path regex captures the leading 
slash, so `pluginGroup` is `"/all"`, `trim()` does not strip it, and the switch 
always falls through to an empty list.
   
   **Fix:** return a new `CredentialPluginInfo` carrying only the fields a 
caller needs to manage their own credentials — plugin name, enabled state, 
OAuth flag, the pre-built authorize URL, and whether *this caller* has 
credentials stored, plus their own stored username. Never a password, never 
another user's anything, never the config.
   
   The server-rendered `/credentials` page keeps the full configs. They never 
leave the server, and `StoragePluginModel` already scopes 
`getUserName()`/`getPassword()` to the active user, so a user sees only their 
own.
   
   ### 2. One user's queries could run with another user's OAuth tokens
   
   `OAuthTokenProvider` cached a single `TokenRegistry` and honoured its 
`username` argument only on the first call:
   
   ```java
   public TokenRegistry getOauthTokenRegistry(String username) {
     if (oauthTokenRegistry == null) {
       initRemoteRegistries(username);   // username used only here
     }
     return oauthTokenRegistry;          // every later caller gets this one
   }
   ```
   
   Under `USER_TRANSLATION` every identity therefore shared one registry, which 
defeats user translation entirely: tokens written for one user were read by 
another, including on the query path (`registerSchemas` → 
`initializeOauthTokenTable(schemaConfig.getUserName())`).
   
   The storage plugins compounded it by caching the registry in a mutable 
field, re-pointed by whichever user planned a query most recently. 
`GoogleSheetsStoragePlugin` was worse than a race — it cached the authenticated 
`Sheets` and `Drive` services and the Google `DataStore` and returned the first 
caller's to every later caller regardless of who asked.
   
   **Fix:** resolve registries per identity in `OAuthTokenProvider`, and 
resolve the token table, data store and Google services per identity in the 
plugins, from the user the call site already holds (`queryUserName` in the HTTP 
schema factories, `schemaConfig.getUserName()` in `FileSystemSchemaFactory`).
   
   ### Also included
   
   - Non-admin writes to a plugin's **shared** OAuth tokens are refused with 
HTTP 403. Overwriting those changes the identity *all* users' queries run 
under. Because the `/storage/*` OAuth endpoints are deprecated in favour of the 
`/credentials/*` ones, admins and non-admins reach the same code, so the check 
lives in `OAuthRequests` rather than in a resource class. `updateAuthToken` now 
resolves the target *before* exchanging the authorization code, so a refused 
caller cannot make Drill spend the client secret or consume a single-use code.
   - Identity is resolved through `DrillUserPrincipal` rather than 
`SecurityContext`. With authentication disabled `WebServer` installs no Jetty 
security handler, so the security context has no principal at all, while 
queries still run as `anonymous`. Using the principal keeps a user translation 
plugin's REST writes pointed at the registry the query path actually reads.
   - The shared registry identity is now spelled consistently. GoogleSheets 
passed its Google-side placeholder name (`anonymous`) where a Drill registry 
identity was expected, sending a shared plugin's lookups to 
`oauth_tokens/anonymous` — a registry the REST endpoints never write to. 
`DrillDataStoreFactory` now carries the registry identity explicitly, separate 
from the Google store id.
   - `PersistentTokenTable.setRefreshToken` and `setExpiresIn` compared the new 
value against the **access** token. `setRefreshToken` also threw NPE when a 
refresh token was stored without an access token.
   - `TestUserTranslationInHttpPlugin#testQueryWithOAuth` stored tokens for 
`TEST_USER_2` and then queried via the default cluster client. It passed only 
because every identity shared one registry — it was asserting the bug. It now 
queries as the user whose tokens it stored.
   
   ## Documentation
   
   New [`docs/dev/OAuthTokenStorage.md`](docs/dev/OAuthTokenStorage.md): where 
each kind of credential lives, the registry layout, who may write what, the 
auth-disabled rules, and the orphaned-token-table limitation.
   
   ## Testing
   
   | Suite | Result |
   |---|---|
   | `CredentialPluginInfoTest` (new, 8 tests) | pass |
   | `OAuthTokenProviderTest` (new, 7 tests) | pass |
   | `TestUserTranslationInHttpPlugin` | 5/5 |
   | `TestOAuthProcess` | 3/3 |
   | `TestOAuthTokenUpdate` | 3/3 |
   | `mvn checkstyle:check -pl exec/java-exec` | 0 violations |
   | Full reactor build | success |
   
   `CredentialPluginInfoTest` asserts the response carries no `config`, no 
`credentialsProvider`, no `userCredentials`, no password, no other user's 
username, and no shared secret. `OAuthTokenProviderTest` asserts registries are 
distinct per identity, that a shared write does not clobber a user's tokens, 
and that `anonymous` is a per-user registry rather than the shared one.
   
   ## Reviewer notes
   
   **GoogleSheets changes are not verified by tests.** 
`GoogleSheetsStoragePlugin` is the largest file in this diff and all three of 
its suites are `@Ignore("Requires live connection to Google Sheets")`. The 
changes mirror the HTTP plugin's, which is covered by the 11 passing tests 
above, but someone with Google credentials should run 
`TestGoogleSheetsQueries`, `TestGoogleSheetsWriter` and 
`TestGoogleSheetsLimitPushdown` before release. The dfs plugin's OAuth path 
likewise has no test coverage in-tree.
   
   **Upgrade note for release.** An auth-disabled deployment with a 
`USER_TRANSLATION` OAuth plugin previously wrote tokens to the shared registry 
and will now read `oauth_tokens/anonymous`. Those users authorize once more; no 
data is lost.
   
   **Known limitation, deliberately not addressed.** Deleting a plugin cannot 
enumerate every user who held tokens for it, because the user is part of the 
persistent store *path* rather than a key inside one store 
(`PersistentAliasRegistry` does the latter and can enumerate). Per-user token 
tables for users inactive since the last restart are left behind. 
`OAuthTokenStorage.md` records the three options for a complete fix; all of 
them are a store format change and none can retroactively find tables already 
orphaned.
   
   🤖 Generated with [Claude Code](https://claude.com/claude-code)
   




> Fix Credential Race Condition and Credential Leak
> -------------------------------------------------
>
>                 Key: DRILL-8558
>                 URL: https://issues.apache.org/jira/browse/DRILL-8558
>             Project: Apache Drill
>          Issue Type: Bug
>          Components: Storage - GoogleSheets, Storage - HTTP
>    Affects Versions: 1.22.0
>            Reporter: Charles Givre
>            Assignee: Charles Givre
>            Priority: Major
>             Fix For: 1.23.0
>
>




--
This message was sent by Atlassian Jira
(v10.3.26#10030026)

Reply via email to