Skip to content

Add mapbox auth profiles: list every stored credential profile - #38

Merged
mattpodwysocki merged 7 commits into
mainfrom
feat/176-auth-profiles
Sep 24, 2026
Merged

mattpodwysocki merged 7 commits into
mainfrom
feat/176-auth-profiles

Conversation

@mattpodwysocki

Copy link
Copy Markdown
Contributor

Summary

  • --profile <name> selects a named credential file, but nothing lists which profiles actually exist — answering that meant ls ~/.mapbox/credentials-*.json by hand.
  • mapbox auth profiles reads the config directory and reports every stored profile's account and expiry, without resolving --token/the environment/a single profile the way whoami does. Different question: "what is stored" vs. "what will the next command use."
  • Read-only, like whoami — loads credentials without refreshing, so listing can't spend a single-use refresh token.
  • profile_name_from_filename (the reverse of credentials_filename) is tested against credentials_filename's own output, and against every other file a config directory can hold (config.json, update-check.json, a lock file), so none of those are mistaken for a profile.

Test plan

  • cargo build, cargo fmt, cargo clippy --all-targets -- -D warnings, cargo test — all clean
  • New unit tests in src/auth.rs for the filename round-trip
  • New tests/auth_profiles.rs: empty state, multiple profiles sorted default-first, non-profile files correctly excluded, absent config directory creates nothing
  • docs/commands.md, README-adjacent CHANGELOG entry, and the two hardcoded command-surface test lists (the_auth_subcommands_that_write_offer_dry_run, the_hand_written_commands_describe_themselves) all updated

--profile <name> selects a named credential file, but nothing lists which
profiles actually exist. Answering that meant `ls ~/.mapbox/credentials-*.json`
by hand, and knowing the naming convention to tell the default profile
apart from a named one.

`mapbox auth profiles` reads the config directory rather than resolving
--token/the environment/a single stored profile the way `whoami` does —
it answers "what is stored" rather than "what will the next command use",
which is a different question with a different audience: someone who has
forgotten which named profiles they have logged into.

Read-only, like whoami: it loads credentials without refreshing them, so
listing profiles cannot itself spend a single-use refresh token. Reports
each profile's account (the stored username, falling back to the token's
own `u` claim) and expiry, decoded locally the same way whoami's fields
are.

profile_name_from_filename is credentials_filename's reverse, tested
against credentials_filename's own output rather than only hand-written
examples so the two cannot quietly drift apart, and checked against every
other file a config directory can hold (config.json, update-check.json,
a lock file) so none of them are mistaken for a profile.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@mattpodwysocki
mattpodwysocki requested a review from a team as a code owner September 23, 2026 03:35

@zmofei zmofei 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 the changes. The feature looks good to me. A few things I'd like to flag before merging:

  1. The human-readable text output could use clearer formatting. It's currently hard to scan.

    Current text output
  2. Could we introduce a short, system-generated profile ID? As the screenshot shows, profile names can be long and cumbersome to read or reference. This could go into a follow-up PR; if so, please create a ticket before merging this one.

  3. Bugs:

    • Stray credentials-default.json creates a duplicate default row with wrong data.
    • profiles triggers directory-create/chmod even though it's supposed to be read-only.
    • Expired (not clock-skewed) profiles wrongly show "check this machine's clock".
  4. Docs out of date:

    • docs/commands.md still says all commands take --profile/--output, contradicting the new profiles section.
    • tests/docs_contract.rs comment still says "four auth commands" (now five).
    • README.md doesn't list profiles.
    • --profile flag is silently ignored by profiles, undocumented.

mattpodwysocki and others added 2 commits September 23, 2026 09:12
Bugs:
- A stray credentials-default.json (never a spelling credentials_filename
  writes) no longer produces a second `default` row with data read from
  the wrong file. profile_name_from_filename rejects "default" from the
  credentials-<name>.json pattern, since the real default is always the
  bare credentials.json.
- Listing profiles no longer creates or hardens the config directory as
  a side effect. load_credentials_readonly/credentials_path_readonly read
  through config_dir_path rather than config_dir, so asking what is
  stored is never why a directory starts to exist or its permissions
  change.
- An expired (not clock-skewed) profile now reads as "expired" rather
  than "check this machine's clock" — that phrasing fit whoami's context
  (a token about to be used right now, where a small negative result is
  as likely to be skew as a real expiry) and was wrong far more often
  than right for a profile that may have sat untouched for weeks.

Text output is now a padded table instead of a raw tab-separated line,
which rendered misaligned across rows with differently sized account
names.

--profile is declared globally so it still parses on this command, but it
selects a single profile to act on and this command's whole point is
every one of them — so typing it now warns on stderr rather than
silently doing nothing, the same shape completion::warn_output_ignored
already uses for --output.

Docs: the auth profiles section now calls out the --profile exception to
the page-wide table, the docs_contract.rs comment counting auth
subcommands is five now, and README lists the command.

Filed mapbox-cli-private#179 for the reviewer's short-profile-ID
suggestion, as requested, rather than folding it into this PR.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@mattpodwysocki

Copy link
Copy Markdown
Contributor Author

Thanks for the thorough review. Addressed everything:

  1. Text output is now a padded table (columns aligned to the widest name/account) instead of a raw tab-separated line.
  2. Filed mapbox-cli-private#179 for the short-profile-ID follow-up rather than folding it into this PR.
  3. Bugs fixed:
    • A stray credentials-default.json no longer produces a second default row with data from the wrong file — profile_name_from_filename now rejects default from that pattern, since credentials_filename never writes that spelling for the default profile.
    • profiles no longer creates or hardens the config directory — added load_credentials_readonly/credentials_path_readonly, which read through config_dir_path (never creates) instead of config_dir (always creates+chmods).
    • An expired (not clock-skewed) profile now reads as expired rather than "check this machine's clock" — that phrasing fit whoami's context (a token about to be used right now) and was wrong far more often than right for a profile that may have sat untouched for weeks.
  4. Docs fixed: the auth profiles section now calls out the --profile exception to the page-wide table (and that it's warned about, not silently ignored), the stale "four auth commands" comment in tests/docs_contract.rs is five now, and README lists the command.

New tests for all of the above in tests/auth_profiles.rs (stray-file, directory-permissions, the warning, expired-vs-clock-skew wording, column alignment).

@zmofei

zmofei commented Sep 24, 2026

Copy link
Copy Markdown
Member

Reviewed and tested locally — cargo build, cargo fmt --check, cargo clippy --all-targets -- -D warnings, and cargo test are all clean on this branch. Two things worth fixing before merge:

  1. list_profile_names (src/auth.rs) propagates a hard error via entry.with_context(...)? if a single read_dir entry fails, which contradicts this function's own stated design ("nothing to read" rather than a failure) and is inconsistent with load_credentials_readonly's .ok() a few lines below. A transient filesystem error on one entry would make auth profiles fail for every profile instead of just skipping the bad one.

  2. docs/commands.md still says "All four commands take [--profile]," but this PR adds a fifth, auth profiles, which is documented as the explicit exception that does not honor --profile. That line is now stale.

Lower priority, fine to defer:

  • config.rs's set/unset do an unlocked read-modify-write of config.json, unlike credentials.json's lock file in auth.rs — concurrent config set/unset calls could silently overwrite each other.
  • The new config command group has no --schema contract test, unlike auth profiles in this same PR.
  • The --profile-ignored warning is now hand-copied a third time (completion::warn_output_ignored, tilesets_cli::warn_output_ignored, and this one) instead of reused.
  • list_profile_names/profile_name_from_filename don't check that a matching directory entry is a regular file, so a same-named directory or symlink in ~/.mapbox would produce a fabricated profile row.

list_profile_names propagated a hard error via ? if a single read_dir
entry failed, contradicting this function's own stated design ("nothing to
read" rather than a failure) and inconsistent with load_credentials_readonly's
.ok() a few lines below — a transient filesystem error on one entry would
have made auth profiles fail for every profile instead of just skipping the
bad one. Fixed with the same let-else skip config_dir_path already uses
above it.

docs/commands.md had two references to "four" auth commands left over from
before this PR's fifth: the page's own opening count, and the Auth
section's "All five commands take" table, which now also says profiles
parses --profile without honoring it, matching the note its own section
already gives.

Deferred per reviewer's own priority call: the config.rs lock-file
inconsistency, a --schema contract test for config, deduplicating
warn_output_ignored, and the regular-file check on a matching directory
entry.

482 tests, fmt and clippy clean.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@mattpodwysocki

Copy link
Copy Markdown
Contributor Author

Addressed both must-fix items:

  1. list_profile_names no longer propagates a hard error if a single read_dir entry fails — skipped with the same let-else shape config_dir_path already uses above it, consistent with load_credentials_readonly's .ok().
  2. Both stale "four" references in docs/commands.md fixed (the page's opening count, and the Auth section's own table intro, which now also notes profiles parses --profile without honoring it).

Deferring the four lower-priority items per your own call — happy to pick any of them up if you'd rather they land in this PR instead of a follow-up.

@mattpodwysocki
mattpodwysocki merged commit c6e8512 into main Sep 24, 2026
8 checks passed
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