feat: add spcs_pat for Snowflake SPCS gateway auth (DM-3656)#27
Conversation
Add `spcs_pat` to `DataMasqueInstanceConfig` for reaching DataMasque instances hosted behind Snowflake SPCS (Snowpark Container Services) app ingress (`*.snowflakecomputing.app`). When set, the client sends the Programmatic Access Token on the `X-SF-SPCS-Authorization` header so the Snowflake gateway lets the request through; the gateway strips it before forwarding, leaving DataMasque's own auth untouched. A response hook raises the new `SpcsGatewayAuthError` (deliberately outside the `DataMasqueApiError` subtree, so the 401 re-auth retry never loops on a gateway rejection) with the Snowflake detail and a likely-cause hint. This replaces a process-global `requests.request` monkeypatch previously carried in the AIT runner (DM-3656).
|
Companion MR that removes the AIT-runner monkeypatch and consumes this feature: https://git.datamasque.com/datamasque/datamasque-automation/-/merge_requests/1045 Merge this PR first; the runner pins |
DataMasque running inside Snowflake SPCS saves connections with `snowflake_stage_location=spcs`, a value the client's enum did not model. Because `create_or_update_connection` lists and deserialises every connection to find a match, a single SPCS-staged Snowflake connection on a shared instance made the client raise `ValidationError` during setup of any other connection. Add `spcs` to `SnowflakeStageLocation` (no associated storage fields; the container stages on its own storage). Server-side validation already covers the per-stage required fields, so no client-side validator change is needed. Lets ui-testing retract the create_connection workaround in MR !185 (DM-3656).
|
Follow-up commit This is the durable fix for the failure mode that ui-testing MR !185 works around. When DataMasque runs inside Snowflake SPCS it saves connections with Regression test: |
cph-datamasque
left a comment
There was a problem hiding this comment.
Needs a rebase.
Please ask your claude to write in sembr and to use our house style for code references (single-backticks, not double).
I don't think it's necessary to include the implementation details of how DM and Snowflake handle the auth header. A user of this library wouldn't really care - debugging anything in that scope is out of their hands, really.
Actual code impl is fine.
Suggest 1.1.3 for the version (1.1.2 is the latest), though if you wanted to go to 1.2.0 I've no strong objections.
| The client calls `token_source` on each authentication attempt, | ||
| so the callable is free to fetch and refresh tokens out-of-band (e.g. from a secrets manager). | ||
|
|
||
| `spcs_pat` is an optional Snowflake Programmatic Access Token for reaching a |
There was a problem hiding this comment.
tell your claude to look at CONTRIBUTING.rst, where you'll see guidelines for semantic breaking.
Do we need to talk about the implementation details?
| verify_ssl: bool = True | ||
| token_source: Optional[Callable[[], str]] = None | ||
| spcs_pat: Optional[str] = None | ||
| """Snowflake Programmatic Access Token for a DataMasque instance hosted behind |
There was a problem hiding this comment.
D213 and sembr. Ruff should have yelled at you.
Same thoughts re implementation details may be unnecessary.
Mint is casual language - perhaps Create
|
|
||
| class SpcsGatewayAuthError(DataMasqueException): | ||
| """ | ||
| Raised when a Snowflake SPCS app gateway rejects the configured ``spcs_pat``. |
There was a problem hiding this comment.
Sembr.
Good comment explaining why this doesn't inherit from DataMasqueApiError.
| @@ -0,0 +1,176 @@ | |||
| """ | |||
| Snowflake SPCS app gateway authentication for :class:`DataMasqueClient`. | |||
There was a problem hiding this comment.
Guess what two things I'm going to say again.
| """ | ||
| True if at least one header-level Snowflake gateway marker is present. | ||
|
|
||
| Looks for either ``Server: _`` (the gateway's literal Server header value) |
There was a problem hiding this comment.
sembr again... and the comment
I'm not going to comment any more individual instances, have your claude fix them all up please
| [project] | ||
| name = "datamasque-python" | ||
| version = "1.1.2" | ||
| version = "1.2.0" |
| raised by ``start_masking_run`` when the server rejects the run. | ||
| - ``DataMasqueUserError`` — | ||
| raised by user-management methods when the input is invalid. | ||
| - ``SpcsGatewayAuthError`` — |
There was a problem hiding this comment.
Not to say the snowflake stuff isn't valuable, but is this "worthy" (mainly, frequently-used) enough of being included in top level README?
| For a DataMasque instance hosted behind Snowflake SPCS (Snowpark Container Services) app ingress | ||
| (a ``*.snowflakecomputing.app`` ``base_url``), | ||
| pass a Snowflake Programmatic Access Token as ``spcs_pat`` on ``DataMasqueInstanceConfig``; | ||
| the client sends it on the ``X-SF-SPCS-Authorization`` header to clear the Snowflake gateway, |
There was a problem hiding this comment.
more implementation detail that doesn't need to be in the README, if even present at all
| client = DataMasqueClient(config) | ||
| client.authenticate() | ||
|
|
||
| Mint the PAT in Snowsight (User profile → Programmatic access tokens) for an |
| * Added ``spcs_pat`` to ``DataMasqueInstanceConfig`` for authenticating to | ||
| DataMasque instances hosted behind Snowflake SPCS (Snowpark Container Services) | ||
| app ingress. When set, the client sends the Programmatic Access Token on the | ||
| ``X-SF-SPCS-Authorization`` header to clear the Snowflake gateway, independently | ||
| of the instance's own DataMasque auth. | ||
| * Added ``SpcsGatewayAuthError``, raised when the SPCS gateway rejects the PAT | ||
| before the request reaches DataMasque (with the Snowflake detail and a hint at | ||
| the likely cause). | ||
| * Added ``spcs`` to ``SnowflakeStageLocation`` so connections staged inside | ||
| Snowflake SPCS deserialise correctly. Previously, listing connections on an | ||
| instance that held an SPCS-staged Snowflake connection raised a | ||
| ``ValidationError`` on the unknown stage value. |
There was a problem hiding this comment.
Follow existing style - nowhere near as verbose. And, you guessed it, sembr.
| * Added ``spcs_pat`` to ``DataMasqueInstanceConfig`` for authenticating to | |
| DataMasque instances hosted behind Snowflake SPCS (Snowpark Container Services) | |
| app ingress. When set, the client sends the Programmatic Access Token on the | |
| ``X-SF-SPCS-Authorization`` header to clear the Snowflake gateway, independently | |
| of the instance's own DataMasque auth. | |
| * Added ``SpcsGatewayAuthError``, raised when the SPCS gateway rejects the PAT | |
| before the request reaches DataMasque (with the Snowflake detail and a hint at | |
| the likely cause). | |
| * Added ``spcs`` to ``SnowflakeStageLocation`` so connections staged inside | |
| Snowflake SPCS deserialise correctly. Previously, listing connections on an | |
| instance that held an SPCS-staged Snowflake connection raised a | |
| ``ValidationError`` on the unknown stage value. | |
| * Added ``spcs_pat`` to ``DataMasqueInstanceConfig`` for authenticating to DataMasque instances hosted on Snowpark Container Services. | |
| * Added ``SpcsGatewayAuthError``, raised when the SPCS gateway rejects the PAT. | |
| * Added ``spcs`` option to ``SnowflakeStageLocation``. |
Could also structure as a "Added support for DataMasque deployments on Snowpark Container Services (SPCS)" heading with the three bullets nested below it.
What
Adds first-class support for authenticating to DataMasque instances hosted behind Snowflake SPCS (Snowpark Container Services) app ingress (
*.snowflakecomputing.app).spcs_pat: Optional[str]field onDataMasqueInstanceConfig. When set, the client sends the Programmatic Access Token on theX-SF-SPCS-Authorizationheader so the Snowflake gateway lets the request through; the gateway strips it before forwarding, leaving DataMasque's ownAuthorizationauth untouched. It is orthogonal to the existingpassword/token_sourcechoice.datamasque/client/spcs.py: sets the header on the client'srequests.Session(so it rides on every request, including the unauthenticated login) and registers a response hook that detects a gateway-originated 401/403 and raises a clear error.SpcsGatewayAuthError— deliberately outside theDataMasqueApiErrorsubtree, so the client's 401 re-auth-and-retry path never loops on a gateway rejection. The message carries the Snowflake detail, request id, and a likely-cause hint.Why
The AIT runner (datamasque-automation, DM-3656) carried this as a process-global monkeypatch of the module-level
requests.request. The SDK now owns a per-clientrequests.Session, so the behaviour lives here cleanly as a session header + response hook — no global side effects, no host-scoping bookkeeping. The companion MR removes the hack from the runner.Tests / docs
tests/test_spcs.py— header present on authenticated + login requests; no header when unset; gateway 401 raisesSpcsGatewayAuthErrorand does not retry-loop; genuine DM 401 still retries; hint mapping; config coexistence with password/token_source.sphinx-build -Wall green.usage.rst/README.rstSPCS section,client.rstautodoc,HISTORY.rst+ version bump to1.2.0.Ticket: https://datamasque.atlassian.net/browse/DM-3656