Skip to content

fix(auth): report why credentials failed, and as whom (0.22.3) - #183

Merged
johnyaku merged 3 commits into
mainfrom
fix/informative-credential-errors
Aug 17, 2026
Merged

johnyaku merged 3 commits into
mainfrom
fix/informative-credential-errors

Conversation

@johnyaku

Copy link
Copy Markdown
Contributor

The report that prompted this

$ dt auth credentials install bcarc_wts
Error: No credentials were installed (no usable secrets found).

That is the entire diagnostic output. Five distinct causes collapse into that one sentence — denied, absent, empty, legacy DVC-INI format, unrecognised format — and each was printed only under -v. The reason was known at the point of failure and thrown away.

A denial is not self-explanatory either. GCP returns PERMISSION_DENIED for secrets that do not exist, and dt may be authenticating as a service account inherited from GOOGLE_APPLICATION_CREDENTIALS rather than as the human — in which case "I already granted her access" and "permission denied" are simultaneously true. We lost days to exactly that.

Now

Authenticating to GCP as: service account ctp-archive@ctp-archive.iam.gserviceaccount.com
  (from $GOOGLE_APPLICATION_CREDENTIALS=/g/data/a56/dvc/config/ctp-archive-...json)
Reading secrets from project: bcarc-489101
  ⚠ This is a service account, not your user account -- IAM grants made to your
    email do not apply to it.
    Unset GOOGLE_APPLICATION_CREDENTIALS to authenticate as yourself instead.
  ⚠ bcarc_wts: Cannot read secret 'dvc-remote-bcarc_wts' in project 'bcarc-489101'.

No -v required.

Changes

  • Failure reasons print unconditionally. A failure reason is not verbose-level information.
  • The summary AuthError repeats every reason and the identity. stdout gets piped away, scrolled past, and pasted selectively by collaborators — the exception alone now suffices.
  • GCPIdentity resolves who we authenticate as in true ADC precedence: GOOGLE_APPLICATION_CREDENTIALS → ADC file → the gcloud CLI account used by the fallback. Files and environment only, no network call, so it is cheap enough to report every time.
  • _get_secret_backend hands over the account it already resolved, so reporting costs no second gcloud auth list (~20s cold on NCI).
  • auth_setup no longer claims "No S3 endpoints" when endpoints exist but GCP auth failed. It told users their repo had no S3 remote when it plainly did, and invited them to dismiss the real error printed directly underneath.
  • The GCP auth warning in auth_setup prints without -v, matching the error already surfaced in the report.

A deliberate omission

For user ADC we report user login (from ADC file ...; the file does not record which account) rather than an email. gcloud auth application-default login genuinely does not record one. The tempting fix — showing the gcloud auth list account — would be wrong: that is a different identity, and the case where they diverge is precisely the bug worth catching.

Verification

  • 39 new tests in tests/unit/test_credential_error_reporting.py
  • Full suite on a compute node: 2208 passed, 2 skipped, 0 failed
  • Exercised end-to-end against a simulated off-NCI box (empty XDG_CONFIG_DIRS, isolated HOME, no gcloud login) and against all three real identity paths on Gadi: GAC service account, user ADC, and CLI fallback.

🤖 Generated with Claude Code

johnyaku and others added 3 commits August 17, 2026 19:45
`dt auth credentials install bcarc_wts` answered a real failure with:

    Error: No credentials were installed (no usable secrets found).

Five causes collapse into that sentence -- denied, absent, empty, legacy
format, unrecognised format -- and every one was printed only under -v.
The reason was known and discarded, so diagnosing it took a round trip
through a collaborator on another continent.

A denial is also uninterpretable on its own. GCP returns PERMISSION_DENIED
for secrets that do not exist, and dt may be authenticating as a service
account inherited from GOOGLE_APPLICATION_CREDENTIALS rather than as the
human, in which case "I already granted her access" and "permission denied"
are both true. So report the identity on every fetch, not on request:

    Authenticating to GCP as: service account ctp-archive@... (from
      $GOOGLE_APPLICATION_CREDENTIALS=/g/data/a56/dvc/config/...json)
    Reading secrets from project: bcarc-489101
      ! This is a service account, not your user account -- IAM grants
        made to your email do not apply to it.

Changes:

* Per-repo failure reasons print unconditionally. A failure reason is not
  verbose-level information.
* The summary AuthError repeats every reason and the identity, so the
  exception alone is enough to diagnose from -- stdout gets piped away,
  and collaborators paste the last line.
* GCPIdentity resolves who we authenticate as in true ADC precedence
  (GOOGLE_APPLICATION_CREDENTIALS, then the ADC file, then the gcloud CLI
  account used by the fallback), from files only, no network call. It
  admits when the email is unknowable rather than showing the gcloud CLI
  account, which is a different identity and often a different person.
* _get_secret_backend hands its already-resolved gcloud account to the
  backend, so reporting the identity costs no second `gcloud auth list`
  (~20s cold on NCI).
* auth_setup no longer prints "No S3 endpoints -- credential install
  skipped" when endpoints exist but GCP auth failed. It said the repo had
  no S3 remote when it plainly did, inviting users to dismiss the real
  error printed underneath.
* The GCP auth warning in auth_setup prints without -v, matching the
  error already surfaced in the report.

2208 passed, 2 skipped.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two ways a GCP denial stayed invisible.

1. The Python client's PermissionDenied was swallowed whenever the gcloud
   CLI fallback succeeded. Verified against the real backend: reading
   dvc-remote-bcarc_wts from bcarc-489101 as the ctp-archive service
   account is refused, the fallback rescues it as the human, and the user
   sees nothing at all. The broken ADC identity stays in place until it is
   used somewhere with no fallback -- a container without gcloud -- where
   it resurfaces as a mystery.

2. When the fallback was *also* refused, only the CLI's denial was raised.
   The library denial, under a different identity, was dropped. Showing
   one account invites "but I granted that account access" about the
   other.

Now every denial is echoed as it happens, naming the identity refused:

    ! Permission denied reading secret 'dvc-remote-bcarc_wts' from GCP
      project 'bcarc-489101'
        authenticated as: service account ctp-archive@... (from
          $GOOGLE_APPLICATION_CREDENTIALS=/g/data/a56/dvc/config/...json)
        This is a service account, not your user account -- IAM grants
        made to your email do not apply to it.
          Unset GOOGLE_APPLICATION_CREDENTIALS to authenticate as
          yourself instead.
        retrying via the gcloud CLI, which may use a different account ...

and when both are refused the error names both, via "Refused for:" and
"Also refused for:".

_cli_identity_desc reports the *CLI* account, which is deliberately
distinct from GCPIdentity: when both exist they are often different
accounts, and that divergence is the whole reason to attach an identity
to a denial. It reuses the cached account where possible so an error path
costs no extra `gcloud auth list`.

NOT_FOUND is untouched -- it is not a denial and does not echo one.

2222 passed, 2 skipped.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Three things.

1. Denial echoes and per-repo failure reasons move to stderr. They are
   diagnostics and must survive `dt ... > out` or `| head`, which is how
   they get lost before reaching whoever is helping.

2. `dt auth credentials list` is now suggested wherever a read fails --
   the denied message, both NOT_FOUND messages, and the install summary.
   It answers the question the messages structurally cannot: a denial and
   an absence are the same PERMISSION_DENIED, so a secret named
   differently from its repo, or a typo, is indistinguishable from a
   missing grant until you list what is actually visible.

3. Audited the rest of `dt auth credentials`. migrate, set, status and
   uninstall already converted SecretError to AuthError and were fine.
   Two were not:

   * `list` raised a bare "gcloud error listing secrets: <stderr>" with no
     project, no identity, and no handling of PERMISSION_DENIED. It now
     names both and points out that secretmanager.secrets.list is a
     separate grant from reading any single secret -- being able to read
     one does not imply being able to list.

   * `check` reported "secret not found" whenever existence could not be
     determined, including denials. That sends people off to create a
     secret that already exists. It now distinguishes "could not confirm
     the secret exists" from a genuine absence, and suggests listing in
     the latter case.

`list` output also names the project it read, because secrets.gcp.project
can differ by config scope between two people on one machine, and a bare
list of names hides which project produced it.

`install`'s summary said "Not found" for every failure; failures may be
denials, empty secrets, or wrong formats. Now "Failed".

2239 passed, 2 skipped.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@johnyaku
johnyaku merged commit f4faf18 into main Aug 17, 2026
1 check passed
@johnyaku
johnyaku deleted the fix/informative-credential-errors branch August 17, 2026 12:01
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