"""鉴权依赖:从 Authorization header 解析当前用户,提供权限校验。""" from __future__ import annotations from typing import Any from fastapi import Depends, HTTPException, Query, Request, status from app.db.platform_store import get_platform_store # 无需鉴权的路径前缀(健康检查、登录等) PUBLIC_PATHS = ("/health", "/login", "/system-info") OWNER_TABLES = { "dataset": ("datasets", "created_by"), "model": ("models", "created_by"), "trained_model": ("trained_models", "created_by"), "eval": ("eval_tasks", "created_by"), "fine-tune": ("fine_tune_tasks", "payload"), "fine_tune_task": ("fine_tune_tasks", "payload"), "compare": ("compare_tasks", "payload"), "inference": ("compare_tasks", "payload"), "project": ("projects", "created_by"), "data_process": ("data_process_tasks", "created_by"), } def _extract_token(request: Request) -> str | None: """从 Authorization header 提取 token(格式: Bearer platform-token-{user_id})。""" auth = request.headers.get("Authorization", "") token = auth.replace("Bearer ", "").strip() if token.startswith("platform-token-"): return token[len("platform-token-"):] return None def _session_token(user_id: str, session_id: str) -> str: return f"platform-token-{user_id}.{session_id}" def get_current_user(request: Request) -> dict[str, Any]: """ FastAPI 依赖:解析当前登录用户。 - 公开路径(/health, /login 等)直接放行,返回匿名用户。 - 无 token 或 token 无效时抛 401。 - admin 用户标记为超级管理员,拥有全部权限。 """ path = request.url.path # 去掉路由前缀后判断 for prefix in PUBLIC_PATHS: if path.endswith(prefix): return {"id": None, "username": "anonymous", "role": "viewer", "permissions": [], "protected": False} token_value = _extract_token(request) if not token_value: raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="missing or invalid token") store = get_platform_store() user_id, _, session_id = token_value.partition(".") if session_id: with store.connect() as conn: session = conn.execute( "SELECT user_id, logout_at, expires_at FROM sessions WHERE id=?", (session_id,) ).fetchone() if not session or session["user_id"] != user_id or session["logout_at"]: raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="session expired") if session["expires_at"]: from datetime import datetime, timezone try: if datetime.fromisoformat(str(session["expires_at"]).replace("Z", "+00:00")) <= datetime.now(timezone.utc): raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="session expired") except ValueError: pass with store.connect() as conn: user_row = conn.execute("SELECT * FROM users WHERE id=?", (user_id,)).fetchone() if user_row: return store._user(user_row) raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="user not found") def require_admin(current_user: dict[str, Any] = Depends(get_current_user)) -> dict[str, Any]: """FastAPI 依赖:要求当前用户是管理员(role=admin 或 protected)。""" if current_user.get("role") == "admin" or current_user.get("protected"): return current_user raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="admin permission required") def is_admin(user: dict[str, Any]) -> bool: """判断用户是否为管理员(admin 角色或 protected 标记)。""" return user.get("role") == "admin" or user.get("protected", False) def has_resource_access( resource_type: str, resource_id: str, user: dict[str, Any], permission: str = "read", ) -> bool: """ 检查用户对某资源是否有指定权限。 - admin/protected 用户直接放行(旁路)。 - 其他用户检查 acls 表中是否有对应授权。 """ if user.get("role") == "admin" or user.get("protected"): return True store = get_platform_store() acls = store.get_acl(resource_type, resource_id) user_id = user.get("id") user_role = user.get("role") owner_tables = OWNER_TABLES table_info = owner_tables.get(resource_type) if table_info and user_id: table, column = table_info with store.connect() as conn: row = conn.execute(f"SELECT {column} FROM {table} WHERE id=?", (resource_id,)).fetchone() if row: owner = row[column] if column == "payload": try: import json owner = json.loads(owner or "{}").get("created_by") except (TypeError, ValueError): owner = None if owner == user_id: return True for entry in acls: # 按 user 授权 if entry.get("principal_type") == "user" and entry.get("principal_id") == user_id: if _permission_covers(entry.get("permission"), permission): return True # 按 role 授权 if entry.get("principal_type") == "role" and entry.get("principal_id") == user_role: if _permission_covers(entry.get("permission"), permission): return True return False def _permission_covers(granted: str | None, required: str) -> bool: """权限覆盖判断:write/execute 覆盖 read;admin 覆盖一切。""" if not granted: return False if granted == "admin": return True if granted == required: return True # write 覆盖 read if required == "read" and granted in ("write", "execute"): return True return False def filter_accessible_resource_ids( resource_type: str, all_ids: list[str], user: dict[str, Any], ) -> list[str]: """ 从全部资源 ID 中过滤出当前用户可访问的 ID 列表。 - admin 直接返回全部。 - 普通用户查 acls 表取交集。 """ if user.get("role") == "admin" or user.get("protected"): return all_ids if not all_ids: return [] store = get_platform_store() user_id = user.get("id") user_role = user.get("role") # 查询该用户在该资源类型下有 read 权限的所有 resource_id with store.connect() as conn: rows = conn.execute( """ SELECT DISTINCT resource_id FROM acls WHERE resource_type=? AND ( (principal_type='user' AND principal_id=?) OR (principal_type='role' AND principal_id=?) ) """, (resource_type, user_id, user_role), ).fetchall() accessible = {r["resource_id"] for r in rows} if resource_type in owner_tables: table, column = owner_tables[resource_type] with store.connect() as conn: owned = conn.execute(f"SELECT id FROM {table} WHERE {column}=?", (user_id,)).fetchall() accessible.update(row["id"] for row in owned) return [rid for rid in all_ids if rid in accessible] def filter_accessible_resource_ids_batch( resource_type: str, resource_ids: list[str], user: dict[str, Any], ) -> set[str]: """Filter a list endpoint with one ACL query instead of one query per row.""" if is_admin(user): return set(resource_ids) if not resource_ids: return set() store = get_platform_store() placeholders = ",".join("?" for _ in resource_ids) with store.connect() as conn: rows = conn.execute( f"SELECT DISTINCT resource_id FROM acls WHERE resource_type=? AND resource_id IN ({placeholders}) " "AND ((principal_type='user' AND principal_id=?) OR (principal_type='role' AND principal_id=?))", (resource_type, *resource_ids, user.get("id"), user.get("role")), ).fetchall() accessible = {row["resource_id"] for row in rows} table_info = OWNER_TABLES.get(resource_type) if table_info and user.get("id"): table, column = table_info with store.connect() as conn: owned = conn.execute( f"SELECT id FROM {table} WHERE id IN ({placeholders}) AND {column}=?", (*resource_ids, user["id"]), ).fetchall() accessible.update(row["id"] for row in owned) return accessible