feat: support Snowflake OAuth for Posit Team Native Apps#321
Conversation
Add Snowflake as a headless-auth IdP and a pluggable HTTP client-auth seam so VIP can verify Posit Team deployed as a Snowflake Native App. - idp.py: `_fill_snowflake_login` drives Snowflake's OAuth sign-in form and walks the multi-hop chain (product-host SPCS ingress, then the controller host that acts as the products' OIDC IdP), filling each sign-in form and clicking the optional "Allow" consent until the flow leaves Snowflake. Registered as idp "snowflake". - client_auth.py: a generic registry (`register_client_auth` / `build_client_auth`) + an injectable `httpx.Auth` on the product clients, so a downstream extension can authenticate the HTTP clients with a non-static scheme (e.g. a Snowflake JWT) without VIP depending on Snowflake. Client fixtures skip the Connect API-key requirement when such a provider is registered. - cli.py / vip.toml.example: document idp "snowflake". Tests: selftests/test_idp.py, selftests/test_client_auth.py.
There was a problem hiding this comment.
Pull request overview
This PR adds Snowflake Native App support by (1) introducing a Snowflake-specific headless browser IdP automation strategy for Snowflake OAuth redirects, and (2) adding a pluggable httpx.Auth registry so product API clients can use non-static/per-host authorization schemes (while keeping VIP Snowflake-agnostic).
Changes:
- Add
idp="snowflake"Playwright form-fill strategy that can handle multi-hop Snowflake OAuth and optional consent. - Introduce
vip.client_authregistry and thread an optionalhttpx.AuththroughBaseClientand product clients, with fixtures consulting the registry. - Update CLI/config examples and add selftests for the new IdP strategy + auth registry.
Reviewed changes
Copilot reviewed 12 out of 12 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
vip.toml.example |
Documents idp="snowflake" usage for Snowflake Native App deployments. |
src/vip/idp.py |
Adds Snowflake OAuth headless login strategy and registers it under supported IdPs. |
src/vip/clients/base.py |
Adds optional httpx.Auth support to the shared client base. |
src/vip/clients/connect.py |
Threads optional httpx.Auth into Connect client construction. |
src/vip/clients/workbench.py |
Threads optional httpx.Auth into Workbench client construction. |
src/vip/clients/packagemanager.py |
Threads optional httpx.Auth into Package Manager client construction. |
src/vip/client_auth.py |
New module: registry + builder for IdP-keyed httpx.Auth factories. |
src/vip/cli.py |
Updates --idp help text to include "snowflake". |
src/vip/auth.py |
Expands supported IdP messaging and includes "snowflake" in docs/errors. |
src/vip_tests/conftest.py |
Injects registry-provided httpx.Auth into product clients; relaxes Connect API-key requirement when auth is provided. |
selftests/test_idp.py |
Adds tests covering Snowflake IdP strategy dispatch and looping/consent behavior. |
selftests/test_client_auth.py |
Adds tests for registry behavior and BaseClient auth injection. |
Comments suppressed due to low confidence (1)
src/vip/clients/base.py:58
- BaseClient's docstring says
authtakes precedence overauth_header_value, but the implementation always sets theAuthorizationheader whenauth_header_valueis non-empty, even if a customhttpx.Authis provided. This can result in sending the static API key/token alongside (or instead of) the custom auth scheme, which is especially problematic for per-host schemes like Snowflake ingress auth.
headers: dict[str, str] = {}
if auth_header_value:
headers["Authorization"] = auth_header_value
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| # Identity provider for --headless-auth: "keycloak", "okta", "snowflake" | ||
| # Only required when provider is "oidc", "saml", or "oauth2". | ||
| # For Snowflake Native App deployments, use provider = "oauth2" and | ||
| # idp = "snowflake". |
|
Preview Links
|
…S ingress When Connect runs behind a Snowflake SPCS ingress, the ingress consumes the Authorization header for its own `Snowflake Token="..."` (supplied per request by the injected httpx.Auth). The Connect API key, previously sent as `Authorization: Key <key>`, collided on that single header and was overwritten, so every Connect API call returned 401. Send the key via Connect's alternate `X-RSC-Authorization` header instead, which Connect honors universally (harmless for non-Snowflake deployments), letting the ingress token and the Connect key coexist. - base.py: add `extra_headers` for static headers that must not occupy Authorization; store `self._auth` for ad-hoc requests. - connect.py: pass the key via X-RSC-Authorization; fetch_content now carries both the key (X-RSC-Authorization) and the ingress auth (auth=self._auth) — previously it copied only Authorization and never applied ingress auth. Verified live against Connect on a Snowflake Native App: login_api and all read-API tests pass, and content deploy/build/serve succeed.
mypy 'Cannot infer type of lambda' on the wait_for_url callback in _fill_snowflake_login (the target arg is a union, so the lambda's param type can't be inferred). Replace the lambda with an explicitly-typed nested function; the _before default still snapshots the per-hop URL and avoids closing over the loop variable. uv run mypy src/vip/ passes.
|
@ian-flores Can I get a sanity check on this PR? |
📸 Preview ScreenshotsSuccessfully captured 8 screenshots from the PR preview deployments for PR #321. Website PreviewPreview base: Home PageReport PreviewPreview base: Report Home (Index)Summary
Note: Report screenshots are viewport-only (not full-page) due to font-loading timeouts on the report pages. The website screenshots are all full-page captures. All screenshots are available as permanent assets in the Warning Firewall blocked 7 domainsThe following domains were blocked by the firewall during workflow execution:
network:
allowed:
- defaults
- "accounts.google.com"
- "android.clients.google.com"
- "cdn.jsdelivr.net"
- "clients2.google.com"
- "fonts.googleapis.com"
- "safebrowsingohttpgateway.googleapis.com"
- "www.google.com"See Network Configuration for more information.
|
📸 Preview Screenshots for PR #321Successfully captured 8 screenshots from the website and report preview deployments. 🌐 Website PreviewBase URL: Home PageGetting StartedFeature MatrixShiny AppTestsReport (embedded in website)📊 Report PreviewBase URL: Note: Report pages experienced timeout issues with full-page screenshots, so viewport screenshots are provided instead. Home (Summary)Details📋 Summary
URLs CapturedWebsite:
Report:
Known Issues
Warning Firewall blocked 7 domainsThe following domains were blocked by the firewall during workflow execution:
network:
allowed:
- defaults
- "accounts.google.com"
- "android.clients.google.com"
- "cdn.jsdelivr.net"
- "clients2.google.com"
- "fonts.googleapis.com"
- "safebrowsingohttpgateway.googleapis.com"
- "www.google.com"See Network Configuration for more information.
|
📸 Preview ScreenshotsCaptured screenshots of the PR preview deployments for PR #321. Summary
🌐 Website PreviewBase URL: Home PageGetting StartedShiny AppTest InventoryFeature MatrixExample Report (on website)📊 Report PreviewBase URL: Home PageDetailed Results📋 Captured URLsWebsite Preview:
Report Preview:
Automated screenshot gallery generated by preview-screenshot-gallery workflow Warning Firewall blocked 7 domainsThe following domains were blocked by the firewall during workflow execution:
network:
allowed:
- defaults
- "accounts.google.com"
- "android.clients.google.com"
- "cdn.jsdelivr.net"
- "clients2.google.com"
- "fonts.googleapis.com"
- "safebrowsingohttpgateway.googleapis.com"
- "www.google.com"See Network Configuration for more information.
|
📸 Preview ScreenshotsI've captured screenshots of the preview deployments for PR #321. Here's what was captured: Summary
Website Preview (
|











Adds the auth plumbing VIP needs to verify Posit Team deployed as a Snowflake Native App, in two prongs.
1. Browser auth — Snowflake IdP strategy
idp.pygains_fill_snowflake_login, registered asidp = "snowflake". Posit Team products delegate OIDC to the deployment's controller, which itself sits behind the Snowpark Container Services ingress — so reaching a product bounces through Snowflake OAuth more than once (product-host ingress, then controller host). The strategy loops, filling each sign-in form in the chain (waiting for each hop to settle) and clicking the optional Allow consent, until the flow leaves Snowflake.2. HTTP client auth — pluggable seam
Snowflake's SPCS ingress expects a per-host
Authorization: Snowflake Token="…"(a key-pair JWT exchange), which a static API key can't express. Rather than teach VIP about Snowflake, this adds a small generic seam:client_auth.py:register_client_auth(idp, factory)/build_client_auth(...).auth: httpx.AuthonBaseClientand the product clients.A downstream extension (in
partnership-snowflake) registers a Snowflakehttpx.Authagainstidp="snowflake"; VIP stays Snowflake-agnostic.Validated
Live against a Snowflake posit-team deployment: headless double-bounce login succeeds; Workbench API/security/health tests pass via the JWT prong; the full Workbench UI subset passes (run serially — high xdist concurrency contends on session launches, unrelated to auth).
Tests
selftests/test_idp.py,selftests/test_client_auth.py; full selftest suite green, ruff + mypy clean. No new dependencies.