from __future__ import annotations from functools import wraps from typing import Iterable from flask import current_app, g, jsonify, request from app.models.result import failed READ_GOVERNANCE = "governance:read" EDIT_GOVERNANCE = "governance:edit" APPROVE_REVIEW = "review:approve" MANAGE_USERS = "users:manage" ACTIVATE_WORKFLOW = "workflow:activate" OPERATE_ORDERS = "orders:operate" DATASOURCE_POOL_MANAGE = "datasources:pools:manage" ROLE_PERMISSIONS = { "viewer": frozenset({READ_GOVERNANCE}), "editor": frozenset( {READ_GOVERNANCE, EDIT_GOVERNANCE, APPROVE_REVIEW, OPERATE_ORDERS} ), "admin": frozenset( { READ_GOVERNANCE, EDIT_GOVERNANCE, APPROVE_REVIEW, MANAGE_USERS, ACTIVATE_WORKFLOW, OPERATE_ORDERS, DATASOURCE_POOL_MANAGE, } ), } PUBLIC = "public" def permission_for_request(path: str, method: str) -> tuple[str, ...]: """Classify every API request in one auditable, deny-by-default policy.""" method = method.upper() if path in {"/api/system/health", "/api/system/auth/login"}: return (PUBLIC,) if path.startswith("/api/system/users"): return (MANAGE_USERS,) if path.startswith("/api/system/workbench"): return (READ_GOVERNANCE,) if ( path == "/api/datasource/pools" or ( path.startswith("/api/datasource/") and ( path.endswith("/pool") or path.endswith("/pool/invalidate") ) ) ): return (DATASOURCE_POOL_MANAGE,) if path == "/api/datasource/list": return (READ_GOVERNANCE,) if path.startswith("/api/system/") and not path.startswith("/api/system/auth/me"): return (MANAGE_USERS,) if path.startswith("/api/dataflow/") and any( marker in path.lower() for marker in ("activate", "publish", "execute") ): return (ACTIVATE_WORKFLOW,) if "order" in path.lower() and method != "GET": return (OPERATE_ORDERS,) if method == "GET": return (READ_GOVERNANCE,) if method in {"POST", "PUT", "PATCH", "DELETE"}: return (EDIT_GOVERNANCE,) return (MANAGE_USERS,) def permissions_for_roles(roles: Iterable[str]) -> frozenset[str]: result: set[str] = set() for role in roles: result.update(ROLE_PERMISSIONS.get(role, ())) return frozenset(result) def authenticate_request() -> dict | None: from app.core.system.auth import load_identity_from_token existing = getattr(g, "current_user", None) if existing: return existing header = request.headers.get("Authorization", "") if not header.startswith("Bearer "): return None token = header[7:].strip() if not token: return None return load_identity_from_token(token, secret=current_app.config["SECRET_KEY"]) def configure_api_authorization(app) -> None: @app.before_request def enforce_api_policy(): if not request.path.startswith("/api/") or request.method == "OPTIONS": return None required = permission_for_request(request.path, request.method) if required == (PUBLIC,): return None identity = authenticate_request() if identity is None: return jsonify(failed("未登录或登录已过期", code=401)), 401 permissions = permissions_for_roles(identity["roles"]) if not set(required).issubset(permissions): return jsonify(failed("权限不足", code=403)), 403 identity["permissions"] = sorted(permissions) g.current_user = identity return None def require_permissions(*required: str): def decorator(view): @wraps(view) def wrapped(*args, **kwargs): identity = authenticate_request() if identity is None: return jsonify(failed("未登录或登录已过期", code=401)), 401 permissions = permissions_for_roles(identity["roles"]) if not set(required).issubset(permissions): return jsonify(failed("权限不足", code=403)), 403 identity["permissions"] = sorted(permissions) g.current_user = identity return view(*args, **kwargs) wrapped.required_permissions = tuple(required) return wrapped return decorator