mirror of
https://github.com/langgenius/dify.git
synced 2026-08-29 03:45:08 +08:00
fix(api): handle suspended email domains (#41004)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
@@ -6,11 +6,14 @@ from constants.languages import supported_language
|
||||
from controllers.common.schema import query_params_from_model, register_schema_models
|
||||
from controllers.console import console_ns
|
||||
from controllers.console.auth.error import InvitationAccountMismatchError as InvitationAccountMismatchHTTPError
|
||||
from controllers.console.error import AccountInFreezeError, AlreadyActivateError
|
||||
from controllers.console.error import AccountInFreezeError, AlreadyActivateError, EmailDomainSuspendedError
|
||||
from extensions.ext_application_services import application_services
|
||||
from libs.helper import EmailStr, dump_response, timezone
|
||||
from libs.login import current_account_with_tenant
|
||||
from libs.token import extract_access_token
|
||||
from services.account_activation_service import (
|
||||
EmailDomainSuspendedError as EmailDomainSuspendedRegistrationError,
|
||||
)
|
||||
from services.account_activation_service import (
|
||||
FrozenAccountError,
|
||||
InvalidInvitationError,
|
||||
@@ -141,6 +144,8 @@ class ActivateApi(Resource):
|
||||
raise AlreadyActivateError() from None
|
||||
except InvitationAccountMismatchError:
|
||||
raise InvitationAccountMismatchHTTPError() from None
|
||||
except EmailDomainSuspendedRegistrationError:
|
||||
raise EmailDomainSuspendedError() from None
|
||||
except FrozenAccountError:
|
||||
raise AccountInFreezeError() from None
|
||||
|
||||
|
||||
@@ -24,9 +24,15 @@ from libs.password import valid_password
|
||||
from models import Account
|
||||
from services.account_service import AccountService
|
||||
from services.billing_service import BillingService
|
||||
from services.errors.account import AccountRegisterError, SeatsLimitExceededError
|
||||
from services.errors.account import (
|
||||
AccountRegisterError,
|
||||
SeatsLimitExceededError,
|
||||
)
|
||||
from services.errors.account import (
|
||||
EmailDomainSuspendedError as EmailDomainSuspendedRegistrationError,
|
||||
)
|
||||
|
||||
from ..error import AccountInFreezeError, EmailSendIpLimitError, SeatsLimitExceeded
|
||||
from ..error import AccountInFreezeError, EmailDomainSuspendedError, EmailSendIpLimitError, SeatsLimitExceeded
|
||||
from ..wraps import email_password_login_enabled, email_register_enabled, model_validate, setup_required
|
||||
|
||||
|
||||
@@ -99,10 +105,12 @@ class EmailRegisterSendEmailApi(Resource):
|
||||
if req_data.language is not None and req_data.language in languages:
|
||||
language = req_data.language
|
||||
|
||||
if dify_config.DEPLOYMENT_EDITION == DeploymentEdition.CLOUD and BillingService.is_email_in_freeze(
|
||||
normalized_email
|
||||
):
|
||||
raise AccountInFreezeError()
|
||||
if dify_config.DEPLOYMENT_EDITION == DeploymentEdition.CLOUD:
|
||||
freeze_type = BillingService.get_email_freeze_type(normalized_email)
|
||||
if freeze_type:
|
||||
if freeze_type == "email_domain_suspended":
|
||||
raise EmailDomainSuspendedError()
|
||||
raise AccountInFreezeError()
|
||||
|
||||
account = AccountService.get_account_by_email_with_case_fallback(req_data.email, session=db.session())
|
||||
token = AccountService.send_email_register_email(email=normalized_email, account=account, language=language)
|
||||
@@ -217,5 +225,7 @@ class EmailRegisterResetApi(Resource):
|
||||
)
|
||||
except SeatsLimitExceededError:
|
||||
raise SeatsLimitExceeded()
|
||||
except AccountRegisterError:
|
||||
raise AccountInFreezeError()
|
||||
except EmailDomainSuspendedRegistrationError as exc:
|
||||
raise EmailDomainSuspendedError() from exc
|
||||
except AccountRegisterError as exc:
|
||||
raise AccountInFreezeError() from exc
|
||||
|
||||
@@ -34,6 +34,7 @@ from controllers.console.error import (
|
||||
AccountBannedError,
|
||||
AccountInFreezeError,
|
||||
AccountNotFound,
|
||||
EmailDomainSuspendedError,
|
||||
EmailSendIpLimitError,
|
||||
NotAllowedCreateWorkspace,
|
||||
SeatsLimitExceeded,
|
||||
@@ -74,6 +75,9 @@ from services.errors.account import (
|
||||
RefreshTokenNotFoundError,
|
||||
SeatsLimitExceededError,
|
||||
)
|
||||
from services.errors.account import (
|
||||
EmailDomainSuspendedError as EmailDomainSuspendedRegistrationError,
|
||||
)
|
||||
from services.errors.workspace import WorkSpaceNotAllowedCreateError, WorkspacesLimitExceededError
|
||||
from services.feature_service import FeatureService
|
||||
from services.turnstile_service import (
|
||||
@@ -149,11 +153,13 @@ class LoginApi(Resource):
|
||||
request_email = req_data.email
|
||||
normalized_email = request_email.lower()
|
||||
|
||||
if dify_config.DEPLOYMENT_EDITION == DeploymentEdition.CLOUD and BillingService.is_email_in_freeze(
|
||||
normalized_email
|
||||
):
|
||||
_log_console_login_failure(email=normalized_email, reason=LoginFailureReason.ACCOUNT_IN_FREEZE)
|
||||
raise AccountInFreezeError()
|
||||
if dify_config.DEPLOYMENT_EDITION == DeploymentEdition.CLOUD:
|
||||
freeze_type = BillingService.get_email_freeze_type(normalized_email)
|
||||
if freeze_type:
|
||||
_log_console_login_failure(email=normalized_email, reason=LoginFailureReason.ACCOUNT_IN_FREEZE)
|
||||
if freeze_type == "email_domain_suspended":
|
||||
raise EmailDomainSuspendedError()
|
||||
raise AccountInFreezeError()
|
||||
|
||||
is_login_error_rate_limit = AccountService.is_login_error_rate_limit(normalized_email)
|
||||
if is_login_error_rate_limit:
|
||||
@@ -255,8 +261,10 @@ class ResetPasswordSendEmailApi(Resource):
|
||||
language = "en-US"
|
||||
try:
|
||||
account = _get_account_with_case_fallback(req_data.email)
|
||||
except AccountRegisterError:
|
||||
raise AccountInFreezeError()
|
||||
except EmailDomainSuspendedRegistrationError as exc:
|
||||
raise EmailDomainSuspendedError() from exc
|
||||
except AccountRegisterError as exc:
|
||||
raise AccountInFreezeError() from exc
|
||||
|
||||
token = AccountService.send_reset_password_email(
|
||||
email=normalized_email,
|
||||
@@ -297,8 +305,10 @@ class EmailCodeLoginSendEmailApi(Resource):
|
||||
language = "en-US"
|
||||
try:
|
||||
account = _get_account_with_case_fallback(req_data.email)
|
||||
except AccountRegisterError:
|
||||
raise AccountInFreezeError()
|
||||
except EmailDomainSuspendedRegistrationError as exc:
|
||||
raise EmailDomainSuspendedError() from exc
|
||||
except AccountRegisterError as exc:
|
||||
raise AccountInFreezeError() from exc
|
||||
|
||||
if account is None:
|
||||
if FeatureService.get_system_features().is_allow_register:
|
||||
@@ -377,9 +387,12 @@ class EmailCodeLoginApi(Resource):
|
||||
except Unauthorized as exc:
|
||||
_log_console_login_failure(email=user_email, reason=LoginFailureReason.ACCOUNT_BANNED)
|
||||
raise AccountBannedError() from exc
|
||||
except AccountRegisterError:
|
||||
except EmailDomainSuspendedRegistrationError as exc:
|
||||
_log_console_login_failure(email=user_email, reason=LoginFailureReason.ACCOUNT_IN_FREEZE)
|
||||
raise AccountInFreezeError()
|
||||
raise EmailDomainSuspendedError() from exc
|
||||
except AccountRegisterError as exc:
|
||||
_log_console_login_failure(email=user_email, reason=LoginFailureReason.ACCOUNT_IN_FREEZE)
|
||||
raise AccountInFreezeError() from exc
|
||||
if account:
|
||||
tenants = TenantService.get_join_tenants(account, session=db.session())
|
||||
if not tenants:
|
||||
@@ -405,9 +418,12 @@ class EmailCodeLoginApi(Resource):
|
||||
raise NotAllowedCreateWorkspace()
|
||||
except SeatsLimitExceededError:
|
||||
raise SeatsLimitExceeded()
|
||||
except AccountRegisterError:
|
||||
except EmailDomainSuspendedRegistrationError as exc:
|
||||
_log_console_login_failure(email=user_email, reason=LoginFailureReason.ACCOUNT_IN_FREEZE)
|
||||
raise AccountInFreezeError()
|
||||
raise EmailDomainSuspendedError() from exc
|
||||
except AccountRegisterError as exc:
|
||||
_log_console_login_failure(email=user_email, reason=LoginFailureReason.ACCOUNT_IN_FREEZE)
|
||||
raise AccountInFreezeError() from exc
|
||||
except WorkspacesLimitExceededError:
|
||||
raise WorkspacesLimitExceeded()
|
||||
token_pair = AccountService.login(account, session=db.session(), ip_address=ip_address)
|
||||
|
||||
@@ -12,6 +12,7 @@ from configs import dify_config
|
||||
from constants.languages import languages
|
||||
from controllers.common.fields import RedirectResponse
|
||||
from controllers.common.schema import query_params_from_model, register_response_schema_model, register_schema_models
|
||||
from controllers.console.error import AccountInFreezeError, EmailDomainSuspendedError
|
||||
from enums import DeploymentEdition
|
||||
from extensions.ext_database import db
|
||||
from libs.datetime_utils import naive_utc_now
|
||||
@@ -26,7 +27,14 @@ from libs.token import (
|
||||
from models import Account, AccountStatus
|
||||
from services.account_service import AccountService, RegisterService, TenantService
|
||||
from services.billing_service import BillingService
|
||||
from services.errors.account import AccountNotFoundError, AccountRegisterError, SeatsLimitExceededError
|
||||
from services.errors.account import (
|
||||
AccountNotFoundError,
|
||||
AccountRegisterError,
|
||||
SeatsLimitExceededError,
|
||||
)
|
||||
from services.errors.account import (
|
||||
EmailDomainSuspendedError as EmailDomainSuspendedRegistrationError,
|
||||
)
|
||||
from services.errors.workspace import WorkSpaceNotAllowedCreateError, WorkSpaceNotFoundError
|
||||
from services.feature_service import FeatureService
|
||||
|
||||
@@ -249,8 +257,10 @@ class OAuthCallback(Resource):
|
||||
)
|
||||
except SeatsLimitExceededError:
|
||||
return redirect(f"{dify_config.CONSOLE_WEB_URL}/signin?message=Licensed seats limit exceeded.")
|
||||
except AccountRegisterError as e:
|
||||
return redirect(f"{dify_config.CONSOLE_WEB_URL}/signin?message={e.description}")
|
||||
except EmailDomainSuspendedRegistrationError:
|
||||
return redirect(f"{dify_config.CONSOLE_WEB_URL}/signin?message={EmailDomainSuspendedError.description}")
|
||||
except AccountRegisterError as exc:
|
||||
return redirect(f"{dify_config.CONSOLE_WEB_URL}/signin?message={exc.description}")
|
||||
|
||||
# Check account status
|
||||
if account.status == AccountStatus.BANNED:
|
||||
@@ -309,15 +319,12 @@ def _generate_account(
|
||||
normalized_email = user_info.email.lower()
|
||||
oauth_new_user = True
|
||||
if not FeatureService.get_system_features().is_allow_register:
|
||||
if dify_config.DEPLOYMENT_EDITION == DeploymentEdition.CLOUD and BillingService.is_email_in_freeze(
|
||||
normalized_email
|
||||
):
|
||||
raise AccountRegisterError(
|
||||
description=(
|
||||
"This email account has been deleted within the past "
|
||||
"30 days and is temporarily unavailable for new account registration"
|
||||
)
|
||||
)
|
||||
if dify_config.DEPLOYMENT_EDITION == DeploymentEdition.CLOUD:
|
||||
freeze_type = BillingService.get_email_freeze_type(normalized_email)
|
||||
if freeze_type:
|
||||
if freeze_type == "email_domain_suspended":
|
||||
raise EmailDomainSuspendedRegistrationError()
|
||||
raise AccountRegisterError(description=AccountInFreezeError.description or "")
|
||||
raise AccountRegisterError(description=("Invalid email or password"))
|
||||
account_name = user_info.name or "Dify"
|
||||
interface_language = _preferred_interface_language(language)
|
||||
|
||||
@@ -92,11 +92,17 @@ class AccountInFreezeError(BaseHTTPException):
|
||||
error_code = "account_in_freeze"
|
||||
code = 400
|
||||
description = (
|
||||
"This email account has been deleted within the past 30 days"
|
||||
"This email account has been deleted within the past 30 days "
|
||||
"and is temporarily unavailable for new account registration."
|
||||
)
|
||||
|
||||
|
||||
class EmailDomainSuspendedError(BaseHTTPException):
|
||||
error_code = "email_domain_suspended"
|
||||
code = 400
|
||||
description = "This email domain has been suspended."
|
||||
|
||||
|
||||
class EducationVerifyLimitError(BaseHTTPException):
|
||||
error_code = "education_verify_limit"
|
||||
description = "Rate limit exceeded"
|
||||
|
||||
@@ -29,7 +29,12 @@ from controllers.console.auth.error import (
|
||||
InvalidEmailError,
|
||||
InvalidTokenError,
|
||||
)
|
||||
from controllers.console.error import AccountInFreezeError, AccountNotFound, EmailSendIpLimitError
|
||||
from controllers.console.error import (
|
||||
AccountInFreezeError,
|
||||
AccountNotFound,
|
||||
EmailDomainSuspendedError,
|
||||
EmailSendIpLimitError,
|
||||
)
|
||||
from controllers.console.flask_admission import console_account_admission
|
||||
from controllers.console.workspace.error import (
|
||||
AccountAlreadyInitedError,
|
||||
@@ -769,7 +774,10 @@ class ChangeEmailResetApi(Resource):
|
||||
args = ChangeEmailResetPayload.model_validate(payload)
|
||||
normalized_new_email = args.new_email.lower()
|
||||
|
||||
if AccountService.is_account_in_freeze(normalized_new_email):
|
||||
freeze_type = AccountService.get_account_freeze_type(normalized_new_email)
|
||||
if freeze_type:
|
||||
if freeze_type == "email_domain_suspended":
|
||||
raise EmailDomainSuspendedError()
|
||||
raise AccountInFreezeError()
|
||||
|
||||
if not AccountService.check_email_unique(normalized_new_email, session=db.session()):
|
||||
@@ -816,7 +824,10 @@ class CheckEmailUnique(Resource):
|
||||
payload = console_ns.payload or {}
|
||||
args = CheckEmailUniquePayload.model_validate(payload)
|
||||
normalized_email = args.email.lower()
|
||||
if AccountService.is_account_in_freeze(normalized_email):
|
||||
freeze_type = AccountService.get_account_freeze_type(normalized_email)
|
||||
if freeze_type:
|
||||
if freeze_type == "email_domain_suspended":
|
||||
raise EmailDomainSuspendedError()
|
||||
raise AccountInFreezeError()
|
||||
if not AccountService.check_email_unique(normalized_email, session=db.session()):
|
||||
raise EmailAlreadyInUseError()
|
||||
|
||||
@@ -54,8 +54,10 @@ class BillingAccountActivationEligibility(AccountActivationEligibility):
|
||||
self._enabled = enabled
|
||||
|
||||
@override
|
||||
def is_frozen(self, email: str) -> bool:
|
||||
return self._enabled and BillingService.is_email_in_freeze(email)
|
||||
def get_freeze_type(self, email: str) -> str | None:
|
||||
if not self._enabled:
|
||||
return None
|
||||
return BillingService.get_email_freeze_type(email)
|
||||
|
||||
|
||||
class BillingWorkspaceMembershipCache(WorkspaceMembershipCache):
|
||||
|
||||
@@ -41,7 +41,7 @@ class WorkspaceInvitePolicy(Protocol):
|
||||
|
||||
|
||||
class AccountActivationEligibility(Protocol):
|
||||
def is_frozen(self, email: str) -> bool: ...
|
||||
def get_freeze_type(self, email: str) -> str | None: ...
|
||||
|
||||
|
||||
class WorkspaceMembershipCache(Protocol):
|
||||
@@ -60,6 +60,10 @@ class FrozenAccountError(Exception):
|
||||
"""The invited account is temporarily ineligible for activation."""
|
||||
|
||||
|
||||
class EmailDomainSuspendedError(Exception):
|
||||
"""The invited account uses a suspended email domain."""
|
||||
|
||||
|
||||
class AccountActivationService:
|
||||
def __init__(
|
||||
self,
|
||||
@@ -101,7 +105,10 @@ class AccountActivationService:
|
||||
if authenticated_account_id is not None and authenticated_account_id != invitation.account_id:
|
||||
raise InvitationAccountMismatchError
|
||||
|
||||
if self._eligibility.is_frozen(invitation.account_email):
|
||||
freeze_type = self._eligibility.get_freeze_type(invitation.account_email)
|
||||
if freeze_type == "email_domain_suspended":
|
||||
raise EmailDomainSuspendedError
|
||||
if freeze_type:
|
||||
raise FrozenAccountError
|
||||
|
||||
setup = self._resolve_setup(invitation, command)
|
||||
|
||||
@@ -67,6 +67,7 @@ from services.errors.account import (
|
||||
AccountRegisterError,
|
||||
CannotOperateSelfError,
|
||||
CurrentPasswordIncorrectError,
|
||||
EmailDomainSuspendedError,
|
||||
InvalidActionError,
|
||||
LinkAccountIntegrateError,
|
||||
MemberNotInTenantError,
|
||||
@@ -470,6 +471,9 @@ class AccountService:
|
||||
raise SeatsLimitExceededError("licensed seats limit exceeded")
|
||||
|
||||
if dify_config.DEPLOYMENT_EDITION == DeploymentEdition.CLOUD and BillingService.is_email_in_freeze(email):
|
||||
freeze_type = BillingService.get_email_freeze_type(email) or "freeze"
|
||||
if freeze_type == "email_domain_suspended":
|
||||
raise EmailDomainSuspendedError()
|
||||
raise AccountRegisterError(
|
||||
description=(
|
||||
"This email account has been deleted within the past "
|
||||
@@ -1070,6 +1074,9 @@ class AccountService:
|
||||
@classmethod
|
||||
def get_user_through_email(cls, email: str, *, session: Session):
|
||||
if dify_config.DEPLOYMENT_EDITION == DeploymentEdition.CLOUD and BillingService.is_email_in_freeze(email):
|
||||
freeze_type = BillingService.get_email_freeze_type(email) or "freeze"
|
||||
if freeze_type == "email_domain_suspended":
|
||||
raise EmailDomainSuspendedError()
|
||||
raise AccountRegisterError(
|
||||
description=(
|
||||
"This email account has been deleted within the past "
|
||||
@@ -1088,9 +1095,13 @@ class AccountService:
|
||||
|
||||
@classmethod
|
||||
def is_account_in_freeze(cls, email: str) -> bool:
|
||||
if dify_config.DEPLOYMENT_EDITION == DeploymentEdition.CLOUD and BillingService.is_email_in_freeze(email):
|
||||
return True
|
||||
return False
|
||||
return cls.get_account_freeze_type(email) is not None
|
||||
|
||||
@classmethod
|
||||
def get_account_freeze_type(cls, email: str):
|
||||
if dify_config.DEPLOYMENT_EDITION != DeploymentEdition.CLOUD:
|
||||
return None
|
||||
return BillingService.get_email_freeze_type(email)
|
||||
|
||||
@staticmethod
|
||||
@redis_fallback(default_return=None)
|
||||
|
||||
@@ -28,6 +28,9 @@ _http_client: httpx.Client = get_pooled_http_client(
|
||||
)
|
||||
|
||||
|
||||
EmailFreezeType = Literal["freeze", "email_domain_suspended"]
|
||||
|
||||
|
||||
class SubscriptionPlan(TypedDict):
|
||||
"""Tenant subscriptionplan information."""
|
||||
|
||||
@@ -479,13 +482,26 @@ class BillingService:
|
||||
return cls._send_request("DELETE", "/account", params=params)
|
||||
|
||||
@classmethod
|
||||
def is_email_in_freeze(cls, email: str) -> bool:
|
||||
def get_email_freeze_type(cls, email: str) -> EmailFreezeType | None:
|
||||
params = {"email": email}
|
||||
try:
|
||||
response = cls._send_request("GET", "/account/in-freeze", params=params)
|
||||
return bool(response.get("data", False))
|
||||
if not response.get("data", False):
|
||||
return None
|
||||
|
||||
freeze_type = response.get("freeze_type") or response.get("freezeType")
|
||||
if freeze_type in ("freeze", "email_domain_suspended"):
|
||||
return freeze_type
|
||||
|
||||
# Keep compatibility with older billing services that only return
|
||||
# the boolean `data` field.
|
||||
return "freeze"
|
||||
except Exception:
|
||||
return False
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def is_email_in_freeze(cls, email: str) -> bool:
|
||||
return cls.get_email_freeze_type(email) is not None
|
||||
|
||||
@classmethod
|
||||
def update_account_deletion_feedback(cls, email: str, feedback: str):
|
||||
|
||||
@@ -9,6 +9,11 @@ class AccountRegisterError(BaseServiceError):
|
||||
pass
|
||||
|
||||
|
||||
class EmailDomainSuspendedError(AccountRegisterError):
|
||||
def __init__(self, description: str = "This email domain has been suspended."):
|
||||
super().__init__(description)
|
||||
|
||||
|
||||
class AccountLoginError(BaseServiceError):
|
||||
pass
|
||||
|
||||
|
||||
@@ -9,13 +9,22 @@ from flask import Flask
|
||||
|
||||
from controllers.console.auth.activate import ActivateApi, ActivateCheckApi
|
||||
from controllers.console.auth.error import InvitationAccountMismatchError as InvitationAccountMismatchHTTPError
|
||||
from controllers.console.error import AccountInFreezeError, AlreadyActivateError
|
||||
from controllers.console.error import (
|
||||
AccountInFreezeError,
|
||||
AlreadyActivateError,
|
||||
)
|
||||
from controllers.console.error import (
|
||||
EmailDomainSuspendedError as EmailDomainSuspendedHTTPError,
|
||||
)
|
||||
from services.account_activation_service import (
|
||||
AccountActivationService,
|
||||
FrozenAccountError,
|
||||
InvalidInvitationError,
|
||||
InvitationAccountMismatchError,
|
||||
)
|
||||
from services.account_activation_service import (
|
||||
EmailDomainSuspendedError as EmailDomainSuspendedRegistrationError,
|
||||
)
|
||||
from services.entities.account_activation_entities import (
|
||||
ActivationCheckData,
|
||||
ActivationCheckResult,
|
||||
@@ -172,6 +181,7 @@ class TestActivateApi:
|
||||
(InvalidInvitationError(), AlreadyActivateError),
|
||||
(InvitationAccountMismatchError(), InvitationAccountMismatchHTTPError),
|
||||
(FrozenAccountError(), AccountInFreezeError),
|
||||
(EmailDomainSuspendedRegistrationError(), EmailDomainSuspendedHTTPError),
|
||||
],
|
||||
)
|
||||
def test_translates_application_errors(
|
||||
|
||||
@@ -4,6 +4,7 @@ from __future__ import annotations
|
||||
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from flask import Flask
|
||||
|
||||
from controllers.console.auth.email_register import (
|
||||
@@ -11,14 +12,21 @@ from controllers.console.auth.email_register import (
|
||||
EmailRegisterResetApi,
|
||||
EmailRegisterSendEmailApi,
|
||||
)
|
||||
from controllers.console.error import AccountInFreezeError, EmailDomainSuspendedError
|
||||
from enums import DeploymentEdition
|
||||
from services.entities.feature_entities import SystemFeatureModel
|
||||
from services.errors.account import (
|
||||
AccountRegisterError,
|
||||
)
|
||||
from services.errors.account import (
|
||||
EmailDomainSuspendedError as EmailDomainSuspendedRegistrationError,
|
||||
)
|
||||
|
||||
|
||||
class TestEmailRegisterSendEmailApi:
|
||||
@patch("controllers.console.auth.email_register.AccountService.get_account_by_email_with_case_fallback")
|
||||
@patch("controllers.console.auth.email_register.AccountService.send_email_register_email")
|
||||
@patch("controllers.console.auth.email_register.BillingService.is_email_in_freeze")
|
||||
@patch("controllers.console.auth.email_register.BillingService.get_email_freeze_type")
|
||||
@patch("controllers.console.auth.email_register.AccountService.is_email_send_ip_limit", return_value=False)
|
||||
@patch("controllers.console.auth.email_register.extract_remote_ip", return_value="127.0.0.1")
|
||||
def test_send_email_normalizes_and_falls_back(
|
||||
@@ -31,7 +39,7 @@ class TestEmailRegisterSendEmailApi:
|
||||
app: Flask,
|
||||
):
|
||||
mock_send_mail.return_value = "token-123"
|
||||
mock_is_freeze.return_value = False
|
||||
mock_is_freeze.return_value = None
|
||||
mock_account = MagicMock()
|
||||
mock_get_account.return_value = mock_account
|
||||
|
||||
@@ -58,6 +66,49 @@ class TestEmailRegisterSendEmailApi:
|
||||
mock_extract_ip.assert_called_once()
|
||||
mock_is_email_send_ip_limit.assert_called_once_with("127.0.0.1")
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("freeze_type", "expected_error"),
|
||||
[
|
||||
("freeze", AccountInFreezeError),
|
||||
("email_domain_suspended", EmailDomainSuspendedError),
|
||||
],
|
||||
)
|
||||
@patch("controllers.console.auth.email_register.BillingService.get_email_freeze_type")
|
||||
@patch("controllers.console.auth.email_register.AccountService.is_email_send_ip_limit", return_value=False)
|
||||
@patch("controllers.console.auth.email_register.extract_remote_ip", return_value="127.0.0.1")
|
||||
def test_send_email_rejects_frozen_email(
|
||||
self,
|
||||
mock_extract_ip,
|
||||
mock_is_email_send_ip_limit,
|
||||
mock_get_freeze_type,
|
||||
app: Flask,
|
||||
freeze_type,
|
||||
expected_error,
|
||||
):
|
||||
mock_get_freeze_type.return_value = freeze_type
|
||||
feature_flags = SystemFeatureModel(
|
||||
deployment_edition=DeploymentEdition.COMMUNITY,
|
||||
enable_email_password_login=True,
|
||||
is_allow_register=True,
|
||||
)
|
||||
|
||||
with (
|
||||
patch("controllers.console.auth.email_register.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.CLOUD),
|
||||
patch("controllers.console.wraps.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.CLOUD),
|
||||
patch("controllers.console.wraps.FeatureService.get_system_features", return_value=feature_flags),
|
||||
):
|
||||
with app.test_request_context(
|
||||
"/email-register/send-email",
|
||||
method="POST",
|
||||
json={"email": "Invitee@Example.com"},
|
||||
):
|
||||
with pytest.raises(expected_error):
|
||||
EmailRegisterSendEmailApi().post()
|
||||
|
||||
mock_get_freeze_type.assert_called_once_with("invitee@example.com")
|
||||
mock_is_email_send_ip_limit.assert_called_once_with("127.0.0.1")
|
||||
mock_extract_ip.assert_called_once()
|
||||
|
||||
|
||||
class TestEmailRegisterCheckApi:
|
||||
@patch("controllers.console.auth.email_register.AccountService.reset_email_register_error_rate_limit")
|
||||
@@ -107,6 +158,28 @@ class TestEmailRegisterCheckApi:
|
||||
|
||||
|
||||
class TestEmailRegisterResetApi:
|
||||
@pytest.mark.parametrize(
|
||||
("service_error", "expected_error"),
|
||||
[
|
||||
(EmailDomainSuspendedRegistrationError(), EmailDomainSuspendedError),
|
||||
(AccountRegisterError("frozen"), AccountInFreezeError),
|
||||
],
|
||||
)
|
||||
@patch("controllers.console.auth.email_register.AccountService.create_account_and_tenant")
|
||||
def test_create_new_account_translates_freeze_errors(
|
||||
self,
|
||||
mock_create_account,
|
||||
service_error,
|
||||
expected_error,
|
||||
):
|
||||
mock_create_account.side_effect = service_error
|
||||
|
||||
with pytest.raises(expected_error):
|
||||
EmailRegisterResetApi()._create_new_account(
|
||||
email="user@example.com",
|
||||
password="ValidPass123!",
|
||||
)
|
||||
|
||||
@patch("controllers.console.auth.email_register.AccountService.reset_login_error_rate_limit")
|
||||
@patch("controllers.console.auth.email_register.AccountService.login")
|
||||
@patch("controllers.console.auth.email_register.EmailRegisterResetApi._create_new_account")
|
||||
|
||||
@@ -33,6 +33,7 @@ from controllers.console.auth.login import (
|
||||
from controllers.console.error import (
|
||||
AccountInFreezeError,
|
||||
AccountNotFound,
|
||||
EmailDomainSuspendedError,
|
||||
EmailSendIpLimitError,
|
||||
NotAllowedCreateWorkspace,
|
||||
WorkspacesLimitExceeded,
|
||||
@@ -43,7 +44,12 @@ from services.email_code_login_challenge import (
|
||||
EmailCodeLoginChallengeStatus,
|
||||
EmailCodeLoginChallengeUnavailableError,
|
||||
)
|
||||
from services.errors.account import AccountRegisterError
|
||||
from services.errors.account import (
|
||||
AccountRegisterError,
|
||||
)
|
||||
from services.errors.account import (
|
||||
EmailDomainSuspendedError as EmailDomainSuspendedRegistrationError,
|
||||
)
|
||||
from services.turnstile_service import TurnstileChallengeRejectedError, TurnstileUpstreamError
|
||||
|
||||
TEST_TOKEN = "00000000-0000-4000-8000-000000000001"
|
||||
@@ -308,7 +314,22 @@ class TestEmailCodeLoginSendEmailApi:
|
||||
@patch("controllers.console.wraps.db")
|
||||
@patch("controllers.console.auth.login.AccountService.is_email_send_ip_limit")
|
||||
@patch("controllers.console.auth.login.AccountService.get_user_through_email")
|
||||
def test_send_email_code_frozen_account(self, mock_get_user, mock_is_ip_limit, mock_db, app: Flask):
|
||||
@pytest.mark.parametrize(
|
||||
("service_error", "expected_error"),
|
||||
[
|
||||
(AccountRegisterError("Account frozen"), AccountInFreezeError),
|
||||
(EmailDomainSuspendedRegistrationError(), EmailDomainSuspendedError),
|
||||
],
|
||||
)
|
||||
def test_send_email_code_frozen_account(
|
||||
self,
|
||||
mock_get_user,
|
||||
mock_is_ip_limit,
|
||||
mock_db,
|
||||
app: Flask,
|
||||
service_error,
|
||||
expected_error,
|
||||
):
|
||||
"""
|
||||
Test email code sending to frozen account.
|
||||
|
||||
@@ -317,12 +338,12 @@ class TestEmailCodeLoginSendEmailApi:
|
||||
"""
|
||||
# Arrange
|
||||
mock_is_ip_limit.return_value = False
|
||||
mock_get_user.side_effect = AccountRegisterError("Account frozen")
|
||||
mock_get_user.side_effect = service_error
|
||||
|
||||
# Act & Assert
|
||||
with app.test_request_context("/email-code-login", method="POST", json={"email": "frozen@example.com"}):
|
||||
api = EmailCodeLoginSendEmailApi()
|
||||
with pytest.raises(AccountInFreezeError):
|
||||
with pytest.raises(expected_error):
|
||||
api.post()
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
||||
@@ -22,17 +22,26 @@ from controllers.console.auth.error import (
|
||||
EmailPasswordLoginLimitError,
|
||||
InvalidEmailError,
|
||||
)
|
||||
from controllers.console.auth.login import EmailCodeLoginApi, LoginApi, LogoutApi
|
||||
from controllers.console.auth.login import EmailCodeLoginApi, LoginApi, LogoutApi, ResetPasswordSendEmailApi
|
||||
from controllers.console.error import (
|
||||
AccountBannedError,
|
||||
AccountInFreezeError,
|
||||
EmailDomainSuspendedError,
|
||||
SeatsLimitExceeded,
|
||||
WorkspacesLimitExceeded,
|
||||
)
|
||||
from enums import DeploymentEdition
|
||||
from services.email_code_login_challenge import EmailCodeLoginChallengeResult, EmailCodeLoginChallengeStatus
|
||||
from services.entities.auth_entities import LoginFailureReason
|
||||
from services.errors.account import AccountLoginError, AccountPasswordError, SeatsLimitExceededError
|
||||
from services.errors.account import (
|
||||
AccountLoginError,
|
||||
AccountPasswordError,
|
||||
AccountRegisterError,
|
||||
SeatsLimitExceededError,
|
||||
)
|
||||
from services.errors.account import (
|
||||
EmailDomainSuspendedError as EmailDomainSuspendedRegistrationError,
|
||||
)
|
||||
|
||||
TEST_TOKEN = "00000000-0000-4000-8000-000000000001"
|
||||
|
||||
@@ -228,7 +237,7 @@ class TestLoginApi:
|
||||
|
||||
@patch("controllers.console.wraps.db")
|
||||
@patch("controllers.console.auth.login.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.CLOUD)
|
||||
@patch("controllers.console.auth.login.BillingService.is_email_in_freeze")
|
||||
@patch("controllers.console.auth.login.BillingService.get_email_freeze_type")
|
||||
def test_login_fails_when_account_frozen(
|
||||
self, mock_is_frozen, mock_db, app: Flask, caplog: pytest.LogCaptureFixture
|
||||
):
|
||||
@@ -240,7 +249,7 @@ class TestLoginApi:
|
||||
- AccountInFreezeError is raised for frozen accounts
|
||||
"""
|
||||
# Arrange
|
||||
mock_is_frozen.return_value = True
|
||||
mock_is_frozen.return_value = "freeze"
|
||||
|
||||
# Act & Assert
|
||||
with app.test_request_context(
|
||||
@@ -257,6 +266,116 @@ class TestLoginApi:
|
||||
assert warn_records[0].args[0] == "frozen@example.com"
|
||||
assert warn_records[0].args[1] == LoginFailureReason.ACCOUNT_IN_FREEZE
|
||||
|
||||
@patch("controllers.console.wraps.db")
|
||||
@patch("controllers.console.auth.login.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.CLOUD)
|
||||
@patch("controllers.console.auth.login.BillingService.get_email_freeze_type")
|
||||
def test_login_fails_when_email_domain_is_suspended(self, mock_get_freeze_type, mock_db, app: Flask):
|
||||
mock_get_freeze_type.return_value = "email_domain_suspended"
|
||||
|
||||
with app.test_request_context(
|
||||
"/login",
|
||||
method="POST",
|
||||
json={"email": "user@suspended.example", "password": encode_password("password")},
|
||||
):
|
||||
with pytest.raises(EmailDomainSuspendedError):
|
||||
LoginApi().post()
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("service_error", "expected_error"),
|
||||
[
|
||||
(EmailDomainSuspendedRegistrationError(), EmailDomainSuspendedError),
|
||||
(AccountRegisterError("frozen"), AccountInFreezeError),
|
||||
],
|
||||
)
|
||||
@patch("controllers.console.wraps.db")
|
||||
@patch("controllers.console.auth.login._get_account_with_case_fallback")
|
||||
@patch("controllers.console.auth.login.AccountService.verify_email_code_login_challenge")
|
||||
def test_email_code_login_translates_freeze_errors(
|
||||
self,
|
||||
mock_verify_challenge,
|
||||
mock_get_account,
|
||||
mock_db,
|
||||
app: Flask,
|
||||
service_error,
|
||||
expected_error,
|
||||
):
|
||||
mock_verify_challenge.return_value = EmailCodeLoginChallengeResult(
|
||||
status=EmailCodeLoginChallengeStatus.VERIFIED
|
||||
)
|
||||
mock_get_account.side_effect = service_error
|
||||
|
||||
with app.test_request_context(
|
||||
"/email-code-login/validity",
|
||||
method="POST",
|
||||
json={"email": "User@Example.com", "code": encode_code("123456"), "token": TEST_TOKEN},
|
||||
):
|
||||
with pytest.raises(expected_error):
|
||||
EmailCodeLoginApi().post()
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("service_error", "expected_error"),
|
||||
[
|
||||
(EmailDomainSuspendedRegistrationError(), EmailDomainSuspendedError),
|
||||
(AccountRegisterError("frozen"), AccountInFreezeError),
|
||||
],
|
||||
)
|
||||
@patch("controllers.console.wraps.db")
|
||||
@patch("controllers.console.auth.login.db")
|
||||
@patch("controllers.console.auth.login.AccountService.create_account_and_tenant")
|
||||
@patch("controllers.console.auth.login.AccountService.verify_email_code_login_challenge")
|
||||
@patch("controllers.console.auth.login._get_account_with_case_fallback")
|
||||
def test_email_code_login_translates_account_creation_freeze_errors(
|
||||
self,
|
||||
mock_get_account,
|
||||
mock_verify_challenge,
|
||||
mock_create_account,
|
||||
mock_login_db,
|
||||
mock_db,
|
||||
app: Flask,
|
||||
service_error,
|
||||
expected_error,
|
||||
):
|
||||
mock_verify_challenge.return_value = EmailCodeLoginChallengeResult(
|
||||
status=EmailCodeLoginChallengeStatus.VERIFIED
|
||||
)
|
||||
mock_get_account.return_value = None
|
||||
mock_create_account.side_effect = service_error
|
||||
|
||||
with app.test_request_context(
|
||||
"/email-code-login/validity",
|
||||
method="POST",
|
||||
json={"email": "User@Example.com", "code": encode_code("123456"), "token": TEST_TOKEN},
|
||||
):
|
||||
with pytest.raises(expected_error):
|
||||
EmailCodeLoginApi().post()
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("service_error", "expected_error"),
|
||||
[
|
||||
(EmailDomainSuspendedRegistrationError(), EmailDomainSuspendedError),
|
||||
(AccountRegisterError("frozen"), AccountInFreezeError),
|
||||
],
|
||||
)
|
||||
@patch("controllers.console.wraps.db")
|
||||
@patch("controllers.console.auth.login._get_account_with_case_fallback")
|
||||
def test_reset_password_translates_freeze_errors(
|
||||
self,
|
||||
mock_get_account,
|
||||
mock_db,
|
||||
app: Flask,
|
||||
service_error,
|
||||
expected_error,
|
||||
):
|
||||
mock_get_account.side_effect = service_error
|
||||
|
||||
with app.test_request_context(
|
||||
"/reset-password",
|
||||
method="POST",
|
||||
json={"email": "User@Example.com"},
|
||||
):
|
||||
with pytest.raises(expected_error):
|
||||
ResetPasswordSendEmailApi().post()
|
||||
|
||||
@patch("controllers.console.wraps.db")
|
||||
@patch("controllers.console.auth.login.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.COMMUNITY)
|
||||
@patch("controllers.console.auth.login.AccountService.is_login_error_rate_limit")
|
||||
|
||||
@@ -14,9 +14,15 @@ from controllers.console.auth.oauth import (
|
||||
_get_account_by_openid_or_email,
|
||||
get_oauth_providers,
|
||||
)
|
||||
from enums import DeploymentEdition
|
||||
from libs.oauth import OAuthUserInfo, encode_oauth_state
|
||||
from models.account import AccountStatus
|
||||
from services.errors.account import AccountRegisterError
|
||||
from services.errors.account import (
|
||||
AccountRegisterError,
|
||||
)
|
||||
from services.errors.account import (
|
||||
EmailDomainSuspendedError as EmailDomainSuspendedRegistrationError,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
@@ -231,6 +237,38 @@ class TestOAuthCallback:
|
||||
)
|
||||
mock_redirect.assert_called_once_with("http://localhost:3000?oauth_new_user=true")
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("service_error", "expected_message"),
|
||||
[
|
||||
(
|
||||
EmailDomainSuspendedRegistrationError(),
|
||||
"This email domain has been suspended.",
|
||||
),
|
||||
(AccountRegisterError("This email account is frozen."), "This email account is frozen."),
|
||||
],
|
||||
)
|
||||
@patch("controllers.console.auth.oauth.get_oauth_providers")
|
||||
@patch("controllers.console.auth.oauth._generate_account")
|
||||
@patch("controllers.console.auth.oauth.redirect")
|
||||
def test_should_translate_registration_freeze_errors(
|
||||
self,
|
||||
mock_redirect,
|
||||
mock_generate_account,
|
||||
mock_get_providers,
|
||||
resource: OAuthCallback,
|
||||
app: Flask,
|
||||
oauth_setup,
|
||||
service_error,
|
||||
expected_message,
|
||||
):
|
||||
mock_get_providers.return_value = {"github": oauth_setup["provider"]}
|
||||
mock_generate_account.side_effect = service_error
|
||||
|
||||
with app.test_request_context("/auth/oauth/github/callback?code=test_code"):
|
||||
resource.get("github")
|
||||
|
||||
mock_redirect.assert_called_once_with(f"http://localhost:3000/signin?message={expected_message}")
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("exception", "expected_error"),
|
||||
[
|
||||
@@ -537,6 +575,36 @@ class TestAccountGeneration:
|
||||
else:
|
||||
mock_register_service.register.assert_not_called()
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("freeze_type", "expected_error"),
|
||||
[
|
||||
("email_domain_suspended", EmailDomainSuspendedRegistrationError),
|
||||
("freeze", AccountRegisterError),
|
||||
],
|
||||
)
|
||||
@patch("controllers.console.auth.oauth.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.CLOUD)
|
||||
@patch("controllers.console.auth.oauth.BillingService.get_email_freeze_type")
|
||||
@patch("controllers.console.auth.oauth._get_account_by_openid_or_email", return_value=None)
|
||||
@patch("controllers.console.auth.oauth.FeatureService")
|
||||
def test_should_reject_registration_for_frozen_email(
|
||||
self,
|
||||
mock_feature_service,
|
||||
mock_get_account,
|
||||
mock_get_freeze_type,
|
||||
freeze_type,
|
||||
expected_error,
|
||||
app: Flask,
|
||||
user_info: OAuthUserInfo,
|
||||
):
|
||||
mock_feature_service.get_system_features.return_value.is_allow_register = False
|
||||
mock_get_freeze_type.return_value = freeze_type
|
||||
|
||||
with app.test_request_context("/"):
|
||||
with pytest.raises(expected_error):
|
||||
_generate_account("github", user_info)
|
||||
|
||||
mock_get_freeze_type.assert_called_once_with("test@example.com")
|
||||
|
||||
@patch("controllers.console.auth.oauth._get_account_by_openid_or_email", return_value=None)
|
||||
@patch("controllers.console.auth.oauth.FeatureService")
|
||||
@patch("controllers.console.auth.oauth.RegisterService")
|
||||
|
||||
@@ -8,6 +8,7 @@ import pytest
|
||||
from flask import Flask
|
||||
from sqlalchemy.orm import Session, scoped_session, sessionmaker
|
||||
|
||||
from controllers.console.error import EmailDomainSuspendedError
|
||||
from controllers.console.workspace.account import (
|
||||
AccountDeleteUpdateFeedbackApi,
|
||||
ChangeEmailCheckApi,
|
||||
@@ -442,10 +443,29 @@ class TestChangeEmailValidity:
|
||||
|
||||
|
||||
class TestChangeEmailReset:
|
||||
@patch(
|
||||
"controllers.console.workspace.account.AccountService.get_account_freeze_type",
|
||||
return_value="email_domain_suspended",
|
||||
)
|
||||
def test_should_reject_suspended_email_domain(self, mock_get_freeze_type, app: Flask):
|
||||
current_user = _build_account("old@example.com", "email-reset-account")
|
||||
|
||||
with app.test_request_context(
|
||||
"/account/change-email/reset",
|
||||
method="POST",
|
||||
json={"new_email": "new@suspended.example", "token": "token-123"},
|
||||
):
|
||||
api = ChangeEmailResetApi()
|
||||
method = inspect.unwrap(api.post)
|
||||
with pytest.raises(EmailDomainSuspendedError):
|
||||
method(api, current_user)
|
||||
|
||||
mock_get_freeze_type.assert_called_once_with("new@suspended.example")
|
||||
|
||||
@patch("controllers.console.workspace.account.AccountService.send_change_email_completed_notify_email")
|
||||
@patch("controllers.console.workspace.account.AccountService.revoke_change_email_token")
|
||||
@patch("controllers.console.workspace.account.AccountService.get_change_email_data")
|
||||
@patch("controllers.console.workspace.account.AccountService.is_account_in_freeze")
|
||||
@patch("controllers.console.workspace.account.AccountService.get_account_freeze_type")
|
||||
@pytest.mark.parametrize(
|
||||
"sqlite_session",
|
||||
[(Account, Tenant, TenantAccountJoin, AccountIntegrate)],
|
||||
@@ -507,7 +527,7 @@ class TestChangeEmailReset:
|
||||
@patch("controllers.console.workspace.account.AccountService.revoke_change_email_token")
|
||||
@patch("controllers.console.workspace.account.AccountService.get_change_email_data")
|
||||
@patch("controllers.console.workspace.account.AccountService.check_email_unique")
|
||||
@patch("controllers.console.workspace.account.AccountService.is_account_in_freeze")
|
||||
@patch("controllers.console.workspace.account.AccountService.get_account_freeze_type")
|
||||
def test_should_reject_reset_when_token_phase_is_not_new_verified(
|
||||
self,
|
||||
mock_is_freeze: MagicMock,
|
||||
@@ -550,7 +570,7 @@ class TestChangeEmailReset:
|
||||
@patch("controllers.console.workspace.account.AccountService.revoke_change_email_token")
|
||||
@patch("controllers.console.workspace.account.AccountService.get_change_email_data")
|
||||
@patch("controllers.console.workspace.account.AccountService.check_email_unique")
|
||||
@patch("controllers.console.workspace.account.AccountService.is_account_in_freeze")
|
||||
@patch("controllers.console.workspace.account.AccountService.get_account_freeze_type")
|
||||
def test_should_reject_reset_when_token_email_differs_from_payload_new_email(
|
||||
self,
|
||||
mock_is_freeze: MagicMock,
|
||||
@@ -593,7 +613,7 @@ class TestChangeEmailReset:
|
||||
@patch("controllers.console.workspace.account.AccountService.revoke_change_email_token")
|
||||
@patch("controllers.console.workspace.account.AccountService.get_change_email_data")
|
||||
@patch("controllers.console.workspace.account.AccountService.check_email_unique")
|
||||
@patch("controllers.console.workspace.account.AccountService.is_account_in_freeze")
|
||||
@patch("controllers.console.workspace.account.AccountService.get_account_freeze_type")
|
||||
def test_should_reject_reset_when_token_account_id_does_not_match_current_user(
|
||||
self,
|
||||
mock_is_freeze: MagicMock,
|
||||
@@ -736,7 +756,7 @@ class TestAccountDeletionFeedback:
|
||||
|
||||
|
||||
class TestCheckEmailUnique:
|
||||
@patch("controllers.console.workspace.account.AccountService.is_account_in_freeze")
|
||||
@patch("controllers.console.workspace.account.AccountService.get_account_freeze_type")
|
||||
@pytest.mark.parametrize(
|
||||
"sqlite_session",
|
||||
[(Account, Tenant, TenantAccountJoin)],
|
||||
|
||||
@@ -15,7 +15,7 @@ from controllers.console.auth.error import (
|
||||
EmailAlreadyInUseError,
|
||||
EmailCodeError,
|
||||
)
|
||||
from controllers.console.error import AccountInFreezeError
|
||||
from controllers.console.error import AccountInFreezeError, EmailDomainSuspendedError
|
||||
from controllers.console.workspace.account import (
|
||||
AccountAvatarApi,
|
||||
AccountAvatarQuery,
|
||||
@@ -603,7 +603,7 @@ class TestChangeEmailApis:
|
||||
new_callable=PropertyMock,
|
||||
return_value=payload,
|
||||
),
|
||||
patch("controllers.console.workspace.account.AccountService.is_account_in_freeze", return_value=False),
|
||||
patch("controllers.console.workspace.account.AccountService.get_account_freeze_type", return_value=None),
|
||||
patch("controllers.console.workspace.account.AccountService.check_email_unique", return_value=False),
|
||||
):
|
||||
with pytest.raises(EmailAlreadyInUseError):
|
||||
@@ -625,7 +625,7 @@ class TestCheckEmailUniqueApi:
|
||||
new_callable=PropertyMock,
|
||||
return_value=payload,
|
||||
),
|
||||
patch("controllers.console.workspace.account.AccountService.is_account_in_freeze", return_value=False),
|
||||
patch("controllers.console.workspace.account.AccountService.get_account_freeze_type", return_value=None),
|
||||
patch("controllers.console.workspace.account.AccountService.check_email_unique", return_value=True),
|
||||
):
|
||||
result = method(api)
|
||||
@@ -646,7 +646,32 @@ class TestCheckEmailUniqueApi:
|
||||
new_callable=PropertyMock,
|
||||
return_value=payload,
|
||||
),
|
||||
patch("controllers.console.workspace.account.AccountService.is_account_in_freeze", return_value=True),
|
||||
patch(
|
||||
"controllers.console.workspace.account.AccountService.get_account_freeze_type",
|
||||
return_value="freeze",
|
||||
),
|
||||
):
|
||||
with pytest.raises(AccountInFreezeError):
|
||||
method(api)
|
||||
|
||||
def test_email_domain_is_suspended(self, app: Flask):
|
||||
api = CheckEmailUnique()
|
||||
method = inspect.unwrap(api.post)
|
||||
|
||||
payload = {"email": "user@suspended.example"}
|
||||
|
||||
with (
|
||||
app.test_request_context("/", json=payload),
|
||||
patch.object(
|
||||
type(console_ns),
|
||||
"payload",
|
||||
new_callable=PropertyMock,
|
||||
return_value=payload,
|
||||
),
|
||||
patch(
|
||||
"controllers.console.workspace.account.AccountService.get_account_freeze_type",
|
||||
return_value="email_domain_suspended",
|
||||
),
|
||||
):
|
||||
with pytest.raises(EmailDomainSuspendedError):
|
||||
method(api)
|
||||
|
||||
@@ -42,11 +42,22 @@ def test_invitation_token_store_revokes_with_legacy_key_inputs() -> None:
|
||||
|
||||
|
||||
def test_billing_eligibility_skips_gateway_when_disabled() -> None:
|
||||
with patch("services.account_activation_adapters.BillingService.is_email_in_freeze") as is_frozen:
|
||||
result = BillingAccountActivationEligibility(enabled=False).is_frozen("invitee@example.com")
|
||||
with patch("services.account_activation_adapters.BillingService.get_email_freeze_type") as get_freeze_type:
|
||||
result = BillingAccountActivationEligibility(enabled=False).get_freeze_type("invitee@example.com")
|
||||
|
||||
assert result is False
|
||||
is_frozen.assert_not_called()
|
||||
assert result is None
|
||||
get_freeze_type.assert_not_called()
|
||||
|
||||
|
||||
def test_billing_eligibility_returns_freeze_type_when_enabled() -> None:
|
||||
with patch(
|
||||
"services.account_activation_adapters.BillingService.get_email_freeze_type",
|
||||
return_value="email_domain_suspended",
|
||||
) as get_freeze_type:
|
||||
result = BillingAccountActivationEligibility(enabled=True).get_freeze_type("invitee@example.com")
|
||||
|
||||
assert result == "email_domain_suspended"
|
||||
get_freeze_type.assert_called_once_with("invitee@example.com")
|
||||
|
||||
|
||||
def test_membership_cache_skips_gateway_when_disabled() -> None:
|
||||
|
||||
@@ -6,6 +6,7 @@ from services.account_activation_service import (
|
||||
AccountActivationEligibility,
|
||||
AccountActivationRepository,
|
||||
AccountActivationService,
|
||||
EmailDomainSuspendedError,
|
||||
FrozenAccountError,
|
||||
InvalidInvitationError,
|
||||
InvitationAccountMismatchError,
|
||||
@@ -60,7 +61,7 @@ def _service() -> tuple[AccountActivationService, Mock, Mock, Mock, Mock, Mock]:
|
||||
policy = Mock(spec=WorkspaceInvitePolicy)
|
||||
eligibility = Mock(spec=AccountActivationEligibility)
|
||||
membership_cache = Mock(spec=WorkspaceMembershipCache)
|
||||
eligibility.is_frozen.return_value = False
|
||||
eligibility.get_freeze_type.return_value = None
|
||||
service = AccountActivationService(
|
||||
tokens=tokens,
|
||||
accounts=accounts,
|
||||
@@ -133,7 +134,7 @@ class TestActivateInvitation:
|
||||
authenticated_account_id="different-account",
|
||||
)
|
||||
|
||||
eligibility.is_frozen.assert_not_called()
|
||||
eligibility.get_freeze_type.assert_not_called()
|
||||
tokens.revoke.assert_not_called()
|
||||
accounts.activate.assert_not_called()
|
||||
|
||||
@@ -141,12 +142,12 @@ class TestActivateInvitation:
|
||||
service, tokens, accounts, _, eligibility, _ = _service()
|
||||
tokens.find.return_value = _token()
|
||||
accounts.resolve.return_value = _invitation()
|
||||
eligibility.is_frozen.return_value = True
|
||||
eligibility.get_freeze_type.return_value = "freeze"
|
||||
|
||||
with pytest.raises(FrozenAccountError):
|
||||
service.activate(ActivationCommand(invitation=_lookup()), authenticated_account_id=None)
|
||||
|
||||
eligibility.is_frozen.assert_called_once_with("invitee@example.com")
|
||||
eligibility.get_freeze_type.assert_called_once_with("invitee@example.com")
|
||||
tokens.revoke.assert_not_called()
|
||||
accounts.activate.assert_not_called()
|
||||
|
||||
@@ -164,6 +165,19 @@ class TestActivateInvitation:
|
||||
tokens.revoke.assert_not_called()
|
||||
accounts.activate.assert_not_called()
|
||||
|
||||
def test_rejects_suspended_email_domain_without_consuming_token(self) -> None:
|
||||
service, tokens, accounts, _, eligibility, _ = _service()
|
||||
tokens.find.return_value = _token()
|
||||
accounts.resolve.return_value = _invitation()
|
||||
eligibility.get_freeze_type.return_value = "email_domain_suspended"
|
||||
|
||||
with pytest.raises(EmailDomainSuspendedError):
|
||||
service.activate(ActivationCommand(invitation=_lookup()), authenticated_account_id=None)
|
||||
|
||||
eligibility.get_freeze_type.assert_called_once_with("invitee@example.com")
|
||||
tokens.revoke.assert_not_called()
|
||||
accounts.activate.assert_not_called()
|
||||
|
||||
def test_activates_anonymous_invitation_and_invalidates_new_membership_cache(self) -> None:
|
||||
service, tokens, accounts, _, eligibility, membership_cache = _service()
|
||||
tokens.find.return_value = _token()
|
||||
@@ -179,7 +193,7 @@ class TestActivateInvitation:
|
||||
|
||||
service.activate(command, authenticated_account_id=None)
|
||||
|
||||
eligibility.is_frozen.assert_called_once_with("invitee@example.com")
|
||||
eligibility.get_freeze_type.assert_called_once_with("invitee@example.com")
|
||||
tokens.revoke.assert_called_once_with(_lookup("invitee@example.com"))
|
||||
accounts.activate.assert_called_once_with(
|
||||
invitation,
|
||||
|
||||
@@ -29,6 +29,7 @@ from services.errors.account import (
|
||||
AccountPasswordError,
|
||||
AccountRegisterError,
|
||||
CurrentPasswordIncorrectError,
|
||||
EmailDomainSuspendedError,
|
||||
NoPermissionError,
|
||||
)
|
||||
|
||||
@@ -312,6 +313,50 @@ class TestAccountService:
|
||||
session=unbound_session,
|
||||
)
|
||||
|
||||
def test_create_account_suspended_email_domain(
|
||||
self, unbound_session: Session, mock_external_service_dependencies: _MockDependencies
|
||||
) -> None:
|
||||
mock_external_service_dependencies["feature_service"].get_system_features.return_value.is_allow_register = True
|
||||
mock_external_service_dependencies["billing_service"].is_email_in_freeze.return_value = True
|
||||
mock_external_service_dependencies[
|
||||
"billing_service"
|
||||
].get_email_freeze_type.return_value = "email_domain_suspended"
|
||||
|
||||
with patch("services.account_service.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.CLOUD):
|
||||
with pytest.raises(EmailDomainSuspendedError):
|
||||
AccountService.create_account(
|
||||
email="user@suspended.example",
|
||||
name="Test User",
|
||||
interface_language="en-US",
|
||||
session=unbound_session,
|
||||
)
|
||||
|
||||
def test_get_user_through_email_rejects_suspended_email_domain(
|
||||
self, unbound_session: Session, mock_external_service_dependencies: _MockDependencies
|
||||
) -> None:
|
||||
mock_external_service_dependencies["billing_service"].is_email_in_freeze.return_value = True
|
||||
mock_external_service_dependencies[
|
||||
"billing_service"
|
||||
].get_email_freeze_type.return_value = "email_domain_suspended"
|
||||
|
||||
with patch("services.account_service.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.CLOUD):
|
||||
with pytest.raises(EmailDomainSuspendedError):
|
||||
AccountService.get_user_through_email("user@suspended.example", session=unbound_session)
|
||||
|
||||
def test_get_account_freeze_type_is_enabled_only_for_cloud(
|
||||
self, mock_external_service_dependencies: _MockDependencies
|
||||
) -> None:
|
||||
mock_external_service_dependencies["billing_service"].get_email_freeze_type.return_value = "freeze"
|
||||
|
||||
with patch("services.account_service.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.CLOUD):
|
||||
assert AccountService.get_account_freeze_type("frozen@example.com") == "freeze"
|
||||
with patch("services.account_service.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.COMMUNITY):
|
||||
assert AccountService.get_account_freeze_type("frozen@example.com") is None
|
||||
|
||||
mock_external_service_dependencies["billing_service"].get_email_freeze_type.assert_called_once_with(
|
||||
"frozen@example.com"
|
||||
)
|
||||
|
||||
def test_create_account_without_password(
|
||||
self,
|
||||
sqlite_session_factory: sessionmaker[Session],
|
||||
|
||||
@@ -1297,6 +1297,15 @@ class TestBillingServiceAccountManagement:
|
||||
assert result is True
|
||||
mock_send_request.assert_called_once_with("GET", "/account/in-freeze", params={"email": email})
|
||||
|
||||
def test_get_email_freeze_type_for_suspended_domain(self, mock_send_request):
|
||||
email = "user@suspended.example"
|
||||
mock_send_request.return_value = {"data": True, "freezeType": "email_domain_suspended"}
|
||||
|
||||
result = BillingService.get_email_freeze_type(email)
|
||||
|
||||
assert result == "email_domain_suspended"
|
||||
mock_send_request.assert_called_once_with("GET", "/account/in-freeze", params={"email": email})
|
||||
|
||||
def test_is_email_in_freeze_false(self, mock_send_request):
|
||||
"""Test checking if email is frozen (returns False)."""
|
||||
# Arrange
|
||||
|
||||
Reference in New Issue
Block a user