From e0995f074b853868cb0f4e559535d1c268327737 Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Mon, 30 Sep 2024 15:43:53 +0530 Subject: [PATCH 1/6] fixes stuff --- .pylintrc | 3 +- .vscode/launch.json | 15 + supertokens_python/__init__.py | 5 + .../recipe/multifactorauth/__init__.py | 35 ++ supertokens_python/recipe/totp/__init__.py | 33 ++ supertokens_python/supertokens.py | 9 +- tests/auth-react/flask-server/app.py | 553 ++++++++++++++++-- tests/multitenancy/test_tenants_crud.py | 8 +- ...test_validate_claims_for_session_handle.py | 2 +- tests/test-server/multitenancy.py | 2 +- tests/userroles/test_claims.py | 7 +- 11 files changed, 623 insertions(+), 49 deletions(-) create mode 100644 supertokens_python/recipe/multifactorauth/__init__.py create mode 100644 supertokens_python/recipe/totp/__init__.py diff --git a/.pylintrc b/.pylintrc index f9b838184..c271a2fb9 100644 --- a/.pylintrc +++ b/.pylintrc @@ -123,7 +123,8 @@ disable=raw-checker-failed, consider-using-in, no-else-return, no-self-use, - no-else-raise + no-else-raise, + too-many-nested-blocks, # Enable the message, report, category or checker with the given id(s). You can diff --git a/.vscode/launch.json b/.vscode/launch.json index a6f58a695..9c01eaf88 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -15,6 +15,21 @@ "FLASK_DEBUG": "1" }, "jinja": true + }, + { + "name": "Python: Flask, supertokens-auth-react tests", + "type": "python", + "request": "launch", + "program": "${workspaceFolder}/tests/auth-react/flask-server/app.py", + "args": [ + "--port", + "8083" + ], + "cwd": "${workspaceFolder}/tests/auth-react/flask-server", + "env": { + "FLASK_DEBUG": "1" + }, + "jinja": true } ] } \ No newline at end of file diff --git a/supertokens_python/__init__.py b/supertokens_python/__init__.py index ea7ba506a..43d3573b2 100644 --- a/supertokens_python/__init__.py +++ b/supertokens_python/__init__.py @@ -17,6 +17,7 @@ from typing_extensions import Literal from supertokens_python.framework.request import BaseRequest +from supertokens_python.types import RecipeUserId from . import supertokens from .recipe_module import RecipeModule @@ -49,3 +50,7 @@ def get_request_from_user_context( user_context: Optional[Dict[str, Any]], ) -> Optional[BaseRequest]: return Supertokens.get_instance().get_request_from_user_context(user_context) + + +def convert_to_recipe_user_id(user_id: str) -> RecipeUserId: + return RecipeUserId(user_id) diff --git a/supertokens_python/recipe/multifactorauth/__init__.py b/supertokens_python/recipe/multifactorauth/__init__.py new file mode 100644 index 000000000..fa3aaf6f8 --- /dev/null +++ b/supertokens_python/recipe/multifactorauth/__init__.py @@ -0,0 +1,35 @@ +# Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. +# +# This software is licensed under the Apache License, Version 2.0 (the +# "License") as published by the Apache Software Foundation. +# +# You may not use this file except in compliance with the License. You may +# obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +from __future__ import annotations + +from typing import TYPE_CHECKING, Callable, List, Optional, Union + +from supertokens_python.recipe.multifactorauth.types import OverrideConfig + +from .recipe import MultiFactorAuthRecipe + +if TYPE_CHECKING: + from supertokens_python.supertokens import AppInfo + + from ...recipe_module import RecipeModule + + +def init( + first_factors: Optional[List[str]] = None, + override: Union[OverrideConfig, None] = None, +) -> Callable[[AppInfo], RecipeModule]: + return MultiFactorAuthRecipe.init( + first_factors, + override, + ) diff --git a/supertokens_python/recipe/totp/__init__.py b/supertokens_python/recipe/totp/__init__.py new file mode 100644 index 000000000..f89944688 --- /dev/null +++ b/supertokens_python/recipe/totp/__init__.py @@ -0,0 +1,33 @@ +# Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. +# +# This software is licensed under the Apache License, Version 2.0 (the +# "License") as published by the Apache Software Foundation. +# +# You may not use this file except in compliance with the License. You may +# obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +from __future__ import annotations + +from typing import TYPE_CHECKING, Callable, Union + +from supertokens_python.recipe.totp.types import TOTPConfig + +from .recipe import TOTPRecipe + +if TYPE_CHECKING: + from supertokens_python.supertokens import AppInfo + + from ...recipe_module import RecipeModule + + +def init( + config: Union[TOTPConfig, None] = None, +) -> Callable[[AppInfo], RecipeModule]: + return TOTPRecipe.init( + config=config, + ) diff --git a/supertokens_python/supertokens.py b/supertokens_python/supertokens.py index d4509a74c..6aae8aa9b 100644 --- a/supertokens_python/supertokens.py +++ b/supertokens_python/supertokens.py @@ -255,11 +255,6 @@ def __init__( "Please provide at least one recipe to the supertokens.init function call" ) - from supertokens_python.recipe.multifactorauth.recipe import ( - MultiFactorAuthRecipe, - ) - from supertokens_python.recipe.totp.recipe import TOTPRecipe - multitenancy_found = False totp_found = False user_metadata_found = False @@ -272,9 +267,9 @@ def make_recipe(recipe: Callable[[AppInfo], RecipeModule]) -> RecipeModule: multitenancy_found = True elif recipe_module.get_recipe_id() == "usermetadata": user_metadata_found = True - elif recipe_module.get_recipe_id() == MultiFactorAuthRecipe.recipe_id: + elif recipe_module.get_recipe_id() == "multifactorauth": multi_factor_auth_found = True - elif recipe_module.get_recipe_id() == TOTPRecipe.recipe_id: + elif recipe_module.get_recipe_id() == "totp": totp_found = True return recipe_module diff --git a/tests/auth-react/flask-server/app.py b/tests/auth-react/flask-server/app.py index c44758c08..55ed847c8 100644 --- a/tests/auth-react/flask-server/app.py +++ b/tests/auth-react/flask-server/app.py @@ -12,7 +12,7 @@ # License for the specific language governing permissions and limitations # under the License. import os -from typing import Any, Dict, List, Optional, Union +from typing import Any, Awaitable, Callable, Dict, List, Optional, Union from dotenv import load_dotenv from flask import Flask, g, jsonify, make_response, request @@ -26,20 +26,56 @@ get_all_cors_headers, init, ) +from supertokens_python.auth_utils import LinkingToSessionUserFailedError from supertokens_python.framework.flask.flask_middleware import Middleware from supertokens_python.framework.request import BaseRequest from supertokens_python.recipe import ( + accountlinking, emailpassword, emailverification, passwordless, session, thirdparty, + totp, userroles, ) +from supertokens_python.recipe.accountlinking.recipe import AccountLinkingRecipe +from supertokens_python.recipe.accountlinking.types import ( + AccountInfoWithRecipeIdAndUserId, +) from supertokens_python.recipe.dashboard import DashboardRecipe from supertokens_python.recipe.emailpassword import EmailPasswordRecipe from supertokens_python.recipe.emailpassword.interfaces import ( APIInterface as EmailPasswordAPIInterface, + EmailAlreadyExistsError, + UnknownUserIdError, + UpdateEmailOrPasswordEmailChangeNotAllowedError, + UpdateEmailOrPasswordOkResult, +) +from supertokens_python.recipe.multifactorauth.interfaces import ( + ResyncSessionAndFetchMFAInfoPUTOkResult, +) +from supertokens_python.recipe.multifactorauth.recipe import MultiFactorAuthRecipe +from supertokens_python.recipe.multifactorauth.types import MFARequirementList +from supertokens_python.recipe.multitenancy.interfaces import ( + AssociateUserToTenantEmailAlreadyExistsError, + AssociateUserToTenantOkResult, + AssociateUserToTenantPhoneNumberAlreadyExistsError, + AssociateUserToTenantThirdPartyUserAlreadyExistsError, + AssociateUserToTenantUnknownUserIdError, + TenantConfigCreateOrUpdate, +) +from supertokens_python.recipe.multitenancy.syncio import ( + associate_user_to_tenant, + create_or_update_tenant, + create_or_update_third_party_config, + delete_tenant, + disassociate_user_from_tenant, +) +from supertokens_python.recipe.passwordless.syncio import update_user +from supertokens_python.recipe.session.exceptions import ( + ClaimValidationError, + InvalidClaimsError, ) from supertokens_python.recipe.thirdparty.provider import Provider, RedirectUriInfo from supertokens_python.recipe.emailpassword.interfaces import ( @@ -72,6 +108,11 @@ ) from supertokens_python.recipe.passwordless.interfaces import ( APIInterface as PasswordlessAPIInterface, + EmailChangeNotAllowedError, + UpdateUserEmailAlreadyExistsError, + UpdateUserOkResult, + UpdateUserPhoneNumberAlreadyExistsError, + UpdateUserUnknownUserIdError, ) from supertokens_python.recipe.passwordless.interfaces import APIOptions as PAPIOptions from supertokens_python.recipe.session import SessionRecipe @@ -86,12 +127,16 @@ SessionClaimValidator, SessionContainer, ) -from supertokens_python.recipe.thirdparty import ThirdPartyRecipe +from supertokens_python.recipe.thirdparty import ProviderConfig, ThirdPartyRecipe from supertokens_python.recipe.thirdparty.interfaces import ( APIInterface as ThirdpartyAPIInterface, + ManuallyCreateOrUpdateUserOkResult, + SignInUpNotAllowed, ) from supertokens_python.recipe.thirdparty.interfaces import APIOptions as TPAPIOptions from supertokens_python.recipe.thirdparty.provider import Provider +from supertokens_python.recipe.thirdparty.syncio import manually_create_or_update_user +from supertokens_python.recipe.totp.recipe import TOTPRecipe from supertokens_python.recipe.userroles import ( PermissionClaim, @@ -104,10 +149,12 @@ ) from supertokens_python.types import ( AccountInfo, + RecipeUserId, User, GeneralErrorResponse, ) -from supertokens_python.syncio import delete_user, list_users_by_account_info +from supertokens_python.syncio import delete_user, get_user, list_users_by_account_info +from supertokens_python.recipe import multifactorauth load_dotenv() @@ -129,6 +176,14 @@ def get_website_domain(): latest_url_with_token = None code_store: Dict[str, List[Dict[str, Any]]] = {} +accountlinking_config: Dict[str, Any] = {} +enabled_providers: Optional[List[Any]] = None +enabled_recipes: Optional[List[Any]] = None +mfa_info: Dict[str, Any] = {} +contact_method: Union[None, Literal["PHONE", "EMAIL", "EMAIL_OR_PHONE"]] = None +flow_type: Union[ + None, Literal["USER_INPUT_CODE", "MAGIC_LINK", "USER_INPUT_CODE_AND_MAGIC_LINK"] +] = None class CustomPlessEmailService( @@ -266,12 +321,11 @@ async def get_user_info( # pylint: disable=no-self-use return oi -def custom_init( - contact_method: Union[None, Literal["PHONE", "EMAIL", "EMAIL_OR_PHONE"]] = None, - flow_type: Union[ - None, Literal["USER_INPUT_CODE", "MAGIC_LINK", "USER_INPUT_CODE_AND_MAGIC_LINK"] - ] = None, -): +def custom_init(): + global contact_method + global flow_type + + AccountLinkingRecipe.reset() UserRolesRecipe.reset() PasswordlessRecipe.reset() JWTRecipe.reset() @@ -283,6 +337,8 @@ def custom_init( DashboardRecipe.reset() MultitenancyRecipe.reset() Supertokens.reset() + TOTPRecipe.reset() + MultiFactorAuthRecipe.reset() def override_email_verification_apis( original_implementation_email_verification: EmailVerificationAPIInterface, @@ -659,6 +715,14 @@ async def resend_code_post( ), ] + global enabled_providers + if enabled_providers is not None: + providers_list = [ + provider + for provider in providers_list + if provider.config.third_party_id in enabled_providers + ] + if contact_method is not None and flow_type is not None: if contact_method == "PHONE": passwordless_init = passwordless.init( @@ -706,33 +770,243 @@ async def get_allowed_domains_for_tenant_id( ) -> List[str]: return [tenant_id + ".example.com", "localhost"] - recipe_list = [ - userroles.init(), - session.init(override=session.InputOverrideConfig(apis=override_session_apis)), - emailverification.init( - mode="OPTIONAL", - email_delivery=emailverification.EmailDeliveryConfig( - CustomEVEmailService() + global mfa_info + + from supertokens_python.recipe.multifactorauth.interfaces import ( + RecipeInterface as MFARecipeInterface, + APIInterface as MFAApiInterface, + APIOptions as MFAApiOptions, + ) + + def override_mfa_functions(original_implementation: MFARecipeInterface): + og_get_factors_setup_for_user = ( + original_implementation.get_factors_setup_for_user + ) + + async def get_factors_setup_for_user( + user: User, + user_context: Dict[str, Any], + ): + res = await og_get_factors_setup_for_user(user, user_context) + if mfa_info.get("alreadySetup"): + return mfa_info.get("alreadySetup", []) + return res + + og_assert_allowed_to_setup_factor = ( + original_implementation.assert_allowed_to_setup_factor_else_throw_invalid_claim_error + ) + + async def assert_allowed_to_setup_factor_else_throw_invalid_claim_error( + session: SessionContainer, + factor_id: str, + mfa_requirements_for_auth: Callable[[], Awaitable[MFARequirementList]], + factors_set_up_for_user: Callable[[], Awaitable[List[str]]], + user_context: Dict[str, Any], + ): + if mfa_info.get("allowedToSetup"): + if factor_id not in mfa_info["allowedToSetup"]: + raise InvalidClaimsError( + msg="INVALID_CLAIMS", + payload=[ + ClaimValidationError(id_="test", reason="test override") + ], + ) + else: + await og_assert_allowed_to_setup_factor( + session, + factor_id, + mfa_requirements_for_auth, + factors_set_up_for_user, + user_context, + ) + + og_get_mfa_requirements_for_auth = ( + original_implementation.get_mfa_requirements_for_auth + ) + + async def get_mfa_requirements_for_auth( + tenant_id: str, + access_token_payload: Dict[str, Any], + completed_factors: Dict[str, int], + user: Callable[[], Awaitable[User]], + factors_set_up_for_user: Callable[[], Awaitable[List[str]]], + required_secondary_factors_for_user: Callable[[], Awaitable[List[str]]], + required_secondary_factors_for_tenant: Callable[[], Awaitable[List[str]]], + user_context: Dict[str, Any], + ) -> MFARequirementList: + res = await og_get_mfa_requirements_for_auth( + tenant_id, + access_token_payload, + completed_factors, + user, + factors_set_up_for_user, + required_secondary_factors_for_user, + required_secondary_factors_for_tenant, + user_context, + ) + if mfa_info.get("requirements"): + return mfa_info["requirements"] + return res + + original_implementation.get_mfa_requirements_for_auth = ( + get_mfa_requirements_for_auth + ) + + original_implementation.assert_allowed_to_setup_factor_else_throw_invalid_claim_error = ( + assert_allowed_to_setup_factor_else_throw_invalid_claim_error + ) + + original_implementation.get_factors_setup_for_user = get_factors_setup_for_user + return original_implementation + + def override_mfa_apis(original_implementation: MFAApiInterface): + og_resync_session_and_fetch_mfa_info_put = ( + original_implementation.resync_session_and_fetch_mfa_info_put + ) + + async def resync_session_and_fetch_mfa_info_put( + api_options: MFAApiOptions, + session: SessionContainer, + user_context: Dict[str, Any], + ) -> Union[ResyncSessionAndFetchMFAInfoPUTOkResult, GeneralErrorResponse]: + res = await og_resync_session_and_fetch_mfa_info_put( + api_options, session, user_context + ) + + if isinstance(res, ResyncSessionAndFetchMFAInfoPUTOkResult): + if mfa_info.get("alreadySetup"): + res.factors.already_setup = mfa_info["alreadySetup"][:] + + if mfa_info.get("noContacts"): + res.emails = {} + res.phone_numbers = {} + + return res + + original_implementation.resync_session_and_fetch_mfa_info_put = ( + resync_session_and_fetch_mfa_info_put + ) + return original_implementation + + recipe_list: List[Any] = [ + {"id": "userroles", "init": userroles.init()}, + { + "id": "session", + "init": session.init( + override=session.InputOverrideConfig(apis=override_session_apis) ), - override=EVInputOverrideConfig(apis=override_email_verification_apis), - ), - emailpassword.init( - sign_up_feature=emailpassword.InputSignUpFeature(form_fields), - email_delivery=emailpassword.EmailDeliveryConfig(CustomEPEmailService()), - override=emailpassword.InputOverrideConfig( - apis=override_email_password_apis, + }, + { + "id": "emailverification", + "init": emailverification.init( + mode="OPTIONAL", + email_delivery=emailverification.EmailDeliveryConfig( + CustomEVEmailService() + ), + override=EVInputOverrideConfig(apis=override_email_verification_apis), ), - ), - thirdparty.init( - sign_in_and_up_feature=thirdparty.SignInAndUpFeature(providers_list), - override=thirdparty.InputOverrideConfig(apis=override_thirdparty_apis), - ), - passwordless_init, - multitenancy.init( - get_allowed_domains_for_tenant_id=get_allowed_domains_for_tenant_id - ), + }, + { + "id": "emailpassword", + "init": emailpassword.init( + sign_up_feature=emailpassword.InputSignUpFeature(form_fields), + email_delivery=emailpassword.EmailDeliveryConfig( + CustomEPEmailService() + ), + override=emailpassword.InputOverrideConfig( + apis=override_email_password_apis, + ), + ), + }, + { + "id": "thirdparty", + "init": thirdparty.init( + sign_in_and_up_feature=thirdparty.SignInAndUpFeature(providers_list), + override=thirdparty.InputOverrideConfig(apis=override_thirdparty_apis), + ), + }, + { + "id": "passwordless", + "init": passwordless_init, + }, + { + "id": "multitenancy", + "init": multitenancy.init( + get_allowed_domains_for_tenant_id=get_allowed_domains_for_tenant_id + ), + }, + { + "id": "multifactorauth", + "init": multifactorauth.init( + first_factors=mfa_info.get("firstFactors", None), + override=multifactorauth.OverrideConfig( + functions=override_mfa_functions, + apis=override_mfa_apis, + ), + ), + }, + { + "id": "totp", + "init": totp.init( + config=totp.TOTPConfig( + default_period=1, + default_skew=30, + ) + ), + }, ] + global accountlinking_config + + accountlinking_config_input = { + "enabled": False, + "shouldAutoLink": { + "shouldAutomaticallyLink": True, + "shouldRequireVerification": True, + }, + **accountlinking_config, + } + + async def should_do_automatic_account_linking( + _: AccountInfoWithRecipeIdAndUserId, + __: Optional[User], + ___: Optional[SessionContainer], + ____: str, + _____: Dict[str, Any], + ) -> Union[ + accountlinking.ShouldNotAutomaticallyLink, + accountlinking.ShouldAutomaticallyLink, + ]: + should_auto_link = accountlinking_config_input["shouldAutoLink"] + assert isinstance(should_auto_link, dict) + should_automatically_link = should_auto_link["shouldAutomaticallyLink"] + assert isinstance(should_automatically_link, bool) + if should_automatically_link: + should_require_verification = should_auto_link["shouldRequireVerification"] + assert isinstance(should_require_verification, bool) + return accountlinking.ShouldAutomaticallyLink( + should_require_verification=should_require_verification + ) + return accountlinking.ShouldNotAutomaticallyLink() + + if accountlinking_config_input["enabled"]: + recipe_list.append( + { + "id": "accountlinking", + "init": accountlinking.init( + should_do_automatic_account_linking=should_do_automatic_account_linking + ), + } + ) + + global enabled_recipes + if enabled_recipes is not None: + recipe_list = [ + item["init"] for item in recipe_list if item["id"] in enabled_recipes + ] + else: + recipe_list = [item["init"] for item in recipe_list] + init( supertokens_config=SupertokensConfig("http://localhost:9000"), app_info=InputAppInfo( @@ -771,6 +1045,180 @@ def ping(): return "success" +@app.route("/changeEmail", methods=["POST"]) # type: ignore +def change_email(): + body: Union[Any, None] = request.get_json() + if body is None: + raise Exception("Should never come here") + from supertokens_python.recipe.emailpassword.syncio import update_email_or_password + from supertokens_python import convert_to_recipe_user_id + + if body["rid"] == "emailpassword": + resp = update_email_or_password( + recipe_user_id=convert_to_recipe_user_id(body["recipeUserId"]), + email=body["email"], + tenant_id_for_password_policy=body["tenantId"], + ) + if isinstance(resp, UpdateEmailOrPasswordOkResult): + return jsonify({"status": "OK"}) + if isinstance(resp, EmailAlreadyExistsError): + return jsonify({"status": "EMAIL_ALREADY_EXISTS_ERROR"}) + if isinstance(resp, UnknownUserIdError): + return jsonify({"status": "UNKNOWN_USER_ID_ERROR"}) + if isinstance(resp, UpdateEmailOrPasswordEmailChangeNotAllowedError): + return jsonify( + {"status": "EMAIL_CHANGE_NOT_ALLOWED_ERROR", "reason": resp.reason} + ) + # password policy violation error + return jsonify(resp.to_json()) + elif body["rid"] == "thirdparty": + user = get_user(user_id=body["recipeUserId"]) + assert user is not None + login_method = next( + lm + for lm in user.login_methods + if lm.recipe_user_id.get_as_string() == body["recipeUserId"] + ) + assert login_method is not None + assert login_method.third_party is not None + resp = manually_create_or_update_user( + tenant_id=body["tenantId"], + third_party_id=login_method.third_party.id, + third_party_user_id=login_method.third_party.user_id, + email=body["email"], + is_verified=False, + ) + if isinstance(resp, ManuallyCreateOrUpdateUserOkResult): + return jsonify( + {"status": "OK", "createdNewRecipeUser": resp.created_new_recipe_user} + ) + if isinstance(resp, LinkingToSessionUserFailedError): + raise Exception("Should not come here") + if isinstance(resp, SignInUpNotAllowed): + return jsonify({"status": "SIGN_IN_UP_NOT_ALLOWED", "reason": resp.reason}) + # EmailChangeNotAllowedError + return jsonify( + {"status": "EMAIL_CHANGE_NOT_ALLOWED_ERROR", "reason": resp.reason} + ) + elif body["rid"] == "passwordless": + resp = update_user( + recipe_user_id=convert_to_recipe_user_id(body["recipeUserId"]), + email=body.get("email"), + phone_number=body.get("phoneNumber"), + ) + + if isinstance(resp, UpdateUserOkResult): + return jsonify({"status": "OK"}) + if isinstance(resp, UpdateUserUnknownUserIdError): + return jsonify({"status": "UNKNOWN_USER_ID_ERROR"}) + if isinstance(resp, UpdateUserEmailAlreadyExistsError): + return jsonify({"status": "EMAIL_ALREADY_EXISTS_ERROR"}) + if isinstance(resp, UpdateUserPhoneNumberAlreadyExistsError): + return jsonify({"status": "PHONE_NUMBER_ALREADY_EXISTS_ERROR"}) + if isinstance(resp, EmailChangeNotAllowedError): + return jsonify( + {"status": "EMAIL_CHANGE_NOT_ALLOWED_ERROR", "reason": resp.reason} + ) + return jsonify( + {"status": "PHONE_NUMBER_CHANGE_NOT_ALLOWED_ERROR", "reason": resp.reason} + ) + + raise Exception("Should not come here") + + +@app.route("/setupTenant", methods=["POST"]) # type: ignore +def setup_tenant(): + body = request.get_json() + if body is None: + raise Exception("Should never come here") + tenant_id = body["tenantId"] + login_methods = body["loginMethods"] + core_config = body["coreConfig"] + + first_factors: List[str] = [] + if login_methods.get("emailPassword", {}).get("enabled") == True: + first_factors.append("emailpassword") + if login_methods.get("thirdParty", {}).get("enabled") == True: + first_factors.append("thirdparty") + if login_methods.get("passwordless", {}).get("enabled") == True: + first_factors.extend(["otp-phone", "otp-email", "link-phone", "link-email"]) + + core_resp = create_or_update_tenant( + tenant_id, + config=TenantConfigCreateOrUpdate( + first_factors=first_factors, + core_config=core_config, + ), + ) + + if login_methods.get("thirdParty", {}).get("providers") is not None: + for provider in login_methods["thirdParty"]["providers"]: + if ( + len(provider) > 1 + ): # TODO: remove this once all tests pass, this is just for making sure we pass the right stuff into ProviderConfig + raise Exception("Pass more stuff into ProviderConfig:" + str(provider)) + create_or_update_third_party_config( + tenant_id, + config=ProviderConfig( + third_party_id=provider["id"], + ), + ) + + return jsonify({"status": "OK", "createdNew": core_resp.created_new}) + + +@app.route("/addUserToTenant", methods=["POST"]) # type: ignore +def add_user_to_tenant(): + body = request.get_json() + if body is None: + raise Exception("Should never come here") + tenant_id = body["tenantId"] + recipe_user_id = body["recipeUserId"] + + core_resp = associate_user_to_tenant(tenant_id, RecipeUserId(recipe_user_id)) + + if isinstance(core_resp, AssociateUserToTenantOkResult): + return jsonify( + {"status": "OK", "wasAlreadyAssociated": core_resp.was_already_associated} + ) + elif isinstance(core_resp, AssociateUserToTenantUnknownUserIdError): + return jsonify({"status": "UNKNOWN_USER_ID_ERROR"}) + elif isinstance(core_resp, AssociateUserToTenantEmailAlreadyExistsError): + return jsonify({"status": "EMAIL_ALREADY_EXISTS_ERROR"}) + elif isinstance(core_resp, AssociateUserToTenantPhoneNumberAlreadyExistsError): + return jsonify({"status": "PHONE_NUMBER_ALREADY_EXISTS_ERROR"}) + elif isinstance(core_resp, AssociateUserToTenantThirdPartyUserAlreadyExistsError): + return jsonify({"status": "THIRD_PARTY_USER_ALREADY_EXISTS_ERROR"}) + return jsonify( + {"status": "ASSOCIATION_NOT_ALLOWED_ERROR", "reason": core_resp.reason} + ) + + +@app.route("/removeUserFromTenant", methods=["POST"]) # type: ignore +def remove_user_from_tenant(): + body = request.get_json() + if body is None: + raise Exception("Should never come here") + tenant_id = body["tenantId"] + recipe_user_id = body["recipeUserId"] + + core_resp = disassociate_user_from_tenant(tenant_id, RecipeUserId(recipe_user_id)) + + return jsonify({"status": "OK", "wasAssociated": core_resp.was_associated}) + + +@app.route("/removeTenant", methods=["POST"]) # type: ignore +def remove_tenant(): + body = request.get_json() + if body is None: + raise Exception("Should never come here") + tenant_id = body["tenantId"] + + core_resp = delete_tenant(tenant_id) + + return jsonify({"status": "OK", "didExist": core_resp.did_exist}) + + @app.route("/sessionInfo", methods=["GET"]) # type: ignore @verify_session() def get_session_info(): @@ -794,7 +1242,15 @@ def get_token(): @app.route("/beforeeach", methods=["POST"]) # type: ignore def before_each(): global code_store + global accountlinking_config + global enabled_providers + global enabled_recipes + global mfa_info code_store = dict() + accountlinking_config = {} + enabled_providers = None + enabled_recipes = None + mfa_info = {} custom_init() return "" @@ -804,12 +1260,38 @@ def test_set_flow(): body: Union[Any, None] = request.get_json() if body is None: raise Exception("Should never come here") + global contact_method + global flow_type contact_method = body["contactMethod"] flow_type = body["flowType"] - custom_init(contact_method=contact_method, flow_type=flow_type) + custom_init() return "" +@app.route("/test/setAccountLinkingConfig", methods=["POST"]) # type: ignore +def test_set_account_linking_config(): + global accountlinking_config + body = request.get_json() + if body is None: + raise Exception("Invalid request body") + accountlinking_config = body + custom_init() + return "", 200 + + +@app.route("/test/setEnabledRecipes", methods=["POST"]) # type: ignore +def test_set_enabled_recipes(): + global enabled_recipes + global enabled_providers + body = request.get_json() + if body is None: + raise Exception("Invalid request body") + enabled_recipes = body.get("enabledRecipes") + enabled_providers = body.get("enabledProviders") + custom_init() + return "", 200 + + @app.get("/test/getDevice") # type: ignore def test_get_device(): global code_store @@ -828,6 +1310,11 @@ def test_feature_flags(): "generalerror", "userroles", "multitenancy", + "multitenancyManagementEndpoints", + "accountlinking", + "mfa", + "recipeConfig", + "accountlinking-fixes", ] return jsonify({"available": available}) diff --git a/tests/multitenancy/test_tenants_crud.py b/tests/multitenancy/test_tenants_crud.py index 73d60a388..f048fc99a 100644 --- a/tests/multitenancy/test_tenants_crud.py +++ b/tests/multitenancy/test_tenants_crud.py @@ -42,7 +42,7 @@ create_or_update_third_party_config, delete_third_party_config, associate_user_to_tenant, - dissociate_user_from_tenant, + disassociate_user_from_tenant, ) from supertokens_python.recipe.emailpassword.asyncio import sign_up from supertokens_python.recipe.emailpassword.interfaces import SignUpOkResult @@ -305,9 +305,9 @@ async def test_user_association_and_disassociation_with_tenants(): assert user is not None assert len(user.tenant_ids) == 4 # public + 3 tenants - await dissociate_user_from_tenant("t1", RecipeUserId(user_id)) - await dissociate_user_from_tenant("t2", RecipeUserId(user_id)) - await dissociate_user_from_tenant("t3", RecipeUserId(user_id)) + await disassociate_user_from_tenant("t1", RecipeUserId(user_id)) + await disassociate_user_from_tenant("t2", RecipeUserId(user_id)) + await disassociate_user_from_tenant("t3", RecipeUserId(user_id)) user = await get_user(user_id) assert user is not None diff --git a/tests/sessions/claims/test_validate_claims_for_session_handle.py b/tests/sessions/claims/test_validate_claims_for_session_handle.py index 71c62c69d..ccad5b464 100644 --- a/tests/sessions/claims/test_validate_claims_for_session_handle.py +++ b/tests/sessions/claims/test_validate_claims_for_session_handle.py @@ -41,7 +41,7 @@ async def test_should_return_the_right_validation_errors(): ) assert isinstance(res, ClaimsValidationResult) and len(res.invalid_claims) == 1 - assert res.invalid_claims[0].id == failing_validator.id + assert res.invalid_claims[0].id_ == failing_validator.id assert res.invalid_claims[0].reason == { "message": "value does not exist", "actualValue": None, diff --git a/tests/test-server/multitenancy.py b/tests/test-server/multitenancy.py index ff2aac785..4da9df85a 100644 --- a/tests/test-server/multitenancy.py +++ b/tests/test-server/multitenancy.py @@ -203,7 +203,7 @@ def disassociate_user_from_tenant(): # type: ignore user_id = data["userId"] user_context = data.get("userContext") - response = multitenancy.dissociate_user_from_tenant( + response = multitenancy.disassociate_user_from_tenant( tenant_id, user_id, user_context ) diff --git a/tests/userroles/test_claims.py b/tests/userroles/test_claims.py index 455f915b7..db13577fb 100644 --- a/tests/userroles/test_claims.py +++ b/tests/userroles/test_claims.py @@ -135,7 +135,8 @@ async def test_should_validate_roles(): assert e.typename == "InvalidClaimsError" err: ClaimValidationError (err,) = e.value.payload # type: ignore - assert err.id == UserRoleClaim.key + assert isinstance(err, ClaimValidationError) + assert err.id_ == UserRoleClaim.key assert err.reason == { "message": "wrong value", "expectedToInclude": invalid_role, @@ -196,8 +197,10 @@ async def test_should_validate_permissions(): assert e.typename == "InvalidClaimsError" err: ClaimValidationError (err,) = e.value.payload # type: ignore - assert err.id == PermissionClaim.key + assert isinstance(err, ClaimValidationError) + assert err.id_ == PermissionClaim.key assert err.reason is not None + assert isinstance(err.reason, dict) actual_value = err.reason.pop("actualValue") assert sorted(actual_value) == sorted(permissions) assert err.reason == { From eae7e2de900467fc1d8213c9bfc2b64cd87dc6a9 Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Mon, 30 Sep 2024 15:44:24 +0530 Subject: [PATCH 2/6] cyclic import issue --- supertokens_python/auth_utils.py | 8 +++- .../multifactorauth/api/implementation.py | 23 +++++---- .../api/resync_session_and_fetch_mfa_info.py | 2 +- .../multifactorauth/asyncio/__init__.py | 16 +++++-- .../recipe/multifactorauth/interfaces.py | 6 +-- .../multi_factor_auth_claim.py | 46 +++++++++++------- .../recipe/multifactorauth/recipe.py | 7 +-- .../multifactorauth/recipe_implementation.py | 5 +- .../recipe/multifactorauth/syncio/__init__.py | 4 -- .../recipe/multifactorauth/types.py | 4 +- .../recipe/multifactorauth/utils.py | 47 +++++++++++++------ .../recipe/multitenancy/asyncio/__init__.py | 23 +++++++-- .../recipe/multitenancy/interfaces.py | 11 ++++- .../multitenancy/recipe_implementation.py | 6 ++- .../recipe/multitenancy/syncio/__init__.py | 6 +-- .../recipe/session/exceptions.py | 11 +++-- .../recipe/thirdparty/asyncio/__init__.py | 2 +- .../recipe/thirdparty/syncio/__init__.py | 2 +- supertokens_python/recipe/totp/interfaces.py | 19 +++++++- .../recipe/totp/syncio/__init__.py | 4 -- supertokens_python/recipe/totp/types.py | 8 ---- 21 files changed, 171 insertions(+), 89 deletions(-) diff --git a/supertokens_python/auth_utils.py b/supertokens_python/auth_utils.py index da67d7b87..ff3315ba3 100644 --- a/supertokens_python/auth_utils.py +++ b/supertokens_python/auth_utils.py @@ -923,8 +923,14 @@ async def get_factors_set_up_for_user(): async def get_mfa_requirements_for_auth(): nonlocal mfa_info_prom if mfa_info_prom is None: + from .recipe.multifactorauth.multi_factor_auth_claim import ( + MultiFactorAuthClaim, + ) + mfa_info_prom = await update_and_get_mfa_related_info_in_session( - input_session=session, user_context=user_context + MultiFactorAuthClaim, + input_session=session, + user_context=user_context, ) return mfa_info_prom.mfa_requirements_for_auth diff --git a/supertokens_python/recipe/multifactorauth/api/implementation.py b/supertokens_python/recipe/multifactorauth/api/implementation.py index 9f3dd774b..d6fe15e98 100644 --- a/supertokens_python/recipe/multifactorauth/api/implementation.py +++ b/supertokens_python/recipe/multifactorauth/api/implementation.py @@ -12,15 +12,15 @@ # License for the specific language governing permissions and limitations # under the License. from __future__ import annotations +import importlib -from typing import TYPE_CHECKING, Any, Dict, List, Union +from typing import Any, Dict, List, Union, TYPE_CHECKING from supertokens_python.recipe.session import SessionContainer from supertokens_python.recipe.multifactorauth.utils import ( update_and_get_mfa_related_info_in_session, ) from supertokens_python.recipe.multitenancy.asyncio import get_tenant -from ..multi_factor_auth_claim import MultiFactorAuthClaim from supertokens_python.asyncio import get_user from supertokens_python.recipe.session.exceptions import ( InvalidClaimsError, @@ -28,12 +28,6 @@ UnauthorisedError, ) -if TYPE_CHECKING: - from supertokens_python.recipe.multifactorauth.interfaces import ( - APIInterface, - APIOptions, - ) - from supertokens_python.types import GeneralErrorResponse from ..interfaces import ( APIInterface, @@ -42,6 +36,11 @@ ResyncSessionAndFetchMFAInfoPUTOkResult, ) +if TYPE_CHECKING: + from ..multi_factor_auth_claim import ( + MultiFactorAuthClaimClass as MultiFactorAuthClaimType, + ) + class APIImplementation(APIInterface): async def resync_session_and_fetch_mfa_info_put( @@ -50,6 +49,11 @@ async def resync_session_and_fetch_mfa_info_put( session: SessionContainer, user_context: Dict[str, Any], ) -> Union[ResyncSessionAndFetchMFAInfoPUTOkResult, GeneralErrorResponse]: + + mfa = importlib.import_module("supertokens_python.recipe.multifactorauth") + + MultiFactorAuthClaim: MultiFactorAuthClaimType = mfa.MultiFactorAuthClaim + session_user = await get_user(session.get_user_id(), user_context) if session_user is None: @@ -58,6 +62,7 @@ async def resync_session_and_fetch_mfa_info_put( ) mfa_info = await update_and_get_mfa_related_info_in_session( + MultiFactorAuthClaim, input_session=session, user_context=user_context, ) @@ -144,7 +149,7 @@ async def get_mfa_requirements_for_auth(): ) return ResyncSessionAndFetchMFAInfoPUTOkResult( factors=NextFactors( - next=next_factors, + next_=next_factors, already_setup=factors_setup_for_user, allowed_to_setup=factors_allowed_to_setup, ), diff --git a/supertokens_python/recipe/multifactorauth/api/resync_session_and_fetch_mfa_info.py b/supertokens_python/recipe/multifactorauth/api/resync_session_and_fetch_mfa_info.py index 1048253e6..8d7f1e8eb 100644 --- a/supertokens_python/recipe/multifactorauth/api/resync_session_and_fetch_mfa_info.py +++ b/supertokens_python/recipe/multifactorauth/api/resync_session_and_fetch_mfa_info.py @@ -27,7 +27,7 @@ async def handle_resync_session_and_fetch_mfa_info_api( - tenant_id: str, + _tenant_id: str, api_implementation: APIInterface, api_options: APIOptions, user_context: Dict[str, Any], diff --git a/supertokens_python/recipe/multifactorauth/asyncio/__init__.py b/supertokens_python/recipe/multifactorauth/asyncio/__init__.py index 56817eb5f..4c8af2e54 100644 --- a/supertokens_python/recipe/multifactorauth/asyncio/__init__.py +++ b/supertokens_python/recipe/multifactorauth/asyncio/__init__.py @@ -21,7 +21,6 @@ from ..types import ( MFARequirementList, ) -from ..recipe import MultiFactorAuthRecipe from ..utils import update_and_get_mfa_related_info_in_session from supertokens_python.recipe.accountlinking.asyncio import get_user @@ -34,13 +33,17 @@ async def assert_allowed_to_setup_factor_else_throw_invalid_claim_error( if user_context is None: user_context = {} + from ..multi_factor_auth_claim import MultiFactorAuthClaim + mfa_info = await update_and_get_mfa_related_info_in_session( + MultiFactorAuthClaim, input_session=session, user_context=user_context, ) factors_set_up_for_user = await get_factors_setup_for_user( session.get_user_id(), user_context ) + from ..recipe import MultiFactorAuthRecipe recipe = MultiFactorAuthRecipe.get_instance_or_throw_error() @@ -66,7 +69,10 @@ async def get_mfa_requirements_for_auth( if user_context is None: user_context = {} + from ..multi_factor_auth_claim import MultiFactorAuthClaim + mfa_info = await update_and_get_mfa_related_info_in_session( + MultiFactorAuthClaim, input_session=session, user_context=user_context, ) @@ -81,6 +87,7 @@ async def mark_factor_as_complete_in_session( ) -> None: if user_context is None: user_context = {} + from ..recipe import MultiFactorAuthRecipe recipe = MultiFactorAuthRecipe.get_instance_or_throw_error() await recipe.recipe_implementation.mark_factor_as_complete_in_session( @@ -100,6 +107,7 @@ async def get_factors_setup_for_user( user = await get_user(user_id, user_context) if user is None: raise Exception("Unknown user id") + from ..recipe import MultiFactorAuthRecipe recipe = MultiFactorAuthRecipe.get_instance_or_throw_error() return await recipe.recipe_implementation.get_factors_setup_for_user( @@ -114,6 +122,7 @@ async def get_required_secondary_factors_for_user( ) -> List[str]: if user_context is None: user_context = {} + from ..recipe import MultiFactorAuthRecipe recipe = MultiFactorAuthRecipe.get_instance_or_throw_error() return await recipe.recipe_implementation.get_required_secondary_factors_for_user( @@ -129,6 +138,7 @@ async def add_to_required_secondary_factors_for_user( ) -> None: if user_context is None: user_context = {} + from ..recipe import MultiFactorAuthRecipe recipe = MultiFactorAuthRecipe.get_instance_or_throw_error() await recipe.recipe_implementation.add_to_required_secondary_factors_for_user( @@ -145,6 +155,7 @@ async def remove_from_required_secondary_factors_for_user( ) -> None: if user_context is None: user_context = {} + from ..recipe import MultiFactorAuthRecipe recipe = MultiFactorAuthRecipe.get_instance_or_throw_error() await recipe.recipe_implementation.remove_from_required_secondary_factors_for_user( @@ -152,6 +163,3 @@ async def remove_from_required_secondary_factors_for_user( factor_id=factor_id, user_context=user_context, ) - - -init = MultiFactorAuthRecipe.init diff --git a/supertokens_python/recipe/multifactorauth/interfaces.py b/supertokens_python/recipe/multifactorauth/interfaces.py index 9f237ca98..f960e9ffe 100644 --- a/supertokens_python/recipe/multifactorauth/interfaces.py +++ b/supertokens_python/recipe/multifactorauth/interfaces.py @@ -123,15 +123,15 @@ async def resync_session_and_fetch_mfa_info_put( class NextFactors: def __init__( - self, next: List[str], already_setup: List[str], allowed_to_setup: List[str] + self, next_: List[str], already_setup: List[str], allowed_to_setup: List[str] ): - self.next = next + self.next_ = next_ self.already_setup = already_setup self.allowed_to_setup = allowed_to_setup def to_json(self) -> Dict[str, Any]: return { - "next": self.next, + "next": self.next_, "alreadySetup": self.already_setup, "allowedToSetup": self.allowed_to_setup, } diff --git a/supertokens_python/recipe/multifactorauth/multi_factor_auth_claim.py b/supertokens_python/recipe/multifactorauth/multi_factor_auth_claim.py index 4545f3c63..4b9eeef28 100644 --- a/supertokens_python/recipe/multifactorauth/multi_factor_auth_claim.py +++ b/supertokens_python/recipe/multifactorauth/multi_factor_auth_claim.py @@ -1,3 +1,17 @@ +# Copyright (c) 2023, VRAI Labs and/or its affiliates. All rights reserved. +# +# This software is licensed under the Apache License, Version 2.0 (the +# "License") as published by the Apache Software Foundation. +# +# You may not use this file except in compliance with the License. You may +# obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + from __future__ import annotations from typing import Any, Dict, Optional, Set @@ -15,7 +29,6 @@ MFAClaimValue, MFARequirementList, ) -from .utils import update_and_get_mfa_related_info_in_session class HasCompletedRequirementListSCV(SessionClaimValidator): @@ -29,14 +42,10 @@ def __init__( self.claim: MultiFactorAuthClaimClass = claim self.requirement_list = requirement_list - async def should_refetch( + def should_refetch( self, payload: Dict[str, Any], user_context: Dict[str, Any] ) -> bool: - return ( - True - if self.claim.key not in payload or not payload[self.claim.key] - else False - ) + return bool(self.claim.key not in payload or not payload[self.claim.key]) async def validate( self, payload: JSONObject, user_context: Dict[str, Any] @@ -65,7 +74,7 @@ async def validate( factor_ids = next_set_of_unsatisfied_factors.factor_ids - if next_set_of_unsatisfied_factors.type == "string": + if next_set_of_unsatisfied_factors.type_ == "string": return ClaimValidationResult( is_valid=False, reason={ @@ -74,7 +83,7 @@ async def validate( }, ) - elif next_set_of_unsatisfied_factors.type == "oneOf": + elif next_set_of_unsatisfied_factors.type_ == "oneOf": return ClaimValidationResult( is_valid=False, reason={ @@ -101,15 +110,11 @@ def __init__( super().__init__(id_) self.claim = claim - async def should_refetch( + def should_refetch( self, payload: Dict[str, Any], user_context: Dict[str, Any] ) -> bool: assert self.claim is not None - return ( - True - if self.claim.key not in payload or not payload[self.claim.key] - else False - ) + return bool(self.claim.key not in payload or not payload[self.claim.key]) async def validate( self, payload: JSONObject, user_context: Dict[str, Any] @@ -161,13 +166,16 @@ def __init__(self, key: Optional[str] = None): key = key or "st-mfa" async def fetch_value( - user_id: str, + _user_id: str, recipe_user_id: RecipeUserId, tenant_id: str, current_payload: Dict[str, Any], user_context: Dict[str, Any], ) -> MFAClaimValue: + from .utils import update_and_get_mfa_related_info_in_session + mfa_info = await update_and_get_mfa_related_info_in_session( + self, input_session_recipe_user_id=recipe_user_id, input_tenant_id=tenant_id, input_access_token_payload=current_payload, @@ -209,9 +217,11 @@ def get_next_set_of_unsatisfied_factors( ) if len(next_factors) > 0: - return FactorIdsAndType(factor_ids=list(next_factors), type=factor_type) + return FactorIdsAndType( + factor_ids=list(next_factors), type_=factor_type + ) - return FactorIdsAndType(factor_ids=[], type="string") + return FactorIdsAndType(factor_ids=[], type_="string") def add_to_payload_( self, diff --git a/supertokens_python/recipe/multifactorauth/recipe.py b/supertokens_python/recipe/multifactorauth/recipe.py index f45db7316..f60c30c34 100644 --- a/supertokens_python/recipe/multifactorauth/recipe.py +++ b/supertokens_python/recipe/multifactorauth/recipe.py @@ -47,9 +47,6 @@ GetEmailsForFactorOkResult, GetPhoneNumbersForFactorsOkResult, ) -from .utils import validate_and_normalise_user_input -from .recipe_implementation import RecipeImplementation -from .api.implementation import APIImplementation from .interfaces import APIOptions @@ -79,10 +76,13 @@ def __init__( ] = [] self.is_get_mfa_requirements_for_auth_overridden: bool = False + from .utils import validate_and_normalise_user_input + self.config = validate_and_normalise_user_input( first_factors, override, ) + from .recipe_implementation import RecipeImplementation recipe_implementation = RecipeImplementation( Querier.get_instance(recipe_id), self @@ -92,6 +92,7 @@ def __init__( if self.config.override.functions is None else self.config.override.functions(recipe_implementation) ) + from .api.implementation import APIImplementation api_implementation = APIImplementation() self.api_implementation = ( diff --git a/supertokens_python/recipe/multifactorauth/recipe_implementation.py b/supertokens_python/recipe/multifactorauth/recipe_implementation.py index e2bc64251..9ae8f4a97 100644 --- a/supertokens_python/recipe/multifactorauth/recipe_implementation.py +++ b/supertokens_python/recipe/multifactorauth/recipe_implementation.py @@ -58,10 +58,10 @@ def __init__( self.factor_id = factor_id self.mfa_requirement_for_auth = mfa_requirement_for_auth - async def should_refetch( + def should_refetch( self, payload: Dict[str, Any], user_context: Dict[str, Any] ) -> bool: - return True if self.claim.get_value_from_payload(payload) is None else False + return self.claim.get_value_from_payload(payload) is None async def validate( self, payload: JSONObject, user_context: Dict[str, Any] @@ -174,6 +174,7 @@ async def mark_factor_as_complete_in_session( self, session: SessionContainer, factor_id: str, user_context: Dict[str, Any] ): await update_and_get_mfa_related_info_in_session( + MultiFactorAuthClaim, input_session=session, input_updated_factor_id=factor_id, user_context=user_context, diff --git a/supertokens_python/recipe/multifactorauth/syncio/__init__.py b/supertokens_python/recipe/multifactorauth/syncio/__init__.py index c12a6ef4a..6bd9bf9f2 100644 --- a/supertokens_python/recipe/multifactorauth/syncio/__init__.py +++ b/supertokens_python/recipe/multifactorauth/syncio/__init__.py @@ -22,7 +22,6 @@ from ..interfaces import ( MFARequirementList, ) -from ..recipe import MultiFactorAuthRecipe def assert_allowed_to_setup_factor_else_throw_invalid_claim_error( @@ -125,6 +124,3 @@ def remove_from_required_secondary_factors_for_user( ) return sync(async_func(user_id, factor_id, user_context)) - - -init = MultiFactorAuthRecipe.init diff --git a/supertokens_python/recipe/multifactorauth/types.py b/supertokens_python/recipe/multifactorauth/types.py index d43477291..779c8ad77 100644 --- a/supertokens_python/recipe/multifactorauth/types.py +++ b/supertokens_python/recipe/multifactorauth/types.py @@ -85,10 +85,10 @@ class FactorIdsAndType: def __init__( self, factor_ids: List[str], - type: Union[Literal["string"], Literal["oneOf"], Literal["allOfInAnyOrder"]], + type_: Union[Literal["string"], Literal["oneOf"], Literal["allOfInAnyOrder"]], ): self.factor_ids = factor_ids - self.type = type + self.type_ = type_ class GetFactorsSetupForUserFromOtherRecipesFunc: diff --git a/supertokens_python/recipe/multifactorauth/utils.py b/supertokens_python/recipe/multifactorauth/utils.py index cd9dcbad4..d28a48175 100644 --- a/supertokens_python/recipe/multifactorauth/utils.py +++ b/supertokens_python/recipe/multifactorauth/utils.py @@ -12,19 +12,17 @@ # License for the specific language governing permissions and limitations # under the License. from __future__ import annotations +import importlib -from typing import TYPE_CHECKING, List, Optional, Union -from typing import Dict, Any, Union, List -from supertokens_python.recipe.multitenancy.recipe import MultitenancyRecipe +from typing import TYPE_CHECKING, List, Optional, Union, Dict, Any from supertokens_python.recipe.session import SessionContainer from supertokens_python.recipe.session.asyncio import get_session_information from supertokens_python.recipe.session.exceptions import UnauthorisedError -from supertokens_python.recipe.multitenancy.asyncio import get_tenant from supertokens_python.recipe.accountlinking.recipe import AccountLinkingRecipe -from supertokens_python.recipe.multifactorauth.types import FactorIds from supertokens_python.recipe.multifactorauth.types import ( MFAClaimValue, MFARequirementList, + FactorIds, ) from supertokens_python.types import RecipeUserId import math @@ -34,6 +32,12 @@ if TYPE_CHECKING: from .types import OverrideConfig, MultiFactorAuthConfig + from supertokens_python.recipe.multifactorauth.multi_factor_auth_claim import ( + MultiFactorAuthClaimClass, + ) + from supertokens_python.recipe.multitenancy.recipe import ( + MultitenancyRecipe as MTRecipeType, + ) def validate_and_normalise_user_input( @@ -43,10 +47,12 @@ def validate_and_normalise_user_input( if first_factors is not None and len(first_factors) == 0: raise ValueError("'first_factors' can be either None or a non-empty list") + from .types import OverrideConfig as OC, MultiFactorAuthConfig as MFAC + if override is None: - override = OverrideConfig() + override = OC() - return MultiFactorAuthConfig( + return MFAC( first_factors=first_factors, override=override, ) @@ -67,6 +73,7 @@ def __init__( async def update_and_get_mfa_related_info_in_session( + MultiFactorAuthClaim: MultiFactorAuthClaimClass, user_context: Dict[str, Any], input_session_recipe_user_id: Optional[RecipeUserId] = None, input_tenant_id: Optional[str] = None, @@ -74,9 +81,6 @@ async def update_and_get_mfa_related_info_in_session( input_session: Optional[SessionContainer] = None, input_updated_factor_id: Optional[str] = None, ) -> UpdateAndGetMFARelatedInfoInSessionResult: - from supertokens_python.recipe.multifactorauth.multi_factor_auth_claim import ( - MultiFactorAuthClaim, - ) from supertokens_python.recipe.multifactorauth.recipe import ( MultiFactorAuthRecipe as Recipe, ) @@ -199,7 +203,16 @@ async def user_getter(): async def get_required_secondary_factors_for_tenant( tenant_id: str, user_context: Dict[str, Any] ) -> List[str]: - tenant_info = await get_tenant(tenant_id, user_context) + + MultitenancyRecipe = importlib.import_module( + "supertokens_python.recipe.multitenancy.recipe" + ) + + mt_recipe: MTRecipeType = MultitenancyRecipe.get_instance() + + tenant_info = await mt_recipe.recipe_implementation.get_tenant( + tenant_id=tenant_id, user_context=user_context + ) if tenant_info is None: raise UnauthorisedError("Tenant not found") return ( @@ -262,14 +275,20 @@ async def get_required_secondary_factors_for_tenant_helper() -> List[str]: async def is_valid_first_factor( tenant_id: str, factor_id: str, user_context: Dict[str, Any] ) -> Literal["OK", "INVALID_FIRST_FACTOR_ERROR", "TENANT_NOT_FOUND_ERROR"]: - tenant_info = await get_tenant(tenant_id=tenant_id, user_context=user_context) + + MultitenancyRecipe = importlib.import_module( + "supertokens_python.recipe.multitenancy.recipe" + ) + + mt_recipe: MTRecipeType = MultitenancyRecipe.get_instance() + tenant_info = await mt_recipe.recipe_implementation.get_tenant( + tenant_id=tenant_id, user_context=user_context + ) if tenant_info is None: return "TENANT_NOT_FOUND_ERROR" tenant_config = tenant_info - mt_recipe = MultitenancyRecipe.get_instance() - first_factors_from_mfa = mt_recipe.static_first_factors log_debug_message( diff --git a/supertokens_python/recipe/multitenancy/asyncio/__init__.py b/supertokens_python/recipe/multitenancy/asyncio/__init__.py index 4f8020401..c9b1c6ce7 100644 --- a/supertokens_python/recipe/multitenancy/asyncio/__init__.py +++ b/supertokens_python/recipe/multitenancy/asyncio/__init__.py @@ -17,6 +17,7 @@ from supertokens_python.types import RecipeUserId from ..interfaces import ( + AssociateUserToTenantNotAllowedError, TenantConfig, CreateOrUpdateTenantOkResult, DeleteTenantOkResult, @@ -31,7 +32,6 @@ DisassociateUserFromTenantOkResult, TenantConfigCreateOrUpdate, ) -from ..recipe import MultitenancyRecipe if TYPE_CHECKING: from ..interfaces import ProviderConfig @@ -44,6 +44,8 @@ async def create_or_update_tenant( ) -> CreateOrUpdateTenantOkResult: if user_context is None: user_context = {} + from ..recipe import MultitenancyRecipe + recipe = MultitenancyRecipe.get_instance() return await recipe.recipe_implementation.create_or_update_tenant( @@ -56,6 +58,8 @@ async def delete_tenant( ) -> DeleteTenantOkResult: if user_context is None: user_context = {} + from ..recipe import MultitenancyRecipe + recipe = MultitenancyRecipe.get_instance() return await recipe.recipe_implementation.delete_tenant(tenant_id, user_context) @@ -66,6 +70,8 @@ async def get_tenant( ) -> Optional[TenantConfig]: if user_context is None: user_context = {} + from ..recipe import MultitenancyRecipe + recipe = MultitenancyRecipe.get_instance() return await recipe.recipe_implementation.get_tenant(tenant_id, user_context) @@ -77,6 +83,8 @@ async def list_all_tenants( if user_context is None: user_context = {} + from ..recipe import MultitenancyRecipe + recipe = MultitenancyRecipe.get_instance() return await recipe.recipe_implementation.list_all_tenants(user_context) @@ -91,6 +99,8 @@ async def create_or_update_third_party_config( if user_context is None: user_context = {} + from ..recipe import MultitenancyRecipe + recipe = MultitenancyRecipe.get_instance() return await recipe.recipe_implementation.create_or_update_third_party_config( @@ -106,6 +116,8 @@ async def delete_third_party_config( if user_context is None: user_context = {} + from ..recipe import MultitenancyRecipe + recipe = MultitenancyRecipe.get_instance() return await recipe.recipe_implementation.delete_third_party_config( @@ -123,10 +135,13 @@ async def associate_user_to_tenant( AssociateUserToTenantEmailAlreadyExistsError, AssociateUserToTenantPhoneNumberAlreadyExistsError, AssociateUserToTenantThirdPartyUserAlreadyExistsError, + AssociateUserToTenantNotAllowedError, ]: if user_context is None: user_context = {} + from ..recipe import MultitenancyRecipe + recipe = MultitenancyRecipe.get_instance() return await recipe.recipe_implementation.associate_user_to_tenant( @@ -134,7 +149,7 @@ async def associate_user_to_tenant( ) -async def dissociate_user_from_tenant( +async def disassociate_user_from_tenant( tenant_id: str, recipe_user_id: RecipeUserId, user_context: Optional[Dict[str, Any]] = None, @@ -142,8 +157,10 @@ async def dissociate_user_from_tenant( if user_context is None: user_context = {} + from ..recipe import MultitenancyRecipe + recipe = MultitenancyRecipe.get_instance() - return await recipe.recipe_implementation.dissociate_user_from_tenant( + return await recipe.recipe_implementation.disassociate_user_from_tenant( tenant_id, recipe_user_id, user_context ) diff --git a/supertokens_python/recipe/multitenancy/interfaces.py b/supertokens_python/recipe/multitenancy/interfaces.py index 4fc5ef551..52e66b3f7 100644 --- a/supertokens_python/recipe/multitenancy/interfaces.py +++ b/supertokens_python/recipe/multitenancy/interfaces.py @@ -136,6 +136,14 @@ class AssociateUserToTenantThirdPartyUserAlreadyExistsError: status = "THIRD_PARTY_USER_ALREADY_EXISTS_ERROR" +class AssociateUserToTenantNotAllowedError: + status = "ASSOCIATION_NOT_ALLOWED_ERROR" + + def __init__(self, reason: str): + self.status = "ASSOCIATION_NOT_ALLOWED_ERROR" + self.reason = reason + + class DisassociateUserFromTenantOkResult: status = "OK" @@ -213,11 +221,12 @@ async def associate_user_to_tenant( AssociateUserToTenantEmailAlreadyExistsError, AssociateUserToTenantPhoneNumberAlreadyExistsError, AssociateUserToTenantThirdPartyUserAlreadyExistsError, + AssociateUserToTenantNotAllowedError, ]: pass @abstractmethod - async def dissociate_user_from_tenant( + async def disassociate_user_from_tenant( self, tenant_id: str, recipe_user_id: RecipeUserId, diff --git a/supertokens_python/recipe/multitenancy/recipe_implementation.py b/supertokens_python/recipe/multitenancy/recipe_implementation.py index 432ffc07d..c60f148f8 100644 --- a/supertokens_python/recipe/multitenancy/recipe_implementation.py +++ b/supertokens_python/recipe/multitenancy/recipe_implementation.py @@ -25,6 +25,7 @@ from supertokens_python.types import RecipeUserId from .interfaces import ( + AssociateUserToTenantNotAllowedError, RecipeInterface, TenantConfig, CreateOrUpdateTenantOkResult, @@ -254,6 +255,7 @@ async def associate_user_to_tenant( AssociateUserToTenantEmailAlreadyExistsError, AssociateUserToTenantPhoneNumberAlreadyExistsError, AssociateUserToTenantThirdPartyUserAlreadyExistsError, + AssociateUserToTenantNotAllowedError, ]: response = await self.querier.send_post_request( NormalisedURLPath( @@ -283,10 +285,12 @@ async def associate_user_to_tenant( == AssociateUserToTenantThirdPartyUserAlreadyExistsError.status ): return AssociateUserToTenantThirdPartyUserAlreadyExistsError() + if response["status"] == AssociateUserToTenantNotAllowedError.status: + return AssociateUserToTenantNotAllowedError(response["reason"]) raise Exception("Should never come here") - async def dissociate_user_from_tenant( + async def disassociate_user_from_tenant( self, tenant_id: Optional[str], recipe_user_id: RecipeUserId, diff --git a/supertokens_python/recipe/multitenancy/syncio/__init__.py b/supertokens_python/recipe/multitenancy/syncio/__init__.py index 7384ee6bd..5448f2612 100644 --- a/supertokens_python/recipe/multitenancy/syncio/__init__.py +++ b/supertokens_python/recipe/multitenancy/syncio/__init__.py @@ -108,7 +108,7 @@ def associate_user_to_tenant( return sync(associate_user_to_tenant(tenant_id, recipe_user_id, user_context)) -def dissociate_user_from_tenant( +def disassociate_user_from_tenant( tenant_id: str, recipe_user_id: RecipeUserId, user_context: Optional[Dict[str, Any]] = None, @@ -117,7 +117,7 @@ def dissociate_user_from_tenant( user_context = {} from supertokens_python.recipe.multitenancy.asyncio import ( - dissociate_user_from_tenant, + disassociate_user_from_tenant, ) - return sync(dissociate_user_from_tenant(tenant_id, recipe_user_id, user_context)) + return sync(disassociate_user_from_tenant(tenant_id, recipe_user_id, user_context)) diff --git a/supertokens_python/recipe/session/exceptions.py b/supertokens_python/recipe/session/exceptions.py index 637cf486c..d9eedddc9 100644 --- a/supertokens_python/recipe/session/exceptions.py +++ b/supertokens_python/recipe/session/exceptions.py @@ -87,12 +87,15 @@ def __init__(self, msg: str, payload: List[ClaimValidationError]): class ClaimValidationError: - def __init__(self, id_: str, reason: Optional[Dict[str, Any]]): - self.id = id_ - self.reason = reason + id_: str + reason: Optional[Union[str, Dict[str, Any]]] + + def __init__(self, id_: str, reason: Optional[Union[str, Dict[str, Any]]]): + self.id_: str = id_ + self.reason: Optional[Union[str, Dict[str, Any]]] = reason def to_json(self): - result: Dict[str, Any] = {"id": self.id} + result: Dict[str, Any] = {"id": self.id_} if self.reason is not None: result["reason"] = self.reason diff --git a/supertokens_python/recipe/thirdparty/asyncio/__init__.py b/supertokens_python/recipe/thirdparty/asyncio/__init__.py index e4735ac76..b374e041b 100644 --- a/supertokens_python/recipe/thirdparty/asyncio/__init__.py +++ b/supertokens_python/recipe/thirdparty/asyncio/__init__.py @@ -30,7 +30,7 @@ async def manually_create_or_update_user( third_party_user_id: str, email: str, is_verified: bool, - session: Optional[SessionContainer], + session: Optional[SessionContainer] = None, user_context: Union[None, Dict[str, Any]] = None, ) -> Union[ ManuallyCreateOrUpdateUserOkResult, diff --git a/supertokens_python/recipe/thirdparty/syncio/__init__.py b/supertokens_python/recipe/thirdparty/syncio/__init__.py index 429b4b027..4481c8131 100644 --- a/supertokens_python/recipe/thirdparty/syncio/__init__.py +++ b/supertokens_python/recipe/thirdparty/syncio/__init__.py @@ -29,7 +29,7 @@ def manually_create_or_update_user( third_party_user_id: str, email: str, is_verified: bool, - session: Optional[SessionContainer], + session: Optional[SessionContainer] = None, user_context: Union[None, Dict[str, Any]] = None, ) -> Union[ ManuallyCreateOrUpdateUserOkResult, diff --git a/supertokens_python/recipe/totp/interfaces.py b/supertokens_python/recipe/totp/interfaces.py index 3cb4d484e..64c45f783 100644 --- a/supertokens_python/recipe/totp/interfaces.py +++ b/supertokens_python/recipe/totp/interfaces.py @@ -13,11 +13,26 @@ # under the License. from __future__ import annotations -from typing import Dict, Any, Union, TYPE_CHECKING +from typing import Dict, Any, Union, TYPE_CHECKING, Optional from abc import ABC, abstractmethod if TYPE_CHECKING: - from .types import * + from .types import ( + UserIdentifierInfoOkResult, + UnknownUserIdError, + UserIdentifierInfoDoesNotExistError, + CreateDeviceOkResult, + DeviceAlreadyExistsError, + UpdateDeviceOkResult, + RemoveDeviceOkResult, + VerifyDeviceOkResult, + VerifyTOTPOkResult, + InvalidTOTPError, + LimitReachedError, + UnknownDeviceError, + ListDevicesOkResult, + TOTPNormalisedConfig, + ) from supertokens_python.recipe.session import SessionContainer from supertokens_python import AppInfo from supertokens_python.framework import BaseRequest, BaseResponse diff --git a/supertokens_python/recipe/totp/syncio/__init__.py b/supertokens_python/recipe/totp/syncio/__init__.py index 2eaecf37f..24eb4d2ed 100644 --- a/supertokens_python/recipe/totp/syncio/__init__.py +++ b/supertokens_python/recipe/totp/syncio/__init__.py @@ -18,7 +18,6 @@ from supertokens_python.async_to_sync_wrapper import sync -from ..recipe import TOTPRecipe from supertokens_python.recipe.totp.types import ( CreateDeviceOkResult, DeviceAlreadyExistsError, @@ -124,6 +123,3 @@ def verify_totp( from supertokens_python.recipe.totp.asyncio import verify_totp as async_func return sync(async_func(tenant_id, user_id, totp, user_context)) - - -init = TOTPRecipe.init diff --git a/supertokens_python/recipe/totp/types.py b/supertokens_python/recipe/totp/types.py index fae156fe8..f9599bf65 100644 --- a/supertokens_python/recipe/totp/types.py +++ b/supertokens_python/recipe/totp/types.py @@ -75,9 +75,6 @@ def to_json(self) -> Dict[str, Any]: class UpdateDeviceOkResult(OkResult): - def __init__(self): - super().__init__() - def to_json(self) -> Dict[str, Any]: raise NotImplementedError() @@ -174,11 +171,6 @@ def to_json(self) -> Dict[str, Any]: class VerifyTOTPOkResult(OkResult): - def __init__( - self, - ): - super().__init__() - def to_json(self) -> Dict[str, Any]: return {"status": self.status} From 0cfec6f26a73c71e712f10d4c6584bb5eabc6ac4 Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Mon, 30 Sep 2024 16:13:13 +0530 Subject: [PATCH 3/6] fixes all cyclic imports --- supertokens_python/auth_utils.py | 3 ++- .../recipe/multifactorauth/api/implementation.py | 9 +++++---- .../recipe/multifactorauth/multi_factor_auth_claim.py | 7 +++++-- supertokens_python/recipe/multifactorauth/recipe.py | 10 +++++++--- .../recipe/multifactorauth/recipe_implementation.py | 8 ++++++-- .../recipe/multitenancy/api/implementation.py | 9 +++++++-- supertokens_python/supertokens.py | 7 +++++-- 7 files changed, 37 insertions(+), 16 deletions(-) diff --git a/supertokens_python/auth_utils.py b/supertokens_python/auth_utils.py index ff3315ba3..08d77cb4d 100644 --- a/supertokens_python/auth_utils.py +++ b/supertokens_python/auth_utils.py @@ -912,12 +912,13 @@ async def filter_out_invalid_second_factors_or_throw_if_all_are_invalid( factors_set_up_for_user_prom: Optional[List[str]] = None mfa_info_prom = None - async def get_factors_set_up_for_user(): + async def get_factors_set_up_for_user() -> List[str]: nonlocal factors_set_up_for_user_prom if factors_set_up_for_user_prom is None: factors_set_up_for_user_prom = await mfa_instance.recipe_implementation.get_factors_setup_for_user( user=session_user, user_context=user_context ) + assert factors_set_up_for_user_prom is not None return factors_set_up_for_user_prom async def get_mfa_requirements_for_auth(): diff --git a/supertokens_python/recipe/multifactorauth/api/implementation.py b/supertokens_python/recipe/multifactorauth/api/implementation.py index d6fe15e98..dea5fe91f 100644 --- a/supertokens_python/recipe/multifactorauth/api/implementation.py +++ b/supertokens_python/recipe/multifactorauth/api/implementation.py @@ -17,9 +17,6 @@ from typing import Any, Dict, List, Union, TYPE_CHECKING from supertokens_python.recipe.session import SessionContainer -from supertokens_python.recipe.multifactorauth.utils import ( - update_and_get_mfa_related_info_in_session, -) from supertokens_python.recipe.multitenancy.asyncio import get_tenant from supertokens_python.asyncio import get_user from supertokens_python.recipe.session.exceptions import ( @@ -54,6 +51,10 @@ async def resync_session_and_fetch_mfa_info_put( MultiFactorAuthClaim: MultiFactorAuthClaimType = mfa.MultiFactorAuthClaim + module = importlib.import_module( + "supertokens_python.recipe.multifactorauth.utils" + ) + session_user = await get_user(session.get_user_id(), user_context) if session_user is None: @@ -61,7 +62,7 @@ async def resync_session_and_fetch_mfa_info_put( "Session user not found", ) - mfa_info = await update_and_get_mfa_related_info_in_session( + mfa_info = await module.update_and_get_mfa_related_info_in_session( MultiFactorAuthClaim, input_session=session, user_context=user_context, diff --git a/supertokens_python/recipe/multifactorauth/multi_factor_auth_claim.py b/supertokens_python/recipe/multifactorauth/multi_factor_auth_claim.py index 4b9eeef28..2780bd5f4 100644 --- a/supertokens_python/recipe/multifactorauth/multi_factor_auth_claim.py +++ b/supertokens_python/recipe/multifactorauth/multi_factor_auth_claim.py @@ -13,6 +13,7 @@ # under the License. from __future__ import annotations +import importlib from typing import Any, Dict, Optional, Set @@ -172,9 +173,11 @@ async def fetch_value( current_payload: Dict[str, Any], user_context: Dict[str, Any], ) -> MFAClaimValue: - from .utils import update_and_get_mfa_related_info_in_session + module = importlib.import_module( + "supertokens_python.recipe.multifactorauth.utils" + ) - mfa_info = await update_and_get_mfa_related_info_in_session( + mfa_info = await module.update_and_get_mfa_related_info_in_session( self, input_session_recipe_user_id=recipe_user_id, input_tenant_id=tenant_id, diff --git a/supertokens_python/recipe/multifactorauth/recipe.py b/supertokens_python/recipe/multifactorauth/recipe.py index f60c30c34..139826761 100644 --- a/supertokens_python/recipe/multifactorauth/recipe.py +++ b/supertokens_python/recipe/multifactorauth/recipe.py @@ -12,6 +12,7 @@ # License for the specific language governing permissions and limitations # under the License. from __future__ import annotations +import importlib from os import environ from typing import Any, Dict, Optional, List, Union @@ -31,7 +32,6 @@ MultiFactorAuthClaim, ) from supertokens_python.recipe.multitenancy.interfaces import TenantConfig -from supertokens_python.recipe.multitenancy.recipe import MultitenancyRecipe from supertokens_python.recipe.session.recipe import SessionRecipe from supertokens_python.recipe_module import APIHandled, RecipeModule from supertokens_python.supertokens import AppInfo @@ -76,9 +76,11 @@ def __init__( ] = [] self.is_get_mfa_requirements_for_auth_overridden: bool = False - from .utils import validate_and_normalise_user_input + module = importlib.import_module( + "supertokens_python.recipe.multifactorauth.utils" + ) - self.config = validate_and_normalise_user_input( + self.config = module.validate_and_normalise_user_input( first_factors, override, ) @@ -102,6 +104,8 @@ def __init__( ) def callback(): + from supertokens_python.recipe.multitenancy.recipe import MultitenancyRecipe + mt_recipe = MultitenancyRecipe.get_instance() mt_recipe.static_first_factors = self.config.first_factors diff --git a/supertokens_python/recipe/multifactorauth/recipe_implementation.py b/supertokens_python/recipe/multifactorauth/recipe_implementation.py index 9ae8f4a97..f2cd77d46 100644 --- a/supertokens_python/recipe/multifactorauth/recipe_implementation.py +++ b/supertokens_python/recipe/multifactorauth/recipe_implementation.py @@ -12,6 +12,7 @@ # License for the specific language governing permissions and limitations # under the License. from __future__ import annotations +import importlib from typing import TYPE_CHECKING, Any, Awaitable, Dict, Set, Callable, List @@ -35,7 +36,6 @@ from supertokens_python.recipe.session import SessionContainer from supertokens_python.types import User -from .utils import update_and_get_mfa_related_info_in_session from .interfaces import RecipeInterface if TYPE_CHECKING: @@ -173,7 +173,11 @@ async def assert_allowed_to_setup_factor_else_throw_invalid_claim_error( async def mark_factor_as_complete_in_session( self, session: SessionContainer, factor_id: str, user_context: Dict[str, Any] ): - await update_and_get_mfa_related_info_in_session( + module = importlib.import_module( + "supertokens_python.recipe.multifactorauth.utils" + ) + + await module.update_and_get_mfa_related_info_in_session( MultiFactorAuthClaim, input_session=session, input_updated_factor_id=factor_id, diff --git a/supertokens_python/recipe/multitenancy/api/implementation.py b/supertokens_python/recipe/multitenancy/api/implementation.py index 27a58624f..565773e76 100644 --- a/supertokens_python/recipe/multitenancy/api/implementation.py +++ b/supertokens_python/recipe/multitenancy/api/implementation.py @@ -12,6 +12,7 @@ # License for the specific language governing permissions and limitations # under the License. +import importlib from typing import Any, Dict, Optional, Union, List from ..constants import DEFAULT_TENANT_ID @@ -35,7 +36,9 @@ async def login_methods_get( api_options: APIOptions, user_context: Dict[str, Any], ) -> Union[LoginMethodsGetOkResult, GeneralErrorResponse]: - from ...multifactorauth.utils import is_valid_first_factor + module = importlib.import_module( + "supertokens_python.recipe.multifactorauth.utils" + ) from supertokens_python.recipe.thirdparty.providers.config_utils import ( merge_providers_from_core_and_static, find_and_create_provider_instance, @@ -91,7 +94,9 @@ async def login_methods_get( valid_first_factors: List[str] = [] for factor_id in first_factors: - valid_res = await is_valid_first_factor(tenant_id, factor_id, user_context) + valid_res = await module.is_valid_first_factor( + tenant_id, factor_id, user_context + ) if valid_res == "OK": valid_first_factors.append(factor_id) if valid_res == "TENANT_NOT_FOUND_ERROR": diff --git a/supertokens_python/supertokens.py b/supertokens_python/supertokens.py index 6aae8aa9b..51ec74b81 100644 --- a/supertokens_python/supertokens.py +++ b/supertokens_python/supertokens.py @@ -13,6 +13,7 @@ # under the License. from __future__ import annotations +import importlib from os import environ from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Set, Union, Tuple @@ -276,9 +277,11 @@ def make_recipe(recipe: Callable[[AppInfo], RecipeModule]) -> RecipeModule: self.recipe_modules: List[RecipeModule] = list(map(make_recipe, recipe_list)) if not multitenancy_found: - from supertokens_python.recipe.multitenancy.recipe import MultitenancyRecipe + module = importlib.import_module( + "supertokens_python.recipe.multitenancy.recipe" + ) - self.recipe_modules.append(MultitenancyRecipe.init()(self.app_info)) + self.recipe_modules.append(module.init()(self.app_info)) if totp_found and not multi_factor_auth_found: raise Exception("Please initialize the MultiFactorAuth recipe to use TOTP.") if not user_metadata_found: From 9e78f9993b6f895a83799c9b13f82f93ca2bc15a Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Mon, 30 Sep 2024 16:24:06 +0530 Subject: [PATCH 4/6] small changes --- .../multifactorauth/api/implementation.py | 14 +++------- .../recipe/multifactorauth/utils.py | 28 ++++--------------- supertokens_python/supertokens.py | 7 ++--- 3 files changed, 11 insertions(+), 38 deletions(-) diff --git a/supertokens_python/recipe/multifactorauth/api/implementation.py b/supertokens_python/recipe/multifactorauth/api/implementation.py index dea5fe91f..e16f422b8 100644 --- a/supertokens_python/recipe/multifactorauth/api/implementation.py +++ b/supertokens_python/recipe/multifactorauth/api/implementation.py @@ -14,7 +14,10 @@ from __future__ import annotations import importlib -from typing import Any, Dict, List, Union, TYPE_CHECKING +from typing import Any, Dict, List, Union +from supertokens_python.recipe.multifactorauth.multi_factor_auth_claim import ( + MultiFactorAuthClaim, +) from supertokens_python.recipe.session import SessionContainer from supertokens_python.recipe.multitenancy.asyncio import get_tenant @@ -33,11 +36,6 @@ ResyncSessionAndFetchMFAInfoPUTOkResult, ) -if TYPE_CHECKING: - from ..multi_factor_auth_claim import ( - MultiFactorAuthClaimClass as MultiFactorAuthClaimType, - ) - class APIImplementation(APIInterface): async def resync_session_and_fetch_mfa_info_put( @@ -47,10 +45,6 @@ async def resync_session_and_fetch_mfa_info_put( user_context: Dict[str, Any], ) -> Union[ResyncSessionAndFetchMFAInfoPUTOkResult, GeneralErrorResponse]: - mfa = importlib.import_module("supertokens_python.recipe.multifactorauth") - - MultiFactorAuthClaim: MultiFactorAuthClaimType = mfa.MultiFactorAuthClaim - module = importlib.import_module( "supertokens_python.recipe.multifactorauth.utils" ) diff --git a/supertokens_python/recipe/multifactorauth/utils.py b/supertokens_python/recipe/multifactorauth/utils.py index d28a48175..c4d54cd50 100644 --- a/supertokens_python/recipe/multifactorauth/utils.py +++ b/supertokens_python/recipe/multifactorauth/utils.py @@ -12,9 +12,9 @@ # License for the specific language governing permissions and limitations # under the License. from __future__ import annotations -import importlib - from typing import TYPE_CHECKING, List, Optional, Union, Dict, Any +from supertokens_python.recipe.multitenancy.asyncio import get_tenant +from supertokens_python.recipe.multitenancy.recipe import MultitenancyRecipe from supertokens_python.recipe.session import SessionContainer from supertokens_python.recipe.session.asyncio import get_session_information from supertokens_python.recipe.session.exceptions import UnauthorisedError @@ -35,9 +35,6 @@ from supertokens_python.recipe.multifactorauth.multi_factor_auth_claim import ( MultiFactorAuthClaimClass, ) - from supertokens_python.recipe.multitenancy.recipe import ( - MultitenancyRecipe as MTRecipeType, - ) def validate_and_normalise_user_input( @@ -203,16 +200,7 @@ async def user_getter(): async def get_required_secondary_factors_for_tenant( tenant_id: str, user_context: Dict[str, Any] ) -> List[str]: - - MultitenancyRecipe = importlib.import_module( - "supertokens_python.recipe.multitenancy.recipe" - ) - - mt_recipe: MTRecipeType = MultitenancyRecipe.get_instance() - - tenant_info = await mt_recipe.recipe_implementation.get_tenant( - tenant_id=tenant_id, user_context=user_context - ) + tenant_info = await get_tenant(tenant_id, user_context) if tenant_info is None: raise UnauthorisedError("Tenant not found") return ( @@ -276,14 +264,8 @@ async def is_valid_first_factor( tenant_id: str, factor_id: str, user_context: Dict[str, Any] ) -> Literal["OK", "INVALID_FIRST_FACTOR_ERROR", "TENANT_NOT_FOUND_ERROR"]: - MultitenancyRecipe = importlib.import_module( - "supertokens_python.recipe.multitenancy.recipe" - ) - - mt_recipe: MTRecipeType = MultitenancyRecipe.get_instance() - tenant_info = await mt_recipe.recipe_implementation.get_tenant( - tenant_id=tenant_id, user_context=user_context - ) + mt_recipe = MultitenancyRecipe.get_instance() + tenant_info = await get_tenant(tenant_id=tenant_id, user_context=user_context) if tenant_info is None: return "TENANT_NOT_FOUND_ERROR" diff --git a/supertokens_python/supertokens.py b/supertokens_python/supertokens.py index 51ec74b81..6aae8aa9b 100644 --- a/supertokens_python/supertokens.py +++ b/supertokens_python/supertokens.py @@ -13,7 +13,6 @@ # under the License. from __future__ import annotations -import importlib from os import environ from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Set, Union, Tuple @@ -277,11 +276,9 @@ def make_recipe(recipe: Callable[[AppInfo], RecipeModule]) -> RecipeModule: self.recipe_modules: List[RecipeModule] = list(map(make_recipe, recipe_list)) if not multitenancy_found: - module = importlib.import_module( - "supertokens_python.recipe.multitenancy.recipe" - ) + from supertokens_python.recipe.multitenancy.recipe import MultitenancyRecipe - self.recipe_modules.append(module.init()(self.app_info)) + self.recipe_modules.append(MultitenancyRecipe.init()(self.app_info)) if totp_found and not multi_factor_auth_found: raise Exception("Please initialize the MultiFactorAuth recipe to use TOTP.") if not user_metadata_found: From 5d0c4d395eefdc91ebee315174a76e03cf6f75bf Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Mon, 30 Sep 2024 16:29:23 +0530 Subject: [PATCH 5/6] fixes stuff --- supertokens_python/auth_utils.py | 4 ---- .../recipe/multifactorauth/api/implementation.py | 1 - .../recipe/multifactorauth/asyncio/__init__.py | 6 ------ .../recipe/multifactorauth/multi_factor_auth_claim.py | 1 - .../recipe/multifactorauth/recipe_implementation.py | 1 - supertokens_python/recipe/multifactorauth/utils.py | 7 +++---- 6 files changed, 3 insertions(+), 17 deletions(-) diff --git a/supertokens_python/auth_utils.py b/supertokens_python/auth_utils.py index 08d77cb4d..7aa7a9709 100644 --- a/supertokens_python/auth_utils.py +++ b/supertokens_python/auth_utils.py @@ -924,12 +924,8 @@ async def get_factors_set_up_for_user() -> List[str]: async def get_mfa_requirements_for_auth(): nonlocal mfa_info_prom if mfa_info_prom is None: - from .recipe.multifactorauth.multi_factor_auth_claim import ( - MultiFactorAuthClaim, - ) mfa_info_prom = await update_and_get_mfa_related_info_in_session( - MultiFactorAuthClaim, input_session=session, user_context=user_context, ) diff --git a/supertokens_python/recipe/multifactorauth/api/implementation.py b/supertokens_python/recipe/multifactorauth/api/implementation.py index e16f422b8..edb888256 100644 --- a/supertokens_python/recipe/multifactorauth/api/implementation.py +++ b/supertokens_python/recipe/multifactorauth/api/implementation.py @@ -57,7 +57,6 @@ async def resync_session_and_fetch_mfa_info_put( ) mfa_info = await module.update_and_get_mfa_related_info_in_session( - MultiFactorAuthClaim, input_session=session, user_context=user_context, ) diff --git a/supertokens_python/recipe/multifactorauth/asyncio/__init__.py b/supertokens_python/recipe/multifactorauth/asyncio/__init__.py index 4c8af2e54..8f51ced5b 100644 --- a/supertokens_python/recipe/multifactorauth/asyncio/__init__.py +++ b/supertokens_python/recipe/multifactorauth/asyncio/__init__.py @@ -33,10 +33,7 @@ async def assert_allowed_to_setup_factor_else_throw_invalid_claim_error( if user_context is None: user_context = {} - from ..multi_factor_auth_claim import MultiFactorAuthClaim - mfa_info = await update_and_get_mfa_related_info_in_session( - MultiFactorAuthClaim, input_session=session, user_context=user_context, ) @@ -69,10 +66,7 @@ async def get_mfa_requirements_for_auth( if user_context is None: user_context = {} - from ..multi_factor_auth_claim import MultiFactorAuthClaim - mfa_info = await update_and_get_mfa_related_info_in_session( - MultiFactorAuthClaim, input_session=session, user_context=user_context, ) diff --git a/supertokens_python/recipe/multifactorauth/multi_factor_auth_claim.py b/supertokens_python/recipe/multifactorauth/multi_factor_auth_claim.py index 2780bd5f4..4193697ac 100644 --- a/supertokens_python/recipe/multifactorauth/multi_factor_auth_claim.py +++ b/supertokens_python/recipe/multifactorauth/multi_factor_auth_claim.py @@ -178,7 +178,6 @@ async def fetch_value( ) mfa_info = await module.update_and_get_mfa_related_info_in_session( - self, input_session_recipe_user_id=recipe_user_id, input_tenant_id=tenant_id, input_access_token_payload=current_payload, diff --git a/supertokens_python/recipe/multifactorauth/recipe_implementation.py b/supertokens_python/recipe/multifactorauth/recipe_implementation.py index f2cd77d46..fc5cd1c90 100644 --- a/supertokens_python/recipe/multifactorauth/recipe_implementation.py +++ b/supertokens_python/recipe/multifactorauth/recipe_implementation.py @@ -178,7 +178,6 @@ async def mark_factor_as_complete_in_session( ) await module.update_and_get_mfa_related_info_in_session( - MultiFactorAuthClaim, input_session=session, input_updated_factor_id=factor_id, user_context=user_context, diff --git a/supertokens_python/recipe/multifactorauth/utils.py b/supertokens_python/recipe/multifactorauth/utils.py index c4d54cd50..7b32b92ac 100644 --- a/supertokens_python/recipe/multifactorauth/utils.py +++ b/supertokens_python/recipe/multifactorauth/utils.py @@ -13,6 +13,9 @@ # under the License. from __future__ import annotations from typing import TYPE_CHECKING, List, Optional, Union, Dict, Any +from supertokens_python.recipe.multifactorauth.multi_factor_auth_claim import ( + MultiFactorAuthClaim, +) from supertokens_python.recipe.multitenancy.asyncio import get_tenant from supertokens_python.recipe.multitenancy.recipe import MultitenancyRecipe from supertokens_python.recipe.session import SessionContainer @@ -32,9 +35,6 @@ if TYPE_CHECKING: from .types import OverrideConfig, MultiFactorAuthConfig - from supertokens_python.recipe.multifactorauth.multi_factor_auth_claim import ( - MultiFactorAuthClaimClass, - ) def validate_and_normalise_user_input( @@ -70,7 +70,6 @@ def __init__( async def update_and_get_mfa_related_info_in_session( - MultiFactorAuthClaim: MultiFactorAuthClaimClass, user_context: Dict[str, Any], input_session_recipe_user_id: Optional[RecipeUserId] = None, input_tenant_id: Optional[str] = None, From 91dcf5728473ee5837e3e9da8cf7750ced039ebe Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Mon, 30 Sep 2024 16:36:22 +0530 Subject: [PATCH 6/6] adds comments --- supertokens_python/recipe/multifactorauth/utils.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/supertokens_python/recipe/multifactorauth/utils.py b/supertokens_python/recipe/multifactorauth/utils.py index 7b32b92ac..12f9ea0f6 100644 --- a/supertokens_python/recipe/multifactorauth/utils.py +++ b/supertokens_python/recipe/multifactorauth/utils.py @@ -37,6 +37,8 @@ from .types import OverrideConfig, MultiFactorAuthConfig +# IMPORTANT: If this function signature is modified, please update all tha places where this function is called. +# There will be no type errors cause we use importLib to dynamically import if to prevent cyclic import issues. def validate_and_normalise_user_input( first_factors: Optional[List[str]], override: Union[OverrideConfig, None] = None, @@ -69,6 +71,8 @@ def __init__( ) +# IMPORTANT: If this function signature is modified, please update all tha places where this function is called. +# There will be no type errors cause we use importLib to dynamically import if to prevent cyclic import issues. async def update_and_get_mfa_related_info_in_session( user_context: Dict[str, Any], input_session_recipe_user_id: Optional[RecipeUserId] = None, @@ -259,6 +263,8 @@ async def get_required_secondary_factors_for_tenant_helper() -> List[str]: ) +# IMPORTANT: If this function signature is modified, please update all tha places where this function is called. +# There will be no type errors cause we use importLib to dynamically import if to prevent cyclic import issues. async def is_valid_first_factor( tenant_id: str, factor_id: str, user_context: Dict[str, Any] ) -> Literal["OK", "INVALID_FIRST_FACTOR_ERROR", "TENANT_NOT_FOUND_ERROR"]: