Skip to content

feat(cloud): persist device-flow Cloud SQL IAM credentials into DVC prefs#18

Merged
jirhiker merged 1 commit into
mainfrom
feat/workstation-iam-credentials
May 10, 2026
Merged

feat(cloud): persist device-flow Cloud SQL IAM credentials into DVC prefs#18
jirhiker merged 1 commit into
mainfrom
feat/workstation-iam-credentials

Conversation

@jirhiker
Copy link
Copy Markdown

Summary

Replaces PR #17 (which is held). Implements the Cloud SQL IAM client path required by the latest workstation-onboarding BDD spec — no static Postgres password is parsed, persisted, or transmitted.

The poll-success response now optionally carries a `database_iam` bundle minted off-cluster by the admin tool (companion server PR pychronAPI#43). This PR wires it through the client:

  • New `pychron.cloud.iam_credentials` module — pure helpers for `_validate_iam_bundle`, `write_sa_key_file`, `build_iam_dvc_csv`, `merge_iam_dvc_favorites`, `apply_iam_credentials_to_prefs`.
  • `WorkstationSetup` carries `database_iam` + `default_metadata_repo` attributes after `from_device_code` returns.
  • Cloud preferences pane:
    • Registered / Partial / Unregistered indicator
    • Re-registration confirmation dialog when registration.json + keyring token both exist
    • Auto-writes the SA key file (0600 on POSIX) + populates `pychron.dvc.connection.favorites` with a `connection_method=cloudsql_iam` row
    • Auto-runs the same `whoami` probe the Test Connection button uses
  • SA private key stripped from `DeviceCodePollSuccess.raw` (same defensive treatment as `api_token`).
  • Cross-check that the SA key file's `client_email` matches the claimed `service_account_email` — refuses key-swap bundles even from a leaked bootstrap token.

The client side of `DVCConnectionItem.cloudsql_iam` was already wired (Cloud SQL Python Connector custom-creator pattern at `pychron/database/core/database_adapter.py`) — this PR only populates the favorite, no DVC engine changes.

Tests

  • 23 new unit tests in `test/cloud/test_iam_credentials.py` (validate, write, build, merge, apply round-trip + every rejection path).
  • 3 new tests in `test/cloud/test_device_code_setup.py` (IAM bundle propagation through `from_device_code`, raw redaction).
  • Total cloud suite: 166 passing (was 140, +26 new).

Test plan

  • `pytest test/cloud/` — 166 passed
  • Manual: enroll a workstation against a real bridge that has staged a Cloud SQL IAM bundle; confirm SA key lands at `~/.pychron/keys/cloudsql_.json` (0600) and `pychron.dvc.connection.favorites` is populated
  • Manual: enroll against a bridge with no staged bundle; confirm enrollment still succeeds and existing DVC favorites are untouched
  • Manual: verify the "already-registered" confirmation dialog blocks an accidental re-registration
  • Manual: launch DVC after enrollment; confirm Cloud SQL Python Connector mints a token from the SA key and the lab DB connection works

🤖 Generated with Claude Code

…refs

Replaces the static-Postgres-password client work from PR #17 (held)
with the Cloud SQL IAM auth path required by the latest BDD spec.
No static Postgres password is parsed, persisted, or transmitted.

The poll-success response now optionally carries a database_iam
bundle minted off-cluster by the admin tool (companion server PR
NMGRL#43). This commit wires it through the client:

- pychron/cloud/api_client.py — DeviceCodePollSuccess gains a
  database_iam dict slot. poll_device_code() parses the new block
  from the response body and strips it from the safe_raw debug
  dict so the embedded SA private key cannot leak into caller logs.

- pychron/cloud/paths.py — new cloudsql_key_path(lab) helper that
  returns ~/.pychron/keys/cloudsql_<safe-lab>.json. Lab name is
  filesystem-sanitized so a hostile / weird lab string cannot
  escape the keys directory.

- pychron/cloud/iam_credentials.py (NEW) — pure helpers wrapping
  validate → write SA key → CSV → favorites:

  * _validate_iam_bundle: required-field check + ip_type
    public/private/psc + JSON-decode SA key + cross-check
    key.client_email == service_account_email (defends against
    bridge-side key-swap).

  * write_sa_key_file: atomic-ish write to
    ~/.pychron/keys/cloudsql_<lab>.json with 0600 on POSIX.

  * build_iam_dvc_csv: positional CSV that
    DVCConnectionItem(attrs=...) rehydrates with
    connection_method=cloudsql_iam, the four cloudsql_* fields
    populated, and username/password left empty.

  * merge_iam_dvc_favorites: REPLACES any prior cloud-<lab> row,
    demotes any other default=True favorite. _row_set_field()
    extends short legacy rows rather than silently no-op'ing.

  * apply_iam_credentials_to_prefs: end-to-end glue.

- pychron/cloud/workstation_setup.py — WorkstationSetup gets
  database_iam + default_metadata_repo attributes; from_device_code
  populates them from the poll body. None means HTTP-only.

- pychron/cloud/tasks/preferences.py:

  * New _registration_status field (Registered / Partial /
    Unregistered) bound to a CustomLabel.

  * Start-device-code-enrollment refuses to proceed when the
    workstation is already onboarded (registration.json + keyring
    token both present) without an explicit confirmation dialog.

  * After successful enrollment,
    _persist_iam_credentials_from_setup writes the SA key + DVC
    favorite, and the same whoami probe the manual Test Connection
    button uses runs automatically. A parse failure is non-fatal —
    surfaced via the status badge so enrollment isn't rolled back.

  * Re-onboard / revoke / switch-lab refresh the registration
    status indicator on completion.

- test/cloud/test_iam_credentials.py (NEW) — 23 unit tests:
  write_sa_key_file (correct path, 0600 perms on POSIX, overwrite
  on re-enrollment, slug sanitization), build_iam_dvc_csv (CSV
  field ordering), merge_iam_dvc_favorites (append, replace,
  default flag demotion, short-row extension), _row_set_field
  regression, apply_iam_credentials_to_prefs end-to-end +
  rejection paths (missing field, invalid ip_type, mismatched
  key.client_email, malformed key JSON, non-service_account key).

- test/cloud/test_device_code_setup.py — 3 new tests:
  IAM bundle propagates to WorkstationSetup, missing bundle
  leaves attr at None, database_iam stripped from raw debug dict.

Total: 166 cloud tests passing (was 140, +26 new).

Replaces PR #17. Server: pychronAPI feat/workstation-iam-credentials
(PR NMGRL#43).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@jirhiker jirhiker merged commit 4818fd2 into main May 10, 2026
4 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.

1 participant