Skip to content

feat(cli): add the file credential store and the credential resolver (P2) - #245

Merged
xping-admin merged 4 commits into
feat/cli-authfrom
feat/cli-auth-02-file-store
Oct 6, 2026
Merged

xping-admin merged 4 commits into
feat/cli-authfrom
feat/cli-auth-02-file-store

Conversation

@xping-admin

@xping-admin xping-admin commented Oct 6, 2026 •

Copy link
Copy Markdown
Collaborator

Phase 2 of docs/internals/implementation-specs/cli-auth-cli-spec.md (§19). Merges into the feat/cli-auth integration branch, not main.

Implements contract §10.4 (credential storage) and spec §7.2–§7.7 and §8.1 for the file backend.

What changes

  • CredentialRecord: the §7.2 shape. IsValidFor(cloudUrl) applies the §7.7 corruption rules. ToString leaves both tokens out.
  • ICredentialStore, CredentialReadResult, CredentialStoreException.
  • FileCredentialStore: ~/.xping/credentials.json, one entry per Cloud URL.
    • 0600 file in a 0700 ~/.xping (an existing open one is tightened), owner-only ACL on Windows. Writes are atomic through PrivateFiles.
    • A file with any group or other bit is refused, and the result carries the §7.5 chmod message.
    • Corrupt entries read as nothing, with the §7.7 warning, and are left in place. Writes to other Cloud URLs keep them verbatim.
    • The tokens it reads are registered with Redaction.
  • CredentialStores + CredentialStoreSelector: read order keychain → file, write to the selected store, delete from all. Only the file backend exists until phase 5.
  • CredentialResolver: --api-key, then the stored login, then XPING_APIKEY / Xping:ApiKey, then none (A-8). It makes no network call. It reports ShadowedLogin and FallbackApiKey. FallbackToApiKey() gives the row-3 credential after an invalid login. A store that cannot be read counts as no login, with a warning.
  • Registered in AddXpingCliAuth. No command uses them yet (phase 3), so behaviour is unchanged.

Spec amendments (separate commit)

  • ReadAsync returns CredentialReadResult (record + warning). Corrupt and refused entries have to reach auth status and report as text.
  • CredentialStores owns the §7.4 read/write/delete rules.
  • An unsafe file's entries are never carried into a safe file. A write or delete over a refused file starts empty, so an entry someone else planted cannot become trusted at 0600.
  • §7.7 also treats a record as corrupt when cloudUrl doesn't match its key or dataGatewayUri is missing. The same goes for a whole file that won't parse.
  • ResolveAsync(CliConfiguration). The resolver decides where to fall back and the pipeline decides when, so "no fallback on network error" is tested in phase 6.

Tests

CredentialRecordTests, FileCredentialStoreTests (including the chmod 644 / group-write refusal), CredentialStoreSelectorTests, CredentialResolverTests. 70 new tests. All 1830 CLI tests pass on macOS. The credential tests also pass on Linux (mcr.microsoft.com/dotnet/sdk:10.0).

Review fixes

  • If the user can't open ~/.xping (for example it was left root-owned), the store now throws CredentialStoreException. Before, the mode check let a raw UnauthorizedAccessException escape.
  • CredentialStores keeps reading after a store fails, so a locked keychain no longer hides a file login. DeleteAllAsync tries every store before it reports the failures.
  • The resolver reports unreadable stores as StoreFailures, kept apart from corrupt-entry warnings. In phase 3, auth status uses this to exit 17 instead of 10. The spec was amended first (§3.4, §7.3, §7.4, §8.1).
  • Signing out leaves a file it can't parse (or one written by a newer CLI) untouched. Before, signing out of any Cloud URL deleted every login in it. A file other users can read is still removed.
  • Reads open the file with FileShare.Delete. On Windows, a reader without it made the replacing move fail, which could lose a rotated refresh token.
  • XpingHome.Display now checks for an empty home path before GetRelativePath, which threw on it.
  • CredentialStoresTests is its own class. Display is tested without the real HOME. Removed the dictionary copy, the second mode check per delete and the Lazy in the selector.

The regression tests for the unsearchable ~/.xping, the unparseable-file sign-out and the empty home path were checked to fail with their fixes reverted. The CredentialStores and StoreFailures tests use API added by the fix, so they could not be run against the old code. The Windows sharing test can only fail on Windows, and PR CI runs on Ubuntu.

🤖 Generated with Claude Code

xping-admin and others added 2 commits October 6, 2026 12:57
ReadAsync returns the warning with the result, CredentialStores owns the
read order, an unsafe file's entries are never carried into a safe one,
and the resolver takes CliConfiguration and leaves when-to-fall-back to
the authenticated pipeline.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…(P2)

CredentialRecord, ICredentialStore and FileCredentialStore keep sign-ins
in ~/.xping/credentials.json (0600 in a 0700 directory, owner-only ACL on
Windows, atomic replace). Files others can read are refused with chmod
instructions; corrupt entries read as nothing with a warning and are kept.
CredentialStores holds the keychain-then-file read order, and
CredentialResolver picks --api-key, the stored login, then the ambient
key, without a network call.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@codecov

codecov Bot commented Oct 6, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 94.40000% with 14 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/Xping.Cli/Auth/Store/FileCredentialStore.cs 92.24% 8 Missing and 1 partial ⚠️
src/Xping.Cli/Auth/CredentialResolver.cs 92.10% 1 Missing and 2 partials ⚠️
src/Xping.Cli/Auth/Store/ICredentialStore.cs 75.00% 2 Missing ⚠️
Files with missing lines Coverage Δ
src/Xping.Cli/Auth/Store/CredentialRecord.cs 100.00% <100.00%> (ø)
...rc/Xping.Cli/Auth/Store/CredentialStoreSelector.cs 100.00% <100.00%> (ø)
src/Xping.Cli/Auth/Store/CredentialStores.cs 100.00% <100.00%> (ø)
src/Xping.Cli/Auth/XpingHome.cs 92.30% <100.00%> (ø)
...c/Xping.Cli/Hosting/ServiceCollectionExtensions.cs 100.00% <100.00%> (ø)
src/Xping.Cli/Auth/Store/ICredentialStore.cs 75.00% <75.00%> (ø)
src/Xping.Cli/Auth/CredentialResolver.cs 92.10% <92.10%> (ø)
src/Xping.Cli/Auth/Store/FileCredentialStore.cs 92.24% <92.24%> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

xping-admin and others added 2 commits October 6, 2026 13:27
A failing credential store no longer stops the lookup or the sign-out,
an unreadable store is reported separately from a corrupt entry, and a
delete leaves a file it cannot parse untouched.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…iew (P2)

- A ~/.xping the user cannot search is a CredentialStoreException, not a
  raw UnauthorizedAccessException from the mode check.
- CredentialStores keeps reading after a store fails, so a locked keychain
  cannot hide a file login, and DeleteAllAsync tries every store before
  reporting the failures.
- The resolver reports unreadable stores as StoreFailures, apart from
  corrupt-entry warnings, so auth status can exit 17.
- Signing out leaves an unparseable file untouched instead of deleting
  every Cloud URL's sign-in.
- Reads share the file for delete, so on Windows a reader no longer makes
  the replacing move fail.
- XpingHome.Display checks for an empty profile before GetRelativePath.
- CredentialStoresTests split out; Display tested without the real HOME;
  no dictionary copy, one mode check per delete, no Lazy in the selector.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@xping-admin
xping-admin merged commit 6669f81 into feat/cli-auth Oct 6, 2026
2 checks passed
@xping-admin
xping-admin deleted the feat/cli-auth-02-file-store branch October 6, 2026 11:47
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.

1 participant