Skip to content

Commit ecff569

Browse files
feat: add Workload Identity Federation (WIF) support to connection update (#1887)
* @W-24145082 - Add WIF workgroupAuthMode and authFederationServiceAccount 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> * chore: apply black formatting 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> * docs: add CHANGELOG entry for WIF connection update Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: use googlebigquery and add WIF response attrs in test assets Address review on #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> * feat: nest WIF value set under ConnectionItem and extend to bulk and flows Address review on #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> * docs: expand WIF guidance in CHANGELOG Address review on #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> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent f0d3b62 commit ecff569

11 files changed

Lines changed: 597 additions & 0 deletions

‎CHANGELOG.md‎

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,38 @@
11

22
## Unreleased
33

4+
* Added Workload Identity Federation (WIF) support to the connection update
5+
endpoints. `ConnectionItem` has two new string properties,
6+
`workgroup_auth_mode` and `auth_federation_service_account`, that are
7+
emitted on the `<connection>` payload by `workbooks.update_connection`,
8+
`workbooks.update_connections`, `datasources.update_connection`,
9+
`datasources.update_connections`, and `flows.update_connection`. The two
10+
bulk endpoints also accept matching `workgroup_auth_mode` and
11+
`auth_federation_service_account` keyword arguments and apply them to
12+
every connection LUID in the request. Set `workgroup_auth_mode` to one of
13+
the values on the new nested value set
14+
`ConnectionItem.WorkgroupAuthMode`:
15+
- `ConnectionItem.WorkgroupAuthMode.WIF` =
16+
`"workload-identity-federation"` — direct WIF, e.g. Snowflake
17+
- `ConnectionItem.WorkgroupAuthMode.WIF_IMPERSONATION` =
18+
`"workload-identity-federation-impersonation"` — impersonation WIF,
19+
e.g. Google BigQuery
20+
Usage rules:
21+
- Resend `workgroup_auth_mode` on every subsequent update of a WIF
22+
connection; omitting it on a later update returns HTTP 400.
23+
- Leave `auth_type` unset (or `"oauth"`) when sending a WIF mode; the
24+
server derives the stored auth type from the mode.
25+
- Set `auth_federation_service_account` only with `WIF_IMPERSONATION`.
26+
The server rejects a service account with the direct `WIF` mode. TSC
27+
does not validate this combination client-side.
28+
- The server does not echo these attributes on GET responses today, so
29+
`ConnectionItem.workgroup_auth_mode` and
30+
`ConnectionItem.auth_federation_service_account` parsed from a response
31+
will typically be `None` even when the connection is WIF-configured.
32+
Requires Tableau Server REST API 3.30+ (Tableau Server release 26.3) with
33+
WIF configured and enabled on the site. Older servers silently ignore
34+
unknown connection attributes, so these fields are safe to leave unset on
35+
pre-3.30 targets.
436
* Bumped the urllib3 floor to 2.6.3 to pick up the fix for CVE-2026-21441
537
(GHSA-38jv-5279-wg99, 8.9 High): urllib3's streaming decompression
638
safeguards were bypassed when HTTP redirects were followed. TSC's manual

‎tableauserverclient/models/connection_item.py‎

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,50 @@ class ConnectionItem:
5050
5151
database_name: str
5252
The name of the database for the connection.
53+
54+
workgroup_auth_mode: str
55+
Workload Identity Federation (WIF) auth mode. Set to one of the
56+
values on ``ConnectionItem.WorkgroupAuthMode`` to switch an existing
57+
connection to WIF on an update. Requires Tableau Server 26.3 / REST
58+
API 3.30+ with WIF configured and enabled on the site. See the
59+
``WorkgroupAuthMode`` nested class for usage notes. The server does
60+
not echo this attribute on GET responses today, so values parsed
61+
from a response are typically ``None``.
62+
63+
auth_federation_service_account: str
64+
Service-account email required by the server for the
65+
``WorkgroupAuthMode.WIF_IMPERSONATION`` mode (e.g. Google BigQuery)
66+
and rejected for the direct ``WorkgroupAuthMode.WIF`` mode (e.g.
67+
Snowflake). Also requires REST API 3.30+. Not echoed on GET today,
68+
so typically ``None`` when parsed from a response.
5369
"""
5470

71+
class WorkgroupAuthMode:
72+
"""
73+
Workload Identity Federation (WIF) auth-mode values for
74+
:attr:`ConnectionItem.workgroup_auth_mode`.
75+
76+
Requires Tableau Server 26.3 / REST API 3.30+ with WIF configured
77+
and enabled on the site. Older servers silently ignore unknown
78+
connection attributes, so emitting these against an older server is
79+
a no-op rather than an error.
80+
81+
Usage notes:
82+
83+
- Send ``workgroup_auth_mode`` on every subsequent update of a WIF
84+
connection. Once a connection has been switched to WIF, omitting
85+
the attribute on a later update returns HTTP 400.
86+
- Leave :attr:`ConnectionItem.auth_type` unset (or ``"oauth"``) when
87+
sending a WIF mode; the server derives the stored auth type from
88+
the mode.
89+
- Do not set :attr:`ConnectionItem.auth_federation_service_account`
90+
with the direct ``WIF`` mode (the server rejects it). Set it only
91+
with ``WIF_IMPERSONATION``.
92+
"""
93+
94+
WIF = "workload-identity-federation"
95+
WIF_IMPERSONATION = "workload-identity-federation-impersonation"
96+
5597
def __init__(self):
5698
self._datasource_id: str | None = None
5799
self._datasource_name: str | None = None
@@ -66,6 +108,8 @@ def __init__(self):
66108
self._query_tagging: bool | None = None
67109
self._auth_type: str | None = None
68110
self._database_name: str | None = None
111+
self._workgroup_auth_mode: str | None = None
112+
self._auth_federation_service_account: str | None = None
69113

70114
@property
71115
def datasource_id(self) -> str | None:
@@ -106,6 +150,22 @@ def auth_type(self) -> str | None:
106150
def auth_type(self, value: str | None):
107151
self._auth_type = value
108152

153+
@property
154+
def workgroup_auth_mode(self) -> str | None:
155+
return self._workgroup_auth_mode
156+
157+
@workgroup_auth_mode.setter
158+
def workgroup_auth_mode(self, value: str | None):
159+
self._workgroup_auth_mode = value
160+
161+
@property
162+
def auth_federation_service_account(self) -> str | None:
163+
return self._auth_federation_service_account
164+
165+
@auth_federation_service_account.setter
166+
def auth_federation_service_account(self, value: str | None):
167+
self._auth_federation_service_account = value
168+
109169
@property
110170
def database_name(self) -> str | None:
111171
return self._database_name
@@ -136,6 +196,8 @@ def from_response(cls, resp, ns) -> list["ConnectionItem"]:
136196
string_to_bool(s) if (s := connection_xml.get("queryTagging", None)) else None
137197
)
138198
connection_item._auth_type = connection_xml.get("authenticationType", None)
199+
connection_item._workgroup_auth_mode = connection_xml.get("workgroupAuthMode", None)
200+
connection_item._auth_federation_service_account = connection_xml.get("authFederationServiceAccount", None)
139201
# The REST API GET /connections response uses "dbName" for the database
140202
# name attribute. This is different from the publish request body, which
141203
# uses "databaseName" (see _add_connections_element in request_factory.py).

‎tableauserverclient/server/endpoint/datasources_endpoint.py‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -396,6 +396,8 @@ def update_connections(
396396
username: str | None = None,
397397
password: str | None = None,
398398
embed_password: bool | None = None,
399+
workgroup_auth_mode: str | None = None,
400+
auth_federation_service_account: str | None = None,
399401
) -> list[ConnectionItem]:
400402
"""
401403
Bulk updates one or more datasource connections by LUID.
@@ -424,6 +426,20 @@ def update_connections(
424426
embed_password : bool, optional
425427
Whether to embed the password.
426428
429+
workgroup_auth_mode : str, optional
430+
Workload Identity Federation (WIF) auth mode. See
431+
``ConnectionItem.WorkgroupAuthMode`` for allowed values and
432+
usage notes. Applied to every connection LUID in this call.
433+
Requires Tableau Server REST API 3.30+ with WIF configured on
434+
the site.
435+
436+
auth_federation_service_account : str, optional
437+
Service-account email for WIF impersonation. Required with
438+
``ConnectionItem.WorkgroupAuthMode.WIF_IMPERSONATION`` and
439+
rejected by the server with the direct
440+
``ConnectionItem.WorkgroupAuthMode.WIF`` mode. TSC does not
441+
validate the combination client-side.
442+
427443
Returns
428444
-------
429445
Iterable of str
@@ -438,6 +454,8 @@ def update_connections(
438454
username=username,
439455
password=password,
440456
embed_password=embed_password,
457+
workgroup_auth_mode=workgroup_auth_mode,
458+
auth_federation_service_account=auth_federation_service_account,
441459
)
442460
server_response = self.put_request(url, request_body)
443461
connection_items = ConnectionItem.from_response(server_response.content, self.parent_srv.namespace)

‎tableauserverclient/server/endpoint/flows_endpoint.py‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,16 @@ def update_connection(self, flow_item: FlowItem, connection_item: ConnectionItem
264264
265265
REST API: https://help.tableau.com/current/api/rest_api/en-us/REST/rest_api_ref_flow.htm#update_flow_connection
266266
267+
Setting ``connection_item.workgroup_auth_mode`` and
268+
``connection_item.auth_federation_service_account`` (see
269+
``ConnectionItem.WorkgroupAuthMode``) will forward the Workload
270+
Identity Federation attributes to this endpoint via the shared
271+
``RequestFactory.Connection.update_req`` builder. Server-side
272+
handling of these attributes on flow connections is governed by
273+
the flow endpoint and may differ from the workbook / datasource
274+
endpoints — review the REST reference for flow-specific semantics
275+
before relying on it in production.
276+
267277
Parameters
268278
----------
269279
flow_item: FlowItem

‎tableauserverclient/server/endpoint/workbooks_endpoint.py‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -343,6 +343,8 @@ def update_connections(
343343
username: str | None = None,
344344
password: str | None = None,
345345
embed_password: bool | None = None,
346+
workgroup_auth_mode: str | None = None,
347+
auth_federation_service_account: str | None = None,
346348
) -> list[ConnectionItem]:
347349
"""
348350
Bulk updates one or more workbook connections by LUID.
@@ -371,6 +373,20 @@ def update_connections(
371373
embed_password : bool, optional
372374
Whether to embed the password.
373375
376+
workgroup_auth_mode : str, optional
377+
Workload Identity Federation (WIF) auth mode. See
378+
``ConnectionItem.WorkgroupAuthMode`` for allowed values and
379+
usage notes. Applied to every connection LUID in this call.
380+
Requires Tableau Server REST API 3.30+ with WIF configured on
381+
the site.
382+
383+
auth_federation_service_account : str, optional
384+
Service-account email for WIF impersonation. Required with
385+
``ConnectionItem.WorkgroupAuthMode.WIF_IMPERSONATION`` and
386+
rejected by the server with the direct
387+
``ConnectionItem.WorkgroupAuthMode.WIF`` mode. TSC does not
388+
validate the combination client-side.
389+
374390
Returns
375391
-------
376392
Iterable of str
@@ -385,6 +401,8 @@ def update_connections(
385401
username=username,
386402
password=password,
387403
embed_password=embed_password,
404+
workgroup_auth_mode=workgroup_auth_mode,
405+
auth_federation_service_account=auth_federation_service_account,
388406
)
389407

390408
# Send request

‎tableauserverclient/server/request_factory.py‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,8 @@ def update_connections_req(
257257
username: str | None = None,
258258
password: str | None = None,
259259
embed_password: bool | None = None,
260+
workgroup_auth_mode: str | None = None,
261+
auth_federation_service_account: str | None = None,
260262
):
261263
conn_luids_elem = ET.SubElement(element, "connectionLUIDs")
262264
for luid in connection_luids:
@@ -275,6 +277,12 @@ def update_connections_req(
275277
if embed_password is not None:
276278
connection_elem.set("embedPassword", str(embed_password).lower())
277279

280+
if workgroup_auth_mode is not None:
281+
connection_elem.set("workgroupAuthMode", workgroup_auth_mode)
282+
283+
if auth_federation_service_account is not None:
284+
connection_elem.set("authFederationServiceAccount", auth_federation_service_account)
285+
278286

279287
class DQWRequest:
280288
def add_req(self, dqw_item):
@@ -1184,6 +1192,8 @@ def update_connections_req(
11841192
username: str | None = None,
11851193
password: str | None = None,
11861194
embed_password: bool | None = None,
1195+
workgroup_auth_mode: str | None = None,
1196+
auth_federation_service_account: str | None = None,
11871197
):
11881198
conn_luids_elem = ET.SubElement(element, "connectionLUIDs")
11891199
for luid in connection_luids:
@@ -1202,6 +1212,12 @@ def update_connections_req(
12021212
if embed_password is not None:
12031213
connection_elem.set("embedPassword", str(embed_password).lower())
12041214

1215+
if workgroup_auth_mode is not None:
1216+
connection_elem.set("workgroupAuthMode", workgroup_auth_mode)
1217+
1218+
if auth_federation_service_account is not None:
1219+
connection_elem.set("authFederationServiceAccount", auth_federation_service_account)
1220+
12051221

12061222
class Connection:
12071223
@_tsrequest_wrapped
@@ -1226,6 +1242,10 @@ def update_req(self, xml_request: ET.Element, connection_item: "ConnectionItem")
12261242
connection_element.attrib["embedPassword"] = str(connection_item.embed_password).lower()
12271243
if connection_item.query_tagging is not None:
12281244
connection_element.attrib["queryTaggingEnabled"] = str(connection_item.query_tagging).lower()
1245+
if connection_item.workgroup_auth_mode is not None:
1246+
connection_element.attrib["workgroupAuthMode"] = connection_item.workgroup_auth_mode
1247+
if connection_item.auth_federation_service_account is not None:
1248+
connection_element.attrib["authFederationServiceAccount"] = connection_item.auth_federation_service_account
12291249

12301250

12311251
class TaskRequest:
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
<?xml version='1.0' encoding='UTF-8'?>
2+
<tsResponse xmlns="http://tableau.com/api" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
3+
<connection id="be786ae0-d2bf-4a4b-9b34-e2de8d2d4488" type="googlebigquery" serverAddress="www.googleapis.com/bigquery/v2" userName="user@example.com" embedPassword="false" authenticationType="oauth" workgroupAuthMode="workload-identity-federation-impersonation" authFederationServiceAccount="sa@proj.iam.gserviceaccount.com"/>
4+
</tsResponse>
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
<?xml version='1.0' encoding='UTF-8'?>
2+
<tsResponse xmlns="http://tableau.com/api" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
3+
<connection id="17376070-64d1-4d17-acb4-a56e4b5b1768" type="snowflake" serverAddress="account.snowflakecomputing.com" userName="" embedPassword="false" queryTaggingEnabled="false" authenticationType="oauth" workgroupAuthMode="workload-identity-federation"/>
4+
</tsResponse>

0 commit comments

Comments
 (0)