Skip to content

feat: support Snowflake OAuth for Posit Team Native Apps#321

Draft
kfeinauer wants to merge 3 commits into
mainfrom
feat/snowflake-oauth-auth
Draft

feat: support Snowflake OAuth for Posit Team Native Apps#321
kfeinauer wants to merge 3 commits into
mainfrom
feat/snowflake-oauth-auth

Conversation

@kfeinauer
Copy link
Copy Markdown
Contributor

Draft — supports rstudio/partnership-snowflake#3022. Opening for review/visibility; not ready to merge.

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.py gains _fill_snowflake_login, registered as idp = "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(...).
  • An optional auth: httpx.Auth on BaseClient and the product clients.
  • Client fixtures consult the registry and skip the Connect API-key requirement when a provider is registered.

A downstream extension (in partnership-snowflake) registers a Snowflake httpx.Auth against idp="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.

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.
Copilot AI review requested due to automatic review settings May 29, 2026 21:18
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_auth registry and thread an optional httpx.Auth through BaseClient and 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 auth takes precedence over auth_header_value, but the implementation always sets the Authorization header when auth_header_value is non-empty, even if a custom httpx.Auth is 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.

Comment thread vip.toml.example
# 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".
@github-actions
Copy link
Copy Markdown

kfeinauer added 2 commits June 1, 2026 09:59
…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.
@kfeinauer
Copy link
Copy Markdown
Contributor Author

@ian-flores Can I get a sanity check on this PR?

@posit-vip-triage
Copy link
Copy Markdown
Contributor

📸 Preview Screenshots

Successfully captured 8 screenshots from the PR preview deployments for PR #321.

Website Preview

Preview base: https://posit-dev.github.io/vip/pr-preview-site/pr-321/

Home Page

Website Home

📄 Getting Started Page

Getting Started

📋 Tests Page

Tests

📊 Feature Matrix Page

Feature Matrix

✨ Shiny App Page

Shiny App

📈 Report Page

Report

Report Preview

Preview base: https://posit-dev.github.io/vip/pr-preview/pr-321/

Report Home (Index)

Report Home

📑 Report Details Page

Report Details


Summary

  • Total screenshots: 8
  • Website pages captured: 6
    • Home: ✅
    • Getting Started: ✅
    • Tests: ✅
    • Feature Matrix: ✅
    • Shiny App: ✅
    • Report: ✅
  • Report pages captured: 2
    • Index: ✅ (viewport only - full-page timeout)
    • Details: ✅ (viewport only - full-page timeout)

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 assets/preview-screenshot-gallery branch.

Warning

Firewall blocked 7 domains

The following domains were blocked by the firewall during workflow execution:

  • accounts.google.com
  • android.clients.google.com
  • cdn.jsdelivr.net
  • clients2.google.com
  • fonts.googleapis.com
  • safebrowsingohttpgateway.googleapis.com
  • www.google.com

To allow these domains, add them to the network.allowed list in your workflow frontmatter:

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.

Generated by Capture preview screenshots for PRs · ● 6.3M ·

@posit-vip-triage posit-vip-triage Bot mentioned this pull request Jun 1, 2026
@posit-vip-triage
Copy link
Copy Markdown
Contributor

📸 Preview Screenshots for PR #321

Successfully captured 8 screenshots from the website and report preview deployments.

🌐 Website Preview

Base URL: https://posit-dev.github.io/vip/pr-preview-site/pr-321/

Home Page

Website Home

Getting Started

Website Getting Started

Feature Matrix

Website Feature Matrix

Shiny App

Website Shiny App

Tests

Website Tests

Report (embedded in website)

Website Report

📊 Report Preview

Base URL: https://posit-dev.github.io/vip/pr-preview/pr-321/

Note: Report pages experienced timeout issues with full-page screenshots, so viewport screenshots are provided instead.

Home (Summary)

Report Home

Details

Report Details

📋 Summary

Category Pages Captured Status
Website Preview 6 pages ✅ All successful (full-page)
Report Preview 2 pages ⚠️ Viewport only (timeout on full-page)
Total 8 screenshots ✅ Complete

URLs Captured

Website:

Report:

Known Issues

  • Report preview pages timed out when attempting full-page screenshots (font loading delays). Viewport screenshots were captured instead to ensure visibility of the initial content.

Warning

Firewall blocked 7 domains

The following domains were blocked by the firewall during workflow execution:

  • accounts.google.com
  • android.clients.google.com
  • cdn.jsdelivr.net
  • clients2.google.com
  • fonts.googleapis.com
  • safebrowsingohttpgateway.googleapis.com
  • www.google.com

To allow these domains, add them to the network.allowed list in your workflow frontmatter:

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.

Generated by Capture preview screenshots for PRs · ● 8.8M ·

@posit-vip-triage
Copy link
Copy Markdown
Contributor

📸 Preview Screenshots

Captured screenshots of the PR preview deployments for PR #321.

Summary

  • Total screenshots captured: 8
  • Successfully uploaded: 7
  • Failed to upload: 1 (report-home.png - 18MB exceeds 10MB limit)

🌐 Website Preview

Base URL: https://posit-dev.github.io/vip/pr-preview-site/pr-321/

Home Page

website-home

Getting Started

website-getting-started

Shiny App

website-shiny-app

Test Inventory

website-tests

Feature Matrix

website-feature-matrix

Example Report (on website)

website-report


📊 Report Preview

Base URL: https://posit-dev.github.io/vip/pr-preview/pr-321/

Home Page

⚠️ Screenshot too large to upload (18MB, limit is 10MB). The page rendered successfully but contains extensive content. View the live preview

Detailed Results

report-details


📋 Captured URLs

Website Preview:

Report Preview:


Automated screenshot gallery generated by preview-screenshot-gallery workflow

Warning

Firewall blocked 7 domains

The following domains were blocked by the firewall during workflow execution:

  • accounts.google.com
  • android.clients.google.com
  • cdn.jsdelivr.net
  • clients2.google.com
  • fonts.googleapis.com
  • safebrowsingohttpgateway.googleapis.com
  • www.google.com

To allow these domains, add them to the network.allowed list in your workflow frontmatter:

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.

Generated by Capture preview screenshots for PRs · ● 11.6M ·

@posit-vip-triage
Copy link
Copy Markdown
Contributor

📸 Preview Screenshots

I've captured screenshots of the preview deployments for PR #321. Here's what was captured:

Summary

  • Total screenshots: 8
  • Website preview pages: 6 (full-page)
  • Report preview pages: 2 (viewport)

Website Preview (pr-preview-site/pr-321/)

Landing Page

Website Landing

Getting Started

Getting Started

Feature Matrix

Feature Matrix

Tests

Tests

Shiny App

Shiny App

Example Report (on website)

Website Report

Report Preview (pr-preview/pr-321/)

Report Landing Page

Report Landing

Report Details Page

Report Details

Preview URLs

Notes

  • Website pages captured as full-page screenshots
  • Report pages captured as viewport screenshots (full-page screenshots timed out due to font loading issues)
  • Some console errors were detected on pages but did not prevent rendering

Warning

Firewall blocked 7 domains

The following domains were blocked by the firewall during workflow execution:

  • accounts.google.com
  • android.clients.google.com
  • cdn.jsdelivr.net
  • clients2.google.com
  • fonts.googleapis.com
  • safebrowsingohttpgateway.googleapis.com
  • www.google.com

To allow these domains, add them to the network.allowed list in your workflow frontmatter:

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.

Generated by Capture preview screenshots for PRs · ● 12.3M ·

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