permissions.py 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176
  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. INGESTION_RUN = "ingestion:run"
  14. INGESTION_ADMIN = "ingestion:admin"
  15. EVIDENCE_DOWNLOAD = "evidence:download"
  16. DATA_ELEMENTS_EDIT = "data-elements:edit"
  17. DATA_ELEMENTS_PUBLISH = "data-elements:publish"
  18. ONTOLOGIES_EDIT = "ontologies:edit"
  19. ONTOLOGIES_PUBLISH = "ontologies:publish"
  20. ROLE_PERMISSIONS = {
  21. "viewer": frozenset({READ_GOVERNANCE}),
  22. "editor": frozenset(
  23. {
  24. READ_GOVERNANCE,
  25. EDIT_GOVERNANCE,
  26. APPROVE_REVIEW,
  27. OPERATE_ORDERS,
  28. INGESTION_RUN,
  29. DATA_ELEMENTS_EDIT,
  30. ONTOLOGIES_EDIT,
  31. }
  32. ),
  33. "admin": frozenset(
  34. {
  35. READ_GOVERNANCE,
  36. EDIT_GOVERNANCE,
  37. APPROVE_REVIEW,
  38. MANAGE_USERS,
  39. ACTIVATE_WORKFLOW,
  40. OPERATE_ORDERS,
  41. DATASOURCE_POOL_MANAGE,
  42. INGESTION_RUN,
  43. INGESTION_ADMIN,
  44. EVIDENCE_DOWNLOAD,
  45. DATA_ELEMENTS_EDIT,
  46. DATA_ELEMENTS_PUBLISH,
  47. ONTOLOGIES_EDIT,
  48. ONTOLOGIES_PUBLISH,
  49. }
  50. ),
  51. }
  52. PUBLIC = "public"
  53. def permission_for_request(path: str, method: str) -> tuple[str, ...]:
  54. """Classify every API request in one auditable, deny-by-default policy."""
  55. method = method.upper()
  56. if path in {"/api/system/health", "/api/system/auth/login"}:
  57. return (PUBLIC,)
  58. if path.startswith("/api/system/users"):
  59. return (MANAGE_USERS,)
  60. if path.startswith("/api/development/v1/ingestion-jobs"):
  61. if method == "GET":
  62. return (READ_GOVERNANCE,)
  63. if path.endswith("/retry"):
  64. return (INGESTION_ADMIN,)
  65. if path.endswith("/cancel"):
  66. return (INGESTION_RUN,)
  67. if method == "POST":
  68. return (INGESTION_RUN,)
  69. if path.startswith("/api/development/v1/data-elements"):
  70. if method == "GET":
  71. return (READ_GOVERNANCE,)
  72. return (DATA_ELEMENTS_EDIT,)
  73. if path.startswith("/api/development/v1/candidate-decisions"):
  74. return (DATA_ELEMENTS_EDIT,)
  75. if path.startswith("/api/development/v1/sources/files"):
  76. return (INGESTION_RUN,)
  77. if path.startswith("/api/development/v1/evidence"):
  78. return (READ_GOVERNANCE,)
  79. if path.startswith("/api/system/workbench"):
  80. return (READ_GOVERNANCE,)
  81. if (
  82. path == "/api/datasource/pools"
  83. or (
  84. path.startswith("/api/datasource/")
  85. and (
  86. path.endswith("/pool")
  87. or path.endswith("/pool/invalidate")
  88. )
  89. )
  90. ):
  91. return (DATASOURCE_POOL_MANAGE,)
  92. if path == "/api/datasource/list":
  93. return (READ_GOVERNANCE,)
  94. if path.startswith("/api/system/") and not path.startswith("/api/system/auth/me"):
  95. return (MANAGE_USERS,)
  96. if path.startswith("/api/dataflow/") and any(
  97. marker in path.lower() for marker in ("activate", "publish", "execute")
  98. ):
  99. return (ACTIVATE_WORKFLOW,)
  100. if "order" in path.lower() and method != "GET":
  101. return (OPERATE_ORDERS,)
  102. if method == "GET":
  103. return (READ_GOVERNANCE,)
  104. if method in {"POST", "PUT", "PATCH", "DELETE"}:
  105. return (EDIT_GOVERNANCE,)
  106. return (MANAGE_USERS,)
  107. def permissions_for_roles(roles: Iterable[str]) -> frozenset[str]:
  108. result: set[str] = set()
  109. for role in roles:
  110. result.update(ROLE_PERMISSIONS.get(role, ()))
  111. return frozenset(result)
  112. def authenticate_request() -> dict | None:
  113. from app.core.system.auth import load_identity_from_token
  114. existing = getattr(g, "current_user", None)
  115. if existing:
  116. return existing
  117. header = request.headers.get("Authorization", "")
  118. if not header.startswith("Bearer "):
  119. return None
  120. token = header[7:].strip()
  121. if not token:
  122. return None
  123. return load_identity_from_token(token, secret=current_app.config["SECRET_KEY"])
  124. def configure_api_authorization(app) -> None:
  125. @app.before_request
  126. def enforce_api_policy():
  127. if not request.path.startswith("/api/") or request.method == "OPTIONS":
  128. return None
  129. required = permission_for_request(request.path, request.method)
  130. if required == (PUBLIC,):
  131. return None
  132. identity = authenticate_request()
  133. if identity is None:
  134. return jsonify(failed("未登录或登录已过期", code=401)), 401
  135. permissions = permissions_for_roles(identity["roles"])
  136. if not set(required).issubset(permissions):
  137. return jsonify(failed("权限不足", code=403)), 403
  138. identity["permissions"] = sorted(permissions)
  139. g.current_user = identity
  140. return None
  141. def require_permissions(*required: str):
  142. def decorator(view):
  143. @wraps(view)
  144. def wrapped(*args, **kwargs):
  145. identity = authenticate_request()
  146. if identity is None:
  147. return jsonify(failed("未登录或登录已过期", code=401)), 401
  148. permissions = permissions_for_roles(identity["roles"])
  149. if not set(required).issubset(permissions):
  150. return jsonify(failed("权限不足", code=403)), 403
  151. identity["permissions"] = sorted(permissions)
  152. g.current_user = identity
  153. return view(*args, **kwargs)
  154. wrapped.required_permissions = tuple(required)
  155. return wrapped
  156. return decorator