From a64a38414a8f853dad24751f1ce2311fca1addce Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Mon, 28 Aug 2023 14:10:11 +0530 Subject: [PATCH 01/27] adding dev-v0.15.3 tag to this commit to ensure building --- html/supertokens_python/constants.html | 5 +- .../normalised_url_path.html | 12 +++- html/supertokens_python/querier.html | 59 +++++++++++++++++-- 3 files changed, 66 insertions(+), 10 deletions(-) diff --git a/html/supertokens_python/constants.html b/html/supertokens_python/constants.html index 954d8ea4b..ddabaf437 100644 --- a/html/supertokens_python/constants.html +++ b/html/supertokens_python/constants.html @@ -42,7 +42,7 @@

Module supertokens_python.constants

from __future__ import annotations SUPPORTED_CDI_VERSIONS = ["3.0"] -VERSION = "0.15.2" +VERSION = "0.15.3" TELEMETRY = "/telemetry" USER_COUNT = "/users/count" USER_DELETE = "/user/remove" @@ -56,7 +56,8 @@

Module supertokens_python.constants

API_VERSION = "/apiversion" API_VERSION_HEADER = "cdi-version" DASHBOARD_VERSION = "0.7" -HUNDRED_YEARS_IN_MS = 3153600000000 +HUNDRED_YEARS_IN_MS = 3153600000000 +RATE_LIMIT_STATUS_CODE = 429
diff --git a/html/supertokens_python/normalised_url_path.html b/html/supertokens_python/normalised_url_path.html index f5287fcac..d36078bb8 100644 --- a/html/supertokens_python/normalised_url_path.html +++ b/html/supertokens_python/normalised_url_path.html @@ -68,7 +68,9 @@

Module supertokens_python.normalised_url_path

Classes def is_a_recipe_path(self) -> bool: parts = self.__value.split("/") - return parts[1] == "recipe" or parts[2] == "recipe" + return (len(parts) > 1 and parts[1] == "recipe") or ( + len(parts) > 2 and parts[2] == "recipe" + )

Methods

@@ -301,7 +305,9 @@

Methods

def is_a_recipe_path(self) -> bool:
     parts = self.__value.split("/")
-    return parts[1] == "recipe" or parts[2] == "recipe"
+ return (len(parts) > 1 and parts[1] == "recipe") or ( + len(parts) > 2 and parts[2] == "recipe" + )
diff --git a/html/supertokens_python/querier.html b/html/supertokens_python/querier.html index 8636020dc..f95a55798 100644 --- a/html/supertokens_python/querier.html +++ b/html/supertokens_python/querier.html @@ -41,9 +41,11 @@

Module supertokens_python.querier

# under the License. from __future__ import annotations +import asyncio + from json import JSONDecodeError from os import environ -from typing import TYPE_CHECKING, Any, Awaitable, Callable, Dict +from typing import TYPE_CHECKING, Any, Awaitable, Callable, Dict, Optional from httpx import AsyncClient, ConnectTimeout, NetworkError, Response @@ -53,6 +55,7 @@

Module supertokens_python.querier

API_VERSION_HEADER, RID_KEY_HEADER, SUPPORTED_CDI_VERSIONS, + RATE_LIMIT_STATUS_CODE, ) from .normalised_url_path import NormalisedURLPath @@ -250,6 +253,7 @@

Module supertokens_python.querier

method: str, http_function: Callable[[str], Awaitable[Response]], no_of_tries: int, + retry_info_map: Optional[Dict[str, int]] = None, ) -> Any: if no_of_tries == 0: raise_general_exception("No SuperTokens core available to query") @@ -266,6 +270,14 @@

Module supertokens_python.querier

Querier.__last_tried_index %= len(self.__hosts) url = current_host + path.get_as_string_dangerous() + max_retries = 5 + + if retry_info_map is None: + retry_info_map = {} + + if retry_info_map.get(url) is None: + retry_info_map[url] = max_retries + ProcessState.get_instance().add_state( AllowedProcessStates.CALLING_SERVICE_IN_REQUEST_HELPER ) @@ -275,6 +287,20 @@

Module supertokens_python.querier

): Querier.__hosts_alive_for_testing.add(current_host) + if response.status_code == RATE_LIMIT_STATUS_CODE: + retries_left = retry_info_map[url] + + if retries_left > 0: + retry_info_map[url] = retries_left - 1 + + attempts_made = max_retries - retries_left + delay = (10 + attempts_made * 250) / 1000 + + await asyncio.sleep(delay) + return await self.__send_request_helper( + path, method, http_function, no_of_tries, retry_info_map + ) + if is_4xx_error(response.status_code) or is_5xx_error(response.status_code): # type: ignore raise_general_exception( "SuperTokens core threw an error for a " @@ -292,9 +318,9 @@

Module supertokens_python.querier

except JSONDecodeError: return response.text - except (ConnectionError, NetworkError, ConnectTimeout): + except (ConnectionError, NetworkError, ConnectTimeout) as _: return await self.__send_request_helper( - path, method, http_function, no_of_tries - 1 + path, method, http_function, no_of_tries - 1, retry_info_map ) except Exception as e: raise_general_exception(e)
@@ -503,6 +529,7 @@

Classes

method: str, http_function: Callable[[str], Awaitable[Response]], no_of_tries: int, + retry_info_map: Optional[Dict[str, int]] = None, ) -> Any: if no_of_tries == 0: raise_general_exception("No SuperTokens core available to query") @@ -519,6 +546,14 @@

Classes

Querier.__last_tried_index %= len(self.__hosts) url = current_host + path.get_as_string_dangerous() + max_retries = 5 + + if retry_info_map is None: + retry_info_map = {} + + if retry_info_map.get(url) is None: + retry_info_map[url] = max_retries + ProcessState.get_instance().add_state( AllowedProcessStates.CALLING_SERVICE_IN_REQUEST_HELPER ) @@ -528,6 +563,20 @@

Classes

): Querier.__hosts_alive_for_testing.add(current_host) + if response.status_code == RATE_LIMIT_STATUS_CODE: + retries_left = retry_info_map[url] + + if retries_left > 0: + retry_info_map[url] = retries_left - 1 + + attempts_made = max_retries - retries_left + delay = (10 + attempts_made * 250) / 1000 + + await asyncio.sleep(delay) + return await self.__send_request_helper( + path, method, http_function, no_of_tries, retry_info_map + ) + if is_4xx_error(response.status_code) or is_5xx_error(response.status_code): # type: ignore raise_general_exception( "SuperTokens core threw an error for a " @@ -545,9 +594,9 @@

Classes

except JSONDecodeError: return response.text - except (ConnectionError, NetworkError, ConnectTimeout): + except (ConnectionError, NetworkError, ConnectTimeout) as _: return await self.__send_request_helper( - path, method, http_function, no_of_tries - 1 + path, method, http_function, no_of_tries - 1, retry_info_map ) except Exception as e: raise_general_exception(e) From 2cd6cb510f0e40e4946f12b56aa1a598c2cfe96a Mon Sep 17 00:00:00 2001 From: KShivendu Date: Thu, 7 Sep 2023 15:08:48 +0530 Subject: [PATCH 02/27] feat: Add dashboard admin feature --- supertokens_python/framework/request.py | 4 ++ .../recipe/dashboard/__init__.py | 6 ++- .../recipe/dashboard/api/api_key_protector.py | 25 ++++++++-- .../recipe/dashboard/exceptions.py | 4 ++ supertokens_python/recipe/dashboard/recipe.py | 12 +++-- .../recipe/dashboard/recipe_implementation.py | 50 ++++++++++++++++--- supertokens_python/recipe/dashboard/utils.py | 18 ++++++- supertokens_python/utils.py | 4 ++ 8 files changed, 106 insertions(+), 17 deletions(-) diff --git a/supertokens_python/framework/request.py b/supertokens_python/framework/request.py index fc8716f41..6f6533185 100644 --- a/supertokens_python/framework/request.py +++ b/supertokens_python/framework/request.py @@ -25,6 +25,10 @@ def __init__(self): self.wrapper_used = True self.request = None + @abstractmethod + def get_original_url(self) -> str: + pass + @abstractmethod def get_query_param( self, key: str, default: Union[str, None] = None diff --git a/supertokens_python/recipe/dashboard/__init__.py b/supertokens_python/recipe/dashboard/__init__.py index d2438f12b..65a3047fd 100644 --- a/supertokens_python/recipe/dashboard/__init__.py +++ b/supertokens_python/recipe/dashboard/__init__.py @@ -14,7 +14,7 @@ from __future__ import annotations -from typing import Callable, Optional, Union +from typing import Callable, Optional, List from supertokens_python import AppInfo, RecipeModule @@ -26,10 +26,12 @@ def init( - api_key: Union[str, None] = None, + api_key: Optional[str] = None, + admins: Optional[List[str]] = None, override: Optional[InputOverrideConfig] = None, ) -> Callable[[AppInfo], RecipeModule]: return DashboardRecipe.init( api_key, + admins, override, ) diff --git a/supertokens_python/recipe/dashboard/api/api_key_protector.py b/supertokens_python/recipe/dashboard/api/api_key_protector.py index bceaba269..69708fca6 100644 --- a/supertokens_python/recipe/dashboard/api/api_key_protector.py +++ b/supertokens_python/recipe/dashboard/api/api_key_protector.py @@ -13,6 +13,7 @@ # under the License. from __future__ import annotations +import json from typing import TYPE_CHECKING, Callable, Optional, Awaitable, Dict, Any from supertokens_python.framework import BaseResponse @@ -29,6 +30,8 @@ send_non_200_response_with_message, ) +from ..exceptions import DashboardOperationNotAllowedError + async def api_key_protector( api_implementation: APIInterface, @@ -39,9 +42,25 @@ async def api_key_protector( ], user_context: Dict[str, Any], ) -> Optional[BaseResponse]: - should_allow_access = await api_options.recipe_implementation.should_allow_access( - api_options.request, api_options.config, user_context - ) + should_allow_access = False + + try: + should_allow_access = ( + await api_options.recipe_implementation.should_allow_access( + api_options.request, api_options.config, user_context + ) + ) + except DashboardOperationNotAllowedError as _: + # api_options.response.set_status_code(403) + # api_options.response.set_json_content({ + # "message": "You are not permitted to perform this operation" + # }) + # return None + return send_non_200_response_with_message( + json.dumps({"message": "You are not permitted to perform this operation"}), + 403, + api_options.response, + ) if should_allow_access is False: return send_non_200_response_with_message( diff --git a/supertokens_python/recipe/dashboard/exceptions.py b/supertokens_python/recipe/dashboard/exceptions.py index 2e79b0257..c8a2e27d9 100644 --- a/supertokens_python/recipe/dashboard/exceptions.py +++ b/supertokens_python/recipe/dashboard/exceptions.py @@ -3,3 +3,7 @@ class SuperTokensDashboardError(SuperTokensError): pass + + +class DashboardOperationNotAllowedError(SuperTokensDashboardError): + pass diff --git a/supertokens_python/recipe/dashboard/recipe.py b/supertokens_python/recipe/dashboard/recipe.py index c66756e5f..1a4fce10f 100644 --- a/supertokens_python/recipe/dashboard/recipe.py +++ b/supertokens_python/recipe/dashboard/recipe.py @@ -87,12 +87,14 @@ def __init__( self, recipe_id: str, app_info: AppInfo, - api_key: Union[str, None], - override: Union[InputOverrideConfig, None] = None, + api_key: Optional[str], + admins: Optional[List[str]], + override: Optional[InputOverrideConfig] = None, ): super().__init__(recipe_id, app_info) self.config = validate_and_normalise_user_input( api_key, + admins, override, ) recipe_implementation = RecipeImplementation() @@ -349,8 +351,9 @@ def get_all_cors_headers(self) -> List[str]: @staticmethod def init( - api_key: Union[str, None], - override: Union[InputOverrideConfig, None] = None, + api_key: Optional[str], + admins: Optional[List[str]], + override: Optional[InputOverrideConfig] = None, ): def func(app_info: AppInfo): if DashboardRecipe.__instance is None: @@ -358,6 +361,7 @@ def func(app_info: AppInfo): DashboardRecipe.recipe_id, app_info, api_key, + admins, override, ) return DashboardRecipe.__instance diff --git a/supertokens_python/recipe/dashboard/recipe_implementation.py b/supertokens_python/recipe/dashboard/recipe_implementation.py index 9d34fb9ca..7e06abb58 100644 --- a/supertokens_python/recipe/dashboard/recipe_implementation.py +++ b/supertokens_python/recipe/dashboard/recipe_implementation.py @@ -18,10 +18,16 @@ from supertokens_python.constants import DASHBOARD_VERSION from supertokens_python.framework import BaseRequest from supertokens_python.normalised_url_path import NormalisedURLPath +from supertokens_python.utils import log_debug_message from supertokens_python.querier import Querier +from supertokens_python.recipe.dashboard.constants import ( + DASHBOARD_ANALYTICS_API, + EMAIL_PASSSWORD_SIGNOUT, +) from .interfaces import RecipeInterface from .utils import DashboardConfig, validate_api_key +from .exceptions import DashboardOperationNotAllowedError class RecipeImplementation(RecipeInterface): @@ -34,9 +40,9 @@ async def should_allow_access( config: DashboardConfig, user_context: Dict[str, Any], ) -> bool: - if config.auth_mode == "email-password": + # For cases where we're not using the API key, the JWT is being used; we allow their access by default + if config.api_key is not None: auth_header_value = request.get_header("authorization") - if not auth_header_value: return False @@ -47,8 +53,40 @@ async def should_allow_access( {"sessionId": auth_header_value}, ) ) - return ( - "status" in session_verification_response - and session_verification_response["status"] == "OK" - ) + if session_verification_response.get("status") != "OK": + return False + + # For all non GET requests we also want to check if the + # user is allowed to perform this operation + if request.method() != "GET": # TODO: Use normalize http method? + # We dont want to block the analytics API + if request.get_original_url().startswith(DASHBOARD_ANALYTICS_API): + return True + + # We do not want to block the sign out request + if request.get_original_url().endswith(EMAIL_PASSSWORD_SIGNOUT): + return True + + admins = config.admins + + # If the user has provided no admins, allow + if len(admins) == 0: + return True + + email_in_headers = request.get_header("email") + + if email_in_headers is None: + log_debug_message( + "User Dashboard: Returniing OPERATION_NOT_ALLOWED because no email was provided in headers" + ) + return False + + if email_in_headers not in admins: + log_debug_message( + "User Dashboard: Throwing OPERATION_NOT_ALLOWED because user is not an admin" + ) + raise DashboardOperationNotAllowedError() + + return True + return validate_api_key(request, config, user_context) diff --git a/supertokens_python/recipe/dashboard/utils.py b/supertokens_python/recipe/dashboard/utils.py index 549978858..25225d64d 100644 --- a/supertokens_python/recipe/dashboard/utils.py +++ b/supertokens_python/recipe/dashboard/utils.py @@ -43,7 +43,7 @@ get_user_by_id as tppless_get_user_by_id, ) from supertokens_python.types import User -from supertokens_python.utils import Awaitable +from supertokens_python.utils import Awaitable, log_debug_message, normalise_email from ...normalised_url_path import NormalisedURLPath from .constants import ( @@ -181,9 +181,14 @@ def __init__( class DashboardConfig: def __init__( - self, api_key: Union[str, None], override: OverrideConfig, auth_mode: str + self, + api_key: Optional[str], + admins: List[str], + override: OverrideConfig, + auth_mode: str, ): self.api_key = api_key + self.admins = admins self.override = override self.auth_mode = auth_mode @@ -191,14 +196,23 @@ def __init__( def validate_and_normalise_user_input( # app_info: AppInfo, api_key: Union[str, None], + admins: Optional[List[str]], override: Optional[InputOverrideConfig] = None, ) -> DashboardConfig: if override is None: override = InputOverrideConfig() + if api_key is not None and admins is not None: + log_debug_message( + "User Dashboard: Providing 'admins' has no effect when using an api key." + ) + + admins = [normalise_email(a) for a in admins] if admins is not None else [] + return DashboardConfig( api_key, + admins, OverrideConfig( functions=override.functions, apis=override.apis, diff --git a/supertokens_python/utils.py b/supertokens_python/utils.py index d3ca32b37..a79d182c1 100644 --- a/supertokens_python/utils.py +++ b/supertokens_python/utils.py @@ -360,3 +360,7 @@ def __exit__(self, exc_type: Any, exc_value: Any, traceback: Any): if exc_type is not None: raise exc_type(exc_value).with_traceback(traceback) + + +def normalise_email(email: str) -> str: + return email.strip().lower() From 0ee1a53b248398d0abfd7461b237ced04ec9ebd5 Mon Sep 17 00:00:00 2001 From: KShivendu Date: Fri, 8 Sep 2023 14:12:43 +0530 Subject: [PATCH 03/27] refactor: Improve interface and variables --- .../framework/django/django_request.py | 3 +++ .../framework/fastapi/fastapi_request.py | 3 +++ .../framework/flask/flask_request.py | 3 +++ .../recipe/dashboard/constants.py | 4 ++-- supertokens_python/recipe/dashboard/recipe.py | 24 ++++++++----------- .../recipe/dashboard/recipe_implementation.py | 8 +++---- supertokens_python/recipe/dashboard/utils.py | 12 +++++----- 7 files changed, 31 insertions(+), 26 deletions(-) diff --git a/supertokens_python/framework/django/django_request.py b/supertokens_python/framework/django/django_request.py index 4f130a202..e2df2fdbc 100644 --- a/supertokens_python/framework/django/django_request.py +++ b/supertokens_python/framework/django/django_request.py @@ -29,6 +29,9 @@ def __init__(self, request: HttpRequest): super().__init__() self.request = request + def get_original_url(self) -> str: + return self.request.get_raw_uri() + def get_query_param( self, key: str, default: Union[str, None] = None ) -> Union[str, None]: diff --git a/supertokens_python/framework/fastapi/fastapi_request.py b/supertokens_python/framework/fastapi/fastapi_request.py index 4387d6917..fc9525f3f 100644 --- a/supertokens_python/framework/fastapi/fastapi_request.py +++ b/supertokens_python/framework/fastapi/fastapi_request.py @@ -28,6 +28,9 @@ def __init__(self, request: Request): super().__init__() self.request = request + def get_original_url(self) -> str: + return self.request.url.components.geturl() + def get_query_param( self, key: str, default: Union[str, None] = None ) -> Union[str, None]: diff --git a/supertokens_python/framework/flask/flask_request.py b/supertokens_python/framework/flask/flask_request.py index 63fd1157d..0b046f6a2 100644 --- a/supertokens_python/framework/flask/flask_request.py +++ b/supertokens_python/framework/flask/flask_request.py @@ -27,6 +27,9 @@ def __init__(self, req: Request): super().__init__() self.request = req + def get_original_url(self) -> str: + return self.request.url + def get_query_param(self, key: str, default: Union[str, None] = None): return self.request.args.get(key, default) diff --git a/supertokens_python/recipe/dashboard/constants.py b/supertokens_python/recipe/dashboard/constants.py index 1f5b64c13..258718254 100644 --- a/supertokens_python/recipe/dashboard/constants.py +++ b/supertokens_python/recipe/dashboard/constants.py @@ -8,8 +8,8 @@ USER_SESSION_API = "/api/user/sessions" USER_PASSWORD_API = "/api/user/password" USER_EMAIL_VERIFY_TOKEN_API = "/api/user/email/verify/token" -EMAIL_PASSWORD_SIGN_IN = "/api/signin" -EMAIL_PASSSWORD_SIGNOUT = "/api/signout" +SIGN_IN_API = "/api/signin" +SIGN_OUT_API = "/api/signout" SEARCH_TAGS_API = "/api/search/tags" DASHBOARD_ANALYTICS_API = "/api/analytics" TENANTS_LIST_API = "/api/tenants/list" diff --git a/supertokens_python/recipe/dashboard/recipe.py b/supertokens_python/recipe/dashboard/recipe.py index 1a4fce10f..ff2228f98 100644 --- a/supertokens_python/recipe/dashboard/recipe.py +++ b/supertokens_python/recipe/dashboard/recipe.py @@ -14,7 +14,7 @@ from __future__ import annotations from os import environ -from typing import TYPE_CHECKING, Awaitable, Callable, List, Optional, Union, Dict, Any +from typing import TYPE_CHECKING, Awaitable, Callable, List, Optional, Dict, Any from supertokens_python.normalised_url_path import NormalisedURLPath from supertokens_python.recipe_module import APIHandled, RecipeModule @@ -59,8 +59,8 @@ from .constants import ( DASHBOARD_ANALYTICS_API, DASHBOARD_API, - EMAIL_PASSSWORD_SIGNOUT, - EMAIL_PASSWORD_SIGN_IN, + SIGN_OUT_API, + SIGN_IN_API, SEARCH_TAGS_API, USER_API, USER_EMAIL_VERIFY_API, @@ -125,11 +125,9 @@ def get_apis_handled(self) -> List[APIHandled]: False, ), APIHandled( - NormalisedURLPath( - get_api_path_with_dashboard_base(EMAIL_PASSWORD_SIGN_IN) - ), + NormalisedURLPath(get_api_path_with_dashboard_base(SIGN_IN_API)), "post", - EMAIL_PASSWORD_SIGN_IN, + SIGN_IN_API, False, ), APIHandled( @@ -139,11 +137,9 @@ def get_apis_handled(self) -> List[APIHandled]: False, ), APIHandled( - NormalisedURLPath( - get_api_path_with_dashboard_base(EMAIL_PASSSWORD_SIGNOUT) - ), + NormalisedURLPath(get_api_path_with_dashboard_base(SIGN_OUT_API)), "post", - EMAIL_PASSSWORD_SIGNOUT, + SIGN_OUT_API, False, ), APIHandled( @@ -279,7 +275,7 @@ async def handle_api_request( return await handle_validate_key_api( self.api_implementation, api_options, user_context ) - if request_id == EMAIL_PASSWORD_SIGN_IN: + if request_id == SIGN_IN_API: return await handle_emailpassword_signin_api( self.api_implementation, api_options, user_context ) @@ -320,7 +316,7 @@ async def handle_api_request( api_function = handle_user_password_put elif request_id == USER_EMAIL_VERIFY_TOKEN_API: api_function = handle_email_verify_token_post - elif request_id == EMAIL_PASSSWORD_SIGNOUT: + elif request_id == SIGN_OUT_API: api_function = handle_emailpassword_signout_api elif request_id == SEARCH_TAGS_API: api_function = handle_get_tags @@ -352,7 +348,7 @@ def get_all_cors_headers(self) -> List[str]: @staticmethod def init( api_key: Optional[str], - admins: Optional[List[str]], + admins: Optional[List[str]] = None, override: Optional[InputOverrideConfig] = None, ): def func(app_info: AppInfo): diff --git a/supertokens_python/recipe/dashboard/recipe_implementation.py b/supertokens_python/recipe/dashboard/recipe_implementation.py index 7e06abb58..b9cca9709 100644 --- a/supertokens_python/recipe/dashboard/recipe_implementation.py +++ b/supertokens_python/recipe/dashboard/recipe_implementation.py @@ -18,11 +18,11 @@ from supertokens_python.constants import DASHBOARD_VERSION from supertokens_python.framework import BaseRequest from supertokens_python.normalised_url_path import NormalisedURLPath -from supertokens_python.utils import log_debug_message +from supertokens_python.utils import log_debug_message, normalise_http_method from supertokens_python.querier import Querier from supertokens_python.recipe.dashboard.constants import ( DASHBOARD_ANALYTICS_API, - EMAIL_PASSSWORD_SIGNOUT, + SIGN_OUT_API, ) from .interfaces import RecipeInterface @@ -58,13 +58,13 @@ async def should_allow_access( # For all non GET requests we also want to check if the # user is allowed to perform this operation - if request.method() != "GET": # TODO: Use normalize http method? + if normalise_http_method(request.method()) != "get": # We dont want to block the analytics API if request.get_original_url().startswith(DASHBOARD_ANALYTICS_API): return True # We do not want to block the sign out request - if request.get_original_url().endswith(EMAIL_PASSSWORD_SIGNOUT): + if request.get_original_url().endswith(SIGN_OUT_API): return True admins = config.admins diff --git a/supertokens_python/recipe/dashboard/utils.py b/supertokens_python/recipe/dashboard/utils.py index 25225d64d..22d98bd54 100644 --- a/supertokens_python/recipe/dashboard/utils.py +++ b/supertokens_python/recipe/dashboard/utils.py @@ -49,8 +49,8 @@ from .constants import ( DASHBOARD_ANALYTICS_API, DASHBOARD_API, - EMAIL_PASSSWORD_SIGNOUT, - EMAIL_PASSWORD_SIGN_IN, + SIGN_OUT_API, + SIGN_IN_API, SEARCH_TAGS_API, USER_API, USER_EMAIL_VERIFY_API, @@ -259,10 +259,10 @@ def get_api_if_matched(path: NormalisedURLPath, method: str) -> Optional[str]: return USER_PASSWORD_API if path_str.endswith(USER_EMAIL_VERIFY_TOKEN_API) and method == "post": return USER_EMAIL_VERIFY_TOKEN_API - if path_str.endswith(EMAIL_PASSWORD_SIGN_IN) and method == "post": - return EMAIL_PASSWORD_SIGN_IN - if path_str.endswith(EMAIL_PASSSWORD_SIGNOUT) and method == "post": - return EMAIL_PASSSWORD_SIGNOUT + if path_str.endswith(SIGN_IN_API) and method == "post": + return SIGN_IN_API + if path_str.endswith(SIGN_OUT_API) and method == "post": + return SIGN_OUT_API if path_str.endswith(SEARCH_TAGS_API) and method == "get": return SEARCH_TAGS_API if path_str.endswith(DASHBOARD_ANALYTICS_API) and method == "post": From 685e725c053751f931a42e1a47b45aab01a3b63d Mon Sep 17 00:00:00 2001 From: KShivendu Date: Fri, 8 Sep 2023 15:44:11 +0530 Subject: [PATCH 04/27] fix: Updates after manual testing --- CHANGELOG.md | 6 ++++++ supertokens_python/recipe/dashboard/api/validate_key.py | 2 +- .../recipe/dashboard/recipe_implementation.py | 8 ++++---- supertokens_python/recipe/dashboard/utils.py | 2 +- 4 files changed, 12 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a510b2e7..78271cd16 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [unreleased] +### Added + +- The Dashboard recipe now accepts a new `admins` property which can be used to give Dashboard Users write privileges for the user dashboard. + +### Changes +- Dashboard APIs now return a status code `403` for all non-GET requests if the currently logged in Dashboard User is not listed in the `admins` array ## [0.15.3] - 2023-09-24 diff --git a/supertokens_python/recipe/dashboard/api/validate_key.py b/supertokens_python/recipe/dashboard/api/validate_key.py index 12e5758c9..905e6f244 100644 --- a/supertokens_python/recipe/dashboard/api/validate_key.py +++ b/supertokens_python/recipe/dashboard/api/validate_key.py @@ -35,7 +35,7 @@ async def handle_validate_key_api( user_context: Dict[str, Any], ): - is_valid_key = validate_api_key( + is_valid_key = await validate_api_key( api_options.request, api_options.config, user_context ) diff --git a/supertokens_python/recipe/dashboard/recipe_implementation.py b/supertokens_python/recipe/dashboard/recipe_implementation.py index b9cca9709..9fb69ab4e 100644 --- a/supertokens_python/recipe/dashboard/recipe_implementation.py +++ b/supertokens_python/recipe/dashboard/recipe_implementation.py @@ -41,7 +41,7 @@ async def should_allow_access( user_context: Dict[str, Any], ) -> bool: # For cases where we're not using the API key, the JWT is being used; we allow their access by default - if config.api_key is not None: + if config.api_key is None: auth_header_value = request.get_header("authorization") if not auth_header_value: return False @@ -60,7 +60,7 @@ async def should_allow_access( # user is allowed to perform this operation if normalise_http_method(request.method()) != "get": # We dont want to block the analytics API - if request.get_original_url().startswith(DASHBOARD_ANALYTICS_API): + if request.get_original_url().endswith(DASHBOARD_ANALYTICS_API): return True # We do not want to block the sign out request @@ -77,7 +77,7 @@ async def should_allow_access( if email_in_headers is None: log_debug_message( - "User Dashboard: Returniing OPERATION_NOT_ALLOWED because no email was provided in headers" + "User Dashboard: Returning UNAUTHORISED_ERROR because no email was provided in headers" ) return False @@ -89,4 +89,4 @@ async def should_allow_access( return True - return validate_api_key(request, config, user_context) + return await validate_api_key(request, config, user_context) diff --git a/supertokens_python/recipe/dashboard/utils.py b/supertokens_python/recipe/dashboard/utils.py index 22d98bd54..7dabadaef 100644 --- a/supertokens_python/recipe/dashboard/utils.py +++ b/supertokens_python/recipe/dashboard/utils.py @@ -424,7 +424,7 @@ def is_recipe_initialised(recipeId: str) -> bool: return isRecipeInitialised -def validate_api_key( +async def validate_api_key( req: BaseRequest, config: DashboardConfig, _user_context: Dict[str, Any] ) -> bool: api_key_header_value = req.get_header("authorization") From 0409930e3865e9225fe420ab53e3e2886dfb02d5 Mon Sep 17 00:00:00 2001 From: KShivendu Date: Fri, 8 Sep 2023 17:30:55 +0530 Subject: [PATCH 05/27] feat: Remove unused code comment --- supertokens_python/recipe/dashboard/api/api_key_protector.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/supertokens_python/recipe/dashboard/api/api_key_protector.py b/supertokens_python/recipe/dashboard/api/api_key_protector.py index 69708fca6..4aa9b4420 100644 --- a/supertokens_python/recipe/dashboard/api/api_key_protector.py +++ b/supertokens_python/recipe/dashboard/api/api_key_protector.py @@ -51,11 +51,6 @@ async def api_key_protector( ) ) except DashboardOperationNotAllowedError as _: - # api_options.response.set_status_code(403) - # api_options.response.set_json_content({ - # "message": "You are not permitted to perform this operation" - # }) - # return None return send_non_200_response_with_message( json.dumps({"message": "You are not permitted to perform this operation"}), 403, From fe6210ddcf91c365a1dd4505d6a42cf824f5a403 Mon Sep 17 00:00:00 2001 From: KShivendu Date: Mon, 11 Sep 2023 15:33:53 +0530 Subject: [PATCH 06/27] fix: message format --- supertokens_python/recipe/dashboard/api/api_key_protector.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/supertokens_python/recipe/dashboard/api/api_key_protector.py b/supertokens_python/recipe/dashboard/api/api_key_protector.py index 4aa9b4420..0b5370016 100644 --- a/supertokens_python/recipe/dashboard/api/api_key_protector.py +++ b/supertokens_python/recipe/dashboard/api/api_key_protector.py @@ -52,7 +52,7 @@ async def api_key_protector( ) except DashboardOperationNotAllowedError as _: return send_non_200_response_with_message( - json.dumps({"message": "You are not permitted to perform this operation"}), + "You are not permitted to perform this operation", 403, api_options.response, ) From 7353e5e83162841673e508db2a7be418a9219c1b Mon Sep 17 00:00:00 2001 From: KShivendu Date: Tue, 12 Sep 2023 11:58:09 +0530 Subject: [PATCH 07/27] fix: Add missing user session POST API --- .../recipe/dashboard/api/api_key_protector.py | 1 - supertokens_python/recipe/dashboard/recipe.py | 6 ++++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/supertokens_python/recipe/dashboard/api/api_key_protector.py b/supertokens_python/recipe/dashboard/api/api_key_protector.py index 0b5370016..1dee543c2 100644 --- a/supertokens_python/recipe/dashboard/api/api_key_protector.py +++ b/supertokens_python/recipe/dashboard/api/api_key_protector.py @@ -13,7 +13,6 @@ # under the License. from __future__ import annotations -import json from typing import TYPE_CHECKING, Callable, Optional, Awaitable, Dict, Any from supertokens_python.framework import BaseResponse diff --git a/supertokens_python/recipe/dashboard/recipe.py b/supertokens_python/recipe/dashboard/recipe.py index ff2228f98..2ee44998a 100644 --- a/supertokens_python/recipe/dashboard/recipe.py +++ b/supertokens_python/recipe/dashboard/recipe.py @@ -212,6 +212,12 @@ def get_apis_handled(self) -> List[APIHandled]: USER_SESSION_API, False, ), + APIHandled( + NormalisedURLPath(get_api_path_with_dashboard_base(USER_SESSION_API)), + "post", + USER_SESSION_API, + False, + ), APIHandled( NormalisedURLPath(get_api_path_with_dashboard_base(USER_PASSWORD_API)), "put", From 387077c616456789fab017df9dca46bf7d9b2cea Mon Sep 17 00:00:00 2001 From: KShivendu Date: Tue, 12 Sep 2023 13:21:12 +0530 Subject: [PATCH 08/27] feat: Dashboard should check admin based on value from session --- .../recipe/dashboard/recipe_implementation.py | 15 ++++++++++----- supertokens_python/recipe/dashboard/utils.py | 4 ++-- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/supertokens_python/recipe/dashboard/recipe_implementation.py b/supertokens_python/recipe/dashboard/recipe_implementation.py index 9fb69ab4e..5b97cd030 100644 --- a/supertokens_python/recipe/dashboard/recipe_implementation.py +++ b/supertokens_python/recipe/dashboard/recipe_implementation.py @@ -69,19 +69,24 @@ async def should_allow_access( admins = config.admins - # If the user has provided no admins, allow - if len(admins) == 0: + if admins is None: return True - email_in_headers = request.get_header("email") + if len(admins) == 0: + log_debug_message( + "User Dashboard: Throwing OPERATION_NOT_ALLOWED because user is not an admin" + ) + raise DashboardOperationNotAllowedError() + + user_email = session_verification_response.get("email") - if email_in_headers is None: + if user_email is None or not isinstance(user_email, str): log_debug_message( "User Dashboard: Returning UNAUTHORISED_ERROR because no email was provided in headers" ) return False - if email_in_headers not in admins: + if user_email not in admins: log_debug_message( "User Dashboard: Throwing OPERATION_NOT_ALLOWED because user is not an admin" ) diff --git a/supertokens_python/recipe/dashboard/utils.py b/supertokens_python/recipe/dashboard/utils.py index 7dabadaef..a6120a4d1 100644 --- a/supertokens_python/recipe/dashboard/utils.py +++ b/supertokens_python/recipe/dashboard/utils.py @@ -183,7 +183,7 @@ class DashboardConfig: def __init__( self, api_key: Optional[str], - admins: List[str], + admins: Optional[List[str]], override: OverrideConfig, auth_mode: str, ): @@ -208,7 +208,7 @@ def validate_and_normalise_user_input( "User Dashboard: Providing 'admins' has no effect when using an api key." ) - admins = [normalise_email(a) for a in admins] if admins is not None else [] + admins = [normalise_email(a) for a in admins] if admins is not None else None return DashboardConfig( api_key, From ddf1a6cabd9f7e06d17d90f45a6affe95c281b2a Mon Sep 17 00:00:00 2001 From: KShivendu Date: Tue, 12 Sep 2023 14:39:53 +0530 Subject: [PATCH 09/27] fix: ignore protected props in create_new_session functions --- supertokens_python/recipe/session/asyncio/__init__.py | 5 +++++ supertokens_python/recipe/session/constants.py | 1 + .../recipe/session/recipe_implementation.py | 8 +++++++- .../recipe/session/session_request_functions.py | 5 +++++ 4 files changed, 18 insertions(+), 1 deletion(-) diff --git a/supertokens_python/recipe/session/asyncio/__init__.py b/supertokens_python/recipe/session/asyncio/__init__.py index c9a6a4b25..a50422885 100644 --- a/supertokens_python/recipe/session/asyncio/__init__.py +++ b/supertokens_python/recipe/session/asyncio/__init__.py @@ -41,6 +41,7 @@ get_session_from_request, refresh_session_in_request, ) +from ..constants import protected_props from ..utils import get_required_claim_validators from supertokens_python.recipe.multitenancy.constants import DEFAULT_TENANT_ID @@ -106,6 +107,10 @@ async def create_new_session_without_request_response( final_access_token_payload = {**access_token_payload, "iss": issuer} + for prop in protected_props: + if prop in final_access_token_payload: + del final_access_token_payload[prop] + for claim in claims_added_by_other_recipes: update = await claim.build(user_id, tenant_id, user_context) final_access_token_payload = {**final_access_token_payload, **update} diff --git a/supertokens_python/recipe/session/constants.py b/supertokens_python/recipe/session/constants.py index 83b739651..49c634fac 100644 --- a/supertokens_python/recipe/session/constants.py +++ b/supertokens_python/recipe/session/constants.py @@ -42,5 +42,6 @@ "parentRefreshTokenHash1", "refreshTokenHash1", "antiCsrfToken", + "rsub", "tId", ] diff --git a/supertokens_python/recipe/session/recipe_implementation.py b/supertokens_python/recipe/session/recipe_implementation.py index f487dcb56..a9f9aad6d 100644 --- a/supertokens_python/recipe/session/recipe_implementation.py +++ b/supertokens_python/recipe/session/recipe_implementation.py @@ -47,6 +47,7 @@ from supertokens_python import AppInfo from .interfaces import SessionContainer +from .constants import protected_props from supertokens_python.querier import Querier from supertokens_python.recipe.multitenancy.constants import DEFAULT_TENANT_ID @@ -378,8 +379,13 @@ async def merge_into_access_token_payload( if session_info is None: return False + new_access_token_payload = session_info.custom_claims_in_access_token_payload + for k in protected_props: + if k in new_access_token_payload: + del new_access_token_payload[k] + new_access_token_payload = { - **session_info.custom_claims_in_access_token_payload, + **new_access_token_payload, **access_token_payload_update, } for k in access_token_payload_update.keys(): diff --git a/supertokens_python/recipe/session/session_request_functions.py b/supertokens_python/recipe/session/session_request_functions.py index 3bf3fc5ee..104ae4504 100644 --- a/supertokens_python/recipe/session/session_request_functions.py +++ b/supertokens_python/recipe/session/session_request_functions.py @@ -60,6 +60,7 @@ set_request_in_user_context_if_not_defined, ) from supertokens_python.supertokens import Supertokens +from .constants import protected_props if TYPE_CHECKING: from supertokens_python.recipe.session.recipe import SessionRecipe @@ -240,6 +241,10 @@ async def create_new_session_in_request( final_access_token_payload = {**access_token_payload, "iss": issuer} + for prop in protected_props: + if prop in final_access_token_payload: + del final_access_token_payload[prop] + for claim in claims_added_by_other_recipes: update = await claim.build(user_id, tenant_id, user_context) final_access_token_payload = {**final_access_token_payload, **update} From 97e219c0f3389d7444b645448d4e5505373759a4 Mon Sep 17 00:00:00 2001 From: KShivendu Date: Tue, 12 Sep 2023 14:42:40 +0530 Subject: [PATCH 10/27] chores: Bump python version and update CHANGELOG --- CHANGELOG.md | 5 +++++ setup.py | 2 +- supertokens_python/constants.py | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 78271cd16..3e4aa70f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [unreleased] + +## [0.16.0] - 2023-09-13 + + ### Added - The Dashboard recipe now accepts a new `admins` property which can be used to give Dashboard Users write privileges for the user dashboard. @@ -15,6 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changes - Dashboard APIs now return a status code `403` for all non-GET requests if the currently logged in Dashboard User is not listed in the `admins` array +- Now ignoring protected props in the payload in `create_new_session` and `create_new_session_without_request_response` ## [0.15.3] - 2023-09-24 diff --git a/setup.py b/setup.py index 773b7148b..ca2400964 100644 --- a/setup.py +++ b/setup.py @@ -70,7 +70,7 @@ setup( name="supertokens_python", - version="0.15.3", + version="0.16.0", author="SuperTokens", license="Apache 2.0", author_email="team@supertokens.com", diff --git a/supertokens_python/constants.py b/supertokens_python/constants.py index 902c982ea..075ff9309 100644 --- a/supertokens_python/constants.py +++ b/supertokens_python/constants.py @@ -14,7 +14,7 @@ from __future__ import annotations SUPPORTED_CDI_VERSIONS = ["3.0"] -VERSION = "0.15.3" +VERSION = "0.16.0" TELEMETRY = "/telemetry" USER_COUNT = "/users/count" USER_DELETE = "/user/remove" From aa82dbc040c29162df8332fe16797a432fed79e6 Mon Sep 17 00:00:00 2001 From: KShivendu Date: Tue, 12 Sep 2023 15:31:45 +0530 Subject: [PATCH 11/27] test: Add test for ignore protected props in create session --- tests/sessions/test_access_token_version.py | 23 +++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/tests/sessions/test_access_token_version.py b/tests/sessions/test_access_token_version.py index acfb5c269..6e4fe68e2 100644 --- a/tests/sessions/test_access_token_version.py +++ b/tests/sessions/test_access_token_version.py @@ -203,6 +203,29 @@ async def test_should_validate_v3_tokens_with_check_database_enabled(app: TestCl } +async def test_ignore_protected_props_in_create_session(app: TestClient): + init(**get_st_init_args([session.init()])) # type:ignore + start_st() + + create_session_res = app.post("/create", data={"sub": "asdf"}) + + assert create_session_res.status_code == 200 + + info = extract_info(create_session_res) + assert info["accessTokenFromAny"] is not None + assert info["refreshTokenFromAny"] is not None + assert info["frontToken"] is not None + + parsed_token = parse_jwt_without_signature_verification(info["accessTokenFromAny"]) + assert parsed_token.payload["sub"] != "asdf" + + s = await create_new_session_without_request_response( + "public", "user-id", {"sub": "asdf"} + ) + payload = parse_jwt_without_signature_verification(s.access_token).payload + assert payload["sub"] != "asdf" + + async def test_validation_logic_with_keys_that_can_use_json_nulls_values_in_claims(): """We want to make sure that for access token claims that can be null, the SDK does not fail access token validation if the core does not send them as part of the payload. For this we verify that validation passes when the keys are None, empty, From a51d242489e4f0d013937a9d3bf04881b4232981 Mon Sep 17 00:00:00 2001 From: KShivendu Date: Tue, 12 Sep 2023 15:56:40 +0530 Subject: [PATCH 12/27] test: Update tests --- tests/sessions/test_access_token_version.py | 30 ++++++++++----------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/tests/sessions/test_access_token_version.py b/tests/sessions/test_access_token_version.py index 6e4fe68e2..244deb484 100644 --- a/tests/sessions/test_access_token_version.py +++ b/tests/sessions/test_access_token_version.py @@ -203,27 +203,25 @@ async def test_should_validate_v3_tokens_with_check_database_enabled(app: TestCl } -async def test_ignore_protected_props_in_create_session(app: TestClient): - init(**get_st_init_args([session.init()])) # type:ignore +async def test_ignore_protected_props_in_create_session(): + init(**get_st_init_args([session.init()])) start_st() - create_session_res = app.post("/create", data={"sub": "asdf"}) - - assert create_session_res.status_code == 200 - - info = extract_info(create_session_res) - assert info["accessTokenFromAny"] is not None - assert info["refreshTokenFromAny"] is not None - assert info["frontToken"] is not None - - parsed_token = parse_jwt_without_signature_verification(info["accessTokenFromAny"]) - assert parsed_token.payload["sub"] != "asdf" - s = await create_new_session_without_request_response( - "public", "user-id", {"sub": "asdf"} + "public", + "user1", + {"foo": "bar"}, ) payload = parse_jwt_without_signature_verification(s.access_token).payload - assert payload["sub"] != "asdf" + assert payload["foo"] == "bar" + assert payload["sub"] == "user1" + + s2 = await create_new_session_without_request_response( + "public", "user2", s.get_access_token_payload() + ) + payload = parse_jwt_without_signature_verification(s2.access_token).payload + assert payload["foo"] == "bar" + assert payload["sub"] == "user2" async def test_validation_logic_with_keys_that_can_use_json_nulls_values_in_claims(): From 6620ea5d0814952fd93d90025d7da6354ddc9d0c Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Tue, 12 Sep 2023 16:06:58 +0530 Subject: [PATCH 13/27] adding dev-v0.16.0 tag to this commit to ensure building --- html/supertokens_python/constants.html | 2 +- .../framework/django/django_request.html | 20 +++ .../framework/fastapi/fastapi_request.html | 20 +++ .../framework/flask/flask_request.html | 20 +++ .../supertokens_python/framework/request.html | 23 +++ .../recipe/dashboard/api/index.html | 19 ++- .../recipe/dashboard/api/validate_key.html | 4 +- .../recipe/dashboard/constants.html | 4 +- .../recipe/dashboard/exceptions.html | 32 ++++ .../recipe/dashboard/index.html | 12 +- .../recipe/dashboard/recipe.html | 106 +++++++----- .../dashboard/recipe_implementation.html | 159 +++++++++++++++--- .../recipe/dashboard/utils.html | 64 +++++-- .../recipe/session/asyncio/index.html | 9 + .../recipe/session/constants.html | 1 + .../recipe/session/recipe_implementation.html | 22 ++- .../session/session_request_functions.html | 9 + html/supertokens_python/utils.html | 20 ++- 18 files changed, 445 insertions(+), 101 deletions(-) diff --git a/html/supertokens_python/constants.html b/html/supertokens_python/constants.html index ddabaf437..433530761 100644 --- a/html/supertokens_python/constants.html +++ b/html/supertokens_python/constants.html @@ -42,7 +42,7 @@

Module supertokens_python.constants

from __future__ import annotations SUPPORTED_CDI_VERSIONS = ["3.0"] -VERSION = "0.15.3" +VERSION = "0.16.0" TELEMETRY = "/telemetry" USER_COUNT = "/users/count" USER_DELETE = "/user/remove" diff --git a/html/supertokens_python/framework/django/django_request.html b/html/supertokens_python/framework/django/django_request.html index a30c8807f..a225bb0a6 100644 --- a/html/supertokens_python/framework/django/django_request.html +++ b/html/supertokens_python/framework/django/django_request.html @@ -57,6 +57,9 @@

Module supertokens_python.framework.django.django_reques super().__init__() self.request = request + def get_original_url(self) -> str: + return self.request.get_raw_uri() + def get_query_param( self, key: str, default: Union[str, None] = None ) -> Union[str, None]: @@ -126,6 +129,9 @@

Classes

super().__init__() self.request = request + def get_original_url(self) -> str: + return self.request.get_raw_uri() + def get_query_param( self, key: str, default: Union[str, None] = None ) -> Union[str, None]: @@ -217,6 +223,19 @@

Methods

return self.request.META.get(key.upper())
+
+def get_original_url(self) ‑> str +
+
+
+
+ +Expand source code + +
def get_original_url(self) -> str:
+    return self.request.get_raw_uri()
+
+
def get_path(self) ‑> str
@@ -348,6 +367,7 @@

form_data
  • get_cookie
  • get_header
  • +
  • get_original_url
  • get_path
  • get_query_param
  • get_query_params
  • diff --git a/html/supertokens_python/framework/fastapi/fastapi_request.html b/html/supertokens_python/framework/fastapi/fastapi_request.html index f82978d6e..efa01ad1a 100644 --- a/html/supertokens_python/framework/fastapi/fastapi_request.html +++ b/html/supertokens_python/framework/fastapi/fastapi_request.html @@ -56,6 +56,9 @@

    Module supertokens_python.framework.fastapi.fastapi_requ super().__init__() self.request = request + def get_original_url(self) -> str: + return self.request.url.components.geturl() + def get_query_param( self, key: str, default: Union[str, None] = None ) -> Union[str, None]: @@ -126,6 +129,9 @@

    Classes

    super().__init__() self.request = request + def get_original_url(self) -> str: + return self.request.url.components.geturl() + def get_query_param( self, key: str, default: Union[str, None] = None ) -> Union[str, None]: @@ -218,6 +224,19 @@

    Methods

    return self.request.headers.get(key, None)
    +
    +def get_original_url(self) ‑> str +
    +
    +
    +
    + +Expand source code + +
    def get_original_url(self) -> str:
    +    return self.request.url.components.geturl()
    +
    +
    def get_path(self) ‑> str
    @@ -350,6 +369,7 @@

    form_data
  • get_cookie
  • get_header
  • +
  • get_original_url
  • get_path
  • get_query_param
  • get_query_params
  • diff --git a/html/supertokens_python/framework/flask/flask_request.html b/html/supertokens_python/framework/flask/flask_request.html index 3d9ee76db..09d256795 100644 --- a/html/supertokens_python/framework/flask/flask_request.html +++ b/html/supertokens_python/framework/flask/flask_request.html @@ -55,6 +55,9 @@

    Module supertokens_python.framework.flask.flask_request< super().__init__() self.request = req + def get_original_url(self) -> str: + return self.request.url + def get_query_param(self, key: str, default: Union[str, None] = None): return self.request.args.get(key, default) @@ -133,6 +136,9 @@

    Classes

    super().__init__() self.request = req + def get_original_url(self) -> str: + return self.request.url + def get_query_param(self, key: str, default: Union[str, None] = None): return self.request.args.get(key, default) @@ -233,6 +239,19 @@

    Methods

    return self.request.headers.get(key) # type: ignore
    +
    +def get_original_url(self) ‑> str +
    +
    +
    +
    + +Expand source code + +
    def get_original_url(self) -> str:
    +    return self.request.url
    +
    +
    def get_path(self) ‑> str
    @@ -371,6 +390,7 @@

    form_data
  • get_cookie
  • get_header
  • +
  • get_original_url
  • get_path
  • get_query_param
  • get_query_params
  • diff --git a/html/supertokens_python/framework/request.html b/html/supertokens_python/framework/request.html index 3c2be4998..594cf723a 100644 --- a/html/supertokens_python/framework/request.html +++ b/html/supertokens_python/framework/request.html @@ -53,6 +53,10 @@

    Module supertokens_python.framework.request

    self.wrapper_used = True self.request = None + @abstractmethod + def get_original_url(self) -> str: + pass + @abstractmethod def get_query_param( self, key: str, default: Union[str, None] = None @@ -127,6 +131,10 @@

    Classes

    self.wrapper_used = True self.request = None + @abstractmethod + def get_original_url(self) -> str: + pass + @abstractmethod def get_query_param( self, key: str, default: Union[str, None] = None @@ -230,6 +238,20 @@

    Methods

    pass +
    +def get_original_url(self) ‑> str +
    +
    +
    +
    + +Expand source code + +
    @abstractmethod
    +def get_original_url(self) -> str:
    +    pass
    +
    +
    def get_path(self) ‑> str
    @@ -372,6 +394,7 @@

    form_data
  • get_cookie
  • get_header
  • +
  • get_original_url
  • get_path
  • get_query_param
  • get_query_params
  • diff --git a/html/supertokens_python/recipe/dashboard/api/index.html b/html/supertokens_python/recipe/dashboard/api/index.html index 2e8bd0733..8fee3206a 100644 --- a/html/supertokens_python/recipe/dashboard/api/index.html +++ b/html/supertokens_python/recipe/dashboard/api/index.html @@ -158,9 +158,20 @@

    Functions

    ], user_context: Dict[str, Any], ) -> Optional[BaseResponse]: - should_allow_access = await api_options.recipe_implementation.should_allow_access( - api_options.request, api_options.config, user_context - ) + should_allow_access = False + + try: + should_allow_access = ( + await api_options.recipe_implementation.should_allow_access( + api_options.request, api_options.config, user_context + ) + ) + except DashboardOperationNotAllowedError as _: + return send_non_200_response_with_message( + "You are not permitted to perform this operation", + 403, + api_options.response, + ) if should_allow_access is False: return send_non_200_response_with_message( @@ -1113,7 +1124,7 @@

    Functions

    user_context: Dict[str, Any], ): - is_valid_key = validate_api_key( + is_valid_key = await validate_api_key( api_options.request, api_options.config, user_context ) diff --git a/html/supertokens_python/recipe/dashboard/api/validate_key.html b/html/supertokens_python/recipe/dashboard/api/validate_key.html index a692d5c91..ffeab15b9 100644 --- a/html/supertokens_python/recipe/dashboard/api/validate_key.html +++ b/html/supertokens_python/recipe/dashboard/api/validate_key.html @@ -63,7 +63,7 @@

    Module supertokens_python.recipe.dashboard.api.validate_ user_context: Dict[str, Any], ): - is_valid_key = validate_api_key( + is_valid_key = await validate_api_key( api_options.request, api_options.config, user_context ) @@ -94,7 +94,7 @@

    Functions

    user_context: Dict[str, Any], ): - is_valid_key = validate_api_key( + is_valid_key = await validate_api_key( api_options.request, api_options.config, user_context ) diff --git a/html/supertokens_python/recipe/dashboard/constants.html b/html/supertokens_python/recipe/dashboard/constants.html index ab05d20d8..d4762020b 100644 --- a/html/supertokens_python/recipe/dashboard/constants.html +++ b/html/supertokens_python/recipe/dashboard/constants.html @@ -36,8 +36,8 @@

    Module supertokens_python.recipe.dashboard.constants diff --git a/html/supertokens_python/recipe/dashboard/exceptions.html b/html/supertokens_python/recipe/dashboard/exceptions.html index 49844b4ae..55cfffe76 100644 --- a/html/supertokens_python/recipe/dashboard/exceptions.html +++ b/html/supertokens_python/recipe/dashboard/exceptions.html @@ -30,6 +30,10 @@

    Module supertokens_python.recipe.dashboard.exceptions

    @@ -42,6 +46,27 @@

    Module supertokens_python.recipe.dashboard.exceptions

    Classes

    +
    +class DashboardOperationNotAllowedError +(*args, **kwargs) +
    +
    +

    Common base class for all non-exit exceptions.

    +
    + +Expand source code + +
    class DashboardOperationNotAllowedError(SuperTokensDashboardError):
    +    pass
    +
    +

    Ancestors

    + +
    class SuperTokensDashboardError (*args, **kwargs) @@ -61,6 +86,10 @@

    Ancestors

  • builtins.Exception
  • builtins.BaseException
  • +

    Subclasses

    +
    @@ -79,6 +108,9 @@

    Index

  • Classes

    diff --git a/html/supertokens_python/recipe/dashboard/index.html b/html/supertokens_python/recipe/dashboard/index.html index 11d46eedd..f5c97daf0 100644 --- a/html/supertokens_python/recipe/dashboard/index.html +++ b/html/supertokens_python/recipe/dashboard/index.html @@ -42,7 +42,7 @@

    Module supertokens_python.recipe.dashboard

    from __future__ import annotations -from typing import Callable, Optional, Union +from typing import Callable, Optional, List from supertokens_python import AppInfo, RecipeModule @@ -54,11 +54,13 @@

    Module supertokens_python.recipe.dashboard

    def init( - api_key: Union[str, None] = None, + api_key: Optional[str] = None, + admins: Optional[List[str]] = None, override: Optional[InputOverrideConfig] = None, ) -> Callable[[AppInfo], RecipeModule]: return DashboardRecipe.init( api_key, + admins, override, )
    @@ -102,7 +104,7 @@

    Sub-modules

    Functions

    -def init(api_key: Union[str, None] = None, override: Optional[InputOverrideConfig] = None) ‑> Callable[[AppInfo], RecipeModule] +def init(api_key: Optional[str] = None, admins: Optional[List[str]] = None, override: Optional[InputOverrideConfig] = None) ‑> Callable[[AppInfo], RecipeModule]
    @@ -111,11 +113,13 @@

    Functions

    Expand source code
    def init(
    -    api_key: Union[str, None] = None,
    +    api_key: Optional[str] = None,
    +    admins: Optional[List[str]] = None,
         override: Optional[InputOverrideConfig] = None,
     ) -> Callable[[AppInfo], RecipeModule]:
         return DashboardRecipe.init(
             api_key,
    +        admins,
             override,
         )
    diff --git a/html/supertokens_python/recipe/dashboard/recipe.html b/html/supertokens_python/recipe/dashboard/recipe.html index 7350f4f2a..a69beb97c 100644 --- a/html/supertokens_python/recipe/dashboard/recipe.html +++ b/html/supertokens_python/recipe/dashboard/recipe.html @@ -42,7 +42,7 @@

    Module supertokens_python.recipe.dashboard.recipe from __future__ import annotations from os import environ -from typing import TYPE_CHECKING, Awaitable, Callable, List, Optional, Union, Dict, Any +from typing import TYPE_CHECKING, Awaitable, Callable, List, Optional, Dict, Any from supertokens_python.normalised_url_path import NormalisedURLPath from supertokens_python.recipe_module import APIHandled, RecipeModule @@ -87,8 +87,8 @@

    Module supertokens_python.recipe.dashboard.recipe from .constants import ( DASHBOARD_ANALYTICS_API, DASHBOARD_API, - EMAIL_PASSSWORD_SIGNOUT, - EMAIL_PASSWORD_SIGN_IN, + SIGN_OUT_API, + SIGN_IN_API, SEARCH_TAGS_API, USER_API, USER_EMAIL_VERIFY_API, @@ -115,12 +115,14 @@

    Module supertokens_python.recipe.dashboard.recipe self, recipe_id: str, app_info: AppInfo, - api_key: Union[str, None], - override: Union[InputOverrideConfig, None] = None, + api_key: Optional[str], + admins: Optional[List[str]], + override: Optional[InputOverrideConfig] = None, ): super().__init__(recipe_id, app_info) self.config = validate_and_normalise_user_input( api_key, + admins, override, ) recipe_implementation = RecipeImplementation() @@ -151,11 +153,9 @@

    Module supertokens_python.recipe.dashboard.recipe False, ), APIHandled( - NormalisedURLPath( - get_api_path_with_dashboard_base(EMAIL_PASSWORD_SIGN_IN) - ), + NormalisedURLPath(get_api_path_with_dashboard_base(SIGN_IN_API)), "post", - EMAIL_PASSWORD_SIGN_IN, + SIGN_IN_API, False, ), APIHandled( @@ -165,11 +165,9 @@

    Module supertokens_python.recipe.dashboard.recipe False, ), APIHandled( - NormalisedURLPath( - get_api_path_with_dashboard_base(EMAIL_PASSSWORD_SIGNOUT) - ), + NormalisedURLPath(get_api_path_with_dashboard_base(SIGN_OUT_API)), "post", - EMAIL_PASSSWORD_SIGNOUT, + SIGN_OUT_API, False, ), APIHandled( @@ -242,6 +240,12 @@

    Module supertokens_python.recipe.dashboard.recipe USER_SESSION_API, False, ), + APIHandled( + NormalisedURLPath(get_api_path_with_dashboard_base(USER_SESSION_API)), + "post", + USER_SESSION_API, + False, + ), APIHandled( NormalisedURLPath(get_api_path_with_dashboard_base(USER_PASSWORD_API)), "put", @@ -305,7 +309,7 @@

    Module supertokens_python.recipe.dashboard.recipe return await handle_validate_key_api( self.api_implementation, api_options, user_context ) - if request_id == EMAIL_PASSWORD_SIGN_IN: + if request_id == SIGN_IN_API: return await handle_emailpassword_signin_api( self.api_implementation, api_options, user_context ) @@ -346,7 +350,7 @@

    Module supertokens_python.recipe.dashboard.recipe api_function = handle_user_password_put elif request_id == USER_EMAIL_VERIFY_TOKEN_API: api_function = handle_email_verify_token_post - elif request_id == EMAIL_PASSSWORD_SIGNOUT: + elif request_id == SIGN_OUT_API: api_function = handle_emailpassword_signout_api elif request_id == SEARCH_TAGS_API: api_function = handle_get_tags @@ -377,8 +381,9 @@

    Module supertokens_python.recipe.dashboard.recipe @staticmethod def init( - api_key: Union[str, None], - override: Union[InputOverrideConfig, None] = None, + api_key: Optional[str], + admins: Optional[List[str]] = None, + override: Optional[InputOverrideConfig] = None, ): def func(app_info: AppInfo): if DashboardRecipe.__instance is None: @@ -386,6 +391,7 @@

    Module supertokens_python.recipe.dashboard.recipe DashboardRecipe.recipe_id, app_info, api_key, + admins, override, ) return DashboardRecipe.__instance @@ -424,7 +430,7 @@

    Classes

    class DashboardRecipe -(recipe_id: str, app_info: AppInfo, api_key: Union[str, None], override: Union[InputOverrideConfig, None] = None) +(recipe_id: str, app_info: AppInfo, api_key: Optional[str], admins: Optional[List[str]], override: Optional[InputOverrideConfig] = None)

    Helper class that provides a standard way to create an ABC using @@ -441,12 +447,14 @@

    Classes

    self, recipe_id: str, app_info: AppInfo, - api_key: Union[str, None], - override: Union[InputOverrideConfig, None] = None, + api_key: Optional[str], + admins: Optional[List[str]], + override: Optional[InputOverrideConfig] = None, ): super().__init__(recipe_id, app_info) self.config = validate_and_normalise_user_input( api_key, + admins, override, ) recipe_implementation = RecipeImplementation() @@ -477,11 +485,9 @@

    Classes

    False, ), APIHandled( - NormalisedURLPath( - get_api_path_with_dashboard_base(EMAIL_PASSWORD_SIGN_IN) - ), + NormalisedURLPath(get_api_path_with_dashboard_base(SIGN_IN_API)), "post", - EMAIL_PASSWORD_SIGN_IN, + SIGN_IN_API, False, ), APIHandled( @@ -491,11 +497,9 @@

    Classes

    False, ), APIHandled( - NormalisedURLPath( - get_api_path_with_dashboard_base(EMAIL_PASSSWORD_SIGNOUT) - ), + NormalisedURLPath(get_api_path_with_dashboard_base(SIGN_OUT_API)), "post", - EMAIL_PASSSWORD_SIGNOUT, + SIGN_OUT_API, False, ), APIHandled( @@ -568,6 +572,12 @@

    Classes

    USER_SESSION_API, False, ), + APIHandled( + NormalisedURLPath(get_api_path_with_dashboard_base(USER_SESSION_API)), + "post", + USER_SESSION_API, + False, + ), APIHandled( NormalisedURLPath(get_api_path_with_dashboard_base(USER_PASSWORD_API)), "put", @@ -631,7 +641,7 @@

    Classes

    return await handle_validate_key_api( self.api_implementation, api_options, user_context ) - if request_id == EMAIL_PASSWORD_SIGN_IN: + if request_id == SIGN_IN_API: return await handle_emailpassword_signin_api( self.api_implementation, api_options, user_context ) @@ -672,7 +682,7 @@

    Classes

    api_function = handle_user_password_put elif request_id == USER_EMAIL_VERIFY_TOKEN_API: api_function = handle_email_verify_token_post - elif request_id == EMAIL_PASSSWORD_SIGNOUT: + elif request_id == SIGN_OUT_API: api_function = handle_emailpassword_signout_api elif request_id == SEARCH_TAGS_API: api_function = handle_get_tags @@ -703,8 +713,9 @@

    Classes

    @staticmethod def init( - api_key: Union[str, None], - override: Union[InputOverrideConfig, None] = None, + api_key: Optional[str], + admins: Optional[List[str]] = None, + override: Optional[InputOverrideConfig] = None, ): def func(app_info: AppInfo): if DashboardRecipe.__instance is None: @@ -712,6 +723,7 @@

    Classes

    DashboardRecipe.recipe_id, app_info, api_key, + admins, override, ) return DashboardRecipe.__instance @@ -775,7 +787,7 @@

    Static methods

    -def init(api_key: Union[str, None], override: Union[InputOverrideConfig, None] = None) +def init(api_key: Optional[str], admins: Optional[List[str]] = None, override: Optional[InputOverrideConfig] = None)
    @@ -785,8 +797,9 @@

    Static methods

    @staticmethod
     def init(
    -    api_key: Union[str, None],
    -    override: Union[InputOverrideConfig, None] = None,
    +    api_key: Optional[str],
    +    admins: Optional[List[str]] = None,
    +    override: Optional[InputOverrideConfig] = None,
     ):
         def func(app_info: AppInfo):
             if DashboardRecipe.__instance is None:
    @@ -794,6 +807,7 @@ 

    Static methods

    DashboardRecipe.recipe_id, app_info, api_key, + admins, override, ) return DashboardRecipe.__instance @@ -857,11 +871,9 @@

    Methods

    False, ), APIHandled( - NormalisedURLPath( - get_api_path_with_dashboard_base(EMAIL_PASSWORD_SIGN_IN) - ), + NormalisedURLPath(get_api_path_with_dashboard_base(SIGN_IN_API)), "post", - EMAIL_PASSWORD_SIGN_IN, + SIGN_IN_API, False, ), APIHandled( @@ -871,11 +883,9 @@

    Methods

    False, ), APIHandled( - NormalisedURLPath( - get_api_path_with_dashboard_base(EMAIL_PASSSWORD_SIGNOUT) - ), + NormalisedURLPath(get_api_path_with_dashboard_base(SIGN_OUT_API)), "post", - EMAIL_PASSSWORD_SIGNOUT, + SIGN_OUT_API, False, ), APIHandled( @@ -948,6 +958,12 @@

    Methods

    USER_SESSION_API, False, ), + APIHandled( + NormalisedURLPath(get_api_path_with_dashboard_base(USER_SESSION_API)), + "post", + USER_SESSION_API, + False, + ), APIHandled( NormalisedURLPath(get_api_path_with_dashboard_base(USER_PASSWORD_API)), "put", @@ -1021,7 +1037,7 @@

    Methods

    return await handle_validate_key_api( self.api_implementation, api_options, user_context ) - if request_id == EMAIL_PASSWORD_SIGN_IN: + if request_id == SIGN_IN_API: return await handle_emailpassword_signin_api( self.api_implementation, api_options, user_context ) @@ -1062,7 +1078,7 @@

    Methods

    api_function = handle_user_password_put elif request_id == USER_EMAIL_VERIFY_TOKEN_API: api_function = handle_email_verify_token_post - elif request_id == EMAIL_PASSSWORD_SIGNOUT: + elif request_id == SIGN_OUT_API: api_function = handle_emailpassword_signout_api elif request_id == SEARCH_TAGS_API: api_function = handle_get_tags diff --git a/html/supertokens_python/recipe/dashboard/recipe_implementation.html b/html/supertokens_python/recipe/dashboard/recipe_implementation.html index 17557a817..6da507de2 100644 --- a/html/supertokens_python/recipe/dashboard/recipe_implementation.html +++ b/html/supertokens_python/recipe/dashboard/recipe_implementation.html @@ -46,10 +46,16 @@

    Module supertokens_python.recipe.dashboard.recipe_implem from supertokens_python.constants import DASHBOARD_VERSION from supertokens_python.framework import BaseRequest from supertokens_python.normalised_url_path import NormalisedURLPath +from supertokens_python.utils import log_debug_message, normalise_http_method from supertokens_python.querier import Querier +from supertokens_python.recipe.dashboard.constants import ( + DASHBOARD_ANALYTICS_API, + SIGN_OUT_API, +) from .interfaces import RecipeInterface from .utils import DashboardConfig, validate_api_key +from .exceptions import DashboardOperationNotAllowedError class RecipeImplementation(RecipeInterface): @@ -62,9 +68,9 @@

    Module supertokens_python.recipe.dashboard.recipe_implem config: DashboardConfig, user_context: Dict[str, Any], ) -> bool: - if config.auth_mode == "email-password": + # For cases where we're not using the API key, the JWT is being used; we allow their access by default + if config.api_key is None: auth_header_value = request.get_header("authorization") - if not auth_header_value: return False @@ -75,11 +81,48 @@

    Module supertokens_python.recipe.dashboard.recipe_implem {"sessionId": auth_header_value}, ) ) - return ( - "status" in session_verification_response - and session_verification_response["status"] == "OK" - ) - return validate_api_key(request, config, user_context)

    + if session_verification_response.get("status") != "OK": + return False + + # For all non GET requests we also want to check if the + # user is allowed to perform this operation + if normalise_http_method(request.method()) != "get": + # We dont want to block the analytics API + if request.get_original_url().endswith(DASHBOARD_ANALYTICS_API): + return True + + # We do not want to block the sign out request + if request.get_original_url().endswith(SIGN_OUT_API): + return True + + admins = config.admins + + if admins is None: + return True + + if len(admins) == 0: + log_debug_message( + "User Dashboard: Throwing OPERATION_NOT_ALLOWED because user is not an admin" + ) + raise DashboardOperationNotAllowedError() + + user_email = session_verification_response.get("email") + + if user_email is None or not isinstance(user_email, str): + log_debug_message( + "User Dashboard: Returning UNAUTHORISED_ERROR because no email was provided in headers" + ) + return False + + if user_email not in admins: + log_debug_message( + "User Dashboard: Throwing OPERATION_NOT_ALLOWED because user is not an admin" + ) + raise DashboardOperationNotAllowedError() + + return True + + return await validate_api_key(request, config, user_context)
    @@ -111,9 +154,9 @@

    Classes

    config: DashboardConfig, user_context: Dict[str, Any], ) -> bool: - if config.auth_mode == "email-password": + # For cases where we're not using the API key, the JWT is being used; we allow their access by default + if config.api_key is None: auth_header_value = request.get_header("authorization") - if not auth_header_value: return False @@ -124,11 +167,48 @@

    Classes

    {"sessionId": auth_header_value}, ) ) - return ( - "status" in session_verification_response - and session_verification_response["status"] == "OK" - ) - return validate_api_key(request, config, user_context)
    + if session_verification_response.get("status") != "OK": + return False + + # For all non GET requests we also want to check if the + # user is allowed to perform this operation + if normalise_http_method(request.method()) != "get": + # We dont want to block the analytics API + if request.get_original_url().endswith(DASHBOARD_ANALYTICS_API): + return True + + # We do not want to block the sign out request + if request.get_original_url().endswith(SIGN_OUT_API): + return True + + admins = config.admins + + if admins is None: + return True + + if len(admins) == 0: + log_debug_message( + "User Dashboard: Throwing OPERATION_NOT_ALLOWED because user is not an admin" + ) + raise DashboardOperationNotAllowedError() + + user_email = session_verification_response.get("email") + + if user_email is None or not isinstance(user_email, str): + log_debug_message( + "User Dashboard: Returning UNAUTHORISED_ERROR because no email was provided in headers" + ) + return False + + if user_email not in admins: + log_debug_message( + "User Dashboard: Throwing OPERATION_NOT_ALLOWED because user is not an admin" + ) + raise DashboardOperationNotAllowedError() + + return True + + return await validate_api_key(request, config, user_context)

    Ancestors

      @@ -165,9 +245,9 @@

      Methods

      config: DashboardConfig, user_context: Dict[str, Any], ) -> bool: - if config.auth_mode == "email-password": + # For cases where we're not using the API key, the JWT is being used; we allow their access by default + if config.api_key is None: auth_header_value = request.get_header("authorization") - if not auth_header_value: return False @@ -178,11 +258,48 @@

      Methods

      {"sessionId": auth_header_value}, ) ) - return ( - "status" in session_verification_response - and session_verification_response["status"] == "OK" - ) - return validate_api_key(request, config, user_context) + if session_verification_response.get("status") != "OK": + return False + + # For all non GET requests we also want to check if the + # user is allowed to perform this operation + if normalise_http_method(request.method()) != "get": + # We dont want to block the analytics API + if request.get_original_url().endswith(DASHBOARD_ANALYTICS_API): + return True + + # We do not want to block the sign out request + if request.get_original_url().endswith(SIGN_OUT_API): + return True + + admins = config.admins + + if admins is None: + return True + + if len(admins) == 0: + log_debug_message( + "User Dashboard: Throwing OPERATION_NOT_ALLOWED because user is not an admin" + ) + raise DashboardOperationNotAllowedError() + + user_email = session_verification_response.get("email") + + if user_email is None or not isinstance(user_email, str): + log_debug_message( + "User Dashboard: Returning UNAUTHORISED_ERROR because no email was provided in headers" + ) + return False + + if user_email not in admins: + log_debug_message( + "User Dashboard: Throwing OPERATION_NOT_ALLOWED because user is not an admin" + ) + raise DashboardOperationNotAllowedError() + + return True + + return await validate_api_key(request, config, user_context)
    diff --git a/html/supertokens_python/recipe/dashboard/utils.html b/html/supertokens_python/recipe/dashboard/utils.html index 5892ff59c..fe882e84f 100644 --- a/html/supertokens_python/recipe/dashboard/utils.html +++ b/html/supertokens_python/recipe/dashboard/utils.html @@ -71,14 +71,14 @@

    Module supertokens_python.recipe.dashboard.utils< get_user_by_id as tppless_get_user_by_id, ) from supertokens_python.types import User -from supertokens_python.utils import Awaitable +from supertokens_python.utils import Awaitable, log_debug_message, normalise_email from ...normalised_url_path import NormalisedURLPath from .constants import ( DASHBOARD_ANALYTICS_API, DASHBOARD_API, - EMAIL_PASSSWORD_SIGNOUT, - EMAIL_PASSWORD_SIGN_IN, + SIGN_OUT_API, + SIGN_IN_API, SEARCH_TAGS_API, USER_API, USER_EMAIL_VERIFY_API, @@ -209,9 +209,14 @@

    Module supertokens_python.recipe.dashboard.utils< class DashboardConfig: def __init__( - self, api_key: Union[str, None], override: OverrideConfig, auth_mode: str + self, + api_key: Optional[str], + admins: Optional[List[str]], + override: OverrideConfig, + auth_mode: str, ): self.api_key = api_key + self.admins = admins self.override = override self.auth_mode = auth_mode @@ -219,14 +224,23 @@

    Module supertokens_python.recipe.dashboard.utils< def validate_and_normalise_user_input( # app_info: AppInfo, api_key: Union[str, None], + admins: Optional[List[str]], override: Optional[InputOverrideConfig] = None, ) -> DashboardConfig: if override is None: override = InputOverrideConfig() + if api_key is not None and admins is not None: + log_debug_message( + "User Dashboard: Providing 'admins' has no effect when using an api key." + ) + + admins = [normalise_email(a) for a in admins] if admins is not None else None + return DashboardConfig( api_key, + admins, OverrideConfig( functions=override.functions, apis=override.apis, @@ -273,10 +287,10 @@

    Module supertokens_python.recipe.dashboard.utils< return USER_PASSWORD_API if path_str.endswith(USER_EMAIL_VERIFY_TOKEN_API) and method == "post": return USER_EMAIL_VERIFY_TOKEN_API - if path_str.endswith(EMAIL_PASSWORD_SIGN_IN) and method == "post": - return EMAIL_PASSWORD_SIGN_IN - if path_str.endswith(EMAIL_PASSSWORD_SIGNOUT) and method == "post": - return EMAIL_PASSSWORD_SIGNOUT + if path_str.endswith(SIGN_IN_API) and method == "post": + return SIGN_IN_API + if path_str.endswith(SIGN_OUT_API) and method == "post": + return SIGN_OUT_API if path_str.endswith(SEARCH_TAGS_API) and method == "get": return SEARCH_TAGS_API if path_str.endswith(DASHBOARD_ANALYTICS_API) and method == "post": @@ -438,7 +452,7 @@

    Module supertokens_python.recipe.dashboard.utils< return isRecipeInitialised -def validate_api_key( +async def validate_api_key( req: BaseRequest, config: DashboardConfig, _user_context: Dict[str, Any] ) -> bool: api_key_header_value = req.get_header("authorization") @@ -490,10 +504,10 @@

    Functions

    return USER_PASSWORD_API if path_str.endswith(USER_EMAIL_VERIFY_TOKEN_API) and method == "post": return USER_EMAIL_VERIFY_TOKEN_API - if path_str.endswith(EMAIL_PASSWORD_SIGN_IN) and method == "post": - return EMAIL_PASSWORD_SIGN_IN - if path_str.endswith(EMAIL_PASSSWORD_SIGNOUT) and method == "post": - return EMAIL_PASSSWORD_SIGNOUT + if path_str.endswith(SIGN_IN_API) and method == "post": + return SIGN_IN_API + if path_str.endswith(SIGN_OUT_API) and method == "post": + return SIGN_OUT_API if path_str.endswith(SEARCH_TAGS_API) and method == "get": return SEARCH_TAGS_API if path_str.endswith(DASHBOARD_ANALYTICS_API) and method == "post": @@ -695,7 +709,7 @@

    Functions

    -def validate_and_normalise_user_input(api_key: Union[str, None], override: Optional[InputOverrideConfig] = None) ‑> DashboardConfig +def validate_and_normalise_user_input(api_key: Union[str, None], admins: Optional[List[str]], override: Optional[InputOverrideConfig] = None) ‑> DashboardConfig
    @@ -706,14 +720,23 @@

    Functions

    def validate_and_normalise_user_input(
         # app_info: AppInfo,
         api_key: Union[str, None],
    +    admins: Optional[List[str]],
         override: Optional[InputOverrideConfig] = None,
     ) -> DashboardConfig:
     
         if override is None:
             override = InputOverrideConfig()
     
    +    if api_key is not None and admins is not None:
    +        log_debug_message(
    +            "User Dashboard: Providing 'admins' has no effect when using an api key."
    +        )
    +
    +    admins = [normalise_email(a) for a in admins] if admins is not None else None
    +
         return DashboardConfig(
             api_key,
    +        admins,
             OverrideConfig(
                 functions=override.functions,
                 apis=override.apis,
    @@ -723,7 +746,7 @@ 

    Functions

    -def validate_api_key(req: BaseRequest, config: DashboardConfig, _user_context: Dict[str, Any]) ‑> bool +async def validate_api_key(req: BaseRequest, config: DashboardConfig, _user_context: Dict[str, Any]) ‑> bool
    @@ -731,7 +754,7 @@

    Functions

    Expand source code -
    def validate_api_key(
    +
    async def validate_api_key(
         req: BaseRequest, config: DashboardConfig, _user_context: Dict[str, Any]
     ) -> bool:
         api_key_header_value = req.get_header("authorization")
    @@ -749,7 +772,7 @@ 

    Classes

    class DashboardConfig -(api_key: Union[str, None], override: OverrideConfig, auth_mode: str) +(api_key: Optional[str], admins: Optional[List[str]], override: OverrideConfig, auth_mode: str)
    @@ -759,9 +782,14 @@

    Classes

    class DashboardConfig:
         def __init__(
    -        self, api_key: Union[str, None], override: OverrideConfig, auth_mode: str
    +        self,
    +        api_key: Optional[str],
    +        admins: Optional[List[str]],
    +        override: OverrideConfig,
    +        auth_mode: str,
         ):
             self.api_key = api_key
    +        self.admins = admins
             self.override = override
             self.auth_mode = auth_mode
    diff --git a/html/supertokens_python/recipe/session/asyncio/index.html b/html/supertokens_python/recipe/session/asyncio/index.html index 4bd8ea7f5..4b08c49d3 100644 --- a/html/supertokens_python/recipe/session/asyncio/index.html +++ b/html/supertokens_python/recipe/session/asyncio/index.html @@ -69,6 +69,7 @@

    Module supertokens_python.recipe.session.asyncio< get_session_from_request, refresh_session_in_request, ) +from ..constants import protected_props from ..utils import get_required_claim_validators from supertokens_python.recipe.multitenancy.constants import DEFAULT_TENANT_ID @@ -134,6 +135,10 @@

    Module supertokens_python.recipe.session.asyncio< final_access_token_payload = {**access_token_payload, "iss": issuer} + for prop in protected_props: + if prop in final_access_token_payload: + del final_access_token_payload[prop] + for claim in claims_added_by_other_recipes: update = await claim.build(user_id, tenant_id, user_context) final_access_token_payload = {**final_access_token_payload, **update} @@ -668,6 +673,10 @@

    Functions

    final_access_token_payload = {**access_token_payload, "iss": issuer} + for prop in protected_props: + if prop in final_access_token_payload: + del final_access_token_payload[prop] + for claim in claims_added_by_other_recipes: update = await claim.build(user_id, tenant_id, user_context) final_access_token_payload = {**final_access_token_payload, **update} diff --git a/html/supertokens_python/recipe/session/constants.html b/html/supertokens_python/recipe/session/constants.html index cf4eb291e..1c7197924 100644 --- a/html/supertokens_python/recipe/session/constants.html +++ b/html/supertokens_python/recipe/session/constants.html @@ -70,6 +70,7 @@

    Module supertokens_python.recipe.session.constants

    diff --git a/html/supertokens_python/recipe/session/recipe_implementation.html b/html/supertokens_python/recipe/session/recipe_implementation.html index 7b84df195..59376da7c 100644 --- a/html/supertokens_python/recipe/session/recipe_implementation.html +++ b/html/supertokens_python/recipe/session/recipe_implementation.html @@ -75,6 +75,7 @@

    Module supertokens_python.recipe.session.recipe_implemen from supertokens_python import AppInfo from .interfaces import SessionContainer +from .constants import protected_props from supertokens_python.querier import Querier from supertokens_python.recipe.multitenancy.constants import DEFAULT_TENANT_ID @@ -406,8 +407,13 @@

    Module supertokens_python.recipe.session.recipe_implemen if session_info is None: return False + new_access_token_payload = session_info.custom_claims_in_access_token_payload + for k in protected_props: + if k in new_access_token_payload: + del new_access_token_payload[k] + new_access_token_payload = { - **session_info.custom_claims_in_access_token_payload, + **new_access_token_payload, **access_token_payload_update, } for k in access_token_payload_update.keys(): @@ -860,8 +866,13 @@

    Classes

    if session_info is None: return False + new_access_token_payload = session_info.custom_claims_in_access_token_payload + for k in protected_props: + if k in new_access_token_payload: + del new_access_token_payload[k] + new_access_token_payload = { - **session_info.custom_claims_in_access_token_payload, + **new_access_token_payload, **access_token_payload_update, } for k in access_token_payload_update.keys(): @@ -1270,8 +1281,13 @@

    Methods

    if session_info is None: return False + new_access_token_payload = session_info.custom_claims_in_access_token_payload + for k in protected_props: + if k in new_access_token_payload: + del new_access_token_payload[k] + new_access_token_payload = { - **session_info.custom_claims_in_access_token_payload, + **new_access_token_payload, **access_token_payload_update, } for k in access_token_payload_update.keys(): diff --git a/html/supertokens_python/recipe/session/session_request_functions.html b/html/supertokens_python/recipe/session/session_request_functions.html index 50c0f6a92..f7f79339b 100644 --- a/html/supertokens_python/recipe/session/session_request_functions.html +++ b/html/supertokens_python/recipe/session/session_request_functions.html @@ -88,6 +88,7 @@

    Module supertokens_python.recipe.session.session_request set_request_in_user_context_if_not_defined, ) from supertokens_python.supertokens import Supertokens +from .constants import protected_props if TYPE_CHECKING: from supertokens_python.recipe.session.recipe import SessionRecipe @@ -268,6 +269,10 @@

    Module supertokens_python.recipe.session.session_request final_access_token_payload = {**access_token_payload, "iss": issuer} + for prop in protected_props: + if prop in final_access_token_payload: + del final_access_token_payload[prop] + for claim in claims_added_by_other_recipes: update = await claim.build(user_id, tenant_id, user_context) final_access_token_payload = {**final_access_token_payload, **update} @@ -538,6 +543,10 @@

    Functions

    final_access_token_payload = {**access_token_payload, "iss": issuer} + for prop in protected_props: + if prop in final_access_token_payload: + del final_access_token_payload[prop] + for claim in claims_added_by_other_recipes: update = await claim.build(user_id, tenant_id, user_context) final_access_token_payload = {**final_access_token_payload, **update} diff --git a/html/supertokens_python/utils.html b/html/supertokens_python/utils.html index c6ca29c90..9664de780 100644 --- a/html/supertokens_python/utils.html +++ b/html/supertokens_python/utils.html @@ -387,7 +387,11 @@

    Module supertokens_python.utils

    self.mutex.unlock() if exc_type is not None: - raise exc_type(exc_value).with_traceback(traceback)

    + raise exc_type(exc_value).with_traceback(traceback) + + +def normalise_email(email: str) -> str: + return email.strip().lower()
    @@ -702,6 +706,19 @@

    Functions

    return _get_max_version(version, minimum_version) == version
    +
    +def normalise_email(email: str) ‑> str +
    +
    +
    +
    + +Expand source code + +
    def normalise_email(email: str) -> str:
    +    return email.strip().lower()
    +
    +
    def normalise_http_method(method: str) ‑> str
    @@ -1036,6 +1053,7 @@

    Index

  • is_5xx_error
  • is_an_ip_address
  • is_version_gte
  • +
  • normalise_email
  • normalise_http_method
  • resolve
  • send_200_response
  • From 42b8dc497ea62a35154329642cb5ec497cd5946f Mon Sep 17 00:00:00 2001 From: KShivendu Date: Tue, 12 Sep 2023 17:02:06 +0530 Subject: [PATCH 14/27] fix: Failing unit tests --- supertokens_python/recipe/session/session_class.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/supertokens_python/recipe/session/session_class.py b/supertokens_python/recipe/session/session_class.py index e904f14c4..36dfd644e 100644 --- a/supertokens_python/recipe/session/session_class.py +++ b/supertokens_python/recipe/session/session_class.py @@ -207,7 +207,7 @@ async def assert_claims( for k in protected_props: try: del validate_claim_res.access_token_payload_update[k] - except ValueError: + except KeyError: pass await self.merge_into_access_token_payload( validate_claim_res.access_token_payload_update, user_context From 5d6a96e14a5ffd1b1c43cafe9382447937c1b889 Mon Sep 17 00:00:00 2001 From: KShivendu Date: Tue, 12 Sep 2023 17:29:33 +0530 Subject: [PATCH 15/27] test: Add test --- tests/sessions/claims/test_assert_claims.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/tests/sessions/claims/test_assert_claims.py b/tests/sessions/claims/test_assert_claims.py index 71d955a6e..992ae89c4 100644 --- a/tests/sessions/claims/test_assert_claims.py +++ b/tests/sessions/claims/test_assert_claims.py @@ -16,7 +16,9 @@ ) from supertokens_python.recipe.session.session_class import Session from supertokens_python import init -from tests.utils import setup_function, teardown_function, start_st, st_init_common_args +from tests.utils import get_st_init_args, setup_function, teardown_function, start_st, st_init_common_args +from supertokens_python.recipe.session.asyncio import create_new_session_without_request_response +from .utils import TrueClaim _ = setup_function # type:ignore _ = teardown_function # type:ignore @@ -125,3 +127,12 @@ def should_refetch(self, payload: JSONObject, user_context: Dict[str, Any]): assert dummy_claim_validator.validate_calls == {json.dumps(payload): 1} mock.assert_not_called() + + +async def test_assert_claims_should_work(): + init(**get_st_init_args([session.init()])) + start_st() + + validator = TrueClaim.validators.is_true(1) + s = await create_new_session_without_request_response("public", "userid", {}) + await s.assert_claims([validator]) From ba6705a9e724d98028dd564c937ec351ce49cb4b Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Tue, 12 Sep 2023 17:32:02 +0530 Subject: [PATCH 16/27] adding dev-v0.16.0 tag to this commit to ensure building From f1fbdf41bf203e20bbbfe43845e1a59a62733c31 Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Tue, 12 Sep 2023 17:36:46 +0530 Subject: [PATCH 17/27] fixes format --- tests/sessions/claims/test_assert_claims.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/tests/sessions/claims/test_assert_claims.py b/tests/sessions/claims/test_assert_claims.py index 992ae89c4..f1f303884 100644 --- a/tests/sessions/claims/test_assert_claims.py +++ b/tests/sessions/claims/test_assert_claims.py @@ -16,8 +16,16 @@ ) from supertokens_python.recipe.session.session_class import Session from supertokens_python import init -from tests.utils import get_st_init_args, setup_function, teardown_function, start_st, st_init_common_args -from supertokens_python.recipe.session.asyncio import create_new_session_without_request_response +from tests.utils import ( + get_st_init_args, + setup_function, + teardown_function, + start_st, + st_init_common_args, +) +from supertokens_python.recipe.session.asyncio import ( + create_new_session_without_request_response, +) from .utils import TrueClaim _ = setup_function # type:ignore From 34887f8abe0303b4cb3d526254267b5b4c475849 Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Tue, 12 Sep 2023 17:37:54 +0530 Subject: [PATCH 18/27] adding dev-v0.16.0 tag to this commit to ensure building --- html/supertokens_python/recipe/session/session_class.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/html/supertokens_python/recipe/session/session_class.html b/html/supertokens_python/recipe/session/session_class.html index da1bba429..653615bf2 100644 --- a/html/supertokens_python/recipe/session/session_class.html +++ b/html/supertokens_python/recipe/session/session_class.html @@ -235,7 +235,7 @@

    Module supertokens_python.recipe.session.session_classClasses

    for k in protected_props: try: del validate_claim_res.access_token_payload_update[k] - except ValueError: + except KeyError: pass await self.merge_into_access_token_payload( validate_claim_res.access_token_payload_update, user_context @@ -697,7 +697,7 @@

    Methods

    for k in protected_props: try: del validate_claim_res.access_token_payload_update[k] - except ValueError: + except KeyError: pass await self.merge_into_access_token_payload( validate_claim_res.access_token_payload_update, user_context From d6944ce93bb684e8d2f2854cfb1efe10c13f0559 Mon Sep 17 00:00:00 2001 From: KShivendu Date: Tue, 19 Sep 2023 13:08:52 +0530 Subject: [PATCH 19/27] fix: AWS Public URLs tld extract --- CHANGELOG.md | 3 ++ setup.py | 2 +- supertokens_python/constants.py | 2 +- supertokens_python/utils.py | 5 +++ tests/test_config.py | 64 +++++++++++++++++++++++++++++++++ tests/test_utils.py | 28 ++++++++++++++- 6 files changed, 101 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e4aa70f2..13fe645fd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [unreleased] +## [0.16.1] - 2023-09-19 +- Handle AWS Public URLs (ending with `.amazonaws.com`) separately while extracting TLDs for SameSite attribute. + ## [0.16.0] - 2023-09-13 diff --git a/setup.py b/setup.py index ca2400964..2b13aff2e 100644 --- a/setup.py +++ b/setup.py @@ -70,7 +70,7 @@ setup( name="supertokens_python", - version="0.16.0", + version="0.16.1", author="SuperTokens", license="Apache 2.0", author_email="team@supertokens.com", diff --git a/supertokens_python/constants.py b/supertokens_python/constants.py index 075ff9309..292277329 100644 --- a/supertokens_python/constants.py +++ b/supertokens_python/constants.py @@ -14,7 +14,7 @@ from __future__ import annotations SUPPORTED_CDI_VERSIONS = ["3.0"] -VERSION = "0.16.0" +VERSION = "0.16.1" TELEMETRY = "/telemetry" USER_COUNT = "/users/count" USER_DELETE = "/user/remove" diff --git a/supertokens_python/utils.py b/supertokens_python/utils.py index a79d182c1..1b8afd85b 100644 --- a/supertokens_python/utils.py +++ b/supertokens_python/utils.py @@ -299,8 +299,13 @@ def get_top_level_domain_for_same_site_resolution(url: str) -> str: if hostname.startswith("localhost") or is_an_ip_address(hostname): return "localhost" + parsed_url: Any = extract(hostname, include_psl_private_domains=True) if parsed_url.domain == "": # type: ignore + # We need to do this because of https://github.com/supertokens/supertokens-python/issues/394 + if hostname.endswith(".amazonaws.com") and parsed_url.suffix == hostname: + return hostname + raise Exception( "Please make sure that the apiDomain and websiteDomain have correct values" ) diff --git a/tests/test_config.py b/tests/test_config.py index 521827f99..2df57fe59 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -736,3 +736,67 @@ async def test_samesite_invalid_config(): ) else: assert False, "Exception not raised" + + +@mark.asyncio +async def test_cookie_samesite_with_ec2_public_url(): + start_st() + init( + supertokens_config=SupertokensConfig("http://localhost:3567"), + app_info=InputAppInfo( + app_name="SuperTokens Demo", + api_domain="https://ec2-xx-yyy-zzz-0.compute-1.amazonaws.com:3001", + website_domain="https://blog.supertokens.com", + api_base_path="/", + ), + framework="fastapi", + recipe_list=[ + session.init(get_token_transfer_method=lambda _, __, ___: "cookie") + ], + ) + + # domain name isn't provided so browser decides to use the same host + # which will be ec2-xx-yyy-zzz-0.compute-1.amazonaws.com + assert SessionRecipe.get_instance().config.cookie_domain is None + assert SessionRecipe.get_instance().config.cookie_same_site == "none" + assert SessionRecipe.get_instance().config.cookie_secure is True + + reset() + + init( + supertokens_config=SupertokensConfig("http://localhost:3567"), + app_info=InputAppInfo( + app_name="SuperTokens Demo", + api_domain="http://ec2-xx-yyy-zzz-0.compute-1.amazonaws.com:3001", + website_domain="http://ec2-aa-bbb-ccc-0.compute-1.amazonaws.com:3000", + api_base_path="/", + ), + framework="fastapi", + recipe_list=[ + session.init(get_token_transfer_method=lambda _, __, ___: "cookie") + ], + ) + + assert SessionRecipe.get_instance().config.cookie_domain is None + assert SessionRecipe.get_instance().config.cookie_same_site == "none" + assert SessionRecipe.get_instance().config.cookie_secure is False + + reset() + + init( + supertokens_config=SupertokensConfig("http://localhost:3567"), + app_info=InputAppInfo( + app_name="SuperTokens Demo", + api_domain="http://ec2-xx-yyy-zzz-0.compute-1.amazonaws.com:3001", + website_domain="http://ec2-xx-yyy-zzz-0.compute-1.amazonaws.com:3000", + api_base_path="/", + ), + framework="fastapi", + recipe_list=[ + session.init(get_token_transfer_method=lambda _, __, ___: "cookie") + ], + ) + + assert SessionRecipe.get_instance().config.cookie_domain is None + assert SessionRecipe.get_instance().config.cookie_same_site == "lax" + assert SessionRecipe.get_instance().config.cookie_secure is False diff --git a/tests/test_utils.py b/tests/test_utils.py index 28b822539..db41552d2 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -3,7 +3,11 @@ import pytest import threading -from supertokens_python.utils import humanize_time, is_version_gte +from supertokens_python.utils import ( + humanize_time, + is_version_gte, + get_top_level_domain_for_same_site_resolution, +) from supertokens_python.utils import RWMutex from tests.utils import is_subset @@ -171,3 +175,25 @@ def balance_is_valid(): expected_balance -= 10 * 5 # 10 threads withdrawing 5 each actual_balance, _ = account.get_stats() assert actual_balance == expected_balance, "Incorrect account balance" + + +@pytest.mark.parametrize( + "url,res", + [ + ("http://localhost:3001", "localhost"), + ( + "https://ec2-xx-yyy-zzz-0.compute-1.amazonaws.com", + "ec2-xx-yyy-zzz-0.compute-1.amazonaws.com", + ), + ( + "https://foo.vercel.com", + "vercel.com", + ), + ( + "https://blog.supertokens.com", + "supertokens.com", + ), + ], +) +def test_tld_for_same_site(url: str, res: str): + assert get_top_level_domain_for_same_site_resolution(url) == res From d8f4d33fc2a4372e30cb7b5b16d18e7ccc774be1 Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Tue, 19 Sep 2023 18:45:50 +0530 Subject: [PATCH 20/27] adding dev-v0.16.1 tag to this commit to ensure building --- html/supertokens_python/constants.html | 2 +- html/supertokens_python/utils.html | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/html/supertokens_python/constants.html b/html/supertokens_python/constants.html index 433530761..f31525cbb 100644 --- a/html/supertokens_python/constants.html +++ b/html/supertokens_python/constants.html @@ -42,7 +42,7 @@

    Module supertokens_python.constants

    from __future__ import annotations SUPPORTED_CDI_VERSIONS = ["3.0"] -VERSION = "0.16.0" +VERSION = "0.16.1" TELEMETRY = "/telemetry" USER_COUNT = "/users/count" USER_DELETE = "/user/remove" diff --git a/html/supertokens_python/utils.html b/html/supertokens_python/utils.html index 9664de780..acdd825aa 100644 --- a/html/supertokens_python/utils.html +++ b/html/supertokens_python/utils.html @@ -327,8 +327,13 @@

    Module supertokens_python.utils

    if hostname.startswith("localhost") or is_an_ip_address(hostname): return "localhost" + parsed_url: Any = extract(hostname, include_psl_private_domains=True) if parsed_url.domain == "": # type: ignore + # We need to do this because of https://github.com/supertokens/supertokens-python/issues/394 + if hostname.endswith(".amazonaws.com") and parsed_url.suffix == hostname: + return hostname + raise Exception( "Please make sure that the apiDomain and websiteDomain have correct values" ) @@ -581,8 +586,13 @@

    Functions

    if hostname.startswith("localhost") or is_an_ip_address(hostname): return "localhost" + parsed_url: Any = extract(hostname, include_psl_private_domains=True) if parsed_url.domain == "": # type: ignore + # We need to do this because of https://github.com/supertokens/supertokens-python/issues/394 + if hostname.endswith(".amazonaws.com") and parsed_url.suffix == hostname: + return hostname + raise Exception( "Please make sure that the apiDomain and websiteDomain have correct values" ) From b376fbf435f4567b4eb7f8ac798aa9b16b1dd5cc Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Tue, 19 Sep 2023 18:56:25 +0530 Subject: [PATCH 21/27] adding dev-v0.16.1 tag to this commit to ensure building From 001d6483d05240bb40e44f5f64c801b58cdead1e Mon Sep 17 00:00:00 2001 From: KShivendu Date: Wed, 20 Sep 2023 10:37:43 +0530 Subject: [PATCH 22/27] chores: Mention SDK and core versions compatible after 0.15 migration --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 13fe645fd..461fea709 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -537,6 +537,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 } } ``` +### SDK and core compatibility + +- Compatible with Core>=6.0.0 (CDI 4.0) +- Compatible with frontend SDKs: + - supertokens-auth-react@0.34.0 + - supertokens-web-js@0.7.0 + - supertokens-website@17.0.2 + ## [0.14.8] - 2023-07-07 ## Fixes From 09a1dc319a42c3ad5b1ef7b3bd159a2ddac1ca21 Mon Sep 17 00:00:00 2001 From: KShivendu Date: Wed, 20 Sep 2023 19:25:00 +0530 Subject: [PATCH 23/27] feat: Use nest-asyncio when configured with env var --- CHANGELOG.md | 5 ++ supertokens_python/__init__.py | 13 +++- supertokens_python/async_to_sync_wrapper.py | 27 +++++-- supertokens_python/querier.py | 86 +++++++++++++++------ supertokens_python/supertokens.py | 15 ++-- supertokens_python/utils.py | 25 ------ 6 files changed, 107 insertions(+), 64 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 461fea709..4d7ca9e50 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [unreleased] +## [0.16.2] - 2023-09-20 + +- Allow use of [nest-asyncio](https://pypi.org/project/nest-asyncio/) when env var `SUPERTOKENS_NEST_ASYNCIO=1`. +- Retry Querier request on `AsyncLibraryNotFoundError` + ## [0.16.1] - 2023-09-19 - Handle AWS Public URLs (ending with `.amazonaws.com`) separately while extracting TLDs for SameSite attribute. diff --git a/supertokens_python/__init__.py b/supertokens_python/__init__.py index 79035af08..33b96ea0e 100644 --- a/supertokens_python/__init__.py +++ b/supertokens_python/__init__.py @@ -12,7 +12,7 @@ # License for the specific language governing permissions and limitations # under the License. -from typing import Any, Callable, Dict, List, Optional, Union +from typing import Any, Callable, Dict, List, Optional from typing_extensions import Literal @@ -32,11 +32,16 @@ def init( framework: Literal["fastapi", "flask", "django"], supertokens_config: SupertokensConfig, recipe_list: List[Callable[[supertokens.AppInfo], RecipeModule]], - mode: Union[Literal["asgi", "wsgi"], None] = None, - telemetry: Union[bool, None] = None, + mode: Optional[Literal["asgi", "wsgi"]] = None, + telemetry: Optional[bool] = None, ): return Supertokens.init( - app_info, framework, supertokens_config, recipe_list, mode, telemetry + app_info, + framework, + supertokens_config, + recipe_list, + mode, + telemetry, ) diff --git a/supertokens_python/async_to_sync_wrapper.py b/supertokens_python/async_to_sync_wrapper.py index 4a56ea31b..4fd22b49e 100644 --- a/supertokens_python/async_to_sync_wrapper.py +++ b/supertokens_python/async_to_sync_wrapper.py @@ -14,20 +14,37 @@ import asyncio from typing import Any, Coroutine, TypeVar +from os import getenv _T = TypeVar("_T") -def check_event_loop(): +def is_nest_asyncio_enabled(): try: - asyncio.get_event_loop() - except RuntimeError as ex: + import nest_asyncio as _ # type: ignore + + return getenv("SUPERTOKENS_NEST_ASYNCIO", "") == "1" + except Exception: + return False + + +def create_or_get_event_loop() -> asyncio.AbstractEventLoop: + try: + return asyncio.get_event_loop() + except Exception as ex: if "There is no current event loop in thread" in str(ex): loop = asyncio.new_event_loop() + + if is_nest_asyncio_enabled(): + import nest_asyncio # type: ignore + + nest_asyncio.apply(loop) # type: ignore + asyncio.set_event_loop(loop) + return loop + raise ex def sync(co: Coroutine[Any, Any, _T]) -> _T: - check_event_loop() - loop = asyncio.get_event_loop() + loop = create_or_get_event_loop() return loop.run_until_complete(co) diff --git a/supertokens_python/querier.py b/supertokens_python/querier.py index 79f6ae3dc..66e1a2dd7 100644 --- a/supertokens_python/querier.py +++ b/supertokens_python/querier.py @@ -39,6 +39,8 @@ from .exceptions import raise_general_exception from .process_state import AllowedProcessStates, ProcessState from .utils import find_max_version, is_4xx_error, is_5xx_error +from sniffio import AsyncLibraryNotFoundError +from supertokens_python.async_to_sync_wrapper import create_or_get_event_loop class Querier: @@ -71,6 +73,35 @@ def get_hosts_alive_for_testing(): raise_general_exception("calling testing function in non testing env") return Querier.__hosts_alive_for_testing + async def api_request( + self, + url: str, + method: str, + attempts_remaining: int, + *args: Any, + **kwargs: Any, + ) -> Response: + if attempts_remaining == 0: + raise_general_exception("Retry request failed") + + try: + async with AsyncClient() as client: + if method == "GET": + return await client.get(url, *args, **kwargs) # type: ignore + if method == "POST": + return await client.post(url, *args, **kwargs) # type: ignore + if method == "PUT": + return await client.put(url, *args, **kwargs) # type: ignore + if method == "DELETE": + return await client.delete(url, *args, **kwargs) # type: ignore + raise Exception("Shouldn't come here") + except AsyncLibraryNotFoundError: + # Retry + loop = create_or_get_event_loop() + return loop.run_until_complete( + self.api_request(url, method, attempts_remaining - 1, *args, **kwargs) + ) + async def get_api_version(self): if Querier.api_version is not None: return Querier.api_version @@ -79,12 +110,11 @@ async def get_api_version(self): AllowedProcessStates.CALLING_SERVICE_IN_GET_API_VERSION ) - async def f(url: str) -> Response: + async def f(url: str, method: str) -> Response: headers = {} if Querier.__api_key is not None: headers = {API_KEY_HEADER: Querier.__api_key} - async with AsyncClient() as client: - return await client.get(url, headers=headers) # type:ignore + return await self.api_request(url, method, 2, headers=headers) response = await self.__send_request_helper( NormalisedURLPath(API_VERSION), "GET", f, len(self.__hosts) @@ -134,13 +164,14 @@ async def send_get_request( if params is None: params = {} - async def f(url: str) -> Response: - async with AsyncClient() as client: - return await client.get( # type:ignore - url, - params=params, - headers=await self.__get_headers_with_api_version(path), - ) + async def f(url: str, method: str) -> Response: + return await self.api_request( + url, + method, + 2, + headers=await self.__get_headers_with_api_version(path), + params=params, + ) return await self.__send_request_helper(path, "GET", f, len(self.__hosts)) @@ -163,9 +194,14 @@ async def send_post_request( headers = await self.__get_headers_with_api_version(path) headers["content-type"] = "application/json; charset=utf-8" - async def f(url: str) -> Response: - async with AsyncClient() as client: - return await client.post(url, json=data, headers=headers) # type: ignore + async def f(url: str, method: str) -> Response: + return await self.api_request( + url, + method, + 2, + headers=await self.__get_headers_with_api_version(path), + json=data, + ) return await self.__send_request_helper(path, "POST", f, len(self.__hosts)) @@ -175,13 +211,14 @@ async def send_delete_request( if params is None: params = {} - async def f(url: str) -> Response: - async with AsyncClient() as client: - return await client.delete( # type:ignore - url, - params=params, - headers=await self.__get_headers_with_api_version(path), - ) + async def f(url: str, method: str) -> Response: + return await self.api_request( + url, + method, + 2, + headers=await self.__get_headers_with_api_version(path), + params=params, + ) return await self.__send_request_helper(path, "DELETE", f, len(self.__hosts)) @@ -194,9 +231,8 @@ async def send_put_request( headers = await self.__get_headers_with_api_version(path) headers["content-type"] = "application/json; charset=utf-8" - async def f(url: str) -> Response: - async with AsyncClient() as client: - return await client.put(url, json=data, headers=headers) # type: ignore + async def f(url: str, method: str) -> Response: + return await self.api_request(url, method, 2, headers=headers, json=data) return await self.__send_request_helper(path, "PUT", f, len(self.__hosts)) @@ -223,7 +259,7 @@ async def __send_request_helper( self, path: NormalisedURLPath, method: str, - http_function: Callable[[str], Awaitable[Response]], + http_function: Callable[[str, str], Awaitable[Response]], no_of_tries: int, retry_info_map: Optional[Dict[str, int]] = None, ) -> Any: @@ -253,7 +289,7 @@ async def __send_request_helper( ProcessState.get_instance().add_state( AllowedProcessStates.CALLING_SERVICE_IN_REQUEST_HELPER ) - response = await http_function(url) + response = await http_function(url, method) if ("SUPERTOKENS_ENV" in environ) and ( environ["SUPERTOKENS_ENV"] == "testing" ): diff --git a/supertokens_python/supertokens.py b/supertokens_python/supertokens.py index ac7885908..c51eca170 100644 --- a/supertokens_python/supertokens.py +++ b/supertokens_python/supertokens.py @@ -148,8 +148,8 @@ def __init__( framework: Literal["fastapi", "flask", "django"], supertokens_config: SupertokensConfig, recipe_list: List[Callable[[AppInfo], RecipeModule]], - mode: Union[Literal["asgi", "wsgi"], None], - telemetry: Union[bool, None], + mode: Optional[Literal["asgi", "wsgi"]], + telemetry: Optional[bool], ): if not isinstance(app_info, InputAppInfo): # type: ignore raise ValueError("app_info must be an instance of InputAppInfo") @@ -215,12 +215,17 @@ def init( framework: Literal["fastapi", "flask", "django"], supertokens_config: SupertokensConfig, recipe_list: List[Callable[[AppInfo], RecipeModule]], - mode: Union[Literal["asgi", "wsgi"], None], - telemetry: Union[bool, None], + mode: Optional[Literal["asgi", "wsgi"]], + telemetry: Optional[bool], ): if Supertokens.__instance is None: Supertokens.__instance = Supertokens( - app_info, framework, supertokens_config, recipe_list, mode, telemetry + app_info, + framework, + supertokens_config, + recipe_list, + mode, + telemetry, ) PostSTInitCallbacks.run_post_init_callbacks() diff --git a/supertokens_python/utils.py b/supertokens_python/utils.py index 1b8afd85b..3a14a5f7d 100644 --- a/supertokens_python/utils.py +++ b/supertokens_python/utils.py @@ -14,7 +14,6 @@ from __future__ import annotations -import asyncio import json import threading import warnings @@ -27,7 +26,6 @@ Any, Awaitable, Callable, - Coroutine, Dict, List, TypeVar, @@ -39,7 +37,6 @@ from httpx import HTTPStatusError, Response from tldextract import extract # type: ignore -from supertokens_python.async_to_sync_wrapper import check_event_loop from supertokens_python.framework.django.framework import DjangoFramework from supertokens_python.framework.fastapi.framework import FastapiFramework from supertokens_python.framework.flask.framework import FlaskFramework @@ -195,28 +192,6 @@ def find_first_occurrence_in_list( return None -def execute_async(mode: str, func: Callable[[], Coroutine[Any, Any, None]]): - real_mode = None - try: - asyncio.get_running_loop() - real_mode = "asgi" - except RuntimeError: - real_mode = "wsgi" - - if mode != real_mode: - warnings.warn( - "Inconsistent mode detected, check if you are using the right asgi / wsgi mode", - category=RuntimeWarning, - ) - - if real_mode == "wsgi": - asyncio.run(func()) - else: - check_event_loop() - loop = asyncio.get_event_loop() - loop.create_task(func()) - - def frontend_has_interceptor(request: BaseRequest) -> bool: return get_rid_from_header(request) is not None From b9a0b449c55aa9a90165b8ce5d4d6b41757b8362 Mon Sep 17 00:00:00 2001 From: KShivendu Date: Wed, 20 Sep 2023 19:26:00 +0530 Subject: [PATCH 24/27] chores: Bump version to 0.16.2 --- setup.py | 2 +- supertokens_python/constants.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 2b13aff2e..920610b61 100644 --- a/setup.py +++ b/setup.py @@ -70,7 +70,7 @@ setup( name="supertokens_python", - version="0.16.1", + version="0.16.2", author="SuperTokens", license="Apache 2.0", author_email="team@supertokens.com", diff --git a/supertokens_python/constants.py b/supertokens_python/constants.py index 292277329..fbece17e8 100644 --- a/supertokens_python/constants.py +++ b/supertokens_python/constants.py @@ -14,7 +14,7 @@ from __future__ import annotations SUPPORTED_CDI_VERSIONS = ["3.0"] -VERSION = "0.16.1" +VERSION = "0.16.2" TELEMETRY = "/telemetry" USER_COUNT = "/users/count" USER_DELETE = "/user/remove" From 92329b9fd576abf4958431f70347424b9531e8d6 Mon Sep 17 00:00:00 2001 From: KShivendu Date: Thu, 21 Sep 2023 13:34:12 +0530 Subject: [PATCH 25/27] feat: Add e2e flask test with nest-asyncio for circleci --- .circleci/config_continue.yml | 16 +++++++++++++ supertokens_python/async_to_sync_wrapper.py | 11 +++------ tests/test_config.py | 25 +++++++++++++++++++++ 3 files changed, 44 insertions(+), 8 deletions(-) diff --git a/.circleci/config_continue.yml b/.circleci/config_continue.yml index 004004ce1..25ae526f7 100644 --- a/.circleci/config_continue.yml +++ b/.circleci/config_continue.yml @@ -79,6 +79,22 @@ jobs: - run: make with-django2x - run: (cd .circleci/ && ./websiteDjango2x.sh) - slack/status + test-website-flask-nest-asyncio: + docker: + - image: rishabhpoddar/supertokens_python_driver_testing + resource_class: large + environment: + SUPERTOKENS_NEST_ASYNCIO: "1" + steps: + - checkout + - run: update-alternatives --install "/usr/bin/java" "java" "/usr/java/jdk-15.0.1/bin/java" 2 + - run: update-alternatives --install "/usr/bin/javac" "javac" "/usr/java/jdk-15.0.1/bin/javac" 2 + - run: git config --global url."https://github.com/".insteadOf ssh://git@github.com/ + - run: echo "127.0.0.1 localhost.org" >> /etc/hosts + - run: make with-flask + - run: python -m pip install nest-asyncio + - run: (cd .circleci/ && ./websiteFlask.sh) + - slack/status test-authreact-fastapi: docker: - image: rishabhpoddar/supertokens_python_driver_testing diff --git a/supertokens_python/async_to_sync_wrapper.py b/supertokens_python/async_to_sync_wrapper.py index 4fd22b49e..8e019336d 100644 --- a/supertokens_python/async_to_sync_wrapper.py +++ b/supertokens_python/async_to_sync_wrapper.py @@ -19,13 +19,8 @@ _T = TypeVar("_T") -def is_nest_asyncio_enabled(): - try: - import nest_asyncio as _ # type: ignore - - return getenv("SUPERTOKENS_NEST_ASYNCIO", "") == "1" - except Exception: - return False +def nest_asyncio_enabled(): + return getenv("SUPERTOKENS_NEST_ASYNCIO", "") == "1" def create_or_get_event_loop() -> asyncio.AbstractEventLoop: @@ -35,7 +30,7 @@ def create_or_get_event_loop() -> asyncio.AbstractEventLoop: if "There is no current event loop in thread" in str(ex): loop = asyncio.new_event_loop() - if is_nest_asyncio_enabled(): + if nest_asyncio_enabled(): import nest_asyncio # type: ignore nest_asyncio.apply(loop) # type: ignore diff --git a/tests/test_config.py b/tests/test_config.py index 2df57fe59..4d0a90ef7 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -11,6 +11,8 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. +import asyncio + from pytest import mark from unittest.mock import MagicMock from supertokens_python import InputAppInfo, SupertokensConfig, init @@ -800,3 +802,26 @@ async def test_cookie_samesite_with_ec2_public_url(): assert SessionRecipe.get_instance().config.cookie_domain is None assert SessionRecipe.get_instance().config.cookie_same_site == "lax" assert SessionRecipe.get_instance().config.cookie_secure is False + + +def test_nest_asyncio_import(): + from supertokens_python.async_to_sync_wrapper import nest_asyncio_enabled, sync + from os import getenv + + circleci = getenv("CIRCLECI", "false") == "true" + + if not circleci: + return + + # Has to be circleci + if nest_asyncio_enabled(): + # nest-asyncio should be installed + sync(asyncio.sleep(0.1)) + else: + # nest-asyncio shouldn't be installed and sync() should throw error + try: + sync(asyncio.sleep(0.1)) + assert False, "Shouldn't come here" + except ModuleNotFoundError: + # should be missing + assert True From 45ae5c896f8d58884d8167dc3285b426ad5ffe66 Mon Sep 17 00:00:00 2001 From: KShivendu Date: Thu, 21 Sep 2023 16:39:12 +0530 Subject: [PATCH 26/27] test: Remove unused test --- tests/test_config.py | 25 ------------------------- 1 file changed, 25 deletions(-) diff --git a/tests/test_config.py b/tests/test_config.py index 4d0a90ef7..2df57fe59 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -11,8 +11,6 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. -import asyncio - from pytest import mark from unittest.mock import MagicMock from supertokens_python import InputAppInfo, SupertokensConfig, init @@ -802,26 +800,3 @@ async def test_cookie_samesite_with_ec2_public_url(): assert SessionRecipe.get_instance().config.cookie_domain is None assert SessionRecipe.get_instance().config.cookie_same_site == "lax" assert SessionRecipe.get_instance().config.cookie_secure is False - - -def test_nest_asyncio_import(): - from supertokens_python.async_to_sync_wrapper import nest_asyncio_enabled, sync - from os import getenv - - circleci = getenv("CIRCLECI", "false") == "true" - - if not circleci: - return - - # Has to be circleci - if nest_asyncio_enabled(): - # nest-asyncio should be installed - sync(asyncio.sleep(0.1)) - else: - # nest-asyncio shouldn't be installed and sync() should throw error - try: - sync(asyncio.sleep(0.1)) - assert False, "Shouldn't come here" - except ModuleNotFoundError: - # should be missing - assert True From 364896de28ade82a0e0d0075e745ec820d6174ab Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Thu, 21 Sep 2023 16:47:14 +0530 Subject: [PATCH 27/27] adding dev-v0.16.2 tag to this commit to ensure building --- .../async_to_sync_wrapper.html | 61 +++- html/supertokens_python/constants.html | 2 +- html/supertokens_python/index.html | 24 +- html/supertokens_python/querier.html | 261 +++++++++++++----- html/supertokens_python/supertokens.html | 45 ++- html/supertokens_python/utils.html | 57 ---- 6 files changed, 283 insertions(+), 167 deletions(-) diff --git a/html/supertokens_python/async_to_sync_wrapper.html b/html/supertokens_python/async_to_sync_wrapper.html index f85f00518..dd7eb001c 100644 --- a/html/supertokens_python/async_to_sync_wrapper.html +++ b/html/supertokens_python/async_to_sync_wrapper.html @@ -42,22 +42,34 @@

    Module supertokens_python.async_to_sync_wrapper @@ -68,8 +80,8 @@

    Module supertokens_python.async_to_sync_wrapper

    Functions

    -
    -def check_event_loop() +
    +def create_or_get_event_loop() ‑> asyncio.events.AbstractEventLoop
    @@ -77,13 +89,34 @@

    Functions

    Expand source code -
    def check_event_loop():
    +
    def create_or_get_event_loop() -> asyncio.AbstractEventLoop:
         try:
    -        asyncio.get_event_loop()
    -    except RuntimeError as ex:
    +        return asyncio.get_event_loop()
    +    except Exception as ex:
             if "There is no current event loop in thread" in str(ex):
                 loop = asyncio.new_event_loop()
    -            asyncio.set_event_loop(loop)
    + + if nest_asyncio_enabled(): + import nest_asyncio # type: ignore + + nest_asyncio.apply(loop) # type: ignore + + asyncio.set_event_loop(loop) + return loop + raise ex
    + +
    +
    +def nest_asyncio_enabled() +
    +
    +
    +
    + +Expand source code + +
    def nest_asyncio_enabled():
    +    return getenv("SUPERTOKENS_NEST_ASYNCIO", "") == "1"
    @@ -96,8 +129,7 @@

    Functions

    Expand source code
    def sync(co: Coroutine[Any, Any, _T]) -> _T:
    -    check_event_loop()
    -    loop = asyncio.get_event_loop()
    +    loop = create_or_get_event_loop()
         return loop.run_until_complete(co)
    @@ -119,7 +151,8 @@

    Index

  • Functions

  • diff --git a/html/supertokens_python/constants.html b/html/supertokens_python/constants.html index f31525cbb..585f80925 100644 --- a/html/supertokens_python/constants.html +++ b/html/supertokens_python/constants.html @@ -42,7 +42,7 @@

    Module supertokens_python.constants

    from __future__ import annotations SUPPORTED_CDI_VERSIONS = ["3.0"] -VERSION = "0.16.1" +VERSION = "0.16.2" TELEMETRY = "/telemetry" USER_COUNT = "/users/count" USER_DELETE = "/user/remove" diff --git a/html/supertokens_python/index.html b/html/supertokens_python/index.html index d59f7af52..1a1b48c88 100644 --- a/html/supertokens_python/index.html +++ b/html/supertokens_python/index.html @@ -40,7 +40,7 @@

    Package supertokens_python

    # License for the specific language governing permissions and limitations # under the License. -from typing import Any, Callable, Dict, List, Optional, Union +from typing import Any, Callable, Dict, List, Optional from typing_extensions import Literal @@ -60,11 +60,16 @@

    Package supertokens_python

    framework: Literal["fastapi", "flask", "django"], supertokens_config: SupertokensConfig, recipe_list: List[Callable[[supertokens.AppInfo], RecipeModule]], - mode: Union[Literal["asgi", "wsgi"], None] = None, - telemetry: Union[bool, None] = None, + mode: Optional[Literal["asgi", "wsgi"]] = None, + telemetry: Optional[bool] = None, ): return Supertokens.init( - app_info, framework, supertokens_config, recipe_list, mode, telemetry + app_info, + framework, + supertokens_config, + recipe_list, + mode, + telemetry, ) @@ -206,11 +211,16 @@

    Functions

    framework: Literal["fastapi", "flask", "django"], supertokens_config: SupertokensConfig, recipe_list: List[Callable[[supertokens.AppInfo], RecipeModule]], - mode: Union[Literal["asgi", "wsgi"], None] = None, - telemetry: Union[bool, None] = None, + mode: Optional[Literal["asgi", "wsgi"]] = None, + telemetry: Optional[bool] = None, ): return Supertokens.init( - app_info, framework, supertokens_config, recipe_list, mode, telemetry + app_info, + framework, + supertokens_config, + recipe_list, + mode, + telemetry, )
    diff --git a/html/supertokens_python/querier.html b/html/supertokens_python/querier.html index f95a55798..9bbef147f 100644 --- a/html/supertokens_python/querier.html +++ b/html/supertokens_python/querier.html @@ -67,6 +67,8 @@

    Module supertokens_python.querier

    from .exceptions import raise_general_exception from .process_state import AllowedProcessStates, ProcessState from .utils import find_max_version, is_4xx_error, is_5xx_error +from sniffio import AsyncLibraryNotFoundError +from supertokens_python.async_to_sync_wrapper import create_or_get_event_loop class Querier: @@ -99,6 +101,35 @@

    Module supertokens_python.querier

    raise_general_exception("calling testing function in non testing env") return Querier.__hosts_alive_for_testing + async def api_request( + self, + url: str, + method: str, + attempts_remaining: int, + *args: Any, + **kwargs: Any, + ) -> Response: + if attempts_remaining == 0: + raise_general_exception("Retry request failed") + + try: + async with AsyncClient() as client: + if method == "GET": + return await client.get(url, *args, **kwargs) # type: ignore + if method == "POST": + return await client.post(url, *args, **kwargs) # type: ignore + if method == "PUT": + return await client.put(url, *args, **kwargs) # type: ignore + if method == "DELETE": + return await client.delete(url, *args, **kwargs) # type: ignore + raise Exception("Shouldn't come here") + except AsyncLibraryNotFoundError: + # Retry + loop = create_or_get_event_loop() + return loop.run_until_complete( + self.api_request(url, method, attempts_remaining - 1, *args, **kwargs) + ) + async def get_api_version(self): if Querier.api_version is not None: return Querier.api_version @@ -107,12 +138,11 @@

    Module supertokens_python.querier

    AllowedProcessStates.CALLING_SERVICE_IN_GET_API_VERSION ) - async def f(url: str) -> Response: + async def f(url: str, method: str) -> Response: headers = {} if Querier.__api_key is not None: headers = {API_KEY_HEADER: Querier.__api_key} - async with AsyncClient() as client: - return await client.get(url, headers=headers) # type:ignore + return await self.api_request(url, method, 2, headers=headers) response = await self.__send_request_helper( NormalisedURLPath(API_VERSION), "GET", f, len(self.__hosts) @@ -162,13 +192,14 @@

    Module supertokens_python.querier

    if params is None: params = {} - async def f(url: str) -> Response: - async with AsyncClient() as client: - return await client.get( # type:ignore - url, - params=params, - headers=await self.__get_headers_with_api_version(path), - ) + async def f(url: str, method: str) -> Response: + return await self.api_request( + url, + method, + 2, + headers=await self.__get_headers_with_api_version(path), + params=params, + ) return await self.__send_request_helper(path, "GET", f, len(self.__hosts)) @@ -191,9 +222,14 @@

    Module supertokens_python.querier

    headers = await self.__get_headers_with_api_version(path) headers["content-type"] = "application/json; charset=utf-8" - async def f(url: str) -> Response: - async with AsyncClient() as client: - return await client.post(url, json=data, headers=headers) # type: ignore + async def f(url: str, method: str) -> Response: + return await self.api_request( + url, + method, + 2, + headers=await self.__get_headers_with_api_version(path), + json=data, + ) return await self.__send_request_helper(path, "POST", f, len(self.__hosts)) @@ -203,13 +239,14 @@

    Module supertokens_python.querier

    if params is None: params = {} - async def f(url: str) -> Response: - async with AsyncClient() as client: - return await client.delete( # type:ignore - url, - params=params, - headers=await self.__get_headers_with_api_version(path), - ) + async def f(url: str, method: str) -> Response: + return await self.api_request( + url, + method, + 2, + headers=await self.__get_headers_with_api_version(path), + params=params, + ) return await self.__send_request_helper(path, "DELETE", f, len(self.__hosts)) @@ -222,9 +259,8 @@

    Module supertokens_python.querier

    headers = await self.__get_headers_with_api_version(path) headers["content-type"] = "application/json; charset=utf-8" - async def f(url: str) -> Response: - async with AsyncClient() as client: - return await client.put(url, json=data, headers=headers) # type: ignore + async def f(url: str, method: str) -> Response: + return await self.api_request(url, method, 2, headers=headers, json=data) return await self.__send_request_helper(path, "PUT", f, len(self.__hosts)) @@ -251,7 +287,7 @@

    Module supertokens_python.querier

    self, path: NormalisedURLPath, method: str, - http_function: Callable[[str], Awaitable[Response]], + http_function: Callable[[str, str], Awaitable[Response]], no_of_tries: int, retry_info_map: Optional[Dict[str, int]] = None, ) -> Any: @@ -281,7 +317,7 @@

    Module supertokens_python.querier

    ProcessState.get_instance().add_state( AllowedProcessStates.CALLING_SERVICE_IN_REQUEST_HELPER ) - response = await http_function(url) + response = await http_function(url, method) if ("SUPERTOKENS_ENV" in environ) and ( environ["SUPERTOKENS_ENV"] == "testing" ): @@ -375,6 +411,35 @@

    Classes

    raise_general_exception("calling testing function in non testing env") return Querier.__hosts_alive_for_testing + async def api_request( + self, + url: str, + method: str, + attempts_remaining: int, + *args: Any, + **kwargs: Any, + ) -> Response: + if attempts_remaining == 0: + raise_general_exception("Retry request failed") + + try: + async with AsyncClient() as client: + if method == "GET": + return await client.get(url, *args, **kwargs) # type: ignore + if method == "POST": + return await client.post(url, *args, **kwargs) # type: ignore + if method == "PUT": + return await client.put(url, *args, **kwargs) # type: ignore + if method == "DELETE": + return await client.delete(url, *args, **kwargs) # type: ignore + raise Exception("Shouldn't come here") + except AsyncLibraryNotFoundError: + # Retry + loop = create_or_get_event_loop() + return loop.run_until_complete( + self.api_request(url, method, attempts_remaining - 1, *args, **kwargs) + ) + async def get_api_version(self): if Querier.api_version is not None: return Querier.api_version @@ -383,12 +448,11 @@

    Classes

    AllowedProcessStates.CALLING_SERVICE_IN_GET_API_VERSION ) - async def f(url: str) -> Response: + async def f(url: str, method: str) -> Response: headers = {} if Querier.__api_key is not None: headers = {API_KEY_HEADER: Querier.__api_key} - async with AsyncClient() as client: - return await client.get(url, headers=headers) # type:ignore + return await self.api_request(url, method, 2, headers=headers) response = await self.__send_request_helper( NormalisedURLPath(API_VERSION), "GET", f, len(self.__hosts) @@ -438,13 +502,14 @@

    Classes

    if params is None: params = {} - async def f(url: str) -> Response: - async with AsyncClient() as client: - return await client.get( # type:ignore - url, - params=params, - headers=await self.__get_headers_with_api_version(path), - ) + async def f(url: str, method: str) -> Response: + return await self.api_request( + url, + method, + 2, + headers=await self.__get_headers_with_api_version(path), + params=params, + ) return await self.__send_request_helper(path, "GET", f, len(self.__hosts)) @@ -467,9 +532,14 @@

    Classes

    headers = await self.__get_headers_with_api_version(path) headers["content-type"] = "application/json; charset=utf-8" - async def f(url: str) -> Response: - async with AsyncClient() as client: - return await client.post(url, json=data, headers=headers) # type: ignore + async def f(url: str, method: str) -> Response: + return await self.api_request( + url, + method, + 2, + headers=await self.__get_headers_with_api_version(path), + json=data, + ) return await self.__send_request_helper(path, "POST", f, len(self.__hosts)) @@ -479,13 +549,14 @@

    Classes

    if params is None: params = {} - async def f(url: str) -> Response: - async with AsyncClient() as client: - return await client.delete( # type:ignore - url, - params=params, - headers=await self.__get_headers_with_api_version(path), - ) + async def f(url: str, method: str) -> Response: + return await self.api_request( + url, + method, + 2, + headers=await self.__get_headers_with_api_version(path), + params=params, + ) return await self.__send_request_helper(path, "DELETE", f, len(self.__hosts)) @@ -498,9 +569,8 @@

    Classes

    headers = await self.__get_headers_with_api_version(path) headers["content-type"] = "application/json; charset=utf-8" - async def f(url: str) -> Response: - async with AsyncClient() as client: - return await client.put(url, json=data, headers=headers) # type: ignore + async def f(url: str, method: str) -> Response: + return await self.api_request(url, method, 2, headers=headers, json=data) return await self.__send_request_helper(path, "PUT", f, len(self.__hosts)) @@ -527,7 +597,7 @@

    Classes

    self, path: NormalisedURLPath, method: str, - http_function: Callable[[str], Awaitable[Response]], + http_function: Callable[[str, str], Awaitable[Response]], no_of_tries: int, retry_info_map: Optional[Dict[str, int]] = None, ) -> Any: @@ -557,7 +627,7 @@

    Classes

    ProcessState.get_instance().add_state( AllowedProcessStates.CALLING_SERVICE_IN_REQUEST_HELPER ) - response = await http_function(url) + response = await http_function(url, method) if ("SUPERTOKENS_ENV" in environ) and ( environ["SUPERTOKENS_ENV"] == "testing" ): @@ -687,6 +757,45 @@

    Static methods

    Methods

    +
    +async def api_request(self, url: str, method: str, attempts_remaining: int, *args: Any, **kwargs: Any) ‑> httpx.Response +
    +
    +
    +
    + +Expand source code + +
    async def api_request(
    +    self,
    +    url: str,
    +    method: str,
    +    attempts_remaining: int,
    +    *args: Any,
    +    **kwargs: Any,
    +) -> Response:
    +    if attempts_remaining == 0:
    +        raise_general_exception("Retry request failed")
    +
    +    try:
    +        async with AsyncClient() as client:
    +            if method == "GET":
    +                return await client.get(url, *args, **kwargs)  # type: ignore
    +            if method == "POST":
    +                return await client.post(url, *args, **kwargs)  # type: ignore
    +            if method == "PUT":
    +                return await client.put(url, *args, **kwargs)  # type: ignore
    +            if method == "DELETE":
    +                return await client.delete(url, *args, **kwargs)  # type: ignore
    +            raise Exception("Shouldn't come here")
    +    except AsyncLibraryNotFoundError:
    +        # Retry
    +        loop = create_or_get_event_loop()
    +        return loop.run_until_complete(
    +            self.api_request(url, method, attempts_remaining - 1, *args, **kwargs)
    +        )
    +
    +
    def get_all_core_urls_for_path(self, path: str) ‑> List[str]
    @@ -733,12 +842,11 @@

    Methods

    AllowedProcessStates.CALLING_SERVICE_IN_GET_API_VERSION ) - async def f(url: str) -> Response: + async def f(url: str, method: str) -> Response: headers = {} if Querier.__api_key is not None: headers = {API_KEY_HEADER: Querier.__api_key} - async with AsyncClient() as client: - return await client.get(url, headers=headers) # type:ignore + return await self.api_request(url, method, 2, headers=headers) response = await self.__send_request_helper( NormalisedURLPath(API_VERSION), "GET", f, len(self.__hosts) @@ -772,13 +880,14 @@

    Methods

    if params is None: params = {} - async def f(url: str) -> Response: - async with AsyncClient() as client: - return await client.delete( # type:ignore - url, - params=params, - headers=await self.__get_headers_with_api_version(path), - ) + async def f(url: str, method: str) -> Response: + return await self.api_request( + url, + method, + 2, + headers=await self.__get_headers_with_api_version(path), + params=params, + ) return await self.__send_request_helper(path, "DELETE", f, len(self.__hosts))
    @@ -798,13 +907,14 @@

    Methods

    if params is None: params = {} - async def f(url: str) -> Response: - async with AsyncClient() as client: - return await client.get( # type:ignore - url, - params=params, - headers=await self.__get_headers_with_api_version(path), - ) + async def f(url: str, method: str) -> Response: + return await self.api_request( + url, + method, + 2, + headers=await self.__get_headers_with_api_version(path), + params=params, + ) return await self.__send_request_helper(path, "GET", f, len(self.__hosts))
    @@ -837,9 +947,14 @@

    Methods

    headers = await self.__get_headers_with_api_version(path) headers["content-type"] = "application/json; charset=utf-8" - async def f(url: str) -> Response: - async with AsyncClient() as client: - return await client.post(url, json=data, headers=headers) # type: ignore + async def f(url: str, method: str) -> Response: + return await self.api_request( + url, + method, + 2, + headers=await self.__get_headers_with_api_version(path), + json=data, + ) return await self.__send_request_helper(path, "POST", f, len(self.__hosts))
    @@ -862,9 +977,8 @@

    Methods

    headers = await self.__get_headers_with_api_version(path) headers["content-type"] = "application/json; charset=utf-8" - async def f(url: str) -> Response: - async with AsyncClient() as client: - return await client.put(url, json=data, headers=headers) # type: ignore + async def f(url: str, method: str) -> Response: + return await self.api_request(url, method, 2, headers=headers, json=data) return await self.__send_request_helper(path, "PUT", f, len(self.__hosts)) @@ -890,6 +1004,7 @@

    Index

  • Querier

      +
    • api_request
    • api_version
    • get_all_core_urls_for_path
    • get_api_version
    • diff --git a/html/supertokens_python/supertokens.html b/html/supertokens_python/supertokens.html index c5a9d6168..d106f386c 100644 --- a/html/supertokens_python/supertokens.html +++ b/html/supertokens_python/supertokens.html @@ -176,8 +176,8 @@

      Module supertokens_python.supertokens

      framework: Literal["fastapi", "flask", "django"], supertokens_config: SupertokensConfig, recipe_list: List[Callable[[AppInfo], RecipeModule]], - mode: Union[Literal["asgi", "wsgi"], None], - telemetry: Union[bool, None], + mode: Optional[Literal["asgi", "wsgi"]], + telemetry: Optional[bool], ): if not isinstance(app_info, InputAppInfo): # type: ignore raise ValueError("app_info must be an instance of InputAppInfo") @@ -243,12 +243,17 @@

      Module supertokens_python.supertokens

      framework: Literal["fastapi", "flask", "django"], supertokens_config: SupertokensConfig, recipe_list: List[Callable[[AppInfo], RecipeModule]], - mode: Union[Literal["asgi", "wsgi"], None], - telemetry: Union[bool, None], + mode: Optional[Literal["asgi", "wsgi"]], + telemetry: Optional[bool], ): if Supertokens.__instance is None: Supertokens.__instance = Supertokens( - app_info, framework, supertokens_config, recipe_list, mode, telemetry + app_info, + framework, + supertokens_config, + recipe_list, + mode, + telemetry, ) PostSTInitCallbacks.run_post_init_callbacks() @@ -785,7 +790,7 @@

      Methods

      class Supertokens -(app_info: InputAppInfo, framework: "Literal['fastapi', 'flask', 'django']", supertokens_config: SupertokensConfig, recipe_list: List[Callable[[AppInfo], RecipeModule]], mode: "Union[Literal['asgi', 'wsgi'], None]", telemetry: Union[bool, None]) +(app_info: InputAppInfo, framework: "Literal['fastapi', 'flask', 'django']", supertokens_config: SupertokensConfig, recipe_list: List[Callable[[AppInfo], RecipeModule]], mode: "Optional[Literal['asgi', 'wsgi']]", telemetry: Optional[bool])
      @@ -802,8 +807,8 @@

      Methods

      framework: Literal["fastapi", "flask", "django"], supertokens_config: SupertokensConfig, recipe_list: List[Callable[[AppInfo], RecipeModule]], - mode: Union[Literal["asgi", "wsgi"], None], - telemetry: Union[bool, None], + mode: Optional[Literal["asgi", "wsgi"]], + telemetry: Optional[bool], ): if not isinstance(app_info, InputAppInfo): # type: ignore raise ValueError("app_info must be an instance of InputAppInfo") @@ -869,12 +874,17 @@

      Methods

      framework: Literal["fastapi", "flask", "django"], supertokens_config: SupertokensConfig, recipe_list: List[Callable[[AppInfo], RecipeModule]], - mode: Union[Literal["asgi", "wsgi"], None], - telemetry: Union[bool, None], + mode: Optional[Literal["asgi", "wsgi"]], + telemetry: Optional[bool], ): if Supertokens.__instance is None: Supertokens.__instance = Supertokens( - app_info, framework, supertokens_config, recipe_list, mode, telemetry + app_info, + framework, + supertokens_config, + recipe_list, + mode, + telemetry, ) PostSTInitCallbacks.run_post_init_callbacks() @@ -1283,7 +1293,7 @@

      Static methods

      -def init(app_info: InputAppInfo, framework: "Literal['fastapi', 'flask', 'django']", supertokens_config: SupertokensConfig, recipe_list: List[Callable[[AppInfo], RecipeModule]], mode: "Union[Literal['asgi', 'wsgi'], None]", telemetry: Union[bool, None]) +def init(app_info: InputAppInfo, framework: "Literal['fastapi', 'flask', 'django']", supertokens_config: SupertokensConfig, recipe_list: List[Callable[[AppInfo], RecipeModule]], mode: "Optional[Literal['asgi', 'wsgi']]", telemetry: Optional[bool])
      @@ -1297,12 +1307,17 @@

      Static methods

      framework: Literal["fastapi", "flask", "django"], supertokens_config: SupertokensConfig, recipe_list: List[Callable[[AppInfo], RecipeModule]], - mode: Union[Literal["asgi", "wsgi"], None], - telemetry: Union[bool, None], + mode: Optional[Literal["asgi", "wsgi"]], + telemetry: Optional[bool], ): if Supertokens.__instance is None: Supertokens.__instance = Supertokens( - app_info, framework, supertokens_config, recipe_list, mode, telemetry + app_info, + framework, + supertokens_config, + recipe_list, + mode, + telemetry, ) PostSTInitCallbacks.run_post_init_callbacks() diff --git a/html/supertokens_python/utils.html b/html/supertokens_python/utils.html index acdd825aa..9de333921 100644 --- a/html/supertokens_python/utils.html +++ b/html/supertokens_python/utils.html @@ -42,7 +42,6 @@

      Module supertokens_python.utils

      from __future__ import annotations -import asyncio import json import threading import warnings @@ -55,7 +54,6 @@

      Module supertokens_python.utils

      Any, Awaitable, Callable, - Coroutine, Dict, List, TypeVar, @@ -67,7 +65,6 @@

      Module supertokens_python.utils

      from httpx import HTTPStatusError, Response from tldextract import extract # type: ignore -from supertokens_python.async_to_sync_wrapper import check_event_loop from supertokens_python.framework.django.framework import DjangoFramework from supertokens_python.framework.fastapi.framework import FastapiFramework from supertokens_python.framework.flask.framework import FlaskFramework @@ -223,28 +220,6 @@

      Module supertokens_python.utils

      return None -def execute_async(mode: str, func: Callable[[], Coroutine[Any, Any, None]]): - real_mode = None - try: - asyncio.get_running_loop() - real_mode = "asgi" - except RuntimeError: - real_mode = "wsgi" - - if mode != real_mode: - warnings.warn( - "Inconsistent mode detected, check if you are using the right asgi / wsgi mode", - category=RuntimeWarning, - ) - - if real_mode == "wsgi": - asyncio.run(func()) - else: - check_event_loop() - loop = asyncio.get_event_loop() - loop.create_task(func()) - - def frontend_has_interceptor(request: BaseRequest) -> bool: return get_rid_from_header(request) is not None @@ -432,37 +407,6 @@

      Functions

      warnings.warn(msg, DeprecationWarning, stacklevel=2)
      -
      -def execute_async(mode: str, func: Callable[[], Coroutine[Any, Any, None]]) -
      -
      -
      -
      - -Expand source code - -
      def execute_async(mode: str, func: Callable[[], Coroutine[Any, Any, None]]):
      -    real_mode = None
      -    try:
      -        asyncio.get_running_loop()
      -        real_mode = "asgi"
      -    except RuntimeError:
      -        real_mode = "wsgi"
      -
      -    if mode != real_mode:
      -        warnings.warn(
      -            "Inconsistent mode detected, check if you are using the right asgi / wsgi mode",
      -            category=RuntimeWarning,
      -        )
      -
      -    if real_mode == "wsgi":
      -        asyncio.run(func())
      -    else:
      -        check_event_loop()
      -        loop = asyncio.get_event_loop()
      -        loop.create_task(func())
      -
      -
      def find_first_occurrence_in_list(condition: Callable[[_T], bool], given_list: List[_T]) ‑> Optional[~_T]
      @@ -1048,7 +992,6 @@

      Index