permissions.py 4.6 KB

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