permissions.py 10 KB

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