Skip to content
Merged
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
32 changes: 32 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,38 @@

## Unreleased

* Added Workload Identity Federation (WIF) support to the connection update
endpoints. `ConnectionItem` has two new string properties,
`workgroup_auth_mode` and `auth_federation_service_account`, that are
emitted on the `<connection>` payload by `workbooks.update_connection`,
`workbooks.update_connections`, `datasources.update_connection`,
`datasources.update_connections`, and `flows.update_connection`. The two
bulk endpoints also accept matching `workgroup_auth_mode` and
`auth_federation_service_account` keyword arguments and apply them to
every connection LUID in the request. Set `workgroup_auth_mode` to one of
the values on the new nested value set
`ConnectionItem.WorkgroupAuthMode`:
- `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
Usage rules:
- Resend `workgroup_auth_mode` on every subsequent update of a WIF
connection; omitting it on a later update returns HTTP 400.
- Leave `auth_type` unset (or `"oauth"`) when sending a WIF mode; the
server derives the stored auth type from the mode.
- Set `auth_federation_service_account` only with `WIF_IMPERSONATION`.
The server rejects a service account with the direct `WIF` mode. TSC
does not validate this combination client-side.
- The server does not echo these attributes on GET responses today, so
`ConnectionItem.workgroup_auth_mode` and
`ConnectionItem.auth_federation_service_account` parsed from a response
will typically be `None` even when the connection is WIF-configured.
Requires Tableau Server REST API 3.30+ (Tableau Server release 26.3) with
WIF configured and enabled on the site. Older servers silently ignore
unknown connection attributes, so these fields are safe to leave unset on
pre-3.30 targets.
* Bumped the urllib3 floor to 2.6.3 to pick up the fix for CVE-2026-21441
(GHSA-38jv-5279-wg99, 8.9 High): urllib3's streaming decompression
safeguards were bypassed when HTTP redirects were followed. TSC's manual
Expand Down
62 changes: 62 additions & 0 deletions tableauserverclient/models/connection_item.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,50 @@ class ConnectionItem:

database_name: str
The name of the database for the connection.

workgroup_auth_mode: str
Workload Identity Federation (WIF) auth mode. Set to one of the
values on ``ConnectionItem.WorkgroupAuthMode`` to switch an existing
connection to WIF on an update. Requires Tableau Server 26.3 / REST
API 3.30+ with WIF configured and enabled on the site. See the
``WorkgroupAuthMode`` nested class for usage notes. The server does
not echo this attribute on GET responses today, so values parsed
from a response are typically ``None``.

auth_federation_service_account: str
Service-account email required by the server for the
``WorkgroupAuthMode.WIF_IMPERSONATION`` mode (e.g. Google BigQuery)
and rejected for the direct ``WorkgroupAuthMode.WIF`` mode (e.g.
Snowflake). Also requires REST API 3.30+. Not echoed on GET today,
so typically ``None`` when parsed from a response.
"""

class WorkgroupAuthMode:
"""
Workload Identity Federation (WIF) auth-mode values for
:attr:`ConnectionItem.workgroup_auth_mode`.

Requires Tableau Server 26.3 / REST API 3.30+ with WIF configured
and enabled on the site. Older servers silently ignore unknown
connection attributes, so emitting these against an older server is
a no-op rather than an error.

Usage notes:

- Send ``workgroup_auth_mode`` on every subsequent update of a WIF
connection. Once a connection has been switched to WIF, omitting
the attribute on a later update returns HTTP 400.
- Leave :attr:`ConnectionItem.auth_type` unset (or ``"oauth"``) when
sending a WIF mode; the server derives the stored auth type from
the mode.
- Do not set :attr:`ConnectionItem.auth_federation_service_account`
with the direct ``WIF`` mode (the server rejects it). Set it only
with ``WIF_IMPERSONATION``.
"""

WIF = "workload-identity-federation"
WIF_IMPERSONATION = "workload-identity-federation-impersonation"

def __init__(self):
self._datasource_id: str | None = None
self._datasource_name: str | None = None
Expand All @@ -66,6 +108,8 @@ def __init__(self):
self._query_tagging: bool | None = None
self._auth_type: str | None = None
self._database_name: str | None = None
self._workgroup_auth_mode: str | None = None
self._auth_federation_service_account: str | None = None

@property
def datasource_id(self) -> str | None:
Expand Down Expand Up @@ -106,6 +150,22 @@ def auth_type(self) -> str | None:
def auth_type(self, value: str | None):
self._auth_type = value

@property
def workgroup_auth_mode(self) -> str | None:
return self._workgroup_auth_mode

@workgroup_auth_mode.setter
def workgroup_auth_mode(self, value: str | None):
self._workgroup_auth_mode = value

@property
def auth_federation_service_account(self) -> str | None:
return self._auth_federation_service_account

@auth_federation_service_account.setter
def auth_federation_service_account(self, value: str | None):
self._auth_federation_service_account = value

@property
def database_name(self) -> str | None:
return self._database_name
Expand Down Expand Up @@ -136,6 +196,8 @@ def from_response(cls, resp, ns) -> list["ConnectionItem"]:
string_to_bool(s) if (s := connection_xml.get("queryTagging", None)) else None
)
connection_item._auth_type = connection_xml.get("authenticationType", None)
connection_item._workgroup_auth_mode = connection_xml.get("workgroupAuthMode", None)
connection_item._auth_federation_service_account = connection_xml.get("authFederationServiceAccount", None)
# The REST API GET /connections response uses "dbName" for the database
# name attribute. This is different from the publish request body, which
# uses "databaseName" (see _add_connections_element in request_factory.py).
Expand Down
18 changes: 18 additions & 0 deletions tableauserverclient/server/endpoint/datasources_endpoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -396,6 +396,8 @@ def update_connections(
username: str | None = None,
password: str | None = None,
embed_password: bool | None = None,
workgroup_auth_mode: str | None = None,
auth_federation_service_account: str | None = None,
) -> list[ConnectionItem]:
"""
Bulk updates one or more datasource connections by LUID.
Expand Down Expand Up @@ -424,6 +426,20 @@ def update_connections(
embed_password : bool, optional
Whether to embed the password.

workgroup_auth_mode : str, optional
Workload Identity Federation (WIF) auth mode. See
``ConnectionItem.WorkgroupAuthMode`` for allowed values and
usage notes. Applied to every connection LUID in this call.
Requires Tableau Server REST API 3.30+ with WIF configured on
the site.

auth_federation_service_account : str, optional
Service-account email for WIF impersonation. Required with
``ConnectionItem.WorkgroupAuthMode.WIF_IMPERSONATION`` and
rejected by the server with the direct
``ConnectionItem.WorkgroupAuthMode.WIF`` mode. TSC does not
validate the combination client-side.

Returns
-------
Iterable of str
Expand All @@ -438,6 +454,8 @@ def update_connections(
username=username,
password=password,
embed_password=embed_password,
workgroup_auth_mode=workgroup_auth_mode,
auth_federation_service_account=auth_federation_service_account,
)
server_response = self.put_request(url, request_body)
connection_items = ConnectionItem.from_response(server_response.content, self.parent_srv.namespace)
Expand Down
10 changes: 10 additions & 0 deletions tableauserverclient/server/endpoint/flows_endpoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,16 @@ def update_connection(self, flow_item: FlowItem, connection_item: ConnectionItem

REST API: https://help.tableau.com/current/api/rest_api/en-us/REST/rest_api_ref_flow.htm#update_flow_connection

Setting ``connection_item.workgroup_auth_mode`` and
``connection_item.auth_federation_service_account`` (see
``ConnectionItem.WorkgroupAuthMode``) will forward the Workload
Identity Federation attributes to this endpoint via the shared
``RequestFactory.Connection.update_req`` builder. Server-side
handling of these attributes on flow connections is governed by
the flow endpoint and may differ from the workbook / datasource
endpoints — review the REST reference for flow-specific semantics
before relying on it in production.

Parameters
----------
flow_item: FlowItem
Expand Down
18 changes: 18 additions & 0 deletions tableauserverclient/server/endpoint/workbooks_endpoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,8 @@ def update_connections(
username: str | None = None,
password: str | None = None,
embed_password: bool | None = None,
workgroup_auth_mode: str | None = None,
auth_federation_service_account: str | None = None,
) -> list[ConnectionItem]:
"""
Bulk updates one or more workbook connections by LUID.
Expand Down Expand Up @@ -371,6 +373,20 @@ def update_connections(
embed_password : bool, optional
Whether to embed the password.

workgroup_auth_mode : str, optional
Workload Identity Federation (WIF) auth mode. See
``ConnectionItem.WorkgroupAuthMode`` for allowed values and
usage notes. Applied to every connection LUID in this call.
Requires Tableau Server REST API 3.30+ with WIF configured on
the site.

auth_federation_service_account : str, optional
Service-account email for WIF impersonation. Required with
``ConnectionItem.WorkgroupAuthMode.WIF_IMPERSONATION`` and
rejected by the server with the direct
``ConnectionItem.WorkgroupAuthMode.WIF`` mode. TSC does not
validate the combination client-side.

Returns
-------
Iterable of str
Expand All @@ -385,6 +401,8 @@ def update_connections(
username=username,
password=password,
embed_password=embed_password,
workgroup_auth_mode=workgroup_auth_mode,
auth_federation_service_account=auth_federation_service_account,
)

# Send request
Expand Down
20 changes: 20 additions & 0 deletions tableauserverclient/server/request_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,8 @@ def update_connections_req(
username: str | None = None,
password: str | None = None,
embed_password: bool | None = None,
workgroup_auth_mode: str | None = None,
auth_federation_service_account: str | None = None,
):
conn_luids_elem = ET.SubElement(element, "connectionLUIDs")
for luid in connection_luids:
Expand All @@ -275,6 +277,12 @@ def update_connections_req(
if embed_password is not None:
connection_elem.set("embedPassword", str(embed_password).lower())

if workgroup_auth_mode is not None:
connection_elem.set("workgroupAuthMode", workgroup_auth_mode)

if auth_federation_service_account is not None:
connection_elem.set("authFederationServiceAccount", auth_federation_service_account)


class DQWRequest:
def add_req(self, dqw_item):
Expand Down Expand Up @@ -1184,6 +1192,8 @@ def update_connections_req(
username: str | None = None,
password: str | None = None,
embed_password: bool | None = None,
workgroup_auth_mode: str | None = None,
auth_federation_service_account: str | None = None,
):
conn_luids_elem = ET.SubElement(element, "connectionLUIDs")
for luid in connection_luids:
Expand All @@ -1202,6 +1212,12 @@ def update_connections_req(
if embed_password is not None:
connection_elem.set("embedPassword", str(embed_password).lower())

if workgroup_auth_mode is not None:
connection_elem.set("workgroupAuthMode", workgroup_auth_mode)

if auth_federation_service_account is not None:
connection_elem.set("authFederationServiceAccount", auth_federation_service_account)


class Connection:
@_tsrequest_wrapped
Expand All @@ -1226,6 +1242,10 @@ def update_req(self, xml_request: ET.Element, connection_item: "ConnectionItem")
connection_element.attrib["embedPassword"] = str(connection_item.embed_password).lower()
if connection_item.query_tagging is not None:
connection_element.attrib["queryTaggingEnabled"] = str(connection_item.query_tagging).lower()
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
Comment on lines +1245 to +1248

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.WorkgroupAuthMode and dropped the two WORKGROUP_AUTH_MODE_WIF* module-level exports (prior review feedback on namespacing).
  • Added a docstring note on flows.update_connection clarifying that WIF fields reach the request via the shared Connection.update_req builder, 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, and test_update_connection_wif_from_response_absent on both workbook and datasource test suites, plus a new test_update_connection_wif on test_flow.py.



class TaskRequest:
Expand Down
4 changes: 4 additions & 0 deletions test/assets/datasource_update_connection_wif.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<?xml version='1.0' encoding='UTF-8'?>
<tsResponse xmlns="http://tableau.com/api" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<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"/>
</tsResponse>
4 changes: 4 additions & 0 deletions test/assets/workbook_update_connection_wif.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<?xml version='1.0' encoding='UTF-8'?>
<tsResponse xmlns="http://tableau.com/api" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<connection id="17376070-64d1-4d17-acb4-a56e4b5b1768" type="snowflake" serverAddress="account.snowflakecomputing.com" userName="" embedPassword="false" queryTaggingEnabled="false" authenticationType="oauth" workgroupAuthMode="workload-identity-federation"/>
</tsResponse>
Loading
Loading