You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

webapp_auth_service.py 6.2KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178
  1. import enum
  2. import secrets
  3. from datetime import UTC, datetime, timedelta
  4. from typing import Any
  5. from werkzeug.exceptions import NotFound, Unauthorized
  6. from configs import dify_config
  7. from extensions.ext_database import db
  8. from libs.helper import TokenManager
  9. from libs.passport import PassportService
  10. from libs.password import compare_password
  11. from models.account import Account, AccountStatus
  12. from models.model import App, EndUser, Site
  13. from services.app_service import AppService
  14. from services.enterprise.enterprise_service import EnterpriseService
  15. from services.errors.account import AccountLoginError, AccountNotFoundError, AccountPasswordError
  16. from tasks.mail_email_code_login import send_email_code_login_mail_task
  17. class WebAppAuthType(enum.StrEnum):
  18. """Enum for web app authentication types."""
  19. PUBLIC = "public"
  20. INTERNAL = "internal"
  21. EXTERNAL = "external"
  22. class WebAppAuthService:
  23. """Service for web app authentication."""
  24. @staticmethod
  25. def authenticate(email: str, password: str) -> Account:
  26. """authenticate account with email and password"""
  27. account = db.session.query(Account).filter_by(email=email).first()
  28. if not account:
  29. raise AccountNotFoundError()
  30. if account.status == AccountStatus.BANNED.value:
  31. raise AccountLoginError("Account is banned.")
  32. if account.password is None or not compare_password(password, account.password, account.password_salt):
  33. raise AccountPasswordError("Invalid email or password.")
  34. return account
  35. @classmethod
  36. def login(cls, account: Account) -> str:
  37. access_token = cls._get_account_jwt_token(account=account)
  38. return access_token
  39. @classmethod
  40. def get_user_through_email(cls, email: str):
  41. account = db.session.query(Account).where(Account.email == email).first()
  42. if not account:
  43. return None
  44. if account.status == AccountStatus.BANNED.value:
  45. raise Unauthorized("Account is banned.")
  46. return account
  47. @classmethod
  48. def send_email_code_login_email(
  49. cls, account: Account | None = None, email: str | None = None, language: str = "en-US"
  50. ):
  51. email = account.email if account else email
  52. if email is None:
  53. raise ValueError("Email must be provided.")
  54. code = "".join([str(secrets.randbelow(exclusive_upper_bound=10)) for _ in range(6)])
  55. token = TokenManager.generate_token(
  56. account=account, email=email, token_type="email_code_login", additional_data={"code": code}
  57. )
  58. send_email_code_login_mail_task.delay(
  59. language=language,
  60. to=account.email if account else email,
  61. code=code,
  62. )
  63. return token
  64. @classmethod
  65. def get_email_code_login_data(cls, token: str) -> dict[str, Any] | None:
  66. return TokenManager.get_token_data(token, "email_code_login")
  67. @classmethod
  68. def revoke_email_code_login_token(cls, token: str):
  69. TokenManager.revoke_token(token, "email_code_login")
  70. @classmethod
  71. def create_end_user(cls, app_code, email) -> EndUser:
  72. site = db.session.query(Site).where(Site.code == app_code).first()
  73. if not site:
  74. raise NotFound("Site not found.")
  75. app_model = db.session.query(App).where(App.id == site.app_id).first()
  76. if not app_model:
  77. raise NotFound("App not found.")
  78. end_user = EndUser(
  79. tenant_id=app_model.tenant_id,
  80. app_id=app_model.id,
  81. type="browser",
  82. is_anonymous=False,
  83. session_id=email,
  84. name="enterpriseuser",
  85. external_user_id="enterpriseuser",
  86. )
  87. db.session.add(end_user)
  88. db.session.commit()
  89. return end_user
  90. @classmethod
  91. def _get_account_jwt_token(cls, account: Account) -> str:
  92. exp_dt = datetime.now(UTC) + timedelta(minutes=dify_config.ACCESS_TOKEN_EXPIRE_MINUTES * 24)
  93. exp = int(exp_dt.timestamp())
  94. payload = {
  95. "sub": "Web API Passport",
  96. "user_id": account.id,
  97. "session_id": account.email,
  98. "token_source": "webapp_login_token",
  99. "auth_type": "internal",
  100. "exp": exp,
  101. }
  102. token: str = PassportService().issue(payload)
  103. return token
  104. @classmethod
  105. def is_app_require_permission_check(
  106. cls, app_code: str | None = None, app_id: str | None = None, access_mode: str | None = None
  107. ) -> bool:
  108. """
  109. Check if the app requires permission check based on its access mode.
  110. """
  111. modes_requiring_permission_check = [
  112. "private",
  113. "private_all",
  114. ]
  115. if access_mode:
  116. return access_mode in modes_requiring_permission_check
  117. if not app_code and not app_id:
  118. raise ValueError("Either app_code or app_id must be provided.")
  119. if app_code:
  120. app_id = AppService.get_app_id_by_code(app_code)
  121. if not app_id:
  122. raise ValueError("App ID could not be determined from the provided app_code.")
  123. webapp_settings = EnterpriseService.WebAppAuth.get_app_access_mode_by_id(app_id)
  124. if webapp_settings and webapp_settings.access_mode in modes_requiring_permission_check:
  125. return True
  126. return False
  127. @classmethod
  128. def get_app_auth_type(cls, app_code: str | None = None, access_mode: str | None = None) -> WebAppAuthType:
  129. """
  130. Get the authentication type for the app based on its access mode.
  131. """
  132. if not app_code and not access_mode:
  133. raise ValueError("Either app_code or access_mode must be provided.")
  134. if access_mode:
  135. if access_mode == "public":
  136. return WebAppAuthType.PUBLIC
  137. elif access_mode in ["private", "private_all"]:
  138. return WebAppAuthType.INTERNAL
  139. elif access_mode == "sso_verified":
  140. return WebAppAuthType.EXTERNAL
  141. if app_code:
  142. webapp_settings = EnterpriseService.WebAppAuth.get_app_access_mode_by_code(app_code)
  143. return cls.get_app_auth_type(access_mode=webapp_settings.access_mode)
  144. raise ValueError("Could not determine app authentication type.")