Skip to content

feat(auth): surface credential-store failures and add ggshield auth status - #1268

Open
AGallouin wants to merge 3 commits into
GitGuardian:mainfrom
AGallouin:agallouin/keyring-failure-feedback
Open

feat(auth): surface credential-store failures and add ggshield auth status#1268
AGallouin wants to merge 3 commits into
GitGuardian:mainfrom
AGallouin:agallouin/keyring-failure-feedback

Conversation

@AGallouin

Copy link
Copy Markdown

Context

When the ggshield binary path changes (mise/asdf reshim, pyenv/pipx reinstall, Homebrew upgrade…), the macOS Keychain entry created by the previous binary can no longer be overwritten: writes fail with -25244 (errSecInvalidOwnerEdit) because the entry's ACL is tied to the old path. ggshield silently fell back to storing the token in cleartext in auth_config.yaml, with only a debug-level log. The availability probe could not catch this, since it writes a fresh key with no ACL conflict.

Fixes #1267.

What has been done

  • Failed credential-store writes are now surfaced to the user: AuthConfig.save() prints a warning with a humanized error (the -25244 case is translated to plain language), states that the token will stay in cleartext, and gives the copy-pasteable recovery commands (security delete-generic-password … + ggshield auth login). The fix advice is gated on the active keyring backend (not the OS), so a custom backend on macOS doesn't get Keychain-specific commands.
  • Successful cleartext → credential-store migrations are confirmed with an info message. Routine re-saves and fresh logins stay silent to avoid noise; the prior on-disk state is used to tell the two apart.
  • New ggshield auth status command diagnosing token storage, in text and --json:
    • reports the credential-store backend (macOS Keychain, Windows Credential Locker, Linux Secret Service, KWallet…), whether it is reachable, and where each instance's token actually lives (ok / failed / plaintext / disabled / skipped), with fix commands when actionable;
    • is strictly read-only: it never writes to the credential store (no token migration as a side effect, and no write-probe — a new is_reachable() read-only check is used instead of is_available());
    • never touches the credential store at all when GGSHIELD_NO_KEYRING is set (reachable is reported as null);
    • the diagnosis is read-centric: a token that reads back fine is ok even if overwriting its entry would fail, because reads are what every command needs at runtime — write failures are surfaced at save time instead.
    • JSON output has a stable shape (credential_store + per-instance {instance, status, message, fix}, with null for non-applicable fields) intended to be parseable by scripts; tests freeze the contract.

Differences from the issue's proposed fixes: instead of making is_available() probe real instance keys (which would put writes on every command's startup path), the conflict is detected where it matters — the save path warns on the actual failed write, and auth status verifies each real instance key with reads only. The diagnostic command lives under ggshield auth status rather than ggshield config keyring-status, and its output is ASCII-only for legacy Windows terminals.

Validation

Unit tests cover the new behavior (failure warning, migration message, silence on re-saves, read-only guarantee, JSON contract). To validate manually on macOS:

  • Authenticate: ggshield auth login, then run ggshield auth status — the instance should report token_storage: ok with the macOS Keychain location.
  • Simulate the ACL conflict: re-create the Keychain entry from another binary path (or reinstall ggshield via mise/brew so its path changes), then re-trigger a save (ggshield auth login) — a warning should explain the failure and print the security delete-generic-password fix; auth status should report plaintext with the same fix.
  • Apply the fix commands — auth status should report ok again, and the save should print the migration confirmation.
  • GGSHIELD_NO_KEYRING=1 ggshield auth status --json should report "reachable": null and not prompt for Keychain access.

PR check list

  • As much as possible, the changes include tests (unit and/or functional)
  • If the changes affect the end user (new feature, behavior change, bug fix) then the PR has a changelog entry (see doc/dev/getting-started.md). If the changes do not affect the end user, then the skip-changelog label has been added to the PR.

🤖 Generated with Claude Code

…tatus

When writing a token to the OS credential store failed (e.g. macOS error
-25244 after the ggshield binary path changed following a mise/asdf
reshim, a pyenv/pipx reinstall or a Homebrew upgrade), the token stayed
in cleartext in auth_config.yaml with only a hidden debug-level log.

- Saves now print an actionable warning with the humanized error and the
  commands fixing the stale Keychain entry
- Successful cleartext -> credential-store migrations are confirmed with
  an info message
- New `ggshield auth status` command (text and --json) diagnoses the
  credential-store backend, its reachability and where each instance
  token actually lives, without ever writing to the store

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@AGallouin
AGallouin requested a review from a team as a code owner June 11, 2026 09:28

@GG-HH GG-HH left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this,
While reviewing it, I wondered if we should have the keyring path depend on the installation path if it can only be accessed by that path ?

WDYT ?

Comment thread ggshield/cmd/auth/__init__.py Outdated
commands={
"login": login_cmd,
"logout": logout_cmd,
"status": auth_status_cmd,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is already a command ggshield api-status that i believe has the intended behavior. What I would suggest is to add an alias with ggshield auth status. WDYT ?

@AGallouin AGallouin Jun 12, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From what i understood, api-status is more "can i talk to GG, is my token valid ?" it does network calls and check the token no matter its origin (plain text, keyring on mac or other password managers).
auth status runs only locally, checks if we have a token and if it is correctly stored. It would be able to be enriched with other OS specific diagnostics.

I don't thing they server the same purpose, so I think that 2 commands is the right call. In a world full of LLMs, I would keep it really straightforward, one command, one scoped mission, simple actionnable output. If you think it will be more manually ("humanly") called than programmatically called, having an "overall dashboard" can make sense, it's more of a product call.

Comment thread ggshield/core/config/auth_config.py Outdated
@codecov

codecov Bot commented Jun 11, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 97.16599% with 7 lines in your changes missing coverage. Please review.
✅ Project coverage is 93.57%. Comparing base (1e6b8da) to head (21880a7).
⚠️ Report is 319 commits behind head on main.

Files with missing lines Patch % Lines
ggshield/cmd/status.py 97.59% 4 Missing ⚠️
ggshield/core/config/auth_config.py 94.44% 2 Missing ⚠️
ggshield/core/config/token_store.py 97.67% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1268      +/-   ##
==========================================
+ Coverage   93.38%   93.57%   +0.18%     
==========================================
  Files         189      189              
  Lines       10782    11291     +509     
==========================================
+ Hits        10069    10565     +496     
- Misses        713      726      +13     
Flag Coverage Δ
unittests 93.57% <97.16%> (+0.18%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Keyring tokens are also "stored"; the function specifically reads the
tokens as written in the config file. Addresses review feedback on GitGuardian#1268.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@AGallouin

AGallouin commented Jun 12, 2026

Copy link
Copy Markdown
Author

Thanks for this, While reviewing it, I wondered if we should have the keyring path depend on the installation path if it can only be accessed by that path ?

WDYT ?

You mean that instead of having (for example in mac):
keyring.set_password(KEYRING_SERVICE, instance_url, token)
we would have something like:
keyring.set_password(f"{installation_path}_{KEYRING_SERVICE}", instance_url, token)
Scoping it only to MacOS ?

I'm not a big fan for many reasons:

  • if you have many installs of ggshield on your computer (brew + mise) both would be able to have different passwords, different behaviors => not really clear for an end user, hard to debug / understand
  • usually, upgrading the software changes its path (for brew, it lives in /opt/homebrew/Cellar/ggshield/1.51.0), so upgrading it to 1.52 would mean: loosing your password (need to re auth) and leaving an orphaned valid token in the keyring. Not great from a security PoV. You would also erase all the ACLs (always allow behaviors) set by user.

In short, if I understood your point correctly, I'd lean toward keeping a single canonical entry, with the failure surfaced loudly and a one-time fix (what this PR does). But open to discussing it!
@GG-HH

@AGallouin
AGallouin requested a review from GG-HH June 12, 2026 12:00
Following review feedback on GitGuardian#1268, instead of a separate `auth status`
command, fold the token-storage diagnostic into the existing `api-status`
command and expose `auth status` as an alias of it.

- `api-status` gains a token-storage section (text) and a `token_storage`
  key (--json), reporting the credential-store backend, its reachability
  and where each instance token actually lives.
- The storage diagnostic is local and read-only, so it is shown even when
  the instance is unreachable or no usable token is configured: the API
  check now degrades to an error line instead of aborting.
- Exit code stays driven solely by the API check, so a token stored in
  plaintext never changes api-status' exit status (non-breaking).
- The alias is wired in ggshield.__main__ so cmd.auth does not import
  cmd.status (cmd.* packages are kept independent).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Keyring migration fails silently when Keychain entry ACL doesn't match current binary (e.g. mise installs)

2 participants