permissions.py 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256
  1. from __future__ import annotations
  2. from functools import wraps
  3. from typing import Iterable
  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. INGESTION_RUN = "ingestion:run"
  24. INGESTION_ADMIN = "ingestion:admin"
  25. EVIDENCE_DOWNLOAD = "evidence:download"
  26. DATA_ELEMENTS_EDIT = "data-elements:edit"
  27. DATA_ELEMENTS_PUBLISH = "data-elements:publish"
  28. ONTOLOGIES_EDIT = "ontologies:edit"
  29. ONTOLOGIES_PUBLISH = "ontologies:publish"
  30. ROLE_PERMISSIONS = {
  31. "viewer": frozenset({READ_GOVERNANCE, RULES_READ}),
  32. "editor": frozenset(
  33. {
  34. READ_GOVERNANCE,
  35. EDIT_GOVERNANCE,
  36. APPROVE_REVIEW,
  37. OPERATE_ORDERS,
  38. RULES_READ,
  39. RULES_EDIT,
  40. RULES_EXECUTE,
  41. INGESTION_RUN,
  42. DATA_ELEMENTS_EDIT,
  43. ONTOLOGIES_EDIT,
  44. }
  45. ),
  46. "admin": frozenset(
  47. {
  48. READ_GOVERNANCE,
  49. EDIT_GOVERNANCE,
  50. APPROVE_REVIEW,
  51. MANAGE_USERS,
  52. ACTIVATE_WORKFLOW,
  53. OPERATE_ORDERS,
  54. DATASOURCE_POOL_MANAGE,
  55. KNOWLEDGE_MANAGE,
  56. RULES_READ,
  57. RULES_EDIT,
  58. RULES_PUBLISH,
  59. DATAFLOW_RELEASE,
  60. DATAFLOW_DEPLOY,
  61. DATAFLOW_CANARY,
  62. DATAFLOW_ACTIVATE,
  63. DATAFLOW_ROLLBACK,
  64. RULES_EXECUTE,
  65. INGESTION_RUN,
  66. INGESTION_ADMIN,
  67. EVIDENCE_DOWNLOAD,
  68. DATA_ELEMENTS_EDIT,
  69. DATA_ELEMENTS_PUBLISH,
  70. ONTOLOGIES_EDIT,
  71. ONTOLOGIES_PUBLISH,
  72. }
  73. ),
  74. }
  75. PUBLIC = "public"
  76. def permission_for_request(path: str, method: str) -> tuple[str, ...]:
  77. """Classify every API request in one auditable, deny-by-default policy."""
  78. method = method.upper()
  79. if path in {"/api/system/health", "/api/system/auth/login"}:
  80. return (PUBLIC,)
  81. if path in {"/api/knowledge/search", "/api/knowledge/ask"}:
  82. return (READ_GOVERNANCE,)
  83. if path.startswith("/api/rules"):
  84. if path.startswith("/api/rules/deployments"):
  85. if method == "GET":
  86. return (RULES_READ,)
  87. if path.endswith("/deploy-disabled"):
  88. return (DATAFLOW_DEPLOY,)
  89. if path.endswith("/canary"):
  90. return (DATAFLOW_CANARY, RULES_EXECUTE)
  91. if path.endswith("/execute"):
  92. return (RULES_EXECUTE,)
  93. if path.endswith("/activate"):
  94. return (DATAFLOW_ACTIVATE,)
  95. if path.endswith("/rollback"):
  96. return (DATAFLOW_ROLLBACK,)
  97. if path.endswith("/reconcile-deploy"):
  98. return (DATAFLOW_DEPLOY,)
  99. if path.endswith("/reconcile-activate"):
  100. return (DATAFLOW_ACTIVATE,)
  101. if path.endswith("/reconcile-rollback"):
  102. return (DATAFLOW_ROLLBACK,)
  103. if path.endswith("/reconcile-execute"):
  104. return (RULES_EXECUTE,)
  105. if method == "POST":
  106. return (DATAFLOW_DEPLOY,)
  107. return (DATAFLOW_DEPLOY,)
  108. if method == "GET":
  109. return (RULES_READ,)
  110. if path.endswith("/publish"):
  111. return (RULES_PUBLISH,)
  112. if path.startswith("/api/rules/production-lines/") and path.endswith(
  113. "/release"
  114. ):
  115. return (DATAFLOW_RELEASE,)
  116. if path.endswith("/test") or path.endswith("/execute"):
  117. return (RULES_EXECUTE,)
  118. if method in {"POST", "PUT", "PATCH", "DELETE"}:
  119. return (RULES_EDIT,)
  120. return (RULES_PUBLISH,)
  121. if path.startswith("/api/knowledge/admin"):
  122. return (KNOWLEDGE_MANAGE,)
  123. if path.startswith("/api/system/users"):
  124. return (MANAGE_USERS,)
  125. if path.startswith("/api/development/v1/ingestion-jobs"):
  126. if method == "GET":
  127. return (READ_GOVERNANCE,)
  128. if path.endswith("/retry"):
  129. return (INGESTION_ADMIN,)
  130. if path.endswith("/cancel"):
  131. return (INGESTION_RUN,)
  132. if method == "POST":
  133. return (INGESTION_RUN,)
  134. if path.startswith("/api/development/v1/data-elements"):
  135. if method == "GET":
  136. return (READ_GOVERNANCE,)
  137. return (DATA_ELEMENTS_EDIT,)
  138. if path.startswith("/api/development/v1/candidate-decisions"):
  139. return (DATA_ELEMENTS_EDIT,)
  140. if path.startswith("/api/development/v1/sources/files"):
  141. return (INGESTION_RUN,)
  142. if path.startswith("/api/development/v1/evidence"):
  143. return (READ_GOVERNANCE,)
  144. if path.startswith("/api/development/v1/ontologies"):
  145. if method == "GET":
  146. return (READ_GOVERNANCE,)
  147. if path.endswith("/publish") or path.endswith("/rollback"):
  148. return (ONTOLOGIES_PUBLISH,)
  149. return (ONTOLOGIES_EDIT,)
  150. if path.startswith("/api/bd/"):
  151. if path in {
  152. "/api/bd/list",
  153. "/api/bd/detail",
  154. "/api/bd/graphall",
  155. "/api/bd/search",
  156. }:
  157. return (READ_GOVERNANCE,)
  158. return (EDIT_GOVERNANCE,)
  159. if path.startswith("/api/system/workbench"):
  160. return (READ_GOVERNANCE,)
  161. if (
  162. path == "/api/datasource/pools"
  163. or (
  164. path.startswith("/api/datasource/")
  165. and (
  166. path.endswith("/pool")
  167. or path.endswith("/pool/invalidate")
  168. )
  169. )
  170. ):
  171. return (DATASOURCE_POOL_MANAGE,)
  172. if path == "/api/datasource/list":
  173. return (READ_GOVERNANCE,)
  174. if path.startswith("/api/system/") and not path.startswith("/api/system/auth/me"):
  175. return (MANAGE_USERS,)
  176. if path.startswith("/api/dataflow/") and any(
  177. marker in path.lower() for marker in ("activate", "publish", "execute")
  178. ):
  179. return (ACTIVATE_WORKFLOW,)
  180. if "order" in path.lower() and method != "GET":
  181. return (OPERATE_ORDERS,)
  182. if method == "GET":
  183. return (READ_GOVERNANCE,)
  184. if method in {"POST", "PUT", "PATCH", "DELETE"}:
  185. return (EDIT_GOVERNANCE,)
  186. return (MANAGE_USERS,)
  187. def permissions_for_roles(roles: Iterable[str]) -> frozenset[str]:
  188. result: set[str] = set()
  189. for role in roles:
  190. result.update(ROLE_PERMISSIONS.get(role, ()))
  191. return frozenset(result)
  192. def authenticate_request() -> dict | None:
  193. from app.core.system.auth import load_identity_from_token
  194. existing = getattr(g, "current_user", None)
  195. if existing:
  196. return existing
  197. header = request.headers.get("Authorization", "")
  198. if not header.startswith("Bearer "):
  199. return None
  200. token = header[7:].strip()
  201. if not token:
  202. return None
  203. return load_identity_from_token(token, secret=current_app.config["SECRET_KEY"])
  204. def configure_api_authorization(app) -> None:
  205. @app.before_request
  206. def enforce_api_policy():
  207. if not request.path.startswith("/api/") or request.method == "OPTIONS":
  208. return None
  209. required = permission_for_request(request.path, request.method)
  210. if required == (PUBLIC,):
  211. return None
  212. identity = authenticate_request()
  213. if identity is None:
  214. return jsonify(failed("未登录或登录已过期", code=401)), 401
  215. permissions = permissions_for_roles(identity["roles"])
  216. if not set(required).issubset(permissions):
  217. return jsonify(failed("权限不足", code=403)), 403
  218. identity["permissions"] = sorted(permissions)
  219. g.current_user = identity
  220. return None
  221. def require_permissions(*required: str):
  222. def decorator(view):
  223. @wraps(view)
  224. def wrapped(*args, **kwargs):
  225. identity = authenticate_request()
  226. if identity is None:
  227. return jsonify(failed("未登录或登录已过期", code=401)), 401
  228. permissions = permissions_for_roles(identity["roles"])
  229. if not set(required).issubset(permissions):
  230. return jsonify(failed("权限不足", code=403)), 403
  231. identity["permissions"] = sorted(permissions)
  232. g.current_user = identity
  233. return view(*args, **kwargs)
  234. wrapped.required_permissions = tuple(required)
  235. return wrapped
  236. return decorator