diff --git a/README.md b/README.md index 09335bdcd..9e1e17212 100644 --- a/README.md +++ b/README.md @@ -556,6 +556,14 @@ descope_client.mgmt.tenant.update( custom_attributes={"attribute-name": "value"}, ) +# Managing the tenant's settings +# Getting the settings +descope_client.mgmt.tenant.load_settings(id="my-custom-id") + +# updating the settings +descope_client.mgmt.tenant.update_settings(id="my-custom-id", self_provisioning_domains=["domain.com"], session_settings_enabled=True, refresh_token_expiration=1, refresh_token_expiration_unit="hours") + + # Tenant deletion cannot be undone. Use carefully. # Pass true to cascade value, in case you want to delete all users/keys associated only with this tenant descope_client.mgmt.tenant.delete(id="my-custom-id", cascade=False) diff --git a/descope/management/common.py b/descope/management/common.py index c7e4ac553..31f3cb0f2 100644 --- a/descope/management/common.py +++ b/descope/management/common.py @@ -1,6 +1,16 @@ from enum import Enum from typing import List, Optional +class SessionExpirationUnit(Enum): + MINUTES = "minutes" + HOURS = "hours" + DAYS = "days" + WEEKS = "weeks" + +class TenantAuthType(Enum): + NONE = "none" + SAML = "saml" + OIDC = "oidc" class AccessType(Enum): OFFLINE = "offline" @@ -35,6 +45,7 @@ class MgmtV1: tenant_update_path = "/v1/mgmt/tenant/update" tenant_delete_path = "/v1/mgmt/tenant/delete" tenant_load_path = "/v1/mgmt/tenant" + tenant_settings_path = "/v1/mgmt/tenant/settings" tenant_load_all_path = "/v1/mgmt/tenant/all" tenant_search_all_path = "/v1/mgmt/tenant/search" @@ -292,7 +303,6 @@ def associated_tenants_to_dict(associated_tenants: List[AssociatedTenant]) -> li ) return associated_tenant_list - class SAMLIDPAttributeMappingInfo: """ Represents a SAML IDP attribute mapping object. use this class for mapping Descope attribute diff --git a/descope/management/tenant.py b/descope/management/tenant.py index 411301415..e14630b06 100644 --- a/descope/management/tenant.py +++ b/descope/management/tenant.py @@ -1,7 +1,7 @@ from typing import Any, List, Optional from descope._http_base import HTTPBase -from descope.management.common import MgmtV1 +from descope.management.common import MgmtV1, TenantAuthType, SessionExpirationUnit class Tenant(HTTPBase): @@ -92,6 +92,73 @@ def update( ), ) + def update_settings( + self, + id: str, + self_provisioning_domains: List[str], + domains: Optional[List[str]] = None, + auth_type: Optional[TenantAuthType] = None, + session_settings_enabled: Optional[bool] = None, + refresh_token_expiration: Optional[int] = None, + refresh_token_expiration_unit: Optional[SessionExpirationUnit] = None, + session_token_expiration: Optional[int] = None, + session_token_expiration_unit: Optional[SessionExpirationUnit] = None, + stepup_token_expiration: Optional[int] = None, + stepup_token_expiration_unit: Optional[SessionExpirationUnit] = None, + enable_inactivity: Optional[bool] = None, + inactivity_time: Optional[int] = None, + inactivity_time_unit: Optional[SessionExpirationUnit] = None, + JITDisabled: Optional[bool] = None + ): + """ + Update an existing tenant's session settings. + + Args: + id (str): The ID of the tenant to update. + self_provisioning_domains (List[str]): Domains for self-provisioning. + domains (Optional[List[str]]): List of domains associated with the tenant. + auth_type (Optional[TenantAuthType]): Authentication type for the tenant. + session_settings_enabled (Optional[bool]): Whether session settings are enabled. + refresh_token_expiration (Optional[int]): Expiration time for refresh tokens. + refresh_token_expiration_unit (Optional[SessionExiprationUnit]): Unit for refresh token expiration. + session_token_expiration (Optional[int]): Expiration time for session tokens. + session_token_expiration_unit (Optional[SessionExiprationUnit]): Unit for session token expiration. + stepup_token_expiration (Optional[int]): Expiration time for step-up tokens. + stepup_token_expiration_unit (Optional[SessionExiprationUnit]): Unit for step-up token expiration. + enable_inactivity (Optional[bool]): Whether inactivity timeout is enabled. + inactivity_time (Optional[int]): Inactivity timeout duration. + inactivity_time_unit (Optional[SessionExiprationUnit]): Unit for inactivity timeout. + JITDisabled (Optional[bool]): Whether JIT is disabled. + + Raise: + AuthException: raised if update operation fails + """ + body: dict[str, Any] = { + "tenantId": id, + "selfProvisioningDomains": self_provisioning_domains, + "domains": domains, + "authType": auth_type, + "enabled": session_settings_enabled, + "refreshTokenExpiration": refresh_token_expiration, + "refreshTokenExpirationUnit": refresh_token_expiration_unit, + "sessionTokenExpiration": session_token_expiration, + "sessionTokenExpirationUnit": session_token_expiration_unit, + "stepupTokenExpiration": stepup_token_expiration, + "stepupTokenExpirationUnit": stepup_token_expiration_unit, + "enableInactivity": enable_inactivity, + "inactivityTime": inactivity_time, + "inactivityTimeUnit": inactivity_time_unit, + "JITDisabled": JITDisabled, + } + + body = {k: v for k, v in body.items() if v is not None} + + self._http.post( + MgmtV1.tenant_settings_path, + body=body, + params=None + ) + def delete( self, id: str, @@ -134,6 +201,35 @@ def load( params={"id": id}, ) return response.json() + + def load_settings( + self, + id: str, + ) -> dict: + """ + Load tenant session settings by id. + + Args: + id (str): The ID of the tenant to load session settings for. + + Return value (dict): + Return dict in the format + { "domains":, "selfProvisioningDomains":, "authType":, + "enabled":, "refreshTokenExpiration":, "refreshTokenExpirationUnit":, + "sessionTokenExpiration":, "sessionTokenExpirationUnit":, + "stepupTokenExpiration":, "stepupTokenExpirationUnit":, + "enableInactivity":, "inactivityTime":, "inactivityTimeUnit":, + "JITDisabled": } + Containing the loaded tenant session settings. + + Raise: + AuthException: raised if load operation fails + """ + response = self._http.get( + MgmtV1.tenant_settings_path, + params={"id": id}, + ) + return response.json() def load_all( self, diff --git a/tests/management/test_tenant.py b/tests/management/test_tenant.py index 0ad89cf1c..89858dfc1 100644 --- a/tests/management/test_tenant.py +++ b/tests/management/test_tenant.py @@ -366,3 +366,91 @@ def test_search_all(self): params=None, timeout=DEFAULT_TIMEOUT_SECONDS, ) + + def test_update_settings(self): + client = DescopeClient( + self.dummy_project_id, + self.public_key_dict, + False, + self.dummy_management_key, + ) + + # Test failed flows + with patch("requests.post") as mock_post: + mock_post.return_value.ok = False + self.assertRaises( + AuthException, + client.mgmt.tenant.update_settings, + "valid-id", + {}, + ) + + # Test success flow + with patch("requests.post") as mock_post: + mock_post.return_value.ok = True + self.assertIsNone( + client.mgmt.tenant.update_settings("t1", self_provisioning_domains=["domain1.com"], domains=["domain1.com", "domain2.com"], auth_type="oidc", session_settings_enabled=True) + ) + mock_post.assert_called_with( + f"{common.DEFAULT_BASE_URL}{MgmtV1.tenant_settings_path}", + headers={ + **common.default_headers, + "Authorization": f"Bearer {self.dummy_project_id}:{self.dummy_management_key}", + "x-descope-project-id": self.dummy_project_id, + }, + json={ + "tenantId": "t1", + "selfProvisioningDomains": ["domain1.com"], + "domains": ["domain1.com", "domain2.com"], + "authType": "oidc", + "enabled": True + }, + allow_redirects=False, + params=None, + verify=True, + timeout=DEFAULT_TIMEOUT_SECONDS, + ) + + def test_load_settings(self): + client = DescopeClient( + self.dummy_project_id, + self.public_key_dict, + False, + self.dummy_management_key, + ) + + # Test failed flows + with patch("requests.get") as mock_get: + mock_get.return_value.ok = False + self.assertRaises( + AuthException, + client.mgmt.tenant.load_settings, + "valid-id", + ) + + # Test success flow + with patch("requests.get") as mock_get: + network_resp = mock.Mock() + network_resp.ok = True + network_resp.json.return_value = json.loads( + """ + {"domains": ["domain1.com", "domain2.com"], "authType": "oidc", "sessionSettingsEnabled": true} + """ + ) + mock_get.return_value = network_resp + resp = client.mgmt.tenant.load_settings("t1") + self.assertEqual(resp["domains"], ["domain1.com", "domain2.com"]) + self.assertEqual(resp["authType"], "oidc") + self.assertEqual(resp["sessionSettingsEnabled"], True) + mock_get.assert_called_with( + f"{common.DEFAULT_BASE_URL}{MgmtV1.tenant_settings_path}", + headers={ + **common.default_headers, + "Authorization": f"Bearer {self.dummy_project_id}:{self.dummy_management_key}", + "x-descope-project-id": self.dummy_project_id, + }, + params={"id": "t1"}, + allow_redirects=True, + verify=True, + timeout=DEFAULT_TIMEOUT_SECONDS, + )