Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.

7 месяцев назад
2 лет назад
2 лет назад
7 месяцев назад
1 год назад
2 лет назад
2 лет назад
2 лет назад
2 лет назад
2 лет назад
2 лет назад
10 месяцев назад
1 год назад
1 год назад
2 лет назад
2 лет назад
2 лет назад
7 месяцев назад
7 месяцев назад
7 месяцев назад
2 лет назад
10 месяцев назад
2 лет назад
2 лет назад
2 лет назад
2 лет назад
2 лет назад
2 лет назад
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353
  1. import time
  2. from collections.abc import Callable
  3. from datetime import timedelta
  4. from enum import StrEnum, auto
  5. from functools import wraps
  6. from typing import Concatenate, ParamSpec, TypeVar
  7. from flask import current_app, request
  8. from flask_login import user_logged_in
  9. from flask_restx import Resource
  10. from pydantic import BaseModel
  11. from sqlalchemy import select, update
  12. from sqlalchemy.orm import Session
  13. from werkzeug.exceptions import Forbidden, NotFound, Unauthorized
  14. from extensions.ext_database import db
  15. from extensions.ext_redis import redis_client
  16. from libs.datetime_utils import naive_utc_now
  17. from libs.login import current_user
  18. from models.account import Account, Tenant, TenantAccountJoin, TenantStatus
  19. from models.dataset import Dataset, RateLimitLog
  20. from models.model import ApiToken, App, DefaultEndUserSessionID, EndUser
  21. from services.feature_service import FeatureService
  22. P = ParamSpec("P")
  23. R = TypeVar("R")
  24. T = TypeVar("T")
  25. class WhereisUserArg(StrEnum):
  26. """
  27. Enum for whereis_user_arg.
  28. """
  29. QUERY = auto()
  30. JSON = auto()
  31. FORM = auto()
  32. class FetchUserArg(BaseModel):
  33. fetch_from: WhereisUserArg
  34. required: bool = False
  35. def validate_app_token(view: Callable[P, R] | None = None, *, fetch_user_arg: FetchUserArg | None = None):
  36. def decorator(view_func: Callable[P, R]):
  37. @wraps(view_func)
  38. def decorated_view(*args: P.args, **kwargs: P.kwargs):
  39. api_token = validate_and_get_api_token("app")
  40. app_model = db.session.query(App).where(App.id == api_token.app_id).first()
  41. if not app_model:
  42. raise Forbidden("The app no longer exists.")
  43. if app_model.status != "normal":
  44. raise Forbidden("The app's status is abnormal.")
  45. if not app_model.enable_api:
  46. raise Forbidden("The app's API service has been disabled.")
  47. tenant = db.session.query(Tenant).where(Tenant.id == app_model.tenant_id).first()
  48. if tenant is None:
  49. raise ValueError("Tenant does not exist.")
  50. if tenant.status == TenantStatus.ARCHIVE:
  51. raise Forbidden("The workspace's status is archived.")
  52. kwargs["app_model"] = app_model
  53. if fetch_user_arg:
  54. if fetch_user_arg.fetch_from == WhereisUserArg.QUERY:
  55. user_id = request.args.get("user")
  56. elif fetch_user_arg.fetch_from == WhereisUserArg.JSON:
  57. user_id = request.get_json().get("user")
  58. elif fetch_user_arg.fetch_from == WhereisUserArg.FORM:
  59. user_id = request.form.get("user")
  60. else:
  61. # use default-user
  62. user_id = None
  63. if not user_id and fetch_user_arg.required:
  64. raise ValueError("Arg user must be provided.")
  65. if user_id:
  66. user_id = str(user_id)
  67. end_user = create_or_update_end_user_for_user_id(app_model, user_id)
  68. kwargs["end_user"] = end_user
  69. # Set EndUser as current logged-in user for flask_login.current_user
  70. current_app.login_manager._update_request_context_with_user(end_user) # type: ignore
  71. user_logged_in.send(current_app._get_current_object(), user=end_user) # type: ignore
  72. return view_func(*args, **kwargs)
  73. return decorated_view
  74. if view is None:
  75. return decorator
  76. else:
  77. return decorator(view)
  78. def cloud_edition_billing_resource_check(resource: str, api_token_type: str):
  79. def interceptor(view: Callable[P, R]):
  80. def decorated(*args: P.args, **kwargs: P.kwargs):
  81. api_token = validate_and_get_api_token(api_token_type)
  82. features = FeatureService.get_features(api_token.tenant_id)
  83. if features.billing.enabled:
  84. members = features.members
  85. apps = features.apps
  86. vector_space = features.vector_space
  87. documents_upload_quota = features.documents_upload_quota
  88. if resource == "members" and 0 < members.limit <= members.size:
  89. raise Forbidden("The number of members has reached the limit of your subscription.")
  90. elif resource == "apps" and 0 < apps.limit <= apps.size:
  91. raise Forbidden("The number of apps has reached the limit of your subscription.")
  92. elif resource == "vector_space" and 0 < vector_space.limit <= vector_space.size:
  93. raise Forbidden("The capacity of the vector space has reached the limit of your subscription.")
  94. elif resource == "documents" and 0 < documents_upload_quota.limit <= documents_upload_quota.size:
  95. raise Forbidden("The number of documents has reached the limit of your subscription.")
  96. else:
  97. return view(*args, **kwargs)
  98. return view(*args, **kwargs)
  99. return decorated
  100. return interceptor
  101. def cloud_edition_billing_knowledge_limit_check(resource: str, api_token_type: str):
  102. def interceptor(view: Callable[P, R]):
  103. @wraps(view)
  104. def decorated(*args: P.args, **kwargs: P.kwargs):
  105. api_token = validate_and_get_api_token(api_token_type)
  106. features = FeatureService.get_features(api_token.tenant_id)
  107. if features.billing.enabled:
  108. if resource == "add_segment":
  109. if features.billing.subscription.plan == "sandbox":
  110. raise Forbidden(
  111. "To unlock this feature and elevate your Dify experience, please upgrade to a paid plan."
  112. )
  113. else:
  114. return view(*args, **kwargs)
  115. return view(*args, **kwargs)
  116. return decorated
  117. return interceptor
  118. def cloud_edition_billing_rate_limit_check(resource: str, api_token_type: str):
  119. def interceptor(view: Callable[P, R]):
  120. @wraps(view)
  121. def decorated(*args: P.args, **kwargs: P.kwargs):
  122. api_token = validate_and_get_api_token(api_token_type)
  123. if resource == "knowledge":
  124. knowledge_rate_limit = FeatureService.get_knowledge_rate_limit(api_token.tenant_id)
  125. if knowledge_rate_limit.enabled:
  126. current_time = int(time.time() * 1000)
  127. key = f"rate_limit_{api_token.tenant_id}"
  128. redis_client.zadd(key, {current_time: current_time})
  129. redis_client.zremrangebyscore(key, 0, current_time - 60000)
  130. request_count = redis_client.zcard(key)
  131. if request_count > knowledge_rate_limit.limit:
  132. # add ratelimit record
  133. rate_limit_log = RateLimitLog(
  134. tenant_id=api_token.tenant_id,
  135. subscription_plan=knowledge_rate_limit.subscription_plan,
  136. operation="knowledge",
  137. )
  138. db.session.add(rate_limit_log)
  139. db.session.commit()
  140. raise Forbidden(
  141. "Sorry, you have reached the knowledge base request rate limit of your subscription."
  142. )
  143. return view(*args, **kwargs)
  144. return decorated
  145. return interceptor
  146. def validate_dataset_token(view: Callable[Concatenate[T, P], R] | None = None):
  147. def decorator(view: Callable[Concatenate[T, P], R]):
  148. @wraps(view)
  149. def decorated(*args: P.args, **kwargs: P.kwargs):
  150. # get url path dataset_id from positional args or kwargs
  151. # Flask passes URL path parameters as positional arguments
  152. dataset_id = None
  153. # First try to get from kwargs (explicit parameter)
  154. dataset_id = kwargs.get("dataset_id")
  155. # If not in kwargs, try to extract from positional args
  156. if not dataset_id and args:
  157. # For class methods: args[0] is self, args[1] is dataset_id (if exists)
  158. # Check if first arg is likely a class instance (has __dict__ or __class__)
  159. if len(args) > 1 and hasattr(args[0], "__dict__"):
  160. # This is a class method, dataset_id should be in args[1]
  161. potential_id = args[1]
  162. # Validate it's a string-like UUID, not another object
  163. try:
  164. # Try to convert to string and check if it's a valid UUID format
  165. str_id = str(potential_id)
  166. # Basic check: UUIDs are 36 chars with hyphens
  167. if len(str_id) == 36 and str_id.count("-") == 4:
  168. dataset_id = str_id
  169. except:
  170. pass
  171. elif len(args) > 0:
  172. # Not a class method, check if args[0] looks like a UUID
  173. potential_id = args[0]
  174. try:
  175. str_id = str(potential_id)
  176. if len(str_id) == 36 and str_id.count("-") == 4:
  177. dataset_id = str_id
  178. except:
  179. pass
  180. # Validate dataset if dataset_id is provided
  181. if dataset_id:
  182. dataset_id = str(dataset_id)
  183. dataset = db.session.query(Dataset).where(Dataset.id == dataset_id).first()
  184. if not dataset:
  185. raise NotFound("Dataset not found.")
  186. if not dataset.enable_api:
  187. raise Forbidden("Dataset api access is not enabled.")
  188. api_token = validate_and_get_api_token("dataset")
  189. tenant_account_join = (
  190. db.session.query(Tenant, TenantAccountJoin)
  191. .where(Tenant.id == api_token.tenant_id)
  192. .where(TenantAccountJoin.tenant_id == Tenant.id)
  193. .where(TenantAccountJoin.role.in_(["owner"]))
  194. .where(Tenant.status == TenantStatus.NORMAL)
  195. .one_or_none()
  196. ) # TODO: only owner information is required, so only one is returned.
  197. if tenant_account_join:
  198. tenant, ta = tenant_account_join
  199. account = db.session.query(Account).where(Account.id == ta.account_id).first()
  200. # Login admin
  201. if account:
  202. account.current_tenant = tenant
  203. current_app.login_manager._update_request_context_with_user(account) # type: ignore
  204. user_logged_in.send(current_app._get_current_object(), user=current_user) # type: ignore
  205. else:
  206. raise Unauthorized("Tenant owner account does not exist.")
  207. else:
  208. raise Unauthorized("Tenant does not exist.")
  209. return view(api_token.tenant_id, *args, **kwargs)
  210. return decorated
  211. if view:
  212. return decorator(view)
  213. # if view is None, it means that the decorator is used without parentheses
  214. # use the decorator as a function for method_decorators
  215. return decorator
  216. def validate_and_get_api_token(scope: str | None = None):
  217. """
  218. Validate and get API token.
  219. """
  220. auth_header = request.headers.get("Authorization")
  221. if auth_header is None or " " not in auth_header:
  222. raise Unauthorized("Authorization header must be provided and start with 'Bearer'")
  223. auth_scheme, auth_token = auth_header.split(None, 1)
  224. auth_scheme = auth_scheme.lower()
  225. if auth_scheme != "bearer":
  226. raise Unauthorized("Authorization scheme must be 'Bearer'")
  227. current_time = naive_utc_now()
  228. cutoff_time = current_time - timedelta(minutes=1)
  229. with Session(db.engine, expire_on_commit=False) as session:
  230. update_stmt = (
  231. update(ApiToken)
  232. .where(
  233. ApiToken.token == auth_token,
  234. (ApiToken.last_used_at.is_(None) | (ApiToken.last_used_at < cutoff_time)),
  235. ApiToken.type == scope,
  236. )
  237. .values(last_used_at=current_time)
  238. .returning(ApiToken)
  239. )
  240. result = session.execute(update_stmt)
  241. api_token = result.scalar_one_or_none()
  242. if not api_token:
  243. stmt = select(ApiToken).where(ApiToken.token == auth_token, ApiToken.type == scope)
  244. api_token = session.scalar(stmt)
  245. if not api_token:
  246. raise Unauthorized("Access token is invalid")
  247. else:
  248. session.commit()
  249. return api_token
  250. def create_or_update_end_user_for_user_id(app_model: App, user_id: str | None = None) -> EndUser:
  251. """
  252. Create or update session terminal based on user ID.
  253. """
  254. if not user_id:
  255. user_id = DefaultEndUserSessionID.DEFAULT_SESSION_ID.value
  256. with Session(db.engine, expire_on_commit=False) as session:
  257. end_user = (
  258. session.query(EndUser)
  259. .where(
  260. EndUser.tenant_id == app_model.tenant_id,
  261. EndUser.app_id == app_model.id,
  262. EndUser.session_id == user_id,
  263. EndUser.type == "service_api",
  264. )
  265. .first()
  266. )
  267. if end_user is None:
  268. end_user = EndUser(
  269. tenant_id=app_model.tenant_id,
  270. app_id=app_model.id,
  271. type="service_api",
  272. is_anonymous=user_id == DefaultEndUserSessionID.DEFAULT_SESSION_ID.value,
  273. session_id=user_id,
  274. )
  275. session.add(end_user)
  276. session.commit()
  277. return end_user
  278. class DatasetApiResource(Resource):
  279. method_decorators = [validate_dataset_token]
  280. def get_dataset(self, dataset_id: str, tenant_id: str) -> Dataset:
  281. dataset = db.session.query(Dataset).where(Dataset.id == dataset_id, Dataset.tenant_id == tenant_id).first()
  282. if not dataset:
  283. raise NotFound("Dataset not found.")
  284. return dataset