Skip to content

Jorwoods/idp #1601

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
May 13, 2025
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
2 changes: 2 additions & 0 deletions tableauserverclient/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
Resource,
RevisionItem,
ScheduleItem,
SiteAuthConfiguration,
SiteItem,
ServerInfoItem,
SubscriptionItem,
Expand Down Expand Up @@ -121,6 +122,7 @@
"ServerInfoItem",
"ServerResponseError",
"SiteItem",
"SiteAuthConfiguration",
"Sort",
"SubscriptionItem",
"TableauAuth",
Expand Down
3 changes: 2 additions & 1 deletion tableauserverclient/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
from tableauserverclient.models.revision_item import RevisionItem
from tableauserverclient.models.schedule_item import ScheduleItem
from tableauserverclient.models.server_info_item import ServerInfoItem
from tableauserverclient.models.site_item import SiteItem
from tableauserverclient.models.site_item import SiteItem, SiteAuthConfiguration
from tableauserverclient.models.subscription_item import SubscriptionItem
from tableauserverclient.models.table_item import TableItem
from tableauserverclient.models.tableau_auth import Credentials, TableauAuth, PersonalAccessTokenAuth, JWTAuth
Expand Down Expand Up @@ -83,6 +83,7 @@
"RevisionItem",
"ScheduleItem",
"ServerInfoItem",
"SiteAuthConfiguration",
"SiteItem",
"SubscriptionItem",
"TableItem",
Expand Down
28 changes: 28 additions & 0 deletions tableauserverclient/models/site_item.py
Original file line number Diff line number Diff line change
Expand Up @@ -1188,6 +1188,34 @@ def _parse_element(site_xml, ns):
)


class SiteAuthConfiguration:
"""
Authentication configuration for a site.
"""

def __init__(self):
self.auth_setting: Optional[str] = None
self.enabled: Optional[bool] = None
self.idp_configuration_id: Optional[str] = None
self.idp_configuration_name: Optional[str] = None
self.known_provider_alias: Optional[str] = None

@classmethod
def from_response(cls, resp: bytes, ns: dict) -> list["SiteAuthConfiguration"]:
all_auth_configs = list()
parsed_response = fromstring(resp)
all_auth_xml = parsed_response.findall(".//t:siteAuthConfiguration", namespaces=ns)
for auth_xml in all_auth_xml:
auth_config = cls()
auth_config.auth_setting = auth_xml.get("authSetting", None)
auth_config.enabled = string_to_bool(auth_xml.get("enabled", ""))
auth_config.idp_configuration_id = auth_xml.get("idpConfigurationId", None)
auth_config.idp_configuration_name = auth_xml.get("idpConfigurationName", None)
auth_config.known_provider_alias = auth_xml.get("knownProviderAlias", None)
all_auth_configs.append(auth_config)
return all_auth_configs


# Used to convert string represented boolean to a boolean type
def string_to_bool(s: str) -> bool:
return s.lower() == "true"
25 changes: 24 additions & 1 deletion tableauserverclient/models/user_item.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from defusedxml.ElementTree import fromstring

from tableauserverclient.datetime_helpers import parse_datetime
from tableauserverclient.models.site_item import SiteAuthConfiguration
from .exceptions import UnpopulatedPropertyError
from .property_decorators import (
property_is_enum,
Expand Down Expand Up @@ -94,6 +95,7 @@ def __init__(
self.name: Optional[str] = name
self.site_role: Optional[str] = site_role
self.auth_setting: Optional[str] = auth_setting
self._idp_configuration_id: Optional[str] = None

return None

Expand Down Expand Up @@ -184,6 +186,18 @@ def groups(self) -> "Pager":
raise UnpopulatedPropertyError(error)
return self._groups()

@property
def idp_configuration_id(self) -> Optional[str]:
"""
IDP configuration id for the user. This is only available on Tableau
Cloud, 3.24 or later
"""
return self._idp_configuration_id

@idp_configuration_id.setter
def idp_configuration_id(self, value: str) -> None:
self._idp_configuration_id = value

def _set_workbooks(self, workbooks) -> None:
self._workbooks = workbooks

Expand All @@ -204,8 +218,9 @@ def _parse_common_tags(self, user_xml, ns) -> "UserItem":
email,
auth_setting,
_,
_,
) = self._parse_element(user_xml, ns)
self._set_values(None, None, site_role, None, None, fullname, email, auth_setting, None)
self._set_values(None, None, site_role, None, None, fullname, email, auth_setting, None, None)
return self

def _set_values(
Expand All @@ -219,6 +234,7 @@ def _set_values(
email,
auth_setting,
domain_name,
idp_configuration_id,
):
if id is not None:
self._id = id
Expand All @@ -238,6 +254,8 @@ def _set_values(
self._auth_setting = auth_setting
if domain_name:
self._domain_name = domain_name
if idp_configuration_id:
self._idp_configuration_id = idp_configuration_id

@classmethod
def from_response(cls, resp, ns) -> list["UserItem"]:
Expand Down Expand Up @@ -265,6 +283,7 @@ def _parse_xml(cls, element_name, resp, ns):
email,
auth_setting,
domain_name,
idp_configuration_id,
) = cls._parse_element(user_xml, ns)
user_item = cls(name, site_role)
user_item._set_values(
Expand All @@ -277,6 +296,7 @@ def _parse_xml(cls, element_name, resp, ns):
email,
auth_setting,
domain_name,
idp_configuration_id,
)
all_user_items.append(user_item)
return all_user_items
Expand All @@ -295,6 +315,7 @@ def _parse_element(user_xml, ns):
fullname = user_xml.get("fullName", None)
email = user_xml.get("email", None)
auth_setting = user_xml.get("authSetting", None)
idp_configuration_id = user_xml.get("idpConfigurationId", None)

domain_name = None
domain_elem = user_xml.find(".//t:domain", namespaces=ns)
Expand All @@ -311,6 +332,7 @@ def _parse_element(user_xml, ns):
email,
auth_setting,
domain_name,
idp_configuration_id,
)

class CSVImport:
Expand Down Expand Up @@ -361,6 +383,7 @@ def create_user_from_line(line: str):
values[UserItem.CSVImport.ColumnType.EMAIL],
values[UserItem.CSVImport.ColumnType.AUTH],
None,
None,
)
return user

Expand Down
19 changes: 18 additions & 1 deletion tableauserverclient/server/endpoint/sites_endpoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from .endpoint import Endpoint, api
from .exceptions import MissingRequiredFieldError
from tableauserverclient.server import RequestFactory
from tableauserverclient.models import SiteItem, PaginationItem
from tableauserverclient.models import SiteAuthConfiguration, SiteItem, PaginationItem

from tableauserverclient.helpers.logging import logger

Expand Down Expand Up @@ -418,3 +418,20 @@ def re_encrypt_extracts(self, site_id: str) -> None:

empty_req = RequestFactory.Empty.empty_req()
self.post_request(url, empty_req)

@api(version="3.24")
def list_auth_configurations(self) -> list[SiteAuthConfiguration]:
"""
Lists all authentication configurations on the current site.

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

Returns
-------
list[SiteAuthConfiguration]
A list of authentication configurations on the current site.
"""
url = f"{self.baseurl}/{self.parent_srv.site_id}/site-auth-configurations"
server_response = self.get_request(url)
auth_configurations = SiteAuthConfiguration.from_response(server_response.content, self.parent_srv.namespace)
return auth_configurations
5 changes: 5 additions & 0 deletions tableauserverclient/server/request_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -913,6 +913,8 @@ def update_req(self, user_item: UserItem, password: Optional[str]) -> bytes:
user_element.attrib["authSetting"] = user_item.auth_setting
if password:
user_element.attrib["password"] = password
if user_item.idp_configuration_id is not None:
user_element.attrib["idpConfigurationId"] = user_item.idp_configuration_id
return ET.tostring(xml_request)

def add_req(self, user_item: UserItem) -> bytes:
Expand All @@ -929,6 +931,9 @@ def add_req(self, user_item: UserItem) -> bytes:

if user_item.auth_setting:
user_element.attrib["authSetting"] = user_item.auth_setting

if user_item.idp_configuration_id is not None:
user_element.attrib["idpConfigurationId"] = user_item.idp_configuration_id
return ET.tostring(xml_request)


Expand Down
18 changes: 18 additions & 0 deletions test/assets/site_auth_configurations.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?xml version='1.0' encoding='UTF-8'?>
<tsResponse xmlns="http://tableau.com/api" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://tableau.com/api http://tableau.com/api/ts-api-2.3.xsd">
<siteAuthConfigurations>
<siteAuthConfiguration
authSetting="OIDC"
enabled="true"
idpConfigurationId="00000000-0000-0000-0000-000000000000"
idpConfigurationName="Initial Salesforce"
knownProviderAlias="Salesforce"
/>
<siteAuthConfiguration
authSetting="SAML"
enabled="true"
idpConfigurationId="11111111-1111-1111-1111-111111111111"
idpConfigurationName="Initial SAML"
/>
</siteAuthConfigurations>
</tsResponse>
26 changes: 26 additions & 0 deletions test/test_site.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
GET_BY_NAME_XML = os.path.join(TEST_ASSET_DIR, "site_get_by_name.xml")
UPDATE_XML = os.path.join(TEST_ASSET_DIR, "site_update.xml")
CREATE_XML = os.path.join(TEST_ASSET_DIR, "site_create.xml")
SITE_AUTH_CONFIG_XML = os.path.join(TEST_ASSET_DIR, "site_auth_configurations.xml")


class SiteTests(unittest.TestCase):
Expand Down Expand Up @@ -260,3 +261,28 @@ def test_decrypt(self) -> None:
with requests_mock.mock() as m:
m.post(self.baseurl + "/0626857c-1def-4503-a7d8-7907c3ff9d9f/decrypt-extracts", status_code=200)
self.server.sites.decrypt_extracts("0626857c-1def-4503-a7d8-7907c3ff9d9f")

def test_list_auth_configurations(self) -> None:
self.server.version = "3.24"
self.baseurl = self.server.sites.baseurl
with open(SITE_AUTH_CONFIG_XML, "rb") as f:
response_xml = f.read().decode("utf-8")

assert self.baseurl == self.server.sites.baseurl

with requests_mock.mock() as m:
m.get(f"{self.baseurl}/{self.server.site_id}/site-auth-configurations", status_code=200, text=response_xml)
configs = self.server.sites.list_auth_configurations()

assert len(configs) == 2, "Expected 2 auth configurations"

assert configs[0].auth_setting == "OIDC"
assert configs[0].enabled
assert configs[0].idp_configuration_id == "00000000-0000-0000-0000-000000000000"
assert configs[0].idp_configuration_name == "Initial Salesforce"
assert configs[0].known_provider_alias == "Salesforce"
assert configs[1].auth_setting == "SAML"
assert configs[1].enabled
assert configs[1].idp_configuration_id == "11111111-1111-1111-1111-111111111111"
assert configs[1].idp_configuration_name == "Initial SAML"
assert configs[1].known_provider_alias is None
36 changes: 36 additions & 0 deletions test/test_user.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import os
import unittest

from defusedxml import ElementTree as ET
import requests_mock

import tableauserverclient as TSC
Expand Down Expand Up @@ -249,3 +250,38 @@ def test_get_users_from_file(self):
users, failures = self.server.users.create_from_file(USERS)
assert users[0].name == "Cassie", users
assert failures == []

def test_add_user_idp_configuration(self) -> None:
with open(ADD_XML) as f:
response_xml = f.read()
user = TSC.UserItem(name="Cassie", site_role="Viewer")
user.idp_configuration_id = "012345"

with requests_mock.mock() as m:
m.post(self.server.users.baseurl, text=response_xml)
user = self.server.users.add(user)

history = m.request_history[0]

tree = ET.fromstring(history.text)
user_elem = tree.find(".//user")
assert user_elem is not None
assert user_elem.attrib["idpConfigurationId"] == "012345"

def test_update_user_idp_configuration(self) -> None:
with open(ADD_XML) as f:
response_xml = f.read()
user = TSC.UserItem(name="Cassie", site_role="Viewer")
user._id = "0123456789"
user.idp_configuration_id = "012345"

with requests_mock.mock() as m:
m.put(f"{self.server.users.baseurl}/{user.id}", text=response_xml)
user = self.server.users.update(user)

history = m.request_history[0]

tree = ET.fromstring(history.text)
user_elem = tree.find(".//user")
assert user_elem is not None
assert user_elem.attrib["idpConfigurationId"] == "012345"
Loading