Skip to content

feat: Add buf beta registry cargo credential provider#4541

Open
kjvalencik wants to merge 1 commit into
bufbuild:mainfrom
kjvalencik:kj/buf-cargo-credential-provider
Open

feat: Add buf beta registry cargo credential provider#4541
kjvalencik wants to merge 1 commit into
bufbuild:mainfrom
kjvalencik:kj/buf-cargo-credential-provider

Conversation

@kjvalencik
Copy link
Copy Markdown

Summary

Adds a new buf beta registry cargo subcommand that implements Cargo's cargo:token-from-stdout credential-provider protocol. With this in place, Cargo can resolve a Bearer token for a BSR-hosted Cargo registry by shelling out to buf, instead of users having to copy a token into ~/.cargo/credentials.toml and keep two copies in sync.

The command is registered under beta so we have room to iterate on the UX before promoting it.

Motivation

Today, a developer pulling crates from a BSR-hosted Cargo registry has to:

  1. Run buf registry login buf.build to authenticate buf.
  2. Separately store the same token in ~/.cargo/credentials.toml (or cargo login) so Cargo can use it.

Two stores for one secret, two places to rotate, two places to leak. This change lets users delegate the second step to buf:

# ~/.cargo/config.toml
[registry]
global-credential-providers = ["cargo:token-from-stdout buf beta registry cargo"]

Now any cargo operation against a BSR-hosted registry invokes buf beta registry cargo, which resolves the token from the same chain buf already uses (BUF_TOKEN env var, then ~/.netrc).

Configuration

Default — public BSR at buf.build:

[registry]
global-credential-providers = ["cargo:token-from-stdout buf beta registry cargo"]

Enterprise BSR (positional args replace the default; buf.build is not implicitly included):

[registry]
global-credential-providers = ["cargo:token-from-stdout buf beta registry cargo bsr.example.com"]

Multiple hosts:

[registry]
global-credential-providers = ["cargo:token-from-stdout buf beta registry cargo buf.build bsr.example.com"]

Token management continues to flow through buf registry login <host>; no Cargo-specific login is required.

How it works

On each invocation Cargo sets CARGO_REGISTRY_INDEX_URL (and other env vars per the spec) and reads a single line from stdout. The command:

  1. Extracts the host from CARGO_REGISTRY_INDEX_URL (strips an optional sparse+ prefix, parses as a URL, lowercases, drops any port).
  2. Checks the host against the positional-argument allow-list (default [buf.build]). Allow-list entries are also lowercased and port-stripped so bsr.example.com:8443 matches sparse+https://bsr.example.com:8443/....
  3. Looks up a token via bufconnect.NewTokenProviderFromContainer (the BUF_TOKEN env var) and falls through to bufconnect.NewNetrcTokenProvider (~/.netrc).
  4. On success, writes Bearer <token>\n to stdout and exits 0.

No Connect clients are constructed and no network calls are made — credential providers are on the hot path of every dependency resolution.

Failure semantics

Situation Behavior Why
CARGO_REGISTRY_INDEX_URL unset, unparseable, or host not in the allow-list Exit non-zero with empty stdout/stderr Lets Cargo fall through to its next configured credential provider
Host in the allow-list but no token resolves Exit non-zero; stderr says Failure: no token found for <host>. Run "buf registry login <host>" ... The user opted in to this host but buf has no credentials for it — surface a fix
BUF_TOKEN is malformed Exit non-zero; stderr says Failure: the BUF_TOKEN environment variable could not be parsed. Either unset BUF_TOKEN, or run "buf registry login <host>" .... Underlying parser error is debug-only The underlying bufconnect parse error embeds the raw token value, so the user-visible message is deliberately redacted

Pass --debug to log host extraction, allow-list matching, and (when present) the raw BUF_TOKEN parser error.

Out of scope

  • No cargo login / cargo logout support — the cargo:token-from-stdout protocol explicitly doesn't support them.
  • No changes to bufconnect or any existing authentication primitive; this is a thin orchestrator over what's already there.

Test plan

  • go test -race ./cmd/buf/internal/command/beta/registry/registrycargo/... (15 unit + command-level tests covering the contract matrix: unset URL, default and explicit allow-lists, scoped vs unscoped BUF_TOKEN, scoped-miss → netrc fallthrough, env-wins-over-netrc, netrc match + miss, uppercase URL host, uppercase positional, positional host:port, malformed BUF_TOKEN with leak-prevention NotContains assertion).
  • golangci-lint run ./cmd/buf/internal/command/beta/registry/registrycargo/... — 0 issues.
  • go build ./... and go vet ./... clean.
  • Manual smoke tests against the built binary:
    • Silent fallback for a non-allow-listed CARGO_REGISTRY_INDEX_URL.
    • Success path returns Bearer <token> and exits 0.
    • Loud failure with full advice when no token resolves for an allow-listed host.
    • --debug surfaces structured logs and (only with --debug) the raw BUF_TOKEN parser error.
    • BUF_TOKEN=secret@host,secret@host → user-facing stderr does not contain secret; only the debug log does.

Notes for review

  • Single new package: cmd/buf/internal/command/beta/registry/registrycargo. The only touches outside it are a 2-line wiring change in cmd/buf/buf.go and a 1-line CHANGELOG.md entry.
  • The command uses the empty-message errSilent sentinel for silent exits, following the same pattern as private/pkg/bandeps/cmd/bandeps/main.go. The top-level wrapError in cmd/buf/buf.go already bypasses its Failure: prefix when err.Error() == "".
  • Placed under beta per the design discussion; promote to buf registry cargo once we've shipped to early users.

Add a new `buf beta registry cargo` subcommand that implements Cargo's
`cargo:token-from-stdout` credential-provider protocol, so Cargo can
obtain a Bearer token for BSR-hosted Cargo registries by shelling out
to buf instead of storing tokens in `credentials.toml`.

Users configure it via `~/.cargo/config.toml`:

    [registry]
    global-credential-providers = ["cargo:token-from-stdout buf beta registry cargo"]

Behavior:

- Reads CARGO_REGISTRY_INDEX_URL, extracts the host (strips an optional
  "sparse+" prefix, lowercases, drops the port).
- Checks the host against a positional-argument allow-list. With no
  positional args the allow-list defaults to [buf.build]; positional
  args replace (not extend) the default. Ports in positional args are
  stripped so they compare equal to URL-extracted hosts.
- For an allow-listed host, resolves a token via the existing bufconnect
  chain: BUF_TOKEN env var first, then ~/.netrc.
- On success, writes `Bearer <token>\n` to stdout and exits 0.
- For hosts outside the allow-list (or a missing/unparseable URL),
  exits non-zero with no output so Cargo can fall through to its next
  configured credential provider.
- For allow-listed hosts with no token, writes a "Failure: ..." message
  pointing at `buf registry login` and exits non-zero.
- A malformed BUF_TOKEN produces a redacted user-visible error; the
  underlying bufconnect parser error (which embeds the raw token value)
  is logged at --debug level only.

No new bufconnect or authentication primitives are introduced; the
command is a thin orchestrator over existing token providers. Cargo
invokes credential providers on every dependency resolution, so the
implementation makes no network calls and constructs no Connect clients.

Includes command-level tests covering the silent/loud contract,
allow-list semantics, BUF_TOKEN precedence over .netrc, host
case-insensitivity, host:port normalization, and a leak-prevention
assertion that the BUF_TOKEN value never reaches stderr.
@CLAassistant
Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

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.

2 participants