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, Optional, cast
  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 cast(Account, 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).filter(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: Optional[Account] = None, email: Optional[str] = None, language: Optional[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) -> Optional[dict[str, Any]]:
  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).filter(Site.code == app_code).first()
  73. if not site:
  74. raise NotFound("Site not found.")
  75. app_model = db.session.query(App).filter(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(hours=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: Optional[str] = None, app_id: Optional[str] = None, access_mode: Optional[str] = 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.")