2026-08-03 09:34:08 +08:00
|
|
|
|
"""鉴权依赖:从 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")
|
2026-08-12 15:21:23 +08:00
|
|
|
|
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"),
|
|
|
|
|
|
}
|
2026-08-03 09:34:08 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
2026-08-12 15:21:23 +08:00
|
|
|
|
def _session_token(user_id: str, session_id: str) -> str:
|
|
|
|
|
|
return f"platform-token-{user_id}.{session_id}"
|
|
|
|
|
|
|
2026-08-03 09:34:08 +08:00
|
|
|
|
|
|
|
|
|
|
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}
|
|
|
|
|
|
|
2026-08-12 15:21:23 +08:00
|
|
|
|
token_value = _extract_token(request)
|
|
|
|
|
|
if not token_value:
|
2026-08-03 09:34:08 +08:00
|
|
|
|
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="missing or invalid token")
|
|
|
|
|
|
|
|
|
|
|
|
store = get_platform_store()
|
2026-08-12 15:21:23 +08:00
|
|
|
|
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)
|
2026-08-03 09:34:08 +08:00
|
|
|
|
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")
|
|
|
|
|
|
|
2026-08-12 15:21:23 +08:00
|
|
|
|
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
|
|
|
|
|
|
|
2026-08-03 09:34:08 +08:00
|
|
|
|
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}
|
2026-08-12 15:21:23 +08:00
|
|
|
|
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)
|
2026-08-03 09:34:08 +08:00
|
|
|
|
return [rid for rid in all_ids if rid in accessible]
|
2026-08-12 15:21:23 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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
|