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