permissions.py 5.3 KB

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