From 489cfc4ee553b462e085272733ff5763acd4b2b9 Mon Sep 17 00:00:00 2001 From: adamjmcgrath Date: Tue, 14 Mar 2023 08:17:16 +0000 Subject: [PATCH 1/7] Add API2 Factor Management Endpoints --- auth0/management/users.py | 92 +++++++++++++++++++++++++++++ auth0/test/management/test_users.py | 78 ++++++++++++++++++++++++ 2 files changed, 170 insertions(+) diff --git a/auth0/management/users.py b/auth0/management/users.py index 642d4c41..67e35125 100644 --- a/auth0/management/users.py +++ b/auth0/management/users.py @@ -419,3 +419,95 @@ def invalidate_remembered_browsers(self, user_id): url = self._url(f"{user_id}/multifactor/actions/invalidate-remember-browser") return self.client.post(url) + + def get_authentication_methods(self, user_id): + """Gets a list of authentication methods + + Args: + user_id (str): The user_id to get a list of authentication methods for. + + See: https://auth0.com/docs/api/management/v2#!/Users/get_authentication_methods + """ + + url = self._url(f"{user_id}/authentication-methods") + return self.client.get(url) + + def get_authentication_method_by_id(self, user_id, authentication_method_id): + """Gets an authentication method by ID. + + Args: + user_id (str): The user_id to get an authentication method by ID for. + authentication_method_id (str): The authentication_method_id to get an authentication method by ID for. + + See: https://auth0.com/docs/api/management/v2#!/Users/get_authentication_methods_by_authentication_method_id + """ + + url = self._url(f"{user_id}/authentication-methods/{authentication_method_id}") + return self.client.get(url) + + def create_authentication_method(self, user_id, body): + """Creates an authentication method for a given user. + + Args: + user_id (str): The user_id to create an authentication method for a given user. + body (dict): the request body to create an authentication method for a given user. + + See: https://auth0.com/docs/api/management/v2#!/Users/post_authentication_methods + """ + + url = self._url(f"{user_id}/authentication-methods") + return self.client.post(url, data=body) + + def update_authentication_methods(self, user_id, body): + """Updates all authentication methods for a user by replacing them with the given ones. + + Args: + user_id (str): The user_id to update all authentication methods for. + body (dict): the request body to update all authentication methods with. + + See: https://auth0.com/docs/api/management/v2#!/Users/put_authentication_methods + """ + + url = self._url(f"{user_id}/authentication-methods") + return self.client.put(url, data=body) + + def update_authentication_method_by_id( + self, user_id, authentication_method_id, body + ): + """Updates an authentication method. + + Args: + user_id (str): The user_id to update an authentication method. + authentication_method_id (str): The authentication_method_id to update an authentication method for. + body (dict): the request body to update an authentication method. + + See: https://auth0.com/docs/api/management/v2#!/Users/patch_authentication_methods_by_authentication_method_id + """ + + url = self._url(f"{user_id}/authentication-methods/{authentication_method_id}") + return self.client.patch(url, data=body) + + def delete_authentication_methods(self, user_id): + """Deletes all authentication methods for the given user. + + Args: + user_id (str): The user_id to delete all authentication methods for the given user for. + + See: https://auth0.com/docs/api/management/v2#!/Users/delete_authentication_methods + """ + + url = self._url(f"{user_id}/authentication-methods") + return self.client.delete(url) + + def delete_authentication_method_by_id(self, user_id, authentication_method_id): + """Deletes an authentication method by ID. + + Args: + user_id (str): The user_id to delete an authentication method by ID for. + authentication_method_id (str): The authentication_method_id to delete an authentication method by ID for. + + See: https://auth0.com/docs/api/management/v2#!/Users/delete_authentication_methods_by_authentication_method_id + """ + + url = self._url(f"{user_id}/authentication-methods/{authentication_method_id}") + return self.client.delete(url) diff --git a/auth0/test/management/test_users.py b/auth0/test/management/test_users.py index 5d454255..aba7e006 100644 --- a/auth0/test/management/test_users.py +++ b/auth0/test/management/test_users.py @@ -325,3 +325,81 @@ def test_invalidate_remembered_browsers(self, mock_rc): "https://domain/api/v2/users/user-id/multifactor/actions/invalidate-remember-browser", args[0], ) + + @mock.patch("auth0.management.users.RestClient") + def test_get_authentication_methods(self, mock_rc): + mock_instance = mock_rc.return_value + + u = Users(domain="domain", token="jwttoken") + u.get_authentication_methods("user_id") + + mock_instance.get.assert_called_with( + "https://domain/api/v2/users/user_id/authentication-methods" + ) + + @mock.patch("auth0.management.users.RestClient") + def test_get_authentication_method_by_id(self, mock_rc): + mock_instance = mock_rc.return_value + + u = Users(domain="domain", token="jwttoken") + u.get_authentication_method_by_id("user_id", "authentication_method_id") + + mock_instance.get.assert_called_with( + "https://domain/api/v2/users/user_id/authentication-methods/authentication_method_id" + ) + + @mock.patch("auth0.management.users.RestClient") + def test_create_authentication_method(self, mock_rc): + mock_instance = mock_rc.return_value + + u = Users(domain="domain", token="jwttoken") + u.create_authentication_method("user_id", {}) + + mock_instance.post.assert_called_with( + "https://domain/api/v2/users/user_id/authentication-methods", data={} + ) + + @mock.patch("auth0.management.users.RestClient") + def test_update_authentication_methods(self, mock_rc): + mock_instance = mock_rc.return_value + + u = Users(domain="domain", token="jwttoken") + u.update_authentication_methods("user_id", {}) + + mock_instance.put.assert_called_with( + "https://domain/api/v2/users/user_id/authentication-methods", data={} + ) + + @mock.patch("auth0.management.users.RestClient") + def test_update_authentication_method_by_id(self, mock_rc): + mock_instance = mock_rc.return_value + + u = Users(domain="domain", token="jwttoken") + u.update_authentication_method_by_id("user_id", "authentication_method_id", {}) + + mock_instance.patch.assert_called_with( + "https://domain/api/v2/users/user_id/authentication-methods/authentication_method_id", + data={}, + ) + + @mock.patch("auth0.management.users.RestClient") + def test_delete_authentication_methods(self, mock_rc): + mock_instance = mock_rc.return_value + + u = Users(domain="domain", token="jwttoken") + u.delete_authentication_methods("user_id") + + mock_instance.delete.assert_called_with( + "https://domain/api/v2/users/user_id/authentication-methods" + ) + + @mock.patch("auth0.management.users.RestClient") + def test_delete_authentication_method_by_id(self, mock_rc): + mock_instance = mock_rc.return_value + + u = Users(domain="domain", token="jwttoken") + u.delete_authentication_method_by_id("user_id", "authentication_method_id") + + mock_instance.delete.assert_called_with( + "https://domain/api/v2/users/user_id/authentication-methods/authentication_method_id" + ) From 8a84e239ec1b6891f7878b4c6f197f6ed640c36c Mon Sep 17 00:00:00 2001 From: adamjmcgrath Date: Tue, 14 Mar 2023 08:45:46 +0000 Subject: [PATCH 2/7] Add branding theme endpoints --- auth0/management/branding.py | 53 ++++++++++++++++++++++ auth0/test/management/test_branding.py | 62 ++++++++++++++++++++++++++ 2 files changed, 115 insertions(+) diff --git a/auth0/management/branding.py b/auth0/management/branding.py index 38084a9c..7d60cc59 100644 --- a/auth0/management/branding.py +++ b/auth0/management/branding.py @@ -93,3 +93,56 @@ def update_template_universal_login(self, body): self._url("templates", "universal-login"), body={"template": body}, ) + + def get_default_branding_theme(self): + """Retrieve default branding theme. + + See: https://auth0.com/docs/api/management/v2#!/Branding/get_default_branding_theme + """ + + return self.client.get(self._url("themes", "default")) + + def get_branding_theme(self, theme_id): + """Retrieve branding theme. + + Args: + theme_id (str): The theme_id to retrieve branding theme for. + + See: https://auth0.com/docs/api/management/v2#!/Branding/get_branding_theme + """ + + return self.client.get(self._url("themes", theme_id)) + + def delete_branding_theme(self, theme_id): + """Delete branding theme. + + Args: + theme_id (str): The theme_id to delete branding theme for. + + See: https://auth0.com/docs/api/management/v2#!/Branding/delete_branding_theme + """ + + return self.client.delete(self._url("themes", theme_id)) + + def update_branding_theme(self, theme_id, body): + """Update branding theme. + + Args: + theme_id (str): The theme_id to update branding theme for. + body (dict): The attributes to set on the theme. + + See: https://auth0.com/docs/api/management/v2#!/Branding/patch_branding_theme + """ + + return self.client.patch(self._url("themes", theme_id), data=body) + + def create_branding_theme(self, body): + """Create branding theme. + + Args: + body (dict): The attributes to set on the theme. + + See: https://auth0.com/docs/api/management/v2#!/Branding/post_branding_theme + """ + + return self.client.post(self._url("themes"), data=body) diff --git a/auth0/test/management/test_branding.py b/auth0/test/management/test_branding.py index 5f200d14..a10bf3b9 100644 --- a/auth0/test/management/test_branding.py +++ b/auth0/test/management/test_branding.py @@ -71,3 +71,65 @@ def test_update_template_universal_login(self, mock_rc): "https://domain/api/v2/branding/templates/universal-login", body={"template": {"a": "b", "c": "d"}}, ) + + @mock.patch("auth0.management.branding.RestClient") + def test_get_default_branding_theme(self, mock_rc): + api = mock_rc.return_value + api.get.return_value = {} + + branding = Branding(domain="domain", token="jwttoken") + branding.get_default_branding_theme() + + api.get.assert_called_with( + "https://domain/api/v2/branding/themes/default", + ) + + @mock.patch("auth0.management.branding.RestClient") + def test_get_branding_theme(self, mock_rc): + api = mock_rc.return_value + api.get.return_value = {} + + branding = Branding(domain="domain", token="jwttoken") + branding.get_branding_theme("theme_id") + + api.get.assert_called_with( + "https://domain/api/v2/branding/themes/theme_id", + ) + + @mock.patch("auth0.management.branding.RestClient") + def test_delete_branding_theme(self, mock_rc): + api = mock_rc.return_value + api.delete.return_value = {} + + branding = Branding(domain="domain", token="jwttoken") + branding.delete_branding_theme("theme_id") + + api.delete.assert_called_with( + "https://domain/api/v2/branding/themes/theme_id", + ) + + @mock.patch("auth0.management.branding.RestClient") + def test_update_branding_theme(self, mock_rc): + api = mock_rc.return_value + api.patch.return_value = {} + + branding = Branding(domain="domain", token="jwttoken") + branding.update_branding_theme("theme_id", {}) + + api.patch.assert_called_with( + "https://domain/api/v2/branding/themes/theme_id", + data={}, + ) + + @mock.patch("auth0.management.branding.RestClient") + def test_create_branding_theme(self, mock_rc): + api = mock_rc.return_value + api.post.return_value = {} + + branding = Branding(domain="domain", token="jwttoken") + branding.create_branding_theme({}) + + api.post.assert_called_with( + "https://domain/api/v2/branding/themes", + data={}, + ) From 2b0f4b966d248f2cfb020f22e51ecda0e5097347 Mon Sep 17 00:00:00 2001 From: adamjmcgrath Date: Tue, 14 Mar 2023 14:31:52 +0000 Subject: [PATCH 3/7] Release 4.1.0 --- CHANGELOG.md | 8 ++++++++ auth0/__init__.py | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 413963d3..350eab80 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Change Log +## [4.1.0](https://github.com/auth0/auth0-python/tree/4.1.0) (2023-03-14) +[Full Changelog](https://github.com/auth0/auth0-python/compare/4.0.0...4.1.0) + +**Added** +- Add branding theme endpoints [\#477](https://github.com/auth0/auth0-python/pull/477) ([adamjmcgrath](https://github.com/adamjmcgrath)) +- [SDK-4011] Add API2 Factor Management Endpoints [\#476](https://github.com/auth0/auth0-python/pull/476) ([adamjmcgrath](https://github.com/adamjmcgrath)) +- Use declarative setup with `pyproject.toml` [\#474](https://github.com/auth0/auth0-python/pull/474) ([Viicos](https://github.com/Viicos)) + ## [4.0.0](https://github.com/auth0/auth0-python/tree/4.0.0) (2023-01-19) [Full Changelog](https://github.com/auth0/auth0-python/compare/3.24.1...4.0.0) diff --git a/auth0/__init__.py b/auth0/__init__.py index 830c2c0c..dc09a987 100644 --- a/auth0/__init__.py +++ b/auth0/__init__.py @@ -1,4 +1,4 @@ -__version__ = "4.0.0" +__version__ = "4.1.0" from auth0.exceptions import Auth0Error, RateLimitError, TokenValidationError From 0b7a67aeb4fa2dc320fcae07ab03447b3d50b95f Mon Sep 17 00:00:00 2001 From: Adam Mcgrath Date: Wed, 15 Mar 2023 14:33:20 +0000 Subject: [PATCH 4/7] Revert "Use declarative setup with `pyproject.toml`" --- pyproject.toml | 39 --------------------------------------- setup.py | 47 +++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 45 insertions(+), 41 deletions(-) delete mode 100644 pyproject.toml diff --git a/pyproject.toml b/pyproject.toml deleted file mode 100644 index 7bc82ff1..00000000 --- a/pyproject.toml +++ /dev/null @@ -1,39 +0,0 @@ -[build-system] -requires = ["setuptools>=65.5.1"] -build-backend = "setuptools.build_meta" - -[project] -name = "auth0-python" -dynamic = ["version"] -description = "Auth0 Python SDK" -readme = "README.md" -authors = [ - {name = "Auth0", email = "support@auth0.com"} -] -license = {file = "LICENSE"} -classifiers = [ - "Development Status :: 5 - Production/Stable", - "Intended Audience :: Developers", - "Operating System :: OS Independent", - "License :: OSI Approved :: MIT License", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", -] -requires-python = ">=3.7" -dependencies = [ - "requests>=2.14.0", - "pyjwt[crypto]>=2.6.0", -] -[project.optional-dependencies] -test = ["coverage", "pre-commit"] -async = ["aiohttp"] -[project.urls] -homepage = "https://github.com/auth0/auth0-python" -documentation = "https://www.auth0.com/docs" -changelog = "https://github.com/auth0/auth0-python/blob/master/CHANGELOG.md" - -[tool.setuptools.dynamic] -version = {attr = "auth0.__version__"} diff --git a/setup.py b/setup.py index 60684932..426eea9a 100644 --- a/setup.py +++ b/setup.py @@ -1,3 +1,46 @@ -from setuptools import setup +import os +import re -setup() +from setuptools import find_packages, setup + + +def find_version(): + file_dir = os.path.dirname(__file__) + with open(os.path.join(file_dir, "auth0", "__init__.py")) as f: + version = re.search(r'^__version__ = [\'"]([^\'"]*)[\'"]', f.read()) + if version: + return version.group(1) + else: + raise RuntimeError("Unable to find version string.") + + +with open("README.md", encoding="utf-8") as f: + long_description = f.read() + + +setup( + name="auth0-python", + version=find_version(), + description="Auth0 Python SDK", + long_description=long_description, + long_description_content_type="text/markdown", + author="Auth0", + author_email="support@auth0.com", + license="MIT", + packages=find_packages(), + install_requires=["requests>=2.14.0", "pyjwt[crypto]>=2.6.0"], + extras_require={"test": ["coverage", "pre-commit"]}, + python_requires=">=3.7", + classifiers=[ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "Operating System :: OS Independent", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + ], + url="https://github.com/auth0/auth0-python", +) From a24ee04a598deca6c089d3edf5e7bc17d10d18e0 Mon Sep 17 00:00:00 2001 From: Adam Mcgrath Date: Thu, 16 Mar 2023 10:52:26 +0000 Subject: [PATCH 5/7] Fix token_verifier import in Examples fixes #480 --- EXAMPLES.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/EXAMPLES.md b/EXAMPLES.md index bbe39e73..366f8033 100644 --- a/EXAMPLES.md +++ b/EXAMPLES.md @@ -32,7 +32,7 @@ For symmetric algorithms like HS256, use the `SymmetricSignatureVerifier` class, The following example demonstrates the verification of an ID token signed with the RS256 signing algorithm: ```python -from auth0.authentication import TokenVerifier, AsymmetricSignatureVerifier +from auth0.authentication.token_verifier import TokenVerifier, AsymmetricSignatureVerifier domain = 'myaccount.auth0.com' client_id = 'exampleid' @@ -194,4 +194,4 @@ async def main(): asyncio.run(main()) -``` \ No newline at end of file +``` From 5301f2137225d1e05e35d8f8c5736d37d323f751 Mon Sep 17 00:00:00 2001 From: Adam Mcgrath Date: Wed, 29 Mar 2023 15:22:28 +0100 Subject: [PATCH 6/7] Fix indenting on EXAMPLES.md --- EXAMPLES.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/EXAMPLES.md b/EXAMPLES.md index 366f8033..a1695a78 100644 --- a/EXAMPLES.md +++ b/EXAMPLES.md @@ -5,8 +5,8 @@ - [Authenticating with a application configured to use `private_key_jwt` token endpoint auth method](#authenticating-with-a-application-configured-to-use-private-key-jwt-token-endpoint-auth-method) - [Management SDK](#management-sdk) - [Connections](#connections) - - [Error handling](#error-handling) - - [Asynchronous environments](#asynchronous-environments) +- [Error handling](#error-handling) +- [Asynchronous environments](#asynchronous-environments) ## Authentication SDK @@ -135,7 +135,7 @@ Success! All endpoints follow a similar structure to `connections`, and try to follow as closely as possible the [API documentation](https://auth0.com/docs/api/v2). -### Error handling +## Error handling When consuming methods from the API clients, the requests could fail for a number of reasons: - Invalid data sent as part of the request: An `Auth0Error` is raised with the error code and description. @@ -143,7 +143,7 @@ When consuming methods from the API clients, the requests could fail for a numbe resets is exposed in the `reset_at` property. When the header is unset, this value will be `-1`. - Network timeouts: Adjustable by passing a `timeout` argument to the client. See the [rate limit docs](https://auth0.com/docs/policies/rate-limits) for details. -### Asynchronous environments +## Asynchronous environments This SDK provides async methods built on top of [asyncio](https://docs.python.org/3/library/asyncio.html). To make them available you must have the [aiohttp](https://docs.aiohttp.org/en/stable/) module installed. From 463631b5b786db22d15d1aa6b546f99bf5616776 Mon Sep 17 00:00:00 2001 From: adamjmcgrath Date: Thu, 30 Mar 2023 13:32:33 +0100 Subject: [PATCH 7/7] Fix intellisense on Auth0 class --- auth0/management/async_auth0.py | 6 +-- auth0/management/auth0.py | 78 ++++++++++++++++----------------- 2 files changed, 41 insertions(+), 43 deletions(-) diff --git a/auth0/management/async_auth0.py b/auth0/management/async_auth0.py index 241c47f6..a0971512 100644 --- a/auth0/management/async_auth0.py +++ b/auth0/management/async_auth0.py @@ -1,7 +1,7 @@ import aiohttp from ..asyncify import asyncify -from .auth0 import modules +from .auth0 import Auth0 class AsyncAuth0: @@ -20,8 +20,8 @@ class AsyncAuth0: def __init__(self, domain, token, rest_options=None): self._services = [] - for name, cls in modules.items(): - cls = asyncify(cls) + for name, attr in vars(Auth0(domain, token, rest_options=rest_options)).items(): + cls = asyncify(attr.__class__) service = cls(domain=domain, token=token, rest_options=rest_options) self._services.append(service) setattr( diff --git a/auth0/management/auth0.py b/auth0/management/auth0.py index 9b1f3502..9e36ce96 100644 --- a/auth0/management/auth0.py +++ b/auth0/management/auth0.py @@ -1,4 +1,3 @@ -from ..utils import is_async_available from .actions import Actions from .attack_protection import AttackProtection from .blacklists import Blacklists @@ -30,39 +29,6 @@ from .users import Users from .users_by_email import UsersByEmail -modules = { - "actions": Actions, - "attack_protection": AttackProtection, - "blacklists": Blacklists, - "branding": Branding, - "client_credentials": ClientCredentials, - "client_grants": ClientGrants, - "clients": Clients, - "connections": Connections, - "custom_domains": CustomDomains, - "device_credentials": DeviceCredentials, - "email_templates": EmailTemplates, - "emails": Emails, - "grants": Grants, - "guardian": Guardian, - "hooks": Hooks, - "jobs": Jobs, - "log_streams": LogStreams, - "logs": Logs, - "organizations": Organizations, - "prompts": Prompts, - "resource_servers": ResourceServers, - "roles": Roles, - "rules_configs": RulesConfigs, - "rules": Rules, - "stats": Stats, - "tenants": Tenants, - "tickets": Tickets, - "user_blocks": UserBlocks, - "users_by_email": UsersByEmail, - "users": Users, -} - class Auth0: """Provides easy access to all endpoint classes @@ -79,9 +45,41 @@ class Auth0: """ def __init__(self, domain, token, rest_options=None): - for name, cls in modules.items(): - setattr( - self, - name, - cls(domain=domain, token=token, rest_options=rest_options), - ) + self.actions = Actions(domain, token, rest_options=rest_options) + self.attack_protection = AttackProtection( + domain, token, rest_options=rest_options + ) + self.blacklists = Blacklists(domain, token, rest_options=rest_options) + self.branding = Branding(domain, token, rest_options=rest_options) + self.client_credentials = ClientCredentials( + domain, token, rest_options=rest_options + ) + self.client_grants = ClientGrants(domain, token, rest_options=rest_options) + self.clients = Clients(domain, token, rest_options=rest_options) + self.connections = Connections(domain, token, rest_options=rest_options) + self.custom_domains = CustomDomains(domain, token, rest_options=rest_options) + self.device_credentials = DeviceCredentials( + domain, token, rest_options=rest_options + ) + self.email_templates = EmailTemplates(domain, token, rest_options=rest_options) + self.emails = Emails(domain, token, rest_options=rest_options) + self.grants = Grants(domain, token, rest_options=rest_options) + self.guardian = Guardian(domain, token, rest_options=rest_options) + self.hooks = Hooks(domain, token, rest_options=rest_options) + self.jobs = Jobs(domain, token, rest_options=rest_options) + self.log_streams = LogStreams(domain, token, rest_options=rest_options) + self.logs = Logs(domain, token, rest_options=rest_options) + self.organizations = Organizations(domain, token, rest_options=rest_options) + self.prompts = Prompts(domain, token, rest_options=rest_options) + self.resource_servers = ResourceServers( + domain, token, rest_options=rest_options + ) + 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.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) + self.user_blocks = UserBlocks(domain, token, rest_options=rest_options) + self.users_by_email = UsersByEmail(domain, token, rest_options=rest_options) + self.users = Users(domain, token, rest_options=rest_options)