permissions.py 4.3 KB

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