Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions UPDATING.md
Original file line number Diff line number Diff line change
Expand Up @@ -734,6 +734,16 @@ floats), and non-finite Decimals in those columns become null. Other chart
result serialization and chart number formatting are unchanged. Re-run queries
whose cached JSON results were produced before upgrading all workers.


### Downgrading backend-only extension secrets

Migration `4c8f2a1d9b73` adds backend-only extension storage for personal secrets,
including OAuth tokens. Downgrading past this migration permanently deletes all
storage rows marked `access=backend` before removing the access classification.
This prevents secrets from becoming readable through the older browser storage
API. Back up the metadata database before downgrading. Users must reconnect their
personal accounts if they upgrade again after the downgrade.

### Version history retention setting

Use `VERSION_HISTORY_RETENTION_DAYS` for both the application setting and
Expand Down
57 changes: 57 additions & 0 deletions docs/developer_docs/extensions/contribution-types.md
Original file line number Diff line number Diff line change
Expand Up @@ -266,3 +266,60 @@ class MySemanticLayer(SemanticLayer[MyConfig, MySemanticView]):
```

See [Semantic Layers Extension Point](./extension-points/semantic-layers.md) for implementation details.

#### Personal OAuth2 connections

Providers can expose a personal connection action by implementing
`get_configuration_actions(configuration)` and
`execute_configuration_action(action_id, configuration, return_url)`. The first
hook returns `SemanticLayerAction` entries describing whether the authenticated
user is connected. The second returns a `SemanticLayerActionResult` containing
the provider's authorization URL. Providers without connection actions can keep
the default implementations.

Store access and refresh tokens using the Python-only
[`get_context().storage.secrets` accessor](./storage.md#backend-only-secrets).
Never put personal credentials in the shared layer configuration, action results,
query results, logs, or exceptions. The host executes provider methods under the
owning extension and requesting user's context, including background chart work.
Semantic View query results, cached dimension values, and shared async query tasks
are partitioned by user,
including providers without connection actions. This reduces cache sharing for
providers with identical results for all users, but declaring no actions does not
establish that a provider returns user-independent data.

When any provider operation requires authentication, raise the core exception:

```python
from superset_core.semantic_layers.layer import SemanticLayerAuthenticationRequiredError

raise SemanticLayerAuthenticationRequiredError("oauth2")
```

Superset translates this into the existing `OAUTH2_REDIRECT` error contract for
view discovery, metadata, filter values, and chart queries. The authorization
link opens a protected start route, which checks access to the originating layer
or specific view before invoking the provider's action. A grant on one view is
sufficient to connect a personal account; it does not grant access to sibling
views or permission to edit the layer.

The host supplies a completion URL as `return_url`. The provider must preserve
that URL in its server-side OAuth state and redirect there **after** validating
the callback and durably saving the authenticated user's tokens. OAuth state,
PKCE validation, token exchange, refresh, and revocation remain the provider's
responsibility. The host completion page sends a correlation ID through
`BroadcastChannel` and the storage-event fallback; it never sends credentials.
The original tab then refreshes the configuration form or retries the failed
operation. Returning browser focus before authorization completes does not
consume the completion notification. Closing an unfinished connection window
allows the user to start another attempt.

The completion URL uses the request's externally visible host and scheme. Operators
must configure their reverse proxy, forwarded headers, and allowed hosts so
Superset sees its trusted public HTTPS origin. See the
[reverse proxy configuration](/admin-docs/configuration/configuring-superset#configuration-behind-a-load-balancer).

Action POST responses contain `result.redirect_url` and a host-generated
`result.tab_id`. Both the configuration modal and the generic OAuth error UI
use the same completion listener. No access or refresh token is returned in
these responses.
50 changes: 46 additions & 4 deletions docs/developer_docs/extensions/storage.md
Original file line number Diff line number Diff line change
Expand Up @@ -438,8 +438,8 @@ import { extensions } from '@apache-superset/core';

const ctx = extensions.getContext();

await ctx.storage.persistent.set('api_token', 'sk-...', { encrypt: true });
const token = await ctx.storage.persistent.get('api_token');
await ctx.storage.persistent.set('private_note', 'draft', { encrypt: true });
const note = await ctx.storage.persistent.get('private_note');
```

```python
Expand All @@ -448,10 +448,52 @@ from superset_core.extensions.storage.persistent import PersistentSetOptions

ctx = get_context()

ctx.storage.persistent.set('api_token', 'sk-...', PersistentSetOptions(encrypt=True))
token = ctx.storage.persistent.get('api_token')
ctx.storage.persistent.set(
'private_note',
'draft',
PersistentSetOptions(encrypt=True),
)
note = ctx.storage.persistent.get('private_note')
```

Encryption protects a value at rest; it does not make an ordinary persistent
value backend-only. The persistent API is intentionally available to frontend
extensions, so the authenticated user's browser can read values written with
`encrypt=True`. Do not use `persistent` for OAuth tokens, private keys, or
other credentials that must stay on the server.

### Backend-only secrets

Backend extensions can store user-scoped credentials through the Python-only
secrets accessor:

```python
from superset_core.extensions.context import get_context

ctx = get_context()
ctx.storage.secrets.set(
"oauth2:quiver",
{
"access_token": access_token,
"refresh_token": refresh_token,
"expires_at": expires_at,
},
)
token = ctx.storage.secrets.get("oauth2:quiver")
ctx.storage.secrets.remove("oauth2:quiver")
```

Secrets are always encrypted at rest and scoped to the current authenticated
user and extension. They cannot be read, listed, overwritten, or deleted
through the extension storage REST API and are not exposed by the TypeScript
SDK. The accessor intentionally has no `list()` or `.shared` operation. Use
deployment configuration for extension-wide client secrets or service
credentials.

Secret operations require an authenticated principal. Background work must
execute with the identity of the user who initiated it; the accessor fails
closed when no user identity is available.

Encryption reuses Superset's existing `EncryptedType` (from `sqlalchemy-utils`) rather than a separate mechanism — the same infrastructure used for database connection credentials. The key is not configured directly: user-scoped values are encrypted with a key derived per-user via HMAC-SHA256 from the deployment's `SECRET_KEY`, so ciphertext for one user cannot be decrypted as another's; shared/global values (written via `.shared`) use `SECRET_KEY` directly. The encryption engine (AES-CBC by default) can be changed for the whole deployment via the existing `SQLALCHEMY_ENCRYPTED_FIELD_ENGINE` config, the same setting that controls encryption for database credentials elsewhere in Superset — there is no separate key list or rotation mechanism specific to extension storage. When `SECRET_KEY` is rotated, extension storage rows are re-encrypted by the existing `SecretsMigrator` tooling alongside database credentials, with no extension-specific steps required.

### Quotas
Expand Down
Loading