fix(keyring): store all tokens in one keyring item (single Keychain prompt) - #7
Merged
Merged
Conversation
The keyring backend stored each workspace as its own Keychain item
(token:<team_id>) plus separate default/workspaces items. macOS authorizes
Keychain access per item, so listing workspaces or resolving -w read every
item and prompted for permission once PER WORKSPACE — a dozen-plus prompts
for users with many workspaces.
Consolidate all state into a single keyring item ('store') as one JSON
KeyringData blob (tokens map + default + workspace order), matching the
file-store shape. The blob is read once per process and cached, so:
- get_workspace_info / list / resolve -w / get_default_or_first = 1 read
- the user is prompted at most once (Always Allow silences it after)
Existing installs migrate transparently on first run (migrate_legacy):
the old per-workspace items are read one final time, consolidated, the
blob is persisted, and only THEN are legacy items deleted — so an
interrupted migration never loses tokens. The cold-start migration writes
via a lock-free write_entry to avoid re-locking the (non-reentrant) cache
mutex held by load().
Public API and CLI behavior are unchanged. Bumps to 0.2.1.
Comment on lines
+189
to
191
| if let Ok(mut guard) = cache().lock() { | ||
| *guard = Some(data.clone()); | ||
| } |
There was a problem hiding this comment.
Bug: The persist() function silently ignores failures when locking the in-memory cache, leading to a stale cache and causing all subsequent keyring operations to fail.
Severity: MEDIUM
Suggested Fix
The persist function should propagate the error when cache().lock() fails instead of silently ignoring it. The if let Ok(...) pattern should be replaced with error handling, such as using ? or map_err, to ensure the caller is notified that the cache update failed and can handle the inconsistent state.
Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent. Verify if this is a real issue. If it is, propose a fix; if not, explain why it's
not valid.
Location: src/auth/storage.rs#L189-L191
Potential issue: In the `persist` function, after successfully writing data to the
system keyring, the code attempts to update an in-memory cache. If locking the cache's
`Mutex` fails, for instance due to being poisoned by a panic in another thread during an
FFI call, the `if let Ok(...)` construct silently ignores the error. The function then
incorrectly returns `Ok(())`, creating a state inconsistency where the persisted data
and the in-memory cache are out of sync. Consequently, all subsequent keyring operations
for the lifetime of the process will fail when they try to access the poisoned cache.
Did we get this right? 👍 / 👎 to inform future reviews.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
On macOS, running a single command like
slack -w mlai-aus messages search ...triggered a dozen-plus Keychain permission prompts.Cause: the keyring backend stored each workspace as its own Keychain item (
token:<team_id>), plus separatedefaultandworkspacesitems. macOS authorizes Keychain access per item, so any operation that lists workspaces or resolves-w/SLACK_WORKSPACElooped over every workspace and read each item individually — one prompt each. Resolving-walso read the matched token a second time.Fix
Store all state in a single keyring item (
store) as one JSONKeyringDatablob ({ tokens, default, workspaces }) — the same shape the file-backed store already uses. The blob is read once per process and cached, so:get_workspace_info/list_workspaces/ resolve-w/get_default_or_first→ 1 readThat takes
slack -w <ws> …from ~21 Keychain reads down to 1–2.Migration (transparent, safe)
Existing installs are migrated on first run: the old per-workspace items are read one final time (the only remaining multi-prompt, once), consolidated into the blob, the blob is persisted first, and only then are the legacy items deleted — so an interrupted migration never loses tokens. The cold-start migration writes via a lock-free helper to avoid re-locking the non-reentrant cache mutex held during load (would otherwise deadlock).
Notes
KeyringStore/TokenStore) and all CLI behavior/flags are unchanged.SLACK_TOKEN_STORE_PATH) already worked this way — no change there.Verified: fmt, clippy (
-D warnings), docs (-D warnings), and full test suite withSLACK_INTEGRATION_TESTS=1all pass.🤖 Generated with pi
Need help on this PR? Tag
@codesmith-botwith what you need. Autofix is disabled.