permissions.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415
  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. DEVICE_QUALITY_EDIT = "device-quality:edit"
  38. DEVICE_QUALITY_EXECUTE = "device-quality:execute"
  39. DEVICE_QUALITY_PUBLISH = "device-quality:publish"
  40. QUALITY_ISSUES_EDIT = "quality-issues:edit"
  41. QUALITY_ISSUES_REVIEW = "quality-issues:review"
  42. DEVICE_OBSERVABILITY_EDIT = "device-observability:edit"
  43. GOVERNANCE_AUDIT_READ = "governance-audit:read"
  44. GOVERNANCE_AUDIT_SEAL = "governance-audit:seal"
  45. DOMAIN_TEMPLATES_READ = "domain-templates:read"
  46. DOMAIN_TEMPLATES_PREVIEW = "domain-templates:preview"
  47. DOMAIN_TEMPLATES_MANAGE = "domain-templates:manage"
  48. ACTIVE_METADATA_READ = "active-metadata:read"
  49. ACTIVE_METADATA_OPERATE = "active-metadata:operate"
  50. ACTIVE_METADATA_MANAGE = "active-metadata:manage"
  51. SEMANTIC_GOVERNANCE_EDIT = "semantic-governance:edit"
  52. SEMANTIC_GOVERNANCE_REVIEW = "semantic-governance:review"
  53. SEMANTIC_GOVERNANCE_PUBLISH = "semantic-governance:publish"
  54. DATA_OBSERVABILITY_READ = "data-observability:read"
  55. DATA_OBSERVABILITY_OPERATE = "data-observability:operate"
  56. DATA_OBSERVABILITY_MANAGE = "data-observability:manage"
  57. WORK_CENTER_READ = "governance:work-center:read"
  58. WORK_CENTER_OPERATE = "governance:work-center:operate"
  59. WORK_CENTER_MANAGE = "governance:work-center:manage"
  60. ROLE_PERMISSIONS = {
  61. "viewer": frozenset(
  62. {
  63. READ_GOVERNANCE,
  64. RULES_READ,
  65. RESPONSIBILITIES_READ,
  66. DOMAIN_TEMPLATES_READ,
  67. ACTIVE_METADATA_READ,
  68. DATA_OBSERVABILITY_READ,
  69. WORK_CENTER_READ,
  70. }
  71. ),
  72. "editor": frozenset(
  73. {
  74. READ_GOVERNANCE,
  75. EDIT_GOVERNANCE,
  76. APPROVE_REVIEW,
  77. OPERATE_ORDERS,
  78. RULES_READ,
  79. RULES_EDIT,
  80. RULES_EXECUTE,
  81. RESPONSIBILITIES_READ,
  82. INGESTION_RUN,
  83. DATA_ELEMENTS_EDIT,
  84. ONTOLOGIES_EDIT,
  85. DEVICE_ASSETS_EDIT,
  86. DEVICE_SEMANTICS_EDIT,
  87. DEVICE_ENTITIES_EDIT,
  88. DEVICE_QUALITY_EDIT,
  89. DEVICE_QUALITY_EXECUTE,
  90. QUALITY_ISSUES_EDIT,
  91. DEVICE_OBSERVABILITY_EDIT,
  92. DOMAIN_TEMPLATES_READ,
  93. DOMAIN_TEMPLATES_PREVIEW,
  94. ACTIVE_METADATA_READ,
  95. ACTIVE_METADATA_OPERATE,
  96. SEMANTIC_GOVERNANCE_EDIT,
  97. DATA_OBSERVABILITY_READ,
  98. DATA_OBSERVABILITY_OPERATE,
  99. WORK_CENTER_READ,
  100. WORK_CENTER_OPERATE,
  101. }
  102. ),
  103. "admin": frozenset(
  104. {
  105. READ_GOVERNANCE,
  106. EDIT_GOVERNANCE,
  107. APPROVE_REVIEW,
  108. MANAGE_USERS,
  109. ACTIVATE_WORKFLOW,
  110. OPERATE_ORDERS,
  111. DATASOURCE_POOL_MANAGE,
  112. RESPONSIBILITIES_READ,
  113. RESPONSIBILITIES_MANAGE,
  114. KNOWLEDGE_MANAGE,
  115. RULES_READ,
  116. RULES_EDIT,
  117. RULES_PUBLISH,
  118. DATAFLOW_RELEASE,
  119. DATAFLOW_DEPLOY,
  120. DATAFLOW_CANARY,
  121. DATAFLOW_ACTIVATE,
  122. DATAFLOW_ROLLBACK,
  123. RULES_EXECUTE,
  124. INGESTION_RUN,
  125. INGESTION_ADMIN,
  126. EVIDENCE_DOWNLOAD,
  127. DATA_ELEMENTS_EDIT,
  128. DATA_ELEMENTS_PUBLISH,
  129. ONTOLOGIES_EDIT,
  130. ONTOLOGIES_PUBLISH,
  131. DEVICE_ASSETS_EDIT,
  132. DEVICE_SEMANTICS_EDIT,
  133. DEVICE_SEMANTICS_REVIEW,
  134. DEVICE_ENTITIES_EDIT,
  135. DEVICE_ENTITIES_REVIEW,
  136. DEVICE_QUALITY_EDIT,
  137. DEVICE_QUALITY_EXECUTE,
  138. DEVICE_QUALITY_PUBLISH,
  139. QUALITY_ISSUES_EDIT,
  140. QUALITY_ISSUES_REVIEW,
  141. DEVICE_OBSERVABILITY_EDIT,
  142. GOVERNANCE_AUDIT_READ,
  143. GOVERNANCE_AUDIT_SEAL,
  144. DOMAIN_TEMPLATES_READ,
  145. DOMAIN_TEMPLATES_PREVIEW,
  146. DOMAIN_TEMPLATES_MANAGE,
  147. ACTIVE_METADATA_READ,
  148. ACTIVE_METADATA_OPERATE,
  149. ACTIVE_METADATA_MANAGE,
  150. SEMANTIC_GOVERNANCE_EDIT,
  151. SEMANTIC_GOVERNANCE_REVIEW,
  152. SEMANTIC_GOVERNANCE_PUBLISH,
  153. DATA_OBSERVABILITY_READ,
  154. DATA_OBSERVABILITY_OPERATE,
  155. DATA_OBSERVABILITY_MANAGE,
  156. WORK_CENTER_READ,
  157. WORK_CENTER_OPERATE,
  158. WORK_CENTER_MANAGE,
  159. }
  160. ),
  161. }
  162. PUBLIC = "public"
  163. def permission_for_request(path: str, method: str) -> tuple[str, ...]:
  164. """Classify every API request in one auditable, deny-by-default policy."""
  165. method = method.upper()
  166. if path in {"/api/system/health", "/api/system/auth/login"}:
  167. return (PUBLIC,)
  168. if path.startswith("/api/system/responsibilities/"):
  169. if method == "GET":
  170. return (RESPONSIBILITIES_READ,)
  171. return (RESPONSIBILITIES_MANAGE,)
  172. if path.startswith("/api/system/work-center"):
  173. if method == "GET":
  174. return (WORK_CENTER_READ,)
  175. if any(
  176. segment in path
  177. for segment in ("/workflows", "/templates", "/timeouts", "/deliver")
  178. ):
  179. return (WORK_CENTER_MANAGE,)
  180. return (WORK_CENTER_OPERATE,)
  181. if path.startswith("/api/system/governance-audit"):
  182. if method == "GET":
  183. return (GOVERNANCE_AUDIT_READ,)
  184. return (GOVERNANCE_AUDIT_SEAL,)
  185. if path.startswith("/api/meta/domain-templates"):
  186. if method == "GET":
  187. return (DOMAIN_TEMPLATES_READ,)
  188. if path == "/api/meta/domain-templates/dry-run":
  189. return (DOMAIN_TEMPLATES_PREVIEW,)
  190. return (DOMAIN_TEMPLATES_MANAGE,)
  191. if path.startswith("/api/meta/active-metadata"):
  192. if method == "GET":
  193. return (ACTIVE_METADATA_READ,)
  194. if path == "/api/meta/active-metadata/plans":
  195. return (ACTIVE_METADATA_MANAGE,)
  196. return (ACTIVE_METADATA_OPERATE,)
  197. if path.startswith("/api/datafactory/observability"):
  198. if method == "GET":
  199. return (DATA_OBSERVABILITY_READ,)
  200. if path == "/api/datafactory/observability/slos":
  201. return (DATA_OBSERVABILITY_MANAGE,)
  202. return (DATA_OBSERVABILITY_OPERATE,)
  203. if path in {"/api/knowledge/search", "/api/knowledge/ask"}:
  204. return (READ_GOVERNANCE,)
  205. if path.startswith("/api/rules"):
  206. if path.startswith("/api/rules/deployments"):
  207. if method == "GET":
  208. return (RULES_READ,)
  209. if path.endswith("/deploy-disabled"):
  210. return (DATAFLOW_DEPLOY,)
  211. if path.endswith("/canary"):
  212. return (DATAFLOW_CANARY, RULES_EXECUTE)
  213. if path.endswith("/execute"):
  214. return (RULES_EXECUTE,)
  215. if path.endswith("/activate"):
  216. return (DATAFLOW_ACTIVATE,)
  217. if path.endswith("/rollback"):
  218. return (DATAFLOW_ROLLBACK,)
  219. if path.endswith("/reconcile-deploy"):
  220. return (DATAFLOW_DEPLOY,)
  221. if path.endswith("/reconcile-activate"):
  222. return (DATAFLOW_ACTIVATE,)
  223. if path.endswith("/reconcile-rollback"):
  224. return (DATAFLOW_ROLLBACK,)
  225. if path.endswith("/reconcile-execute"):
  226. return (RULES_EXECUTE,)
  227. if method == "POST":
  228. return (DATAFLOW_DEPLOY,)
  229. return (DATAFLOW_DEPLOY,)
  230. if method == "GET":
  231. return (RULES_READ,)
  232. if path.endswith("/publish"):
  233. return (RULES_PUBLISH,)
  234. if path.startswith("/api/rules/production-lines/") and path.endswith(
  235. "/release"
  236. ):
  237. return (DATAFLOW_RELEASE,)
  238. if path.endswith("/test") or path.endswith("/execute"):
  239. return (RULES_EXECUTE,)
  240. if method in {"POST", "PUT", "PATCH", "DELETE"}:
  241. return (RULES_EDIT,)
  242. return (RULES_PUBLISH,)
  243. if path.startswith("/api/knowledge/admin"):
  244. return (KNOWLEDGE_MANAGE,)
  245. if path.startswith("/api/system/users"):
  246. return (MANAGE_USERS,)
  247. if path.startswith("/api/development/v1/governance-metrics"):
  248. return (READ_GOVERNANCE,)
  249. if path.startswith("/api/development/v1/semantic"):
  250. if method == "GET":
  251. return (READ_GOVERNANCE,)
  252. if path.endswith("/review"):
  253. return (SEMANTIC_GOVERNANCE_REVIEW,)
  254. if path.endswith("/publish") or path.endswith("/rollback"):
  255. return (SEMANTIC_GOVERNANCE_PUBLISH,)
  256. return (SEMANTIC_GOVERNANCE_EDIT,)
  257. if path.startswith("/api/development/v1/device-assets"):
  258. if method == "GET":
  259. return (READ_GOVERNANCE,)
  260. return (DEVICE_ASSETS_EDIT,)
  261. if path.startswith("/api/development/v1/device-semantics"):
  262. if method == "GET":
  263. return (READ_GOVERNANCE,)
  264. if path.endswith("/review"):
  265. return (DEVICE_SEMANTICS_REVIEW,)
  266. return (DEVICE_SEMANTICS_EDIT,)
  267. if path.startswith("/api/development/v1/device-entities"):
  268. if method == "GET":
  269. return (READ_GOVERNANCE,)
  270. if path.endswith("/review") or path.endswith("/rollback"):
  271. return (DEVICE_ENTITIES_REVIEW,)
  272. return (DEVICE_ENTITIES_EDIT,)
  273. if path.startswith("/api/development/v1/device-observability"):
  274. if method == "GET":
  275. return (READ_GOVERNANCE,)
  276. return (DEVICE_OBSERVABILITY_EDIT,)
  277. if path.startswith("/api/development/v1/device-quality"):
  278. if method == "GET":
  279. return (READ_GOVERNANCE,)
  280. if path.startswith(
  281. "/api/development/v1/device-quality/issues"
  282. ):
  283. if path.endswith("/review"):
  284. return (QUALITY_ISSUES_REVIEW,)
  285. return (QUALITY_ISSUES_EDIT,)
  286. if path.endswith("/publish"):
  287. return (DEVICE_QUALITY_PUBLISH,)
  288. if path == "/api/development/v1/device-quality/runs":
  289. return (DEVICE_QUALITY_EXECUTE,)
  290. return (DEVICE_QUALITY_EDIT,)
  291. if path.startswith("/api/development/v1/ingestion-jobs"):
  292. if method == "GET":
  293. return (READ_GOVERNANCE,)
  294. if path.endswith("/retry"):
  295. return (INGESTION_ADMIN,)
  296. if path.endswith("/cancel"):
  297. return (INGESTION_RUN,)
  298. if method == "POST":
  299. return (INGESTION_RUN,)
  300. if path.startswith("/api/development/v1/data-elements"):
  301. if method == "GET":
  302. return (READ_GOVERNANCE,)
  303. return (DATA_ELEMENTS_EDIT,)
  304. if path.startswith("/api/development/v1/candidate-decisions"):
  305. return (DATA_ELEMENTS_EDIT,)
  306. if path.startswith("/api/development/v1/sources/files"):
  307. return (INGESTION_RUN,)
  308. if path.startswith("/api/development/v1/evidence"):
  309. return (READ_GOVERNANCE,)
  310. if path.startswith("/api/development/v1/ontologies"):
  311. if method == "GET":
  312. return (READ_GOVERNANCE,)
  313. if path.endswith("/publish") or path.endswith("/rollback"):
  314. return (ONTOLOGIES_PUBLISH,)
  315. return (ONTOLOGIES_EDIT,)
  316. if path.startswith("/api/bd/"):
  317. if path in {
  318. "/api/bd/list",
  319. "/api/bd/detail",
  320. "/api/bd/graphall",
  321. "/api/bd/search",
  322. }:
  323. return (READ_GOVERNANCE,)
  324. return (EDIT_GOVERNANCE,)
  325. if path.startswith("/api/system/workbench"):
  326. return (READ_GOVERNANCE,)
  327. if path == "/api/datasource/pools" or (
  328. path.startswith("/api/datasource/")
  329. and (path.endswith("/pool") or path.endswith("/pool/invalidate"))
  330. ):
  331. return (DATASOURCE_POOL_MANAGE,)
  332. if path == "/api/datasource/list":
  333. return (READ_GOVERNANCE,)
  334. if path.startswith("/api/system/") and not path.startswith("/api/system/auth/me"):
  335. return (MANAGE_USERS,)
  336. if path.startswith("/api/dataflow/") and any(
  337. marker in path.lower() for marker in ("activate", "publish", "execute")
  338. ):
  339. return (ACTIVATE_WORKFLOW,)
  340. if "order" in path.lower() and method != "GET":
  341. return (OPERATE_ORDERS,)
  342. if method == "GET":
  343. return (READ_GOVERNANCE,)
  344. if method in {"POST", "PUT", "PATCH", "DELETE"}:
  345. return (EDIT_GOVERNANCE,)
  346. return (MANAGE_USERS,)
  347. def permissions_for_roles(roles: Iterable[str]) -> frozenset[str]:
  348. result: set[str] = set()
  349. for role in roles:
  350. result.update(ROLE_PERMISSIONS.get(role, ()))
  351. return frozenset(result)
  352. def authenticate_request() -> dict | None:
  353. from app.core.system.auth import load_identity_from_token
  354. existing = getattr(g, "current_user", None)
  355. if existing:
  356. return existing
  357. header = request.headers.get("Authorization", "")
  358. if not header.startswith("Bearer "):
  359. return None
  360. token = header[7:].strip()
  361. if not token:
  362. return None
  363. return load_identity_from_token(token, secret=current_app.config["SECRET_KEY"])
  364. def configure_api_authorization(app) -> None:
  365. @app.before_request
  366. def enforce_api_policy():
  367. if not request.path.startswith("/api/") or request.method == "OPTIONS":
  368. return None
  369. required = permission_for_request(request.path, request.method)
  370. if required == (PUBLIC,):
  371. return None
  372. identity = authenticate_request()
  373. if identity is None:
  374. return jsonify(failed("未登录或登录已过期", code=401)), 401
  375. permissions = permissions_for_roles(identity["roles"])
  376. if not set(required).issubset(permissions):
  377. return jsonify(failed("权限不足", code=403)), 403
  378. identity["permissions"] = sorted(permissions)
  379. g.current_user = identity
  380. return None
  381. def require_permissions(*required: str):
  382. def decorator(view):
  383. @wraps(view)
  384. def wrapped(*args, **kwargs):
  385. identity = authenticate_request()
  386. if identity is None:
  387. return jsonify(failed("未登录或登录已过期", code=401)), 401
  388. permissions = permissions_for_roles(identity["roles"])
  389. if not set(required).issubset(permissions):
  390. return jsonify(failed("权限不足", code=403)), 403
  391. identity["permissions"] = sorted(permissions)
  392. g.current_user = identity
  393. return view(*args, **kwargs)
  394. wrapped.required_permissions = tuple(required)
  395. return wrapped
  396. return decorator