Repository navigation
feat: add Workload Identity Federation (WIF) support to connection update - #1887
Conversation
…unt to connection update The Tableau REST API's update-connection endpoints for workbooks and datasources (REST API 3.30+) accept two new XML attributes on <connection> payloads for Workload Identity Federation: - workgroupAuthMode: "workload-identity-federation" (direct, e.g. Snowflake) or "workload-identity-federation-impersonation" (impersonation, e.g. BigQuery) - authFederationServiceAccount: service account email, required for impersonation only Expose both as string properties on ConnectionItem and emit them from RequestFactory.Connection.update_req (the shared builder used by workbooks, datasources, and flows for both single and bulk variants). Also expose two constants on the public API: WORKGROUP_AUTH_MODE_WIF, WORKGROUP_AUTH_MODE_WIF_IMPERSONATION. Tests cover direct and impersonation variants against workbook and datasource update-connection endpoints. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Collapse auth_federation_service_account assignment onto a single line per the project's 120-char limit. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
🟡 Changes recommended
Bulk update_connections endpoints do not yet serialize the advertised WIF fields.
1 open finding
What changed in this PR
Adds Workload Identity Federation support for singular workbook and datasource connection updates.
Changes:
- Adds WIF fields and public constants.
- Serializes WIF attributes for singular updates.
- Adds response parsing, tests, fixtures, exports, and changelog documentation.
| File | Summary |
|---|---|
test/test_workbook.py |
Tests workbook WIF updates. |
test/test_datasource.py |
Tests datasource WIF updates. |
test/assets/workbook_update_connection_wif.xml |
Workbook WIF fixture. |
test/assets/datasource_update_connection_wif.xml |
Datasource WIF fixture. |
tableauserverclient/server/request_factory.py |
Serializes WIF attributes for singular updates. |
tableauserverclient/models/connection_item.py |
Adds WIF fields and response parsing. |
tableauserverclient/models/__init__.py |
Re-exports WIF constants. |
tableauserverclient/__init__.py |
Exports WIF constants. |
CHANGELOG.md |
Documents WIF support. |
🧠 Review effort: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| if connection_item.workgroup_auth_mode is not None: | ||
| connection_element.attrib["workgroupAuthMode"] = connection_item.workgroup_auth_mode | ||
| if connection_item.auth_federation_service_account is not None: | ||
| connection_element.attrib["authFederationServiceAccount"] = connection_item.auth_federation_service_account |
There was a problem hiding this comment.
Addressed in 3e70623.
Both Datasource.update_connections_req and Workbook.update_connections_req now accept workgroup_auth_mode and auth_federation_service_account kwargs and emit them on the single <connection> payload sent for the LUID list. The matching bulk endpoint methods (workbooks.update_connections / datasources.update_connections) thread those kwargs through, and the parametrized test_update_{workbook,datasource}_connections_wif[direct|impersonation] tests assert the emitted attributes.
Also, in the same commit:
- Nested the WIF value set under
ConnectionItem.WorkgroupAuthModeand dropped the twoWORKGROUP_AUTH_MODE_WIF*module-level exports (prior review feedback on namespacing). - Added a docstring note on
flows.update_connectionclarifying that WIF fields reach the request via the sharedConnection.update_reqbuilder, with the caveat that server-side flow handling may differ. - Added
test_update_connection_wif_switch_from_oauth,test_update_connection_wif_cleared_fields_omit_attrs,test_update_connection_wif_service_account_without_mode,test_update_connection_wif_from_response, andtest_update_connection_wif_from_response_absenton both workbook and datasource test suites, plus a newtest_update_connection_wifontest_flow.py.
Address review on tableau#1887: datasource WIF asset was carrying the legacy "bigquery" connector class, and both WIF assets referenced a 404 schemaLocation URL. Switch the datasource asset to the current "googlebigquery" class, drop the broken schemaLocation reference, and populate both WIF response assets with workgroupAuthMode (and authFederationServiceAccount where impersonation applies) so the new from_response coverage actually exercises WIF parsing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…flows Address review on tableau#1887: * Replace the two module-level WORKGROUP_AUTH_MODE_WIF* constants with a nested value set at ConnectionItem.WorkgroupAuthMode (WIF / WIF_IMPERSONATION) and drop the top-level reexports from tableauserverclient and tableauserverclient.models. The nested class carries the usage rules (resend on every update, no service account with direct mode, auth_type unset or "oauth", server echo caveat). * Extend the two bulk endpoints to really support WIF: workbooks.update_connections and datasources.update_connections now accept workgroup_auth_mode and auth_federation_service_account keyword arguments, and both Workbook.update_connections_req and Datasource.update_connections_req builders emit them on the shared <connection> payload applied to every connection LUID. * Document on flows.update_connection that WIF fields on the ConnectionItem reach the endpoint via the shared Connection.update_req builder and that server-side handling on flow connections may differ from workbook / datasource endpoints. * Rewrite the WIF test suite to cover: parametrized direct and impersonation happy paths, OAuth to WIF switch, cleared-fields non emission, service-account-without-mode permissive emit, from_response WIF parsing, from_response absent (older servers), and bulk WIF forwarding. Add a flow WIF test exercising the shared builder reach. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Address review on tableau#1887: expand the Unreleased CHANGELOG entry to name the new nested value set (ConnectionItem.WorkgroupAuthMode.WIF / WIF_IMPERSONATION), note that the bulk workbooks.update_connections and datasources.update_connections endpoints now accept the matching keyword arguments, add flows.update_connection to the list of endpoints that forward WIF, state the usage rules (resend on every update, no service account with direct mode, auth_type unset or "oauth", server does not echo on GET today), and clarify that "3.30+" is a Tableau Server REST API version (26.3 server release) and that older servers silently ignore unknown connection attributes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Pushed three commits addressing the inline review feedback:
Local: |
Adds documentation on the gh-pages site for the WIF support added in PR #1887: - ConnectionItem attribute table: new rows for workgroup_auth_mode and auth_federation_service_account, naming the ConnectionItem.WorkgroupAuthMode nested value set and its two values. - datasources.update_connection: WIF how-to with direct and impersonation examples plus the server-side usage rules. - datasources.update_connections (bulk): updated signature, new parameter rows, version note, and a WIF example. - flows.update_connection: short note that WIF fields reach the request via the shared Connection.update_req builder, with the caveat that server-side flow handling may differ. - workbooks.update_connection: WIF how-to mirroring the datasource section. - workbooks.update_connections (bulk): updated signature, new parameter rows, version note, and a WIF example. Requires REST API 3.30+ (Tableau Server release 26.3) with WIF configured on the site. Older servers silently ignore unknown connection attributes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…1894) * @W-24145098 - Add Workload Identity Federation (WIF) admin REST API Expose the Tableau Server WIF admin surface (REST API 3.30+ / Tableau Server release 2026.3) on the TSC server object as `server.auth_federation`. Site admins can now manage the whole WIF lifecycle from the client, complementing the per-connection WIF attributes delivered in PR #1887. New endpoint methods on `server.auth_federation`: Admin config (`/sites/{siteLuid}/auth-federation/admin`) - get() -> AuthFederationItem - enable(config) -> AuthFederationItem (idempotent PUT; requires enabled=True) - update(config) -> AuthFederationItem (merge-patch; only `token_lifetime_seconds` is mutable - `issuer_url`, `active_key_id`, and `connectors` are read-only and never sent on the wire) - delete() (deletes config + keys + connectors) Connectors (`.../admin/connectors`) - create_connector(connector) -> AuthFederationConnectorItem - list_connectors() -> list[AuthFederationConnectorItem] - get_connector(id) (accepts luid string or item) - delete_connector(id) (accepts luid string or item; idempotent) Keys (`.../admin/keys`) - create_key() -> AuthFederationKeyItem (inactive by default; private key is never returned) - activate_key(id) (atomic; deactivates whatever key was active) - deactivate_key(id) - delete_key(id) (400 if the key is still active) Three new public models exported from both `tableauserverclient.models` and the TSC top-level: `AuthFederationItem`, `AuthFederationConnectorItem`, `AuthFederationKeyItem`. All methods version-gated by @api(version="3.30"). Includes 22 unit tests under `test/test_auth_federation.py` covering the happy path for every method, request-body shape (via defusedxml parsing of `request_history[0].text`) for PUT/PATCH/POST verbs, the read-only-field hygiene of `update()`, the enabled=True guard on `enable()`, the required-field guards on `create_connector`, the list-connectors empty case, both the "pass a str" and "pass an item" forms of every id-taking method, and the EndpointUnavailableError raised when running against a server older than 3.30. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Document that AuthFederation.update() drops `enabled` Server-side AuthFederationAdminAppService.updateConfig rejects any non-null `enabled` on PATCH with HTTP 400 and tells callers to use enable() / delete() to change state. The client's update_req already only sends `tokenLifetimeSeconds`, but the docstring listed only issuer_url, active_key_id, and connectors as "silently ignored", leaving `enabled` as a hidden footgun. Add it to the list and point callers at the correct methods. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Align AuthFederation docstrings with monolith server behaviour Three docstring claims didn't match what the server actually does per AuthFederationService.java and RestApiAuthFederationKeysAppService.java: * enable(): did not mention that AuthFederationService.enableWif auto-generates one active signing key on first enable (AuthFederationService.java:160-164). A caller doesn't need to immediately create_key + activate_key — the returned item's active_key_id is already populated. * list_connectors(): claimed empty-list on "WIF not enabled". The server's requireWifEnabled (AuthFederationService.java:226-233) throws WIF_NOT_ENABLED_FOR_SITE / FAILED_PRECONDITION, which the client surfaces as ServerResponseError. Only an empty configured site returns []. * create_key(): claimed 409 for the max-keys-exceeded case. The server throws BadRequestRestException (HTTP 400), not 409 (RestApiAuthFederationKeysAppService.java:44-48). Docstring-only; no code or test changes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Correct list_connectors() docstring: server returns HTTP 404, not 412 RestApiAuthFederationAdminAppService.listConnectors intercepts TableauStatusCode.FAILED_PRECONDITION from the service-layer requireWifEnabled check and re-throws configNotFound (HTTP 404) with AUTH_FEDERATION_NOT_FOUND -- see RestApiAuthFederationAdminAppService.java:157-162. The service-layer behavior cited in the previous commit does not reach the wire; callers see 404. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Address review feedback on AuthFederation docs and tests - Reword PAT/OAuth-scope guidance: the four key methods are bound to OAuthScopes.NO_ACCESS server-side, so every OAuth-scoped session (including every PAT) is rejected unconditionally. Authenticate with a username/password session instead. Reflected in CHANGELOG and the class docstring. - Drop the misleading "Snowflake" case-sensitivity counter-example in create_connector; just state that class names are case-sensitive and must be the exact lowercase name. - Note in CHANGELOG that delete_key fails with 400 on an active key; deactivate first. - Replace vague "server rejects the request" in the class docstring with the specific HTTP 404 the server returns when WIF has never been enabled. - Make the _coerce_uuid wording in CHANGELOG explicit: None, empty strings, and non-UUID-shaped targets are rejected client-side. - Add a comment in test_auth_federation_update_empty_patch noting that server-side binding of an empty <authFederationAdmin/> must be covered by an integration test, not this unit test. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Correct 403/404 and PAT semantics; add e2e round-trip test Review follow-up on PR #1894. - 403/404 docstring and CHANGELOG rewritten per server behavior: authorizationCheck() runs requireWifFeatureEnabled first, so every method (incl. get()) returns 403 when the per-site WIF feature flag is off — even for a site admin. 404 holds only for update() and the four connector methods; delete() on a never-enabled site is a no-op; create_key() has no enable() gate and leaves an orphan key against the per-site key limit; activate/deactivate/delete_key 404 only on unknown kid. - PAT wording corrected: NO_ACCESS rejects only OAuth-scope-restricted sessions (e.g. connected-app JWT) with 401. PAT and password sessions do not carry scopes and are accepted — this is what the repo's own e2e harness relies on. - delete_key rotation guidance: deactivating the active key with no replacement leaves the site unable to mint tokens. Safe order is create_key() -> activate_key(new) -> delete_key(old). - New test_e2e/test_auth_federation.py: PAT-signed-in round trip across every method. Primarily a wire-shape regression test for the three bare-body routes (PUT/PATCH /admin, POST /admin/connectors) that mocked unit tests cannot catch. Guards against clobbering a live WIF config and tears down to a clean site on exit. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Tighten AuthFederation tests + require update() lifetime arg Three review nits from Wayne Xie's second pass on PR #1894: - Realism in the "never-enabled" GET fixture: use the real server defaults — token lifetime 7200 (AuthFederationService.java:86) and the actual issuer URL shape %s/api/%s/sites/<luid>/auth-federation (:748-766), not the fabricated /sites/mySite/... URL and 3600 lifetime. - UUID-guard matrix: add None to the bad-target list and parametrize over {raw, item} so the item-wrapped None path (connector.luid=None / key.kid=None) is also covered. Widens bad_target to object. - update() token_lifetime_seconds is now required: there is no other mutable field on the config, so a no-arg call is not a meaningful operation. Tightens the matching request_factory signature and drops the test that exercised the empty-patch wire shape. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Address Copilot review comments on WIF admin PR - E2E teardown: let `ServerResponseError` from `auth_federation.delete()` propagate instead of swallowing it. A failed teardown left WIF enabled on the site, so the next run would hit the fixture's `enabled=True` guard and silently skip — the destructive paths stopped being exercised without anyone noticing. - Key-rotation guidance (endpoint docstring, CHANGELOG, and docs): the previous `create_key() -> activate_key(new) -> delete_key(old)` sequence omitted that the old key must stay in the JWKS until every JWT it signed has expired. Deleting it immediately removes it from the key set, and verifiers that fetch JWKS afterwards reject the still-valid tokens signed with it. New guidance keeps the old key inactive (but published) for up to `token_lifetime_seconds` before deleting. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Remove rationale comment from WIF e2e teardown The behavior is self-evident from a bare `delete()` with no try/except; the rationale belongs in the PR / commit history, not the test body. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Rework key-lifecycle docstrings per PR #1894 review - deactivate_key: absorb the "deactivate on the active key with no replacement leaves the site unable to mint" warning, which belongs on deactivate_key itself (not delete_key). - delete_key: drop the create_key / activate_key chain from the rotation guidance. Reframe the docstring around what distinguishes delete from deactivate: both stop new signing with the key, but delete also pulls it from the JWKS, so still-unexpired tokens signed with it stop verifying. Rotation guidance now reduces to "keep the old key deactivated-but-published until its tokens expire, then delete". Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Motivation
Tableau Server REST API 3.30 (Tableau Server release 26.3) adds
Workload Identity Federation (WIF) support to the Update Connection
endpoints via two new
<connection>attributes:workgroupAuthModeandauthFederationServiceAccount. TSC currently has no way to set these,so Python callers against a WIF-enabled 3.30+ site can't programmatically
switch a workbook, data source, or flow connection to WIF (e.g.
Snowflake or Google BigQuery) without hand-rolling raw REST calls.
Behavior change
Additive, opt-in, backward compatible. For users:
ConnectionItemgains two new string properties:workgroup_auth_modeandauth_federation_service_account.ConnectionItem.WorkgroupAuthModeis a new nested value set:ConnectionItem.WorkgroupAuthMode.WIF="workload-identity-federation"— direct WIF (e.g. Snowflake)ConnectionItem.WorkgroupAuthMode.WIF_IMPERSONATION="workload-identity-federation-impersonation"— impersonation WIF (e.g. Google BigQuery)workbooks.update_connection/datasources.update_connection/flows.update_connectionemit both attributes on the<connection>payload when the corresponding
ConnectionItemfields are set.Unset fields are not emitted, so existing callers see no payload
change.
workbooks.update_connections/datasources.update_connections(bulk) accept matching
workgroup_auth_modeandauth_federation_service_accountkeyword arguments and apply themto every connection LUID in the request.
ConnectionItem.from_responseparses the two attributes defensively(REST 3.30 does not echo them on GET today, so parsed values are
typically
None— the parse is forward-compatible with any futureserver echo).
Usage rules the server enforces (TSC is permissive and relies on
server-side validation):
workgroup_auth_modeon every subsequent update of a WIFconnection; omitting it on a later update returns HTTP 400.
auth_typeunset (or"oauth") when sending a WIF mode; theserver derives the stored auth type from the mode.
auth_federation_service_accountonly with the impersonationmode. The server rejects a service account alongside the direct
WIFmode.
Compatibility:
26.3) with WIF configured and enabled on the site for the attributes
to take effect. Older servers silently ignore unknown connection
attributes, so leaving these fields unset is safe on pre-3.30
targets.
@apiversion decorator changes — this matches TSC'sestablished pattern for additive attribute support on existing
endpoints.
CHANGELOG entry added under Unreleased.
Test plan
New unit tests, all mocked (no live server), cover both WIF modes
across all three endpoints:
test/test_workbook.py— parametrizedtest_update_connection_wif[direct|impersonation],test_update_connection_wif_switch_from_oauth,test_update_connection_wif_cleared_fields_omit_attrs,test_update_connection_wif_service_account_without_mode,test_update_connection_wif_from_response,test_update_connection_wif_from_response_absent, and parametrizedtest_update_workbook_connections_wif[direct|impersonation]for thebulk endpoint.
test/test_datasource.py— identical shape against datasourceendpoints.
test/test_flow.py—test_update_connection_wifexercising theshared
Connection.update_reqreach through the flow endpoint.Each singular test populates a
ConnectionItemwith the new fields,calls
update_connection, and asserts against the serialized requestXML — verifying both that the attributes are emitted with the exact
spelling (
workgroupAuthMode/authFederationServiceAccount) andthat
authFederationServiceAccountis absent in direct (non-impersonation)mode. Bulk tests assert the same on the single
<connection>payloademitted for the whole LUID list. The from_response tests assert that
WIF attrs are parsed when the server does echo them and are
Nonewhen it does not.
Baselines still pass:
test_odata_connection,test_update_connections,and
test_update_connections_without_auth_type— the additive.get()calls in
from_responsedegrade cleanly toNoneon response XMLwithout the new attributes.
black --check --line-length 120andmypy tableauserverclient/clean.
pytest -n0 test/test_workbook.py test/test_datasource.py test/test_flow.py test/test_connection_.py— 164 passed.Attribute spellings and value strings verified against the
authoritative REST API 3.30 schema and the server-side bindings for
the Update Connection endpoints. Not exercised end-to-end against a
live 3.30 server in this PR.
🤖 Generated with Claude Code