permissions.py 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198
  1. from __future__ import annotations
  2. from collections.abc import Iterable
  3. from functools import wraps
  4. from flask import current_app, g, jsonify, request
  5. from app.models.result import failed
  6. READ_GOVERNANCE = "governance:read"
  7. EDIT_GOVERNANCE = "governance:edit"
  8. APPROVE_REVIEW = "review:approve"
  9. MANAGE_USERS = "users:manage"
  10. ACTIVATE_WORKFLOW = "workflow:activate"
  11. OPERATE_ORDERS = "orders:operate"
  12. DATASOURCE_POOL_MANAGE = "datasources:pools:manage"
  13. KNOWLEDGE_MANAGE = "knowledge:manage"
  14. RULES_READ = "rules:read"
  15. RULES_EDIT = "rules:edit"
  16. RULES_PUBLISH = "rules:publish"
  17. DATAFLOW_RELEASE = "dataflows:release"
  18. DATAFLOW_DEPLOY = "dataflows:deploy"
  19. DATAFLOW_CANARY = "dataflows:canary"
  20. DATAFLOW_ACTIVATE = "dataflows:activate"
  21. DATAFLOW_ROLLBACK = "dataflows:rollback"
  22. RULES_EXECUTE = "rules:execute"
  23. ROLE_PERMISSIONS = {
  24. "viewer": frozenset({READ_GOVERNANCE, RULES_READ}),
  25. "editor": frozenset(
  26. {
  27. READ_GOVERNANCE,
  28. EDIT_GOVERNANCE,
  29. APPROVE_REVIEW,
  30. OPERATE_ORDERS,
  31. RULES_READ,
  32. RULES_EDIT,
  33. RULES_EXECUTE,
  34. }
  35. ),
  36. "admin": frozenset(
  37. {
  38. READ_GOVERNANCE,
  39. EDIT_GOVERNANCE,
  40. APPROVE_REVIEW,
  41. MANAGE_USERS,
  42. ACTIVATE_WORKFLOW,
  43. OPERATE_ORDERS,
  44. DATASOURCE_POOL_MANAGE,
  45. KNOWLEDGE_MANAGE,
  46. RULES_READ,
  47. RULES_EDIT,
  48. RULES_PUBLISH,
  49. DATAFLOW_RELEASE,
  50. DATAFLOW_DEPLOY,
  51. DATAFLOW_CANARY,
  52. DATAFLOW_ACTIVATE,
  53. DATAFLOW_ROLLBACK,
  54. RULES_EXECUTE,
  55. }
  56. ),
  57. }
  58. PUBLIC = "public"
  59. def permission_for_request(path: str, method: str) -> tuple[str, ...]:
  60. """Classify every API request in one auditable, deny-by-default policy."""
  61. method = method.upper()
  62. if path in {"/api/system/health", "/api/system/auth/login"}:
  63. return (PUBLIC,)
  64. if path in {"/api/knowledge/search", "/api/knowledge/ask"}:
  65. return (READ_GOVERNANCE,)
  66. if path.startswith("/api/rules"):
  67. if path.startswith("/api/rules/deployments"):
  68. if method == "GET":
  69. return (RULES_READ,)
  70. if path.endswith("/deploy-disabled"):
  71. return (DATAFLOW_DEPLOY,)
  72. if path.endswith("/canary"):
  73. return (DATAFLOW_CANARY, RULES_EXECUTE)
  74. if path.endswith("/execute"):
  75. return (RULES_EXECUTE,)
  76. if path.endswith("/activate"):
  77. return (DATAFLOW_ACTIVATE,)
  78. if path.endswith("/rollback"):
  79. return (DATAFLOW_ROLLBACK,)
  80. if path.endswith("/reconcile-deploy"):
  81. return (DATAFLOW_DEPLOY,)
  82. if path.endswith("/reconcile-activate"):
  83. return (DATAFLOW_ACTIVATE,)
  84. if path.endswith("/reconcile-rollback"):
  85. return (DATAFLOW_ROLLBACK,)
  86. if path.endswith("/reconcile-execute"):
  87. return (RULES_EXECUTE,)
  88. if method == "POST":
  89. return (DATAFLOW_DEPLOY,)
  90. return (DATAFLOW_DEPLOY,)
  91. if method == "GET":
  92. return (RULES_READ,)
  93. if path.endswith("/publish"):
  94. return (RULES_PUBLISH,)
  95. if path.startswith("/api/rules/production-lines/") and path.endswith(
  96. "/release"
  97. ):
  98. return (DATAFLOW_RELEASE,)
  99. if path.endswith("/test") or path.endswith("/execute"):
  100. return (RULES_EXECUTE,)
  101. if method in {"POST", "PUT", "PATCH", "DELETE"}:
  102. return (RULES_EDIT,)
  103. return (RULES_PUBLISH,)
  104. if path.startswith("/api/knowledge/admin"):
  105. return (KNOWLEDGE_MANAGE,)
  106. if path.startswith("/api/system/users"):
  107. return (MANAGE_USERS,)
  108. if path.startswith("/api/system/workbench"):
  109. return (READ_GOVERNANCE,)
  110. if path == "/api/datasource/pools" or (
  111. path.startswith("/api/datasource/")
  112. and (path.endswith("/pool") or path.endswith("/pool/invalidate"))
  113. ):
  114. return (DATASOURCE_POOL_MANAGE,)
  115. if path == "/api/datasource/list":
  116. return (READ_GOVERNANCE,)
  117. if path.startswith("/api/system/") and not path.startswith("/api/system/auth/me"):
  118. return (MANAGE_USERS,)
  119. if path.startswith("/api/dataflow/") and any(
  120. marker in path.lower() for marker in ("activate", "publish", "execute")
  121. ):
  122. return (ACTIVATE_WORKFLOW,)
  123. if "order" in path.lower() and method != "GET":
  124. return (OPERATE_ORDERS,)
  125. if method == "GET":
  126. return (READ_GOVERNANCE,)
  127. if method in {"POST", "PUT", "PATCH", "DELETE"}:
  128. return (EDIT_GOVERNANCE,)
  129. return (MANAGE_USERS,)
  130. def permissions_for_roles(roles: Iterable[str]) -> frozenset[str]:
  131. result: set[str] = set()
  132. for role in roles:
  133. result.update(ROLE_PERMISSIONS.get(role, ()))
  134. return frozenset(result)
  135. def authenticate_request() -> dict | None:
  136. from app.core.system.auth import load_identity_from_token
  137. existing = getattr(g, "current_user", None)
  138. if existing:
  139. return existing
  140. header = request.headers.get("Authorization", "")
  141. if not header.startswith("Bearer "):
  142. return None
  143. token = header[7:].strip()
  144. if not token:
  145. return None
  146. return load_identity_from_token(token, secret=current_app.config["SECRET_KEY"])
  147. def configure_api_authorization(app) -> None:
  148. @app.before_request
  149. def enforce_api_policy():
  150. if not request.path.startswith("/api/") or request.method == "OPTIONS":
  151. return None
  152. required = permission_for_request(request.path, request.method)
  153. if required == (PUBLIC,):
  154. return None
  155. identity = authenticate_request()
  156. if identity is None:
  157. return jsonify(failed("未登录或登录已过期", code=401)), 401
  158. permissions = permissions_for_roles(identity["roles"])
  159. if not set(required).issubset(permissions):
  160. return jsonify(failed("权限不足", code=403)), 403
  161. identity["permissions"] = sorted(permissions)
  162. g.current_user = identity
  163. return None
  164. def require_permissions(*required: str):
  165. def decorator(view):
  166. @wraps(view)
  167. def wrapped(*args, **kwargs):
  168. identity = authenticate_request()
  169. if identity is None:
  170. return jsonify(failed("未登录或登录已过期", code=401)), 401
  171. permissions = permissions_for_roles(identity["roles"])
  172. if not set(required).issubset(permissions):
  173. return jsonify(failed("权限不足", code=403)), 403
  174. identity["permissions"] = sorted(permissions)
  175. g.current_user = identity
  176. return view(*args, **kwargs)
  177. wrapped.required_permissions = tuple(required)
  178. return wrapped
  179. return decorator