diff --git a/docs-mintlify/admin/connect-to-data/oauth-authentication.mdx b/docs-mintlify/admin/connect-to-data/oauth-authentication.mdx new file mode 100644 index 0000000000000..52526b928214b --- /dev/null +++ b/docs-mintlify/admin/connect-to-data/oauth-authentication.mdx @@ -0,0 +1,269 @@ +--- +title: Set up per-user OAuth +description: Configure Cube to authenticate each user with their own OAuth token, falling back to a service account for liveness checks. +--- + + + +This feature is in beta. Reach out to your account manager to have it +enabled for your Cube Cloud deployment. + + + +## Use case + +You want each user's queries to run under their own database identity +using OAuth tokens managed by Cube Cloud. When a user's token is +unavailable or expired, Cube falls back to a service account so that +connectivity checks and background operations still work. + +This pattern applies to any data source that supports OAuth, including +[Databricks][ref-databricks-jdbc] and [Snowflake][ref-snowflake]. The +examples below use Databricks; switch the `userCredentials` key and +driver options for any other OAuth-capable data source. + +Because every user connects with different credentials, you also need +per-user query orchestrator state. Without this, one user's cached +connection could leak to another. + +## Prerequisites + +- A [Cube Cloud][ref-cube-cloud] deployment connected to an + OAuth-capable data source +- OAuth configured in your data source so that Cube Cloud can + obtain per-user tokens (via the **User Credentials** feature) +- A service account credential (token or password) stored as an + environment variable for fallback connectivity + + + +The service account credential is used only as a fallback for Cube's +internal liveness checks and background operations. Grant it the minimum +permissions necessary — ideally read-only access to the required schemas — +to limit exposure if the credential is compromised. + + + +## Set up the OAuth app + +Before configuring Cube to use per-user OAuth, register your data +source as an OAuth app in Cube Cloud: + + + + + +In Cube Cloud, go to **Admin → Integrations → OAuth apps** and click +**Add**. + + + Admin Integrations page showing the OAuth apps section with the Add button + + + + + + +Provide the OAuth app metadata from your data source: **Name**, +**Auth URL**, **Token URL**, **Client ID**, **Client Secret**, and any +required **Scopes**. Copy the **Redirect URI** shown in this form and +register it with your data source's OAuth provider, then click +**Create**. + + + New OAuth app form with fields for Name, Auth URL, Token URL, Client ID, Client Secret, Scopes, and Redirect URI + + + + + + +Open the user menu and go to **Connected apps**. Find your OAuth app +and click **Authorize** to generate an access token. + +You'll need to repeat this step whenever the token expires. + + + Connected apps page showing the OAuth integration with an Authorize action + + + + + + +## Configuration + +The configuration uses two options from the +[configuration file reference][ref-config]: + +- [`driver_factory`][ref-driver-factory] — dynamically selects the + authentication credential per request +- [`context_to_orchestrator_id`][ref-context-to-orchestrator-id] — gives + each user their own query orchestrator instance (database connections, + execution queues, pre-aggregation table caches) + +### Environment variables + +Set the environment variables for your data source. The examples below +show Databricks and Snowflake; adapt them to your specific setup. + + + + + +```dotenv +CUBEJS_DB_TYPE=databricks-jdbc +CUBEJS_DB_DATABRICKS_URL=jdbc:databricks://dbc-XXXXXXX-XXXX.cloud.databricks.com:443/default;transportMode=http;ssl=1;httpPath=sql/protocolv1/o/XXXXX/XXXXX;AuthMech=3;UID=token +CUBEJS_DB_DATABRICKS_TOKEN=dapi_service_account_token +CUBEJS_DB_DATABRICKS_ACCEPT_POLICY=true +# Optional: specify a catalog +CUBEJS_DB_DATABRICKS_CATALOG=my_catalog +``` + + + + + +```dotenv +CUBEJS_DB_TYPE=snowflake +CUBEJS_DB_SNOWFLAKE_ACCOUNT=XXXXXXXXX.us-east-1 +CUBEJS_DB_SNOWFLAKE_WAREHOUSE=MY_SNOWFLAKE_WAREHOUSE +CUBEJS_DB_NAME=my_snowflake_database +CUBEJS_DB_USER=service_account_user +CUBEJS_DB_PASS=service_account_password +CUBEJS_DB_SNOWFLAKE_ROLE=MY_ROLE +``` + + + + + +### Configuration file + +The examples below use Databricks. To target a different data source, +swap `userCredentials.databricks` for the matching key (for example, +`userCredentials.snowflake`) and update the `driver_factory` return +value with the correct `type` and driver-specific options. See the +[data sources reference][ref-data-sources] for available drivers. + + + + + +```python cube.py +from cube import config +import os + + +@config("driver_factory") +def driver_factory(ctx: dict) -> dict: + # Extract the Cube Cloud security context, which contains + # per-user OAuth credentials when available. + # For other data sources, swap "databricks" for "snowflake", etc. + databricks_creds = ( + ctx + .get("securityContext", {}) + .get("cubeCloud", {}) + .get("userCredentials", {}) + .get("databricks", {}) + ) + + # Only use the OAuth token when the credential status is "active". + # An expired or revoked token falls back to the service account. + oauth_token = ( + databricks_creds.get("accessToken") + if databricks_creds.get("status") == "active" + else None + ) + + return { + "type": "databricks-jdbc", + "url": os.environ["CUBEJS_DB_DATABRICKS_URL"], + # Prefer the user's OAuth token; fall back to the service account token + "token": oauth_token or os.environ["CUBEJS_DB_DATABRICKS_TOKEN"], + "acceptPolicy": True, + "catalog": os.environ.get("CUBEJS_DB_DATABRICKS_CATALOG"), + } + + +@config("context_to_orchestrator_id") +def context_to_orchestrator_id(ctx: dict) -> str: + # Give each user a separate orchestrator instance (DB connections, + # execution queues, pre-aggregation caches) + username = ( + ctx + .get("securityContext", {}) + .get("cubeCloud", {}) + .get("username", "default") + ) + return f"CUBE_APP_{username}" +``` + + + + + +```javascript cube.js +module.exports = { + driverFactory: ({ securityContext }) => { + // Extract the Cube Cloud security context, which contains + // per-user OAuth credentials when available. + // For other data sources, swap `databricks` for `snowflake`, etc. + const databricksCreds = + securityContext?.cubeCloud?.userCredentials?.databricks ?? {}; + + // Only use the OAuth token when the credential status is "active". + // An expired or revoked token falls back to the service account. + const oauthToken = + databricksCreds.status === "active" + ? databricksCreds.accessToken + : null; + + return { + type: "databricks-jdbc", + url: process.env.CUBEJS_DB_DATABRICKS_URL, + // Prefer the user's OAuth token; fall back to the service account token + token: oauthToken || process.env.CUBEJS_DB_DATABRICKS_TOKEN, + acceptPolicy: true, + catalog: process.env.CUBEJS_DB_DATABRICKS_CATALOG, + }; + }, + + // Give each user a separate orchestrator instance (DB connections, + // execution queues, pre-aggregation caches) + contextToOrchestratorId: ({ securityContext }) => { + const username = securityContext?.cubeCloud?.username ?? "default"; + return `CUBE_APP_${username}`; + }, +}; +``` + + + + + +## How it works + +1. **User makes a request** — Cube Cloud attaches the user's OAuth + credentials to `securityContext.cubeCloud.userCredentials.` + (for example, `.databricks` or `.snowflake`). + +2. **`driver_factory` resolves the credential** — If the credential status + is `active`, the user's OAuth token is used. Otherwise, Cube falls back + to the service account credential stored in environment variables. + +3. **Per-user orchestrator** — + [`context_to_orchestrator_id`][ref-context-to-orchestrator-id] returns + a unique key per username, so each user gets their own database + connection pool, execution queues, and pre-aggregation table cache. + Without this, Cube would share a single cached connection across all + users, causing one user's credentials to be reused for another user's + queries. + +[ref-config]: /reference/configuration/config +[ref-driver-factory]: /reference/configuration/config#driver_factory +[ref-context-to-orchestrator-id]: /reference/configuration/config#context_to_orchestrator_id +[ref-databricks-jdbc]: /admin/connect-to-data/data-sources/databricks-jdbc +[ref-snowflake]: /admin/connect-to-data/data-sources/snowflake +[ref-data-sources]: /admin/connect-to-data/data-sources +[ref-cube-cloud]: /docs/introduction diff --git a/docs-mintlify/docs.json b/docs-mintlify/docs.json index 2aae2264dcc93..0eade0bf5b3f5 100644 --- a/docs-mintlify/docs.json +++ b/docs-mintlify/docs.json @@ -262,7 +262,8 @@ ] }, "admin/connect-to-data/multiple-data-sources", - "admin/connect-to-data/concurrency" + "admin/connect-to-data/concurrency", + "admin/connect-to-data/oauth-authentication" ] }, { diff --git a/docs-mintlify/images/admin/connect-to-data/oauth-add-app.png b/docs-mintlify/images/admin/connect-to-data/oauth-add-app.png new file mode 100644 index 0000000000000..9b8983e091b97 Binary files /dev/null and b/docs-mintlify/images/admin/connect-to-data/oauth-add-app.png differ diff --git a/docs-mintlify/images/admin/connect-to-data/oauth-authorize.png b/docs-mintlify/images/admin/connect-to-data/oauth-authorize.png new file mode 100644 index 0000000000000..db8a271fd8a6a Binary files /dev/null and b/docs-mintlify/images/admin/connect-to-data/oauth-authorize.png differ diff --git a/docs-mintlify/images/admin/connect-to-data/oauth-fill-fields.png b/docs-mintlify/images/admin/connect-to-data/oauth-fill-fields.png new file mode 100644 index 0000000000000..9f8e55f64ebc3 Binary files /dev/null and b/docs-mintlify/images/admin/connect-to-data/oauth-fill-fields.png differ