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
30 changes: 28 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,16 +86,42 @@ baton resources

> **Important**: The user account must have **Site Administrator Explorer** (read-only sync) or **Site Administrator Creator** (sync + provisioning) role. PAT creation must be enabled by a site administrator.

> **Note**: A PAT belongs to an individual, so the connector inherits that account's lifecycle. Tableau also expires every PAT — after 1 to 365 days depending on site settings, and after 15 consecutive days of non-use. For a permanent integration, prefer a connected app.

**Documentation:**
- Tableau Cloud: https://help.tableau.com/current/online/en-us/security_personal_access_tokens.htm
- Tableau Server: https://help.tableau.com/current/server/en-us/security_personal_access_tokens.htm

## Connected app (direct trust)

Tableau Cloud only, October 2023 and later. A connected app is a site-level trust rather than a user's token. Its secret does not expire on its own; you rotate it when you choose to.

1. Sign in as a site administrator and go to **Settings** > **Connected Apps**
2. Click **New Connected App** > **Direct Trust**, name it, and click **Create**
3. Copy the **Client ID**, then generate a secret and copy its **Secret ID** and **Secret Value** — the value is displayed only once
4. Set the app's status to **Enabled** — it is created disabled, and a disabled app refuses every sign-in
5. Choose the Tableau user the connector acts as, and note its email address

> **Important**: The acting user needs the same site administrator role a PAT owner would. Access level and domain allowlist can be left at their defaults; they restrict embedded content, not REST API calls.

> **Note on scopes**: A direct trust app has no scope list of its own. The connector requests scopes in the signed assertion, and that claim is the only scope control, so there is nothing to grant here. It asks for `tableau:content:read`, `tableau:sites:read`, `tableau:users:*`, `tableau:groups:*` and the three `tableau:permissions:*` scopes. It deliberately does not ask for `tableau:projects:*` or `tableau:workbooks:*`: it reads those objects, which `tableau:content:read` covers, and edits their permissions, which the permission scopes cover, so the wildcards would only add project deletion and workbook publishing to what a leaked session could do.

> **Known gap**: Tableau publishes no scope covering site authentication configurations, so IDP discovery during account creation is refused under a connected app — observed as `401002 Unauthorized Access` against a Tableau Cloud site where the same call succeeds under a PAT. The connector treats that refusal as discovery being unavailable only when it signed in with a connected app; under a PAT the same status is a real failure and is reported. When no `idpConfigurationName` is given the connector treats the refusal as discovery being unavailable and falls back to the site's default authentication setting. On a site with several IDPs that is a real behaviour difference: a PAT would stop and ask you to name one, a connected app takes the site default. Set `idpConfigurationName` explicitly if that matters, and provisioning will fail loudly rather than guess.

**Documentation:** https://help.tableau.com/current/online/en-us/connected_apps_direct.htm

## Configuration Flags

Authenticate with **either** a personal access token **or** a connected app, never both. Each set is all-or-nothing; a partial or mixed configuration is rejected before the connector contacts Tableau.

| Flag | Required | Description |
|------|----------|-------------|
| `--access-token-name` | Yes | Name of the Personal Access Token |
| `--access-token-secret` | Yes | Secret value of the Personal Access Token |
| `--access-token-name` | With PAT | Name of the Personal Access Token. Tableau treats the name and secret as a pair, so a stale name alongside a fresh secret fails exactly as an expired token does |
| `--access-token-secret` | With PAT | Secret value of the Personal Access Token |
| `--connected-app-client-id` | With connected app | Client ID of a direct trust connected app |
| `--connected-app-secret-id` | With connected app | Secret ID of the connected app |
| `--connected-app-secret-value` | With connected app | Secret value of the connected app |
| `--connected-app-username` | With connected app | Email address of the Tableau user the connector acts as |
| `--server-path` | Yes | Base URL **without** `/api/<version>` suffix. Examples: `us-east-1.online.tableau.com` (Cloud), `your-server-hostname` (Server) |
| `--site-id` | No | Content URL of the site (e.g., `mycompany`). Can be found after `/site/` in the browser URL. Leave empty for the default site on Tableau Server |
| `--api-version` | No | Tableau REST API version (default: `3.27`). Can be changed to match your server's supported version — see [API version reference](https://help.tableau.com/current/api/rest_api/en-us/REST/rest_api_concepts_versions.htm) |
Expand Down
75 changes: 61 additions & 14 deletions config_schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -95,25 +95,40 @@
{
"name": "access-token-name",
"displayName": "Access Token Name",
"description": "Access token name used to connect to the Tableau API",
"isRequired": true,
"stringField": {
"rules": {
"isRequired": true
}
}
"description": "Access token name used to connect to the Tableau API. Required unless connected app credentials are supplied",
"stringField": {}
},
{
"name": "access-token-secret",
"displayName": "Access Token Secret",
"description": "Access token secret used to connect to the Tableau API",
"isRequired": true,
"description": "Access token secret used to connect to the Tableau API. Required unless connected app credentials are supplied",
"isSecret": true,
"stringField": {
"rules": {
"isRequired": true
}
}
"stringField": {}
},
{
"name": "connected-app-client-id",
"displayName": "Connected App Client ID",
"description": "Client ID of a Tableau connected app configured for direct trust. Use instead of a personal access token",
"stringField": {}
},
{
"name": "connected-app-secret-id",
"displayName": "Connected App Secret ID",
"description": "Secret ID of the Tableau connected app",
"stringField": {}
},
{
"name": "connected-app-secret-value",
"displayName": "Connected App Secret Value",
"description": "Secret value of the Tableau connected app",
"isSecret": true,
"stringField": {}
},
{
"name": "connected-app-username",
"displayName": "Connected App Username",
"description": "Email address of the Tableau user the connector acts as. Needs the same site administrator rights as a personal access token owner",
"stringField": {}
},
{
"name": "server-path",
Expand Down Expand Up @@ -141,6 +156,38 @@
}
}
],
"constraints": [
{
"kind": "CONSTRAINT_KIND_REQUIRED_TOGETHER",
"fieldNames": [
"access-token-name",
"access-token-secret"
]
},
{
"kind": "CONSTRAINT_KIND_REQUIRED_TOGETHER",
"fieldNames": [
"connected-app-client-id",
"connected-app-secret-id",
"connected-app-secret-value",
"connected-app-username"
]
},
{
"kind": "CONSTRAINT_KIND_AT_LEAST_ONE",
"fieldNames": [
"access-token-name",
"connected-app-client-id"
]
},
{
"kind": "CONSTRAINT_KIND_MUTUALLY_EXCLUSIVE",
"fieldNames": [
"access-token-name",
"connected-app-client-id"
]
}
],
"displayName": "Tableau",
"helpUrl": "/docs/baton/tableau",
"iconUrl": "/static/app-icons/tableau.svg"
Expand Down
46 changes: 42 additions & 4 deletions docs/connector.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -66,10 +66,46 @@ In the menu bar at the top of the page, click your profile image or initials and
In the **Personal Access Tokens** area of the page, enter a name for your new token (such as "C1 integration") and then click **Create**.
</Step>
<Step>
Carefully copy and save the newly generated token and its name.
Carefully copy and save the newly generated token and its name. Tableau treats the name and the secret as a pair, so both are required and both must belong to the same token.
</Step>
</Steps>

### Or set up a connected app

A personal access token belongs to an individual, so the connector follows that person's account lifecycle. Tableau also expires every token: after 1 to 365 days, depending on your site settings, and after 15 days without use. A connected app is a site-level trust instead. Its secret does not expire on its own; you rotate it when you choose to.

Configure either a personal access token or a connected app, not both.

<Steps>
<Step>
Sign into Tableau Cloud as a site administrator and navigate to **Settings** > **Connected Apps**.
</Step>
<Step>
Click **New Connected App** > **Direct Trust**, give it a name, and click **Create**.
</Step>
<Step>
Copy the **Client ID**. Then generate a secret and copy both its **Secret ID** and **Secret Value**. The secret value is shown only once.
</Step>
<Step>
Set the connected app's status to **Enabled**. It is created disabled, and a disabled app refuses every sign-in.

There are no scopes to grant here. A direct trust app carries no scope list of its own — the connector requests these in the signed assertion, and that claim is the only scope control:

`tableau:content:read`, `tableau:sites:read`, `tableau:users:*`, `tableau:groups:*`, `tableau:permissions:read`, `tableau:permissions:update`, `tableau:permissions:delete`

The connector does not request `tableau:projects:*` or `tableau:workbooks:*`. It reads those objects, which `tableau:content:read` covers, and edits their permissions, which the permission scopes cover. The wildcards would only add project deletion and workbook publishing to what a leaked session could do.

Leave access level and domain allowlist at their defaults; both restrict embedded content rather than REST API calls.
</Step>
<Step>
Decide which Tableau user the connector acts as, usually a service account. It needs the same site administrator rights a personal access token owner would need. Save its email address.
</Step>
</Steps>

<Note>
Tableau publishes no access scope covering site authentication configurations, so under a connected app that lookup is refused — observed as `401002 Unauthorized Access` against a Tableau Cloud site where it succeeds under a personal access token. The connector reads that refusal as discovery being unavailable only when it signed in with a connected app. Under a personal access token the same status is a genuine failure and is reported rather than absorbed. With no **IDP Configuration Name** set, the connector treats the refusal as discovery being unavailable and falls back to the site's default authentication setting. On a site with more than one IDP that differs from personal access token behaviour, which stops and asks you to name one. Set **IDP Configuration Name** explicitly if the choice matters; provisioning then fails with an explicit error instead of guessing.
</Note>

### Locate your server path and site ID

<Steps>
Expand Down Expand Up @@ -133,10 +169,10 @@ Find the **Settings** area of the page and click **Edit**.
Enter the site ID and server path into the **Site ID** and **Server path** fields.
</Step>
<Step>
Enter the name of the personal access token into the **Access token name** field.
If you are using a personal access token, enter its name into the **Access token name** field and its value into the **Access token secret** field.
</Step>
<Step>
Enter the personal access token value into the **Access token secret** field.
If you are using a connected app instead, leave the access token fields empty and fill in **Connected app client ID**, **Connected app secret ID**, **Connected app secret value**, and **Connected app username**. The username is the email address of the Tableau user the connector acts as.
</Step>
<Step>
Click **Save**.
Expand Down Expand Up @@ -294,8 +330,10 @@ These fields appear in the **provisioning mapping** for the Licenses entitlement
| **IDP Configuration Name** set, IDP found | Account created with the named IDP |
| **IDP Configuration Name** set, IDP not found | Provisioning fails with an explicit error naming the missing IDP |
| **IDP Configuration Name** set, API version < 3.22 | Provisioning fails with an explicit error — upgrade your Tableau Server or remove the field |
| **IDP Configuration Name** set, connected app authentication | Provisioning fails with an explicit error — Tableau refuses IDP discovery to a JWT session, so use a personal access token or remove the field |
| Neither field set, IDP discovery unavailable or refused | Account created with Tableau site default authentication |
| **With MFA** = `true` | Account created with Tableau MFA — **IDP Configuration Name** is ignored regardless of API version |

<Note>
If your Tableau Server uses an API version older than 3.22 and you do not set **IDP Configuration Name**, account provisioning uses the site default authentication without error. The IDP endpoint is only required when you explicitly configure an IDP name.
The IDP endpoint is only consulted when it can be. If your Tableau Server predates API 3.22, or you sign in with a connected app, and you do not set **IDP Configuration Name**, account provisioning falls back to the site default authentication without error.
</Note>
Loading