Skip to content

feat: support OIDC endpoints #1630

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

Open
wants to merge 2 commits into
base: development
Choose a base branch
from
Open
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 @@ -37,6 +37,7 @@
RevisionItem,
ScheduleItem,
SiteAuthConfiguration,
SiteOIDCConfiguration,
SiteItem,
ServerInfoItem,
SubscriptionItem,
Expand Down Expand Up @@ -125,6 +126,7 @@
"ServerResponseError",
"SiteItem",
"SiteAuthConfiguration",
"SiteOIDCConfiguration",
"Sort",
"SubscriptionItem",
"TableauAuth",
Expand Down
3 changes: 3 additions & 0 deletions tableauserverclient/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -79,6 +80,7 @@
"BackgroundJobItem",
"LocationItem",
"MetricItem",
"SiteOIDCConfiguration",
"PaginationItem",
"Permission",
"PermissionsRule",
Expand All @@ -88,6 +90,7 @@
"ServerInfoItem",
"SiteAuthConfiguration",
"SiteItem",
"SiteOIDCConfiguration",
"SubscriptionItem",
"TableItem",
"TableauAuth",
Expand Down
82 changes: 82 additions & 0 deletions tableauserverclient/models/oidc_item.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
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

def __str__(self) -> str:
return (
f"{self.__class__.__qualname__}(enabled={self.enabled}, "
f"test_login_url={self.test_login_url}, "
f"idp_configuration_name={self.idp_configuration_name}, "
f"idp_configuration_id={self.idp_configuration_id}, "
f"client_id={self.client_id})"
)

def __repr__(self) -> str:
return f"<{str(self)}>"

@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"
2 changes: 2 additions & 0 deletions tableauserverclient/server/endpoint/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -52,6 +53,7 @@
"LinkedTasks",
"Metadata",
"Metrics",
"OIDC",
"Projects",
"Schedules",
"ServerInfo",
Expand Down
157 changes: 157 additions & 0 deletions tableauserverclient/server/endpoint/oidc_endpoint.py
Original file line number Diff line number Diff line change
@@ -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)
Loading
Loading