diff --git a/tableauserverclient/__init__.py b/tableauserverclient/__init__.py index 21e2c476..c15e1a6e 100644 --- a/tableauserverclient/__init__.py +++ b/tableauserverclient/__init__.py @@ -37,6 +37,7 @@ RevisionItem, ScheduleItem, SiteAuthConfiguration, + SiteOIDCConfiguration, SiteItem, ServerInfoItem, SubscriptionItem, @@ -125,6 +126,7 @@ "ServerResponseError", "SiteItem", "SiteAuthConfiguration", + "SiteOIDCConfiguration", "Sort", "SubscriptionItem", "TableauAuth", diff --git a/tableauserverclient/models/__init__.py b/tableauserverclient/models/__init__.py index 30cd8810..5ad7ec1c 100644 --- a/tableauserverclient/models/__init__.py +++ b/tableauserverclient/models/__init__.py @@ -30,6 +30,7 @@ ) from tableauserverclient.models.location_item import LocationItem from tableauserverclient.models.metric_item import MetricItem +from tableauserverclient.models.oidc_item import SiteOIDCConfiguration from tableauserverclient.models.pagination_item import PaginationItem from tableauserverclient.models.permissions_item import PermissionsRule, Permission from tableauserverclient.models.project_item import ProjectItem @@ -79,6 +80,7 @@ "BackgroundJobItem", "LocationItem", "MetricItem", + "SiteOIDCConfiguration", "PaginationItem", "Permission", "PermissionsRule", @@ -88,6 +90,7 @@ "ServerInfoItem", "SiteAuthConfiguration", "SiteItem", + "SiteOIDCConfiguration", "SubscriptionItem", "TableItem", "TableauAuth", diff --git a/tableauserverclient/models/oidc_item.py b/tableauserverclient/models/oidc_item.py new file mode 100644 index 00000000..9391ff4f --- /dev/null +++ b/tableauserverclient/models/oidc_item.py @@ -0,0 +1,70 @@ +from typing import Optional +from defusedxml.ElementTree import fromstring + + +class SiteOIDCConfiguration: + def __init__(self) -> None: + self.enabled: bool = False + self.test_login_url: Optional[str] = None + self.known_provider_alias: Optional[str] = None + self.allow_embedded_authentication: bool = False + self.use_full_name: bool = False + self.idp_configuration_name: Optional[str] = None + self.idp_configuration_id: Optional[str] = None + self.client_id: Optional[str] = None + self.client_secret: Optional[str] = None + self.authorization_endpoint: Optional[str] = None + self.token_endpoint: Optional[str] = None + self.userinfo_endpoint: Optional[str] = None + self.jwks_uri: Optional[str] = None + self.end_session_endpoint: Optional[str] = None + self.custom_scope: Optional[str] = None + self.essential_acr_values: Optional[str] = None + self.email_mapping: Optional[str] = None + self.first_name_mapping: Optional[str] = None + self.last_name_mapping: Optional[str] = None + self.full_name_mapping: Optional[str] = None + self.prompt: Optional[str] = None + self.client_authentication: Optional[str] = None + self.voluntary_acr_values: Optional[str] = None + + @classmethod + def from_response(cls, raw_xml: bytes, ns) -> "SiteOIDCConfiguration": + """ + Parses the raw XML bytes and returns a SiteOIDCConfiguration object. + """ + root = fromstring(raw_xml) + elem = root.find("t:siteOIDCConfiguration", namespaces=ns) + if elem is None: + raise ValueError("No siteOIDCConfiguration element found in the XML.") + config = cls() + + config.enabled = str_to_bool(elem.get("enabled", "false")) + config.test_login_url = elem.get("testLoginUrl") + config.known_provider_alias = elem.get("knownProviderAlias") + config.allow_embedded_authentication = str_to_bool(elem.get("allowEmbeddedAuthentication", "false").lower()) + config.use_full_name = str_to_bool(elem.get("useFullName", "false").lower()) + config.idp_configuration_name = elem.get("idpConfigurationName") + config.idp_configuration_id = elem.get("idpConfigurationId") + config.client_id = elem.get("clientId") + config.client_secret = elem.get("clientSecret") + config.authorization_endpoint = elem.get("authorizationEndpoint") + config.token_endpoint = elem.get("tokenEndpoint") + config.userinfo_endpoint = elem.get("userinfoEndpoint") + config.jwks_uri = elem.get("jwksUri") + config.end_session_endpoint = elem.get("endSessionEndpoint") + config.custom_scope = elem.get("customScope") + config.essential_acr_values = elem.get("essentialAcrValues") + config.email_mapping = elem.get("emailMapping") + config.first_name_mapping = elem.get("firstNameMapping") + config.last_name_mapping = elem.get("lastNameMapping") + config.full_name_mapping = elem.get("fullNameMapping") + config.prompt = elem.get("prompt") + config.client_authentication = elem.get("clientAuthentication") + config.voluntary_acr_values = elem.get("voluntaryAcrValues") + + return config + + +def str_to_bool(s: str) -> bool: + return s == "true" diff --git a/tableauserverclient/server/endpoint/__init__.py b/tableauserverclient/server/endpoint/__init__.py index b05b9add..3c1266f9 100644 --- a/tableauserverclient/server/endpoint/__init__.py +++ b/tableauserverclient/server/endpoint/__init__.py @@ -17,6 +17,7 @@ from tableauserverclient.server.endpoint.linked_tasks_endpoint import LinkedTasks from tableauserverclient.server.endpoint.metadata_endpoint import Metadata from tableauserverclient.server.endpoint.metrics_endpoint import Metrics +from tableauserverclient.server.endpoint.oidc_endpoint import OIDC from tableauserverclient.server.endpoint.projects_endpoint import Projects from tableauserverclient.server.endpoint.schedules_endpoint import Schedules from tableauserverclient.server.endpoint.server_info_endpoint import ServerInfo @@ -52,6 +53,7 @@ "LinkedTasks", "Metadata", "Metrics", + "OIDC", "Projects", "Schedules", "ServerInfo", diff --git a/tableauserverclient/server/endpoint/oidc_endpoint.py b/tableauserverclient/server/endpoint/oidc_endpoint.py new file mode 100644 index 00000000..d1600809 --- /dev/null +++ b/tableauserverclient/server/endpoint/oidc_endpoint.py @@ -0,0 +1,157 @@ +from typing import Protocol, Union, TYPE_CHECKING +from tableauserverclient.models.oidc_item import SiteOIDCConfiguration +from tableauserverclient.server.endpoint import Endpoint +from tableauserverclient.server.request_factory import RequestFactory +from tableauserverclient.server.endpoint.endpoint import api + +if TYPE_CHECKING: + from tableauserverclient.models.site_item import SiteAuthConfiguration + from tableauserverclient.server.server import Server + + +class IDPAttributes(Protocol): + idp_configuration_id: str + + +class IDPProperty(Protocol): + @property + def idp_configuration_id(self) -> str: ... + + +HasIdpConfigurationID = Union[str, IDPAttributes] + + +class OIDC(Endpoint): + def __init__(self, server: "Server") -> None: + self.parent_srv = server + + @property + def baseurl(self) -> str: + return f"{self.parent_srv.baseurl}/sites/{self.parent_srv.site_id}/site-oidc-configuration" + + @api(version="3.24") + def get(self) -> list["SiteAuthConfiguration"]: + """ + Get all OpenID Connect (OIDC) configurations for the currently + authenticated Tableau Cloud site. To get all of the configuration + details, use the get_by_id method. + + REST API: https://help.tableau.com/current/api/rest_api/en-us/REST/rest_api_ref_identity_pools.htm#AuthnService_ListAuthConfigurations + + Returns + ------- + list[SiteAuthConfiguration] + """ + return self.parent_srv.sites.list_auth_configurations() + + @api(version="3.24") + def get_by_id(self, id: Union[str, HasIdpConfigurationID]) -> SiteOIDCConfiguration: + """ + Get details about a specific OpenID Connect (OIDC) configuration on the + current Tableau Cloud site. Only retrieves configurations for the + currently authenticated site. + + REST API: https://help.tableau.com/current/api/rest_api/en-us/REST/rest_api_ref_openid_connect.htm#get_openid_connect_configuration + + Parameters + ---------- + id : Union[str, HasID] + The ID of the OIDC configuration to retrieve. Can be either the + ID string or an object with an id attribute. + + Returns + ------- + SiteOIDCConfiguration + The OIDC configuration for the specified site. + """ + target = getattr(id, "idp_configuration_id", id) + url = f"{self.baseurl}/{target}" + response = self.get_request(url) + return SiteOIDCConfiguration.from_response(response.content, self.parent_srv.namespace) + + @api(version="3.22") + def create(self, config_item: SiteOIDCConfiguration) -> SiteOIDCConfiguration: + """ + Create the OpenID Connect (OIDC) configuration for the currently + authenticated Tableau Cloud site. The config_item must have the + following attributes set, others are optional: + + idp_configuration_name + client_id + client_secret + authorization_endpoint + token_endpoint + userinfo_endpoint + enabled + jwks_uri + + The secret in the returned config will be masked. + + REST API: https://help.tableau.com/current/api/rest_api/en-us/REST/rest_api_ref_openid_connect.htm#create_openid_connect_configuration + + Parameters + ---------- + config : SiteOIDCConfiguration + The OIDC configuration to create. + + Returns + ------- + SiteOIDCConfiguration + The created OIDC configuration. + """ + url = self.baseurl + create_req = RequestFactory.OIDC.create_req(config_item) + response = self.put_request(url, create_req) + return SiteOIDCConfiguration.from_response(response.content, self.parent_srv.namespace) + + @api(version="3.24") + def delete_configuration(self, config: Union[str, HasIdpConfigurationID]) -> None: + """ + Delete the OpenID Connect (OIDC) configuration for the currently + authenticated Tableau Cloud site. The config parameter can be either + the ID of the configuration or the configuration object itself. + + **Important**: Before removing the OIDC configuration, make sure that + users who are set to authenticate with OIDC are set to use a different + authentication type. Users who are not set with a different + authentication type before removing the OIDC configuration will not be + able to sign in to Tableau Cloud. + + + REST API: https://help.tableau.com/current/api/rest_api/en-us/REST/rest_api_ref_openid_connect.htm#remove_openid_connect_configuration + + Parameters + ---------- + config : Union[str, HasID] + The OIDC configuration to delete. Can be either the ID of the + configuration or the configuration object itself. + """ + + target = getattr(config, "idp_configuration_id", config) + + url = f"{self.parent_srv.baseurl}/sites/{self.parent_srv.site_id}/disable-site-oidc-configuration?idpConfigurationId={target}" + _ = self.put_request(url) + return None + + @api(version="3.22") + def update(self, config: SiteOIDCConfiguration) -> SiteOIDCConfiguration: + """ + Update the Tableau Cloud site's OpenID Connect (OIDC) configuration. The + secret in the returned config will be masked. + + REST API: https://help.tableau.com/current/api/rest_api/en-us/REST/rest_api_ref_openid_connect.htm#update_openid_connect_configuration + + Parameters + ---------- + config : SiteOIDCConfiguration + The OIDC configuration to update. Must have the id attribute set. + + Returns + ------- + SiteOIDCConfiguration + The updated OIDC configuration. + """ + url = f"{self.baseurl}/{config.idp_configuration_id}" + update_req = RequestFactory.OIDC.update_req(config) + response = self.put_request(url, update_req) + return SiteOIDCConfiguration.from_response(response.content, self.parent_srv.namespace) diff --git a/tableauserverclient/server/request_factory.py b/tableauserverclient/server/request_factory.py index c898004f..f9c9b471 100644 --- a/tableauserverclient/server/request_factory.py +++ b/tableauserverclient/server/request_factory.py @@ -1446,6 +1446,122 @@ def publish(self, xml_request: ET.Element, virtual_connection: VirtualConnection return ET.tostring(xml_request) +class OIDCRequest: + @_tsrequest_wrapped + def create_req(self, xml_request: ET.Element, oidc_item: SiteOIDCConfiguration) -> bytes: + oidc_element = ET.SubElement(xml_request, "siteOIDCConfiguration") + + # Check required attributes first + + if oidc_item.idp_configuration_name is None: + raise ValueError(f"OIDC Item missing idp_configuration_name: {oidc_item}") + if oidc_item.client_id is None: + raise ValueError(f"OIDC Item missing client_id: {oidc_item}") + if oidc_item.client_secret is None: + raise ValueError(f"OIDC Item missing client_secret: {oidc_item}") + if oidc_item.authorization_endpoint is None: + raise ValueError(f"OIDC Item missing authorization_endpoint: {oidc_item}") + if oidc_item.token_endpoint is None: + raise ValueError(f"OIDC Item missing token_endpoint: {oidc_item}") + if oidc_item.userinfo_endpoint is None: + raise ValueError(f"OIDC Item missing userinfo_endpoint: {oidc_item}") + if not isinstance(oidc_item.enabled, bool): + raise ValueError(f"OIDC Item missing enabled: {oidc_item}") + if oidc_item.jwks_uri is None: + raise ValueError(f"OIDC Item missing jwks_uri: {oidc_item}") + + oidc_element.attrib["name"] = oidc_item.idp_configuration_name + oidc_element.attrib["clientId"] = oidc_item.client_id + oidc_element.attrib["clientSecret"] = oidc_item.client_secret + oidc_element.attrib["authorizationEndpoint"] = oidc_item.authorization_endpoint + oidc_element.attrib["tokenEndpoint"] = oidc_item.token_endpoint + oidc_element.attrib["userInfoEndpoint"] = oidc_item.userinfo_endpoint + oidc_element.attrib["enabled"] = str(oidc_item.enabled).lower() + oidc_element.attrib["jwksUri"] = oidc_item.jwks_uri + + if oidc_item.allow_embedded_authentication is not None: + oidc_element.attrib["allowEmbeddedAuthentication"] = str(oidc_item.allow_embedded_authentication).lower() + if oidc_item.custom_scope is not None: + oidc_element.attrib["customScope"] = oidc_item.custom_scope + if oidc_item.prompt is not None: + oidc_element.attrib["prompt"] = oidc_item.prompt + if oidc_item.client_authentication is not None: + oidc_element.attrib["clientAuthentication"] = oidc_item.client_authentication + if oidc_item.essential_acr_values is not None: + oidc_element.attrib["essentialAcrValues"] = oidc_item.essential_acr_values + if oidc_item.voluntary_acr_values is not None: + oidc_element.attrib["voluntaryAcrValues"] = oidc_item.voluntary_acr_values + if oidc_item.email_mapping is not None: + oidc_element.attrib["emailMapping"] = oidc_item.email_mapping + if oidc_item.first_name_mapping is not None: + oidc_element.attrib["firstNameMapping"] = oidc_item.first_name_mapping + if oidc_item.last_name_mapping is not None: + oidc_element.attrib["lastNameMapping"] = oidc_item.last_name_mapping + if oidc_item.full_name_mapping is not None: + oidc_element.attrib["fullNameMapping"] = oidc_item.full_name_mapping + if oidc_item.use_full_name is not None: + oidc_element.attrib["useFullName"] = str(oidc_item.use_full_name).lower() + + return ET.tostring(xml_request) + + @_tsrequest_wrapped + def update_req(self, xml_request: ET.Element, oidc_item: SiteOIDCConfiguration) -> bytes: + oidc_element = ET.SubElement(xml_request, "siteOIDCConfiguration") + + # Check required attributes first + + if oidc_item.idp_configuration_name is None: + raise ValueError(f"OIDC Item missing idp_configuration_name: {oidc_item}") + if oidc_item.client_id is None: + raise ValueError(f"OIDC Item missing client_id: {oidc_item}") + if oidc_item.client_secret is None: + raise ValueError(f"OIDC Item missing client_secret: {oidc_item}") + if oidc_item.authorization_endpoint is None: + raise ValueError(f"OIDC Item missing authorization_endpoint: {oidc_item}") + if oidc_item.token_endpoint is None: + raise ValueError(f"OIDC Item missing token_endpoint: {oidc_item}") + if oidc_item.userinfo_endpoint is None: + raise ValueError(f"OIDC Item missing userinfo_endpoint: {oidc_item}") + if not isinstance(oidc_item.enabled, bool): + raise ValueError(f"OIDC Item missing enabled: {oidc_item}") + if oidc_item.jwks_uri is None: + raise ValueError(f"OIDC Item missing jwks_uri: {oidc_item}") + + oidc_element.attrib["name"] = oidc_item.idp_configuration_name + oidc_element.attrib["clientId"] = oidc_item.client_id + oidc_element.attrib["clientSecret"] = oidc_item.client_secret + oidc_element.attrib["authorizationEndpoint"] = oidc_item.authorization_endpoint + oidc_element.attrib["tokenEndpoint"] = oidc_item.token_endpoint + oidc_element.attrib["userInfoEndpoint"] = oidc_item.userinfo_endpoint + oidc_element.attrib["enabled"] = str(oidc_item.enabled).lower() + oidc_element.attrib["jwksUri"] = oidc_item.jwks_uri + + if oidc_item.allow_embedded_authentication is not None: + oidc_element.attrib["allowEmbeddedAuthentication"] = str(oidc_item.allow_embedded_authentication).lower() + if oidc_item.custom_scope is not None: + oidc_element.attrib["customScope"] = oidc_item.custom_scope + if oidc_item.prompt is not None: + oidc_element.attrib["prompt"] = oidc_item.prompt + if oidc_item.client_authentication is not None: + oidc_element.attrib["clientAuthentication"] = oidc_item.client_authentication + if oidc_item.essential_acr_values is not None: + oidc_element.attrib["essentialAcrValues"] = oidc_item.essential_acr_values + if oidc_item.voluntary_acr_values is not None: + oidc_element.attrib["voluntaryAcrValues"] = oidc_item.voluntary_acr_values + if oidc_item.email_mapping is not None: + oidc_element.attrib["emailMapping"] = oidc_item.email_mapping + if oidc_item.first_name_mapping is not None: + oidc_element.attrib["firstNameMapping"] = oidc_item.first_name_mapping + if oidc_item.last_name_mapping is not None: + oidc_element.attrib["lastNameMapping"] = oidc_item.last_name_mapping + if oidc_item.full_name_mapping is not None: + oidc_element.attrib["fullNameMapping"] = oidc_item.full_name_mapping + if oidc_item.use_full_name is not None: + oidc_element.attrib["useFullName"] = str(oidc_item.use_full_name).lower() + + return ET.tostring(xml_request) + + class RequestFactory: Auth = AuthRequest() Connection = Connection() @@ -1463,6 +1579,7 @@ class RequestFactory: Group = GroupRequest() GroupSet = GroupSetRequest() Metric = MetricRequest() + OIDC = OIDCRequest() Permission = PermissionRequest() Project = ProjectRequest() Schedule = ScheduleRequest() diff --git a/tableauserverclient/server/server.py b/tableauserverclient/server/server.py index d5d163db..9202e3e6 100644 --- a/tableauserverclient/server/server.py +++ b/tableauserverclient/server/server.py @@ -38,6 +38,7 @@ GroupSets, Tags, VirtualConnections, + OIDC, ) from tableauserverclient.server.exceptions import ( ServerInfoEndpointNotFoundError, @@ -183,6 +184,7 @@ def __init__(self, server_address, use_server_version=False, http_options=None, self.group_sets = GroupSets(self) self.tags = Tags(self) self.virtual_connections = VirtualConnections(self) + self.oidc = OIDC(self) self._session = self._session_factory() self._http_options = dict() # must set this before making a server call diff --git a/test/assets/oidc_create.xml b/test/assets/oidc_create.xml new file mode 100644 index 00000000..cbe632f3 --- /dev/null +++ b/test/assets/oidc_create.xml @@ -0,0 +1,30 @@ + + + + diff --git a/test/assets/oidc_get.xml b/test/assets/oidc_get.xml new file mode 100644 index 00000000..cbe632f3 --- /dev/null +++ b/test/assets/oidc_get.xml @@ -0,0 +1,30 @@ + + + + diff --git a/test/assets/oidc_update.xml b/test/assets/oidc_update.xml new file mode 100644 index 00000000..cbe632f3 --- /dev/null +++ b/test/assets/oidc_update.xml @@ -0,0 +1,30 @@ + + + + diff --git a/test/test_oidc.py b/test/test_oidc.py new file mode 100644 index 00000000..4c318735 --- /dev/null +++ b/test/test_oidc.py @@ -0,0 +1,153 @@ +import unittest +import requests_mock +from pathlib import Path + +import tableauserverclient as TSC + +assets = Path(__file__).parent / "assets" +OIDC_GET = assets / "oidc_get.xml" +OIDC_GET_BY_ID = assets / "oidc_get_by_id.xml" +OIDC_UPDATE = assets / "oidc_update.xml" +OIDC_CREATE = assets / "oidc_create.xml" + + +class Testoidc(unittest.TestCase): + def setUp(self) -> None: + self.server = TSC.Server("http://test", False) + + # Fake signin + self.server._site_id = "dad65087-b08b-4603-af4e-2887b8aafc67" + self.server._auth_token = "j80k54ll2lfMZ0tv97mlPvvSCRyD0DOM" + self.server.version = "3.24" + + self.baseurl = self.server.oidc.baseurl + + def test_oidc_get_by_id(self) -> None: + luid = "6561daa3-20e8-407f-ba09-709b178c0b4a" + with requests_mock.mock() as m: + m.get(f"{self.baseurl}/{luid}", text=OIDC_GET.read_text()) + oidc = self.server.oidc.get_by_id(luid) + + assert oidc.enabled is True + assert ( + oidc.test_login_url + == "https://sso.online.tableau.com/public/testLogin?alias=8a04d825-e5d4-408f-bbc2-1042b8bb4818&authSetting=OIDC&idpConfigurationId=78c985b4-5494-4436-bcee-f595e287ba4a" + ) + assert oidc.known_provider_alias == "Google" + assert oidc.allow_embedded_authentication is False + assert oidc.use_full_name is False + assert oidc.idp_configuration_name == "GoogleOIDC" + assert oidc.idp_configuration_id == "78c985b4-5494-4436-bcee-f595e287ba4a" + assert oidc.client_id == "ICcGeDt3XHwzZ1D0nCZt" + assert oidc.client_secret == "omit" + assert oidc.authorization_endpoint == "https://myidp.com/oauth2/v1/authorize" + assert oidc.token_endpoint == "https://myidp.com/oauth2/v1/token" + assert oidc.userinfo_endpoint == "https://myidp.com/oauth2/v1/userinfo" + assert oidc.jwks_uri == "https://myidp.com/oauth2/v1/keys" + assert oidc.end_session_endpoint == "https://myidp.com/oauth2/v1/logout" + assert oidc.custom_scope == "openid, email, profile" + assert oidc.prompt == "login,consent" + assert oidc.client_authentication == "client_secret_basic" + assert oidc.essential_acr_values == "phr" + assert oidc.email_mapping == "email" + assert oidc.first_name_mapping == "given_name" + assert oidc.last_name_mapping == "family_name" + assert oidc.full_name_mapping == "name" + + def test_oidc_delete(self) -> None: + luid = "6561daa3-20e8-407f-ba09-709b178c0b4a" + with requests_mock.mock() as m: + m.put(f"{self.server.baseurl}/sites/{self.server.site_id}/disable-site-oidc-configuration") + self.server.oidc.delete_configuration(luid) + history = m.request_history[0] + + assert "idpconfigurationid" in history.qs + assert history.qs["idpconfigurationid"][0] == luid + + def test_oidc_update(self) -> None: + luid = "6561daa3-20e8-407f-ba09-709b178c0b4a" + oidc = TSC.SiteOIDCConfiguration() + oidc.idp_configuration_id = luid + + # Only include the required fields for updates + oidc.enabled = True + oidc.idp_configuration_name = "GoogleOIDC" + oidc.client_id = "ICcGeDt3XHwzZ1D0nCZt" + oidc.client_secret = "omit" + oidc.authorization_endpoint = "https://myidp.com/oauth2/v1/authorize" + oidc.token_endpoint = "https://myidp.com/oauth2/v1/token" + oidc.userinfo_endpoint = "https://myidp.com/oauth2/v1/userinfo" + oidc.jwks_uri = "https://myidp.com/oauth2/v1/keys" + + with requests_mock.mock() as m: + m.put(f"{self.baseurl}/{luid}", text=OIDC_UPDATE.read_text()) + oidc = self.server.oidc.update(oidc) + + assert oidc.enabled is True + assert ( + oidc.test_login_url + == "https://sso.online.tableau.com/public/testLogin?alias=8a04d825-e5d4-408f-bbc2-1042b8bb4818&authSetting=OIDC&idpConfigurationId=78c985b4-5494-4436-bcee-f595e287ba4a" + ) + assert oidc.known_provider_alias == "Google" + assert oidc.allow_embedded_authentication is False + assert oidc.use_full_name is False + assert oidc.idp_configuration_name == "GoogleOIDC" + assert oidc.idp_configuration_id == "78c985b4-5494-4436-bcee-f595e287ba4a" + assert oidc.client_id == "ICcGeDt3XHwzZ1D0nCZt" + assert oidc.client_secret == "omit" + assert oidc.authorization_endpoint == "https://myidp.com/oauth2/v1/authorize" + assert oidc.token_endpoint == "https://myidp.com/oauth2/v1/token" + assert oidc.userinfo_endpoint == "https://myidp.com/oauth2/v1/userinfo" + assert oidc.jwks_uri == "https://myidp.com/oauth2/v1/keys" + assert oidc.end_session_endpoint == "https://myidp.com/oauth2/v1/logout" + assert oidc.custom_scope == "openid, email, profile" + assert oidc.prompt == "login,consent" + assert oidc.client_authentication == "client_secret_basic" + assert oidc.essential_acr_values == "phr" + assert oidc.email_mapping == "email" + assert oidc.first_name_mapping == "given_name" + assert oidc.last_name_mapping == "family_name" + assert oidc.full_name_mapping == "name" + + def test_oidc_create(self) -> None: + oidc = TSC.SiteOIDCConfiguration() + + # Only include the required fields for creation + oidc.enabled = True + oidc.idp_configuration_name = "GoogleOIDC" + oidc.client_id = "ICcGeDt3XHwzZ1D0nCZt" + oidc.client_secret = "omit" + oidc.authorization_endpoint = "https://myidp.com/oauth2/v1/authorize" + oidc.token_endpoint = "https://myidp.com/oauth2/v1/token" + oidc.userinfo_endpoint = "https://myidp.com/oauth2/v1/userinfo" + oidc.jwks_uri = "https://myidp.com/oauth2/v1/keys" + + with requests_mock.mock() as m: + m.put(self.baseurl, text=OIDC_CREATE.read_text()) + oidc = self.server.oidc.create(oidc) + + assert oidc.enabled is True + assert ( + oidc.test_login_url + == "https://sso.online.tableau.com/public/testLogin?alias=8a04d825-e5d4-408f-bbc2-1042b8bb4818&authSetting=OIDC&idpConfigurationId=78c985b4-5494-4436-bcee-f595e287ba4a" + ) + assert oidc.known_provider_alias == "Google" + assert oidc.allow_embedded_authentication is False + assert oidc.use_full_name is False + assert oidc.idp_configuration_name == "GoogleOIDC" + assert oidc.idp_configuration_id == "78c985b4-5494-4436-bcee-f595e287ba4a" + assert oidc.client_id == "ICcGeDt3XHwzZ1D0nCZt" + assert oidc.client_secret == "omit" + assert oidc.authorization_endpoint == "https://myidp.com/oauth2/v1/authorize" + assert oidc.token_endpoint == "https://myidp.com/oauth2/v1/token" + assert oidc.userinfo_endpoint == "https://myidp.com/oauth2/v1/userinfo" + assert oidc.jwks_uri == "https://myidp.com/oauth2/v1/keys" + assert oidc.end_session_endpoint == "https://myidp.com/oauth2/v1/logout" + assert oidc.custom_scope == "openid, email, profile" + assert oidc.prompt == "login,consent" + assert oidc.client_authentication == "client_secret_basic" + assert oidc.essential_acr_values == "phr" + assert oidc.email_mapping == "email" + assert oidc.first_name_mapping == "given_name" + assert oidc.last_name_mapping == "family_name" + assert oidc.full_name_mapping == "name"