From 13fb92857c46ad6d69cb0a7254b0a4132fa40dc2 Mon Sep 17 00:00:00 2001 From: Snehil Kishore Date: Wed, 4 Jun 2025 19:47:41 +0530 Subject: [PATCH 1/3] =?UTF-8?q?chore:=20merge=20community=20PRs=20?= =?UTF-8?q?=E2=80=93=20bugfixes,=20features,=20and=20dependency=20upgrades?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test.yml | 2 +- README.md | 1 + auth0/management/__init__.py | 2 + auth0/management/auth0.py | 4 + auth0/management/self_service_profiles.py | 180 ++++++++++++++++++ auth0/test/conftest.py | 7 + .../management/test_self_service_profiles.py | 124 ++++++++++++ auth0/test_async/conftest.py | 7 + docs/source/management.rst | 8 + requirements.txt | 8 +- 10 files changed, 338 insertions(+), 5 deletions(-) create mode 100644 auth0/management/self_service_profiles.py create mode 100644 auth0/test/conftest.py create mode 100644 auth0/test/management/test_self_service_profiles.py create mode 100644 auth0/test_async/conftest.py diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8db7e526..ed0f2e94 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -80,6 +80,6 @@ jobs: - if: ${{ matrix.python-version == '3.10' }} name: Upload coverage - uses: codecov/codecov-action@ad3126e916f78f00edff4ed0317cf185271ccc2d # pin@5.4.2 + uses: codecov/codecov-action@18283e04ce6e62d37312384ff67231eb8fd56d24 # pin@5.4.3 with: token: ${{ secrets.CODECOV_TOKEN }} \ No newline at end of file diff --git a/README.md b/README.md index 13200c29..1efbcfa8 100644 --- a/README.md +++ b/README.md @@ -122,6 +122,7 @@ For more code samples on how to integrate the auth0-python SDK in your Python ap - Roles() ( `Auth0().roles` ) - RulesConfigs() ( `Auth0().rules_configs` ) - Rules() ( `Auth0().rules` ) +- SelfServiceProfiles() ( `Auth0().self_service_profiles` ) - Stats() ( `Auth0().stats` ) - Tenants() ( `Auth0().tenants` ) - Tickets() ( `Auth0().tickets` ) diff --git a/auth0/management/__init__.py b/auth0/management/__init__.py index d6fee4bc..62b1e8a9 100644 --- a/auth0/management/__init__.py +++ b/auth0/management/__init__.py @@ -22,6 +22,7 @@ from .roles import Roles from .rules import Rules from .rules_configs import RulesConfigs +from .self_service_profiles import SelfServiceProfiles from .stats import Stats from .tenants import Tenants from .tickets import Tickets @@ -59,6 +60,7 @@ "Roles", "RulesConfigs", "Rules", + "SelfServiceProfiles", "Stats", "Tenants", "Tickets", diff --git a/auth0/management/auth0.py b/auth0/management/auth0.py index 2879a9e7..5615f86c 100644 --- a/auth0/management/auth0.py +++ b/auth0/management/auth0.py @@ -26,6 +26,7 @@ from .roles import Roles from .rules import Rules from .rules_configs import RulesConfigs +from .self_service_profiles import SelfServiceProfiles from .stats import Stats from .tenants import Tenants from .tickets import Tickets @@ -86,6 +87,9 @@ def __init__( self.roles = Roles(domain, token, rest_options=rest_options) self.rules_configs = RulesConfigs(domain, token, rest_options=rest_options) self.rules = Rules(domain, token, rest_options=rest_options) + self.self_service_profiles = SelfServiceProfiles( + domain, token, rest_options=rest_options + ) self.stats = Stats(domain, token, rest_options=rest_options) self.tenants = Tenants(domain, token, rest_options=rest_options) self.tickets = Tickets(domain, token, rest_options=rest_options) diff --git a/auth0/management/self_service_profiles.py b/auth0/management/self_service_profiles.py new file mode 100644 index 00000000..a9a52610 --- /dev/null +++ b/auth0/management/self_service_profiles.py @@ -0,0 +1,180 @@ +from __future__ import annotations + +from typing import Any, List # List is being used as list is already a method. + +from ..rest import RestClient, RestClientOptions +from ..types import TimeoutType + + +class SelfServiceProfiles: + """Auth0 Self Service Profiles endpoints + + Args: + domain (str): Your Auth0 domain, e.g: 'username.auth0.com' + + token (str): Management API v2 Token + + telemetry (bool, optional): Enable or disable Telemetry + (defaults to True) + + timeout (float or tuple, optional): Change the requests + connect and read timeout. Pass a tuple to specify + both values separately or a float to set both to it. + (defaults to 5.0 for both) + + protocol (str, optional): Protocol to use when making requests. + (defaults to "https") + + rest_options (RestClientOptions): Pass an instance of + RestClientOptions to configure additional RestClient + options, such as rate-limit retries. + (defaults to None) + """ + + def __init__( + self, + domain: str, + token: str, + telemetry: bool = True, + timeout: TimeoutType = 5.0, + protocol: str = "https", + rest_options: RestClientOptions | None = None, + ) -> None: + self.domain = domain + self.protocol = protocol + self.client = RestClient( + jwt=token, telemetry=telemetry, timeout=timeout, options=rest_options + ) + + def _url(self, profile_id: str | None = None) -> str: + url = f"{self.protocol}://{self.domain}/api/v2/self-service-profiles" + if profile_id is not None: + return f"{url}/{profile_id}" + return url + + def all( + self, + page: int = 0, + per_page: int = 25, + include_totals: bool = True, + ) -> List[dict[str, Any]]: + """List self-service profiles. + + Args: + page (int, optional): The result's page number (zero based). By default, + retrieves the first page of results. + + per_page (int, optional): The amount of entries per page. By default, + retrieves 25 results per page. + + include_totals (bool, optional): True if the query summary is + to be included in the result, False otherwise. Defaults to True. + + See: https://auth0.com/docs/api/management/v2/self-service-profiles/get-self-service-profiles + """ + + params = { + "page": page, + "per_page": per_page, + "include_totals": str(include_totals).lower(), + } + + return self.client.get(self._url(), params=params) + + def create(self, body: dict[str, Any]) -> dict[str, Any]: + """Create a new self-service profile. + + Args: + body (dict): Attributes for the new self-service profile. + + See: https://auth0.com/docs/api/management/v2/self-service-profiles/post-self-service-profiles + """ + + return self.client.post(self._url(), data=body) + + def get(self, profile_id: str) -> dict[str, Any]: + """Get a self-service profile. + + Args: + id (str): The id of the self-service profile to retrieve. + + See: https://auth0.com/docs/api/management/v2/self-service-profiles/get-self-service-profiles-by-id + """ + + return self.client.get(self._url(profile_id)) + + def delete(self, profile_id: str) -> None: + """Delete a self-service profile. + + Args: + id (str): The id of the self-service profile to delete. + + See: https://auth0.com/docs/api/management/v2/self-service-profiles/delete-self-service-profiles-by-id + """ + + self.client.delete(self._url(profile_id)) + + def update(self, profile_id: str, body: dict[str, Any]) -> dict[str, Any]: + """Update a self-service profile. + + Args: + id (str): The id of the self-service profile to update. + + body (dict): Attributes of the self-service profile to modify. + + See: https://auth0.com/docs/api/management/v2/self-service-profiles/patch-self-service-profiles-by-id + """ + + return self.client.patch(self._url(profile_id), data=body) + + def get_custom_text( + self, profile_id: str, language: str, page: str + ) -> dict[str, Any]: + """Get the custom text for a self-service profile. + + See: https://auth0.com/docs/api/management/v2/self-service-profiles/get-self-service-profile-custom-text + """ + + url = self._url(f"{profile_id}/custom-text/{language}/{page}") + return self.client.get(url) + + def update_custom_text( + self, profile_id: str, language: str, page: str, body: dict[str, Any] + ) -> dict[str, Any]: + """Update the custom text for a self-service profile. + + See: https://auth0.com/docs/api/management/v2/self-service-profiles/put-self-service-profile-custom-text + """ + + url = self._url(f"{profile_id}/custom-text/{language}/{page}") + return self.client.put(url, data=body) + + def create_sso_ticket( + self, profile_id: str, body: dict[str, Any] + ) -> dict[str, Any]: + """Create a single sign-on ticket for a self-service profile. + + Args: + id (str): The id of the self-service profile to create the ticket for. + + body (dict): Attributes for the single sign-on ticket. + + See: https://auth0.com/docs/api/management/v2/self-service-profiles/post-sso-ticket + """ + + url = self._url(f"{profile_id}/sso-ticket") + return self.client.post(url, data=body) + + def revoke_sso_ticket(self, profile_id: str, ticket_id: str) -> None: + """Revoke a single sign-on ticket for a self-service profile. + + Args: + id (str): The id of the self-service profile to revoke the ticket from. + + ticket (str): The ticket to revoke. + + See: https://auth0.com/docs/api/management/v2/self-service-profiles/post-revoke + """ + + url = self._url(f"{profile_id}/sso-ticket/{ticket_id}/revoke") + self.client.post(url) \ No newline at end of file diff --git a/auth0/test/conftest.py b/auth0/test/conftest.py new file mode 100644 index 00000000..1247142f --- /dev/null +++ b/auth0/test/conftest.py @@ -0,0 +1,7 @@ +import pytest +import random + +@pytest.fixture(autouse=True) +def set_random_seed(): + random.seed(42) + print("Random seeded to 42") \ No newline at end of file diff --git a/auth0/test/management/test_self_service_profiles.py b/auth0/test/management/test_self_service_profiles.py new file mode 100644 index 00000000..0bc6fb39 --- /dev/null +++ b/auth0/test/management/test_self_service_profiles.py @@ -0,0 +1,124 @@ +import unittest +from unittest import mock + +from ...management.self_service_profiles import SelfServiceProfiles + + +class TestSelfServiceProfiles(unittest.TestCase): + def test_init_with_optionals(self): + t = SelfServiceProfiles( + domain="domain", token="jwttoken", telemetry=False, timeout=(10, 2) + ) + self.assertEqual(t.client.options.timeout, (10, 2)) + telemetry_header = t.client.base_headers.get("Auth0-Client", None) + self.assertEqual(telemetry_header, None) + + @mock.patch("auth0.management.self_service_profiles.RestClient") + def test_all(self, mock_rc): + mock_instance = mock_rc.return_value + + s = SelfServiceProfiles(domain="domain", token="jwttoken") + s.all() + + mock_instance.get.assert_called_with( + "https://domain/api/v2/self-service-profiles", + params={"page": 0, "per_page": 25, "include_totals": "true"}, + ) + + s.all(page=1, per_page=50, include_totals=False) + + mock_instance.get.assert_called_with( + "https://domain/api/v2/self-service-profiles", + params={"page": 1, "per_page": 50, "include_totals": "false"}, + ) + + @mock.patch("auth0.management.self_service_profiles.RestClient") + def test_create(self, mock_rc): + mock_instance = mock_rc.return_value + + s = SelfServiceProfiles(domain="domain", token="jwttoken") + s.create({"name": "test"}) + + mock_instance.post.assert_called_with( + "https://domain/api/v2/self-service-profiles", data={"name": "test"} + ) + + @mock.patch("auth0.management.self_service_profiles.RestClient") + def test_get(self, mock_rc): + mock_instance = mock_rc.return_value + + s = SelfServiceProfiles(domain="domain", token="jwttoken") + s.get("an-id") + + mock_instance.get.assert_called_with( + "https://domain/api/v2/self-service-profiles/an-id" + ) + + @mock.patch("auth0.management.self_service_profiles.RestClient") + def test_delete(self, mock_rc): + mock_instance = mock_rc.return_value + + s = SelfServiceProfiles(domain="domain", token="jwttoken") + s.delete("an-id") + + mock_instance.delete.assert_called_with( + "https://domain/api/v2/self-service-profiles/an-id" + ) + + @mock.patch("auth0.management.self_service_profiles.RestClient") + def test_update(self, mock_rc): + mock_instance = mock_rc.return_value + + s = SelfServiceProfiles(domain="domain", token="jwttoken") + s.update("an-id", {"a": "b", "c": "d"}) + + mock_instance.patch.assert_called_with( + "https://domain/api/v2/self-service-profiles/an-id", + data={"a": "b", "c": "d"}, + ) + + @mock.patch("auth0.management.self_service_profiles.RestClient") + def test_get_custom_text(self, mock_rc): + mock_instance = mock_rc.return_value + + s = SelfServiceProfiles(domain="domain", token="jwttoken") + s.get_custom_text("an-id", "en", "page") + + mock_instance.get.assert_called_with( + "https://domain/api/v2/self-service-profiles/an-id/custom-text/en/page" + ) + + @mock.patch("auth0.management.self_service_profiles.RestClient") + def test_update_custom_text(self, mock_rc): + mock_instance = mock_rc.return_value + + s = SelfServiceProfiles(domain="domain", token="jwttoken") + s.update_custom_text("an-id", "en", "page", {"a": "b", "c": "d"}) + + mock_instance.put.assert_called_with( + "https://domain/api/v2/self-service-profiles/an-id/custom-text/en/page", + data={"a": "b", "c": "d"}, + ) + + @mock.patch("auth0.management.self_service_profiles.RestClient") + def test_create_sso_ticket(self, mock_rc): + mock_instance = mock_rc.return_value + + s = SelfServiceProfiles(domain="domain", token="jwttoken") + s.create_sso_ticket("an-id", {"a": "b", "c": "d"}) + + mock_instance.post.assert_called_with( + "https://domain/api/v2/self-service-profiles/an-id/sso-ticket", + data={"a": "b", "c": "d"}, + ) + + @mock.patch("auth0.management.self_service_profiles.RestClient") + def test_revoke_sso_ticket(self, mock_rc): + mock_instance = mock_rc.return_value + + s = SelfServiceProfiles(domain="domain", token="jwttoken") + s.revoke_sso_ticket("an-id", "ticket-id") + + mock_instance.post.assert_called_with( + "https://domain/api/v2/self-service-profiles/an-id/sso-ticket/ticket-id/revoke" + ) \ No newline at end of file diff --git a/auth0/test_async/conftest.py b/auth0/test_async/conftest.py new file mode 100644 index 00000000..1247142f --- /dev/null +++ b/auth0/test_async/conftest.py @@ -0,0 +1,7 @@ +import pytest +import random + +@pytest.fixture(autouse=True) +def set_random_seed(): + random.seed(42) + print("Random seeded to 42") \ No newline at end of file diff --git a/docs/source/management.rst b/docs/source/management.rst index e928f008..8bccaa24 100644 --- a/docs/source/management.rst +++ b/docs/source/management.rst @@ -177,6 +177,14 @@ management.rules module :undoc-members: :show-inheritance: +management.self_service_profiles module +----------------------------------------- + +.. automodule:: auth0.management.self_service_profiles + :members: + :undoc-members: + :show-inheritance: + management.stats module -------------------------- diff --git a/requirements.txt b/requirements.txt index 0f578726..0eab0cf8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ aiohttp==3.8.6 ; python_version >= "3.7" and python_version < "4.0" aioresponses==0.7.4 ; python_version >= "3.7" and python_version < "4.0" -aiosignal==1.3.1 ; python_version >= "3.7" and python_version < "4.0" +aiosignal==1.3.2 ; python_version >= "3.7" and python_version < "4.0" argcomplete==3.5.3 ; python_version >= "3.7" and python_version < "4.0" async-timeout==4.0.3 ; python_version >= "3.7" and python_version < "4.0" asynctest==0.13.0 ; python_version >= "3.7" and python_version < "3.8" @@ -8,7 +8,7 @@ attrs==23.1.0 ; python_version >= "3.7" and python_version < "4.0" certifi==2025.1.31 ; python_version >= "3.7" and python_version < "4.0" cffi==1.17.1 ; python_version >= "3.7" and python_version < "4.0" charset-normalizer==3.2.0 ; python_version >= "3.7" and python_version < "4.0" -click==8.1.7 ; python_version >= "3.7" and python_version < "4.0" +click==8.1.8 ; python_version >= "3.7" and python_version < "4.0" colorama==0.4.6 ; python_version >= "3.7" and python_version < "4.0" and sys_platform == "win32" or python_version >= "3.7" and python_version < "4.0" and platform_system == "Windows" coverage[toml]==7.2.7 ; python_version >= "3.7" and python_version < "4.0" cryptography==44.0.1 ; python_version >= "3.7" and python_version < "4.0" @@ -23,7 +23,7 @@ packaging==23.1 ; python_version >= "3.7" and python_version < "4.0" pipx==1.2.0 ; python_version >= "3.7" and python_version < "4.0" pluggy==1.2.0 ; python_version >= "3.7" and python_version < "4.0" pycparser==2.21 ; python_version >= "3.7" and python_version < "4.0" -pyjwt==2.8.0 ; python_version >= "3.7" and python_version < "4.0" +pyjwt==2.9.0 ; python_version >= "3.7" and python_version < "4.0" pyopenssl==25.0.0 ; python_version >= "3.7" and python_version < "4.0" pytest-aiohttp==1.0.4 ; python_version >= "3.7" and python_version < "4.0" pytest-asyncio==0.23.8 ; python_version >= "3.7" and python_version < "4.0" @@ -37,5 +37,5 @@ types-pyyaml==6.0.12.11 ; python_version >= "3.7" and python_version < "4.0" typing-extensions==4.7.1 ; python_version >= "3.7" and python_version < "3.8" urllib3==2.2.2 ; python_version >= "3.7" and python_version < "4.0" userpath==1.9.0 ; python_version >= "3.7" and python_version < "4.0" -yarl==1.9.2 ; python_version >= "3.7" and python_version < "4.0" +yarl==1.20.0 ; python_version >= "3.7" and python_version < "4.0" zipp==3.19.1 ; python_version >= "3.7" and python_version < "3.8" From 70aed0a1eb5bc11ccc4d0301ee614f6fe19c6894 Mon Sep 17 00:00:00 2001 From: Snehil Kishore Date: Thu, 5 Jun 2025 15:19:58 +0530 Subject: [PATCH 2/3] Testing unit test workflow --- .github/workflows/test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ed0f2e94..a896eb91 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -38,7 +38,7 @@ jobs: strategy: matrix: - python-version: ["3.9", "3.10", "3.11", "3.12"] + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] steps: - name: Checkout code @@ -58,7 +58,7 @@ jobs: pip install --user pipx pip install --user setuptools pipx ensurepath - pipx install poetry + pipx install poetry==1.4.2 poetry config virtualenvs.in-project true poetry install --with dev poetry self add "poetry-dynamic-versioning[plugin]" From b67e5631bcd7f95df8d5d8ad7f41dbd2e7c4038f Mon Sep 17 00:00:00 2001 From: Snehil Kishore Date: Thu, 5 Jun 2025 15:33:26 +0530 Subject: [PATCH 3/3] Revert test.yml changes --- .github/workflows/test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a896eb91..ed0f2e94 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -38,7 +38,7 @@ jobs: strategy: matrix: - python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] + python-version: ["3.9", "3.10", "3.11", "3.12"] steps: - name: Checkout code @@ -58,7 +58,7 @@ jobs: pip install --user pipx pip install --user setuptools pipx ensurepath - pipx install poetry==1.4.2 + pipx install poetry poetry config virtualenvs.in-project true poetry install --with dev poetry self add "poetry-dynamic-versioning[plugin]"