hughhhh opened a new pull request, #45068:
URL: https://github.com/apache/superset/pull/45068
### SUMMARY
> **This PR accompanies a SIP and is deliberately a draft.** The third
commit introduces
> a new feature, and the contributing guidelines require a `#SIP` before
such code can be
> reviewed or merged. The proposal is drafted in
> `docs/sip/embedded-fallback-credential.md` in this PR. Feedback on the
code is very
> welcome; the design discussion belongs on the SIP.
Per-user database OAuth2 and embedded dashboards do not currently compose.
An embedded viewer signs in with a guest token rather than a Superset
account, so there
is no per-user OAuth2 token to resolve for them — and an external viewer (a
customer, a
partner, a member of the public) frequently has no identity on the
analytical database at
all, so there is no token that *could* be minted for them. Embedded
dashboards on a
per-user OAuth2 connection therefore cannot run queries, and today they fail
unhelpfully.
Three commits, deliberately separate so they can be reviewed — or split —
independently.
**The first two are plain bug fixes that stand on their own and need no
SIP.**
---
#### 1. `fix(oauth2): don't start the OAuth2 dance for embedded guests`
On any OAuth2-enabled connection — Snowflake, Databricks, Trino, GSheets —
an embedded
guest who hits an authentication failure gets
`AttributeError: 'GuestUser' object has no attribute 'id'` and an HTTP 500,
instead of
the driver's actual error. `start_oauth2_dance` reads `g.user.id` to build
its OAuth2
state and `GuestUser` defines no `id`. More fundamentally, a guest can never
complete the
dance, so starting one is always wrong rather than occasionally fatal.
- **`BaseEngineSpec.needs_oauth2`** returns `False` for guests, covering the
nine call
sites that gate on it, and transitively `execute_with_oauth2_retry`'s four
`start_oauth2_dance()` calls, which all sit behind its `is_oauth2_error`
early-out. The
cheap `isinstance` check moves first so the common not-an-OAuth2-error
case never
reaches the `security_manager` import or the feature-flag lookup.
- **`check_for_oauth2`** needs the check repeated: its condition is
`isinstance(ex, OAuth2TokenRefreshError) or ...needs_oauth2(ex)`, and that
`or` reaches
the dance without consulting `needs_oauth2`. A live path, not a
theoretical one —
Snowflake lists `OAuth2TokenRefreshError` in its `oauth2_exception` tuple.
- **`start_oauth2_dance`** now raises `OAuth2Error` naming the problem
rather than
`AttributeError`, as a backstop for any caller that skips both guards.
Not fixed here but worth flagging: `gsheets.py` reads `g.user.id` directly in
`get_table_names`. A guest is unlikely to reach it, but it is the same
latent bug.
#### 2. `fix(snowflake): allow OAuth2 client config from the connection
dialog`
Snowflake's OAuth2 support is complete on the backend — `supports_oauth2`,
`is_oauth2_enabled`, `get_oauth2_config`, an OAuth2-aware
`impersonate_user`, and
`$.oauth2_client_info.secret` already masked — but unreachable from the UI.
An admin
could only configure it by hand-editing the Secure extra JSON blob.
Two missing pieces, both of which gsheets and bigquery already have:
`oauth2_client_info`
on `SnowflakeParametersSchema`, and
`add_attribute_function(encrypted_field_properties)`
in `parameters_json_schema()`. Together they attach `x-encrypted-extra:
true`, which is
what makes the modal render the existing `OAuth2ClientField` and move the
value into
`masked_encrypted_extra`.
Unlike gsheets the endpoints get no defaults — they live on the customer's
own Snowflake
account, so no URI is right for every deployment. `scope` defaults to
`refresh_token`,
since a refresh token is what makes an OAuth2 connection usable past the
first hour.
Backend-only: `oauth2_client_info` is already in `FormFieldOrder` and
already mapped to
`OAuth2ClientField`. Two leak-prevention properties that held only by
construction —
`build_sqlalchemy_uri` and `update_params_from_encrypted_extra` keeping the
client secret
out of the stored URI and the engine kwargs — are now pinned by test.
#### 3. `feat(db): embedded-only fallback credential on OAuth2 connections`
Lets one connection carry a username and password used **only** when the
requesting
principal is an embedded guest, while logged-in users continue to
authenticate
individually via OAuth2 with no route to the stored credential.
**Behind a new `EMBEDDED_CREDENTIAL_FALLBACK` feature flag, default
`False`**, so nothing
changes for any existing deployment. The credential lives per-connection in
the existing
`encrypted_extra` column — **no migration** — and is configured in the
connection dialog.
`Database.get_embedded_fallback_credentials()` is the single gate and
requires all five
of: the deployment flag; an engine opt-in
(`supports_embedded_credential_fallback`,
Snowflake only); a **complete** stored credential (a half-filled one is
ignored, not
half-applied); an embedded guest principal with `EMBEDDED_SUPERSET` on; and
a connection
that actually uses OAuth2, keeping this a fallback for the per-user flow
rather than a
second identity on an ordinary connection.
**Why impersonation is skipped rather than extended** — load-bearing twice
over:
1. An opted-in engine sets its OAuth2 authenticator during impersonation
whether or not a
token exists (Snowflake sets `authenticator=oauth` unconditionally), so
skipping it is
what lets the stored credential authenticate at all.
2. `get_effective_user` resolves to `g.user.username`, and a `GuestUser`
carries whatever
username the guest token claims — which whoever mints the token controls.
Going
through impersonation would let the token choose the connection identity.
A test is
pinned to exactly this: a token minted with `ACCOUNTADMIN` does not reach
the URL.
The credential is set on the URL rather than in `connect_args` so the
per-process engine
cache key — which has **no** user component — differs between a guest and a
logged-in
user. A test asserts the two produce distinct cache entries.
**Security model, stated plainly.** This grants the embedded-guest principal
the ability
to query **as one operator-configured database user**, on connections where
an admin
configured it, in deployments that enabled it.
- Every embedded viewer of that connection queries as the same database
user, so
warehouse-side RLS or masking keyed to the connecting user cannot
distinguish viewers.
Per-viewer restriction must come from Superset's guest-token RLS rules,
which are
unchanged. The UI says this next to the field, not only in the docs.
- It does **not** widen what a guest can reach in Superset — resource access
is still
governed entirely by the guest token and the embedded dashboard config.
- The password masks on read and is restored from storage when the mask is
re-saved; the
username stays visible so an admin can audit which database user embedded
queries run
as.
- Turning the flag off is a kill switch, not a deletion: the credential
stops being
offered and used but is preserved, so the operation is reversible and
export/import
round-trips.
The full argument, including rejected alternatives, is in the SIP draft.
### BEFORE/AFTER SCREENSHOTS OR ANIMATED GIF
<!-- SCREENSHOTS: drag before-modal.png / after-modal.png / after-bottom.png
here -->
**Before** — the Snowflake connection dialog ends at Warehouse/Role.
**After** — an **OAuth2 client information** section (commit 2) and an
**Embedded guest
credentials** section (commit 3), the latter carrying the warning that every
embedded
viewer shares the credential.
### TESTING INSTRUCTIONS
```bash
pytest tests/unit_tests/db_engine_specs/test_base.py
tests/unit_tests/utils/oauth2_tests.py
pytest tests/unit_tests/db_engine_specs/test_snowflake.py
tests/unit_tests/models/core_test.py
cd superset-frontend && npx jest src/features/databases
```
402 backend tests and 222 frontend tests pass. The full unit suite shows the
same set of
failures as the base commit — none introduced.
The guard tests in commit 1 fail against an unpatched tree with exactly the
reported
error, `AttributeError: 'GuestUser' object has no attribute 'id'` at
`base.py:888`, while
the "still works for a logged-in user" tests pass either way. Commit 3's
security
properties are its tests: the decisive one builds two engines from the same
`Database`
row and asserts the guest gets the fallback credential with **no**
`authenticator=oauth`,
while a logged-in user gets neither the fallback username nor its password.
Each of the
five gate conditions has a negative test, including the kill switch on an
otherwise fully
configured connection and the "disabled, not erased" property.
**Not verified, and it cannot be in CI:** whether Snowflake accepts the
credential on a
connection whose client is configured for OAuth2. Everything up to the
connection URL is
proven by test; the handshake needs an account with an OAuth2 security
integration.
Confirmation from anyone who has one would be very welcome.
Manually, with `snowflake-sqlalchemy` installed (the engine does not appear
in the dialog
at all without it) and both flags on:
1. **+ Database → Snowflake.** Both new sections are present.
2. Fill in the OAuth2 client, and a dedicated Snowflake user under
**Embedded guest
credentials**.
3. Embed a dashboard backed by that connection and load it with a guest
token: queries
run as the fallback user.
4. Open the same dashboard logged in: the OAuth2 flow is used as before.
5. Set `EMBEDDED_CREDENTIAL_FALLBACK = False` and restart: the section
disappears,
embedded queries stop using the credential, and the stored value survives.
Note that Superset runs a real connection test on both POST and PUT, so
against an
unreachable host nothing is written — check for the error toast rather than
reading the
metadata DB afterwards.
### ADDITIONAL INFORMATION
- [ ] Has associated issue:
- [x] Required feature flags: `EMBEDDED_CREDENTIAL_FALLBACK` (new, default
off), and
`EMBEDDED_SUPERSET`
- [x] Changes UI
- [ ] Includes DB Migration (follow approval process in
[SIP-59](https://github.com/apache/superset/issues/13351))
- [ ] Migration is atomic, supports rollback & is backwards-compatible
- [ ] Confirm DB migration upgrade and downgrade tested
- [ ] Runtime estimates and downtime expectations provided
- [x] Introduces new feature or API
- [ ] Removes existing feature or API
No DB migration: the credential is stored in the existing `encrypted_extra`
column.
**Happy to split this** — the first two commits are independent bug fixes
and could land
ahead of the SIP discussion if reviewers prefer.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]