Skip to content

feat: cache non-production credentials per proxy host so staging and prod coexist - #22

Merged
alukach merged 7 commits into
mainfrom
feat/per-environment-cache
Oct 13, 2026
Merged

alukach merged 7 commits into
mainfrom
feat/per-environment-cache

Conversation

@alukach

@alukach alukach commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

Problem

Cached credentials, and since #23 the login session, were keyed with no notion of environment (source-coop-cli / _default and @session in the keychain). Logging into staging overwrote prod, and the reverse. Worse, source-coop creds in a prod profile would then quietly return (and refresh) staging credentials.

Change

Each proxy host is its own environment, with its own login session and role credentials.

  • Production (data.source.coop) keeps the bare names (_default, @session, credentials/<role>.json, session.json), in every build, so existing prod caches stay readable with no re-login. Pinning them to the production host rather than the build's default proxy keeps a staging build and a prod build, which share the keyring, from reading each other's caches.
  • Any other proxy prefixes its keyring accounts with the host (data.staging.source.coop/_default, data.staging.source.coop/@session). Its files go in their own directory, source-coop/env/<host>[/<port>]/, so environments can't collide on disk either.
  • The environment comes from --proxy-url / SOURCE_PROXY_URL, parsed with url::Url (case, trailing slash and default port don't matter). login, creds and auth token (new flag) all read it.
  • Entries from another environment are ignored. Cached credentials and sessions record the proxy they came from and aren't used under any other environment.
  • Migration from 0.3.0: 0.3.0 cached every proxy's credentials in the bare slot. An environment with nothing of its own adopts its entry from there, but only an entry that records a proxy in that environment.
  • login writes credentials under the role lock, so a concurrent creds can't overwrite a fresh login.
  • .env.staging now sets SOURCE_API_AUDIENCE=https://staging.source.coop, so a staging login asks for the staging audience.
  • README: "Multiple environments" section with an AWS profile per environment, and updated storage paths.
[profile source-coop-staging]
credential_process = source-coop creds --proxy-url https://data.staging.source.coop
endpoint_url = https://data.staging.source.coop

Behaviour change: a credential_process = source-coop creds profile without --proxy-url now always means production. Before, it served whichever environment was logged in last. Non-production profiles must pass --proxy-url.

Testing

  • cargo test and cargo test --features staging: 29 passed in each, including env_normalizes_proxy_urls, slots_never_collide_across_environments and entries_belong_to_their_proxys_environment
  • cargo clippy --all-targets (both builds): clean
  • Not covered by unit tests, since they touch the real keyring: the 0.3.0 migration and the session environment check. Their predicates are tested.

Note: the first commit on this branch is titled feat! with a BREAKING CHANGE footer, which no longer applies. Please squash-merge using the PR title so release-please doesn't bump a major version.

🤖 Generated with Claude Code

alukach and others added 2 commits September 30, 2026 11:28
The cache key was the role ARN alone, and both environments default to
`_default`, so a staging login overwrote the prod entry (and `creds`
would silently serve/refresh staging credentials for a prod profile).
Key entries by `<proxy host>/<role>` and let `creds` take
`--proxy-url` / `SOURCE_PROXY_URL` to select the environment.

BREAKING CHANGE: existing cached logins are no longer found; run
`source-coop login` once after upgrading.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Existing prod caches (keyed by role ARN alone) stay readable, so upgrading
needs no re-login. Only non-production proxies get the host prefix.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@alukach alukach changed the title feat!: cache credentials per proxy URL so staging and prod coexist feat: cache non-production credentials per proxy host so staging and prod coexist Sep 30, 2026
@alukach
alukach marked this pull request as ready for review October 6, 2026 02:44
alukach and others added 3 commits October 12, 2026 21:04
Rebuild per-proxy cache keys on main's slot-based cache.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Key the login session by environment as well as role credentials, so a
staging login no longer replaces production's session. Normalize proxy
URLs with url::Url, keep non-default environments in their own cache
directory, ignore cached entries minted at another environment's proxy,
and add --proxy-url to 'auth token'.

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

- The bare (pre-environment) slots now always mean data.source.coop, so
  staging and production builds sharing the keyring can't read each
  other's caches.
- Sessions record their proxy and are ignored under another environment.
- An environment with nothing cached adopts its entry from the production
  slot, where versions before environments stored every proxy's.
- Ports get their own cache-directory segment, since '_' is legal in hosts.
- login writes credentials under the role lock.

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

claude Bot commented Oct 13, 2026 •

Copy link
Copy Markdown

Claude finished @alukach's task in 23s —— View job


✅ No blocking issues — safe to merge.

I read the full src/ diff and traced the lock ordering, the environment normalization, the slot layout and the 0.3.0 migration. I didn't run the tests; the PR reports them passing.

  • Lock order: creds takes the role lock, then the session lock (via session::token). login takes the session lock, releases it, then takes the role lock. Nothing holds the two in opposite orders, so there is no deadlock.
  • Slot collisions: account prefixes use /, which hosts can't contain. Ports get their own path segment. The new tests cover both.
  • Migration: it only adopts a legacy entry that records a proxy in the target environment, so bare entries stay production's. The unlocked copy in the first read_credentials call is idempotent, so a race there is harmless.

Non-blocking notes

  • src/cache.rs read_session: a session with no proxy_url (written before this change) is accepted in the production slot. If it came from a 0.3.0 staging login, prod creds will try to exchange a staging ID token. That fails with "Run 'source-coop login'", so the effect is a confusing error, not a wrong credential. Fixing it needs a proxy_url check or a one-time re-login note in the README.
  • The .env.staging audience change only applies to new logins. Existing staging sessions keep their old audience until the user logs in again.

Simplify (ponytail)

  • src/cache.rs env_dir: it re-parses the host:port string that env() just built (rsplit_once(':') plus a digit check). Have env() return a small {host, port} struct, or build the path from Url directly, and drop the re-parse.
  • src/cache.rs sanitize(s, keep): the keep slice has two callers whose sets differ only by .. Hosts could go through the existing role-style sanitize, because . is rarely a collision risk. Alternatively, keep it as is.
  • src/cache.rs belongs_to and minted_in: two predicates that differ only in the bare-entry case. belongs_to could be self.minted_in(env) || bare, which removes the duplicated match.
    · branch feat/per-environment-cache

💰 Estimated review cost: $0.14 · 0m23s · 4 turns

alukach and others added 2 commits October 13, 2026 13:35
Bare entries record no proxy, so the ones in the production slot are
production's; copying them into an empty staging slot served prod
credentials as staging until they expired.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@alukach
alukach merged commit 8673032 into main Oct 13, 2026
10 checks passed
@alukach
alukach deleted the feat/per-environment-cache branch October 13, 2026 22:36
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