feat: add permission decorator to enhance API access control

This commit is contained in:
shiyu
2026-02-09 12:32:25 +08:00
parent f444ec46cc
commit 451e8555d5
15 changed files with 269 additions and 98 deletions

View File

@@ -5,6 +5,8 @@ from fastapi import APIRouter, Depends, Request
from api.response import success
from domain.audit import AuditAction, audit
from domain.auth import User, get_current_active_user
from domain.permission import require_system_permission
from domain.permission.types import AdapterPermission
from .service import AdapterService
from .types import AdapterCreate
@@ -17,6 +19,7 @@ router = APIRouter(prefix="/api/adapters", tags=["adapters"])
description="创建存储适配器",
body_fields=["name", "type", "path", "sub_path", "enabled"],
)
@require_system_permission(AdapterPermission.CREATE)
async def create_adapter(
request: Request,
data: AdapterCreate,
@@ -28,6 +31,7 @@ async def create_adapter(
@router.get("")
@audit(action=AuditAction.READ, description="获取适配器列表")
@require_system_permission(AdapterPermission.LIST)
async def list_adapters(
request: Request,
current_user: Annotated[User, Depends(get_current_active_user)]
@@ -38,6 +42,7 @@ async def list_adapters(
@router.get("/available")
@audit(action=AuditAction.READ, description="获取可用适配器类型")
@require_system_permission(AdapterPermission.LIST)
async def available_adapter_types(
request: Request,
current_user: Annotated[User, Depends(get_current_active_user)]
@@ -48,6 +53,7 @@ async def available_adapter_types(
@router.get("/{adapter_id}")
@audit(action=AuditAction.READ, description="获取适配器详情")
@require_system_permission(AdapterPermission.LIST)
async def get_adapter(
request: Request,
adapter_id: int,
@@ -63,6 +69,7 @@ async def get_adapter(
description="更新存储适配器",
body_fields=["name", "type", "path", "sub_path", "enabled"],
)
@require_system_permission(AdapterPermission.EDIT)
async def update_adapter(
request: Request,
adapter_id: int,
@@ -75,6 +82,7 @@ async def update_adapter(
@router.delete("/{adapter_id}")
@audit(action=AuditAction.DELETE, description="删除存储适配器")
@require_system_permission(AdapterPermission.DELETE)
async def delete_adapter(
request: Request,
adapter_id: int,

View File

@@ -5,6 +5,8 @@ from fastapi import APIRouter, Depends, HTTPException, Query
from api import response
from domain.auth import User, get_current_active_user
from domain.permission import require_system_permission
from domain.permission.types import SystemPermission
from .service import AuditService
from .types import AuditAction
@@ -27,6 +29,7 @@ def _parse_iso(value: Optional[str], field: str):
@router.get("/logs")
@require_system_permission(SystemPermission.AUDIT_VIEW)
async def list_audit_logs(
current_user: CurrentUser,
page_num: int = Query(1, ge=1, alias="page", description="页码"),
@@ -54,6 +57,7 @@ async def list_audit_logs(
@router.delete("/logs")
@require_system_permission(SystemPermission.AUDIT_VIEW)
async def clear_audit_logs(
current_user: CurrentUser,
start_time: str | None = Query(None, description="开始时间 (ISO 8601)"),

View File

@@ -1,10 +1,13 @@
import datetime
from typing import Annotated
from fastapi import APIRouter, Depends, File, Form, Query, Request, UploadFile
from fastapi.responses import JSONResponse
from domain.audit import AuditAction, audit
from domain.auth import get_current_active_user
from domain.auth import User, get_current_active_user
from domain.permission import require_system_permission
from domain.permission.types import SystemPermission
from .service import BackupService
router = APIRouter(
@@ -16,8 +19,11 @@ router = APIRouter(
@router.get("/export", summary="导出全站数据")
@audit(action=AuditAction.DOWNLOAD, description="导出备份")
@require_system_permission(SystemPermission.CONFIG_EDIT)
async def export_backup(
request: Request, sections: list[str] | None = Query(default=None)
request: Request,
current_user: Annotated[User, Depends(get_current_active_user)],
sections: list[str] | None = Query(default=None),
):
data = await BackupService.export_data(sections=sections)
timestamp = datetime.datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
@@ -27,8 +33,10 @@ async def export_backup(
@router.post("/import", summary="导入数据")
@audit(action=AuditAction.UPLOAD, description="导入备份")
@require_system_permission(SystemPermission.CONFIG_EDIT)
async def import_backup(
request: Request,
current_user: Annotated[User, Depends(get_current_active_user)],
file: UploadFile = File(...),
mode: str = Form("replace"),
):

View File

@@ -5,7 +5,7 @@ from fastapi import APIRouter, Depends, Form, Request
from api.response import success
from domain.audit import AuditAction, audit
from domain.auth import User, get_current_active_user
from domain.permission.service import PermissionService
from domain.permission import require_system_permission
from domain.permission.types import SystemPermission
from .service import ConfigService
from .types import ConfigItem
@@ -23,42 +23,36 @@ PUBLIC_CONFIG_KEYS = [
@router.get("/")
@audit(action=AuditAction.READ, description="获取配置")
@require_system_permission(SystemPermission.CONFIG_EDIT)
async def get_config(
request: Request,
current_user: Annotated[User, Depends(get_current_active_user)],
key: str,
):
await PermissionService.require_system_permission(
current_user.id, SystemPermission.CONFIG_EDIT
)
value = await ConfigService.get(key)
return success(ConfigItem(key=key, value=value).model_dump())
@router.post("/")
@audit(action=AuditAction.UPDATE, description="设置配置", body_fields=["key", "value"])
@require_system_permission(SystemPermission.CONFIG_EDIT)
async def set_config(
request: Request,
current_user: Annotated[User, Depends(get_current_active_user)],
key: str = Form(...),
value: str = Form(""),
):
await PermissionService.require_system_permission(
current_user.id, SystemPermission.CONFIG_EDIT
)
await ConfigService.set(key, value)
return success(ConfigItem(key=key, value=value).model_dump())
@router.get("/all")
@audit(action=AuditAction.READ, description="获取全部配置")
@require_system_permission(SystemPermission.CONFIG_EDIT)
async def get_all_config(
request: Request,
current_user: Annotated[User, Depends(get_current_active_user)],
):
await PermissionService.require_system_permission(
current_user.id, SystemPermission.CONFIG_EDIT
)
configs = await ConfigService.get_all()
return success(configs)
@@ -66,7 +60,6 @@ async def get_all_config(
@audit(action=AuditAction.READ, description="获取公开配置")
async def get_public_config(
request: Request,
current_user: Annotated[User, Depends(get_current_active_user)],
):
data = {}
for key in PUBLIC_CONFIG_KEYS:

View File

@@ -5,6 +5,8 @@ from fastapi import APIRouter, Depends, Request
from api.response import success
from domain.audit import AuditAction, audit
from domain.auth import User, get_current_active_user
from domain.permission import require_path_permission
from domain.permission.types import PathAction
from .service import OfflineDownloadService
from .types import OfflineDownloadCreate
@@ -22,6 +24,7 @@ router = APIRouter(
description="创建离线下载任务",
body_fields=["url", "dest_dir", "filename"],
)
@require_path_permission(PathAction.WRITE, "payload.dest_dir")
async def create_offline_download(request: Request, payload: OfflineDownloadCreate, current_user: CurrentUser):
data = await OfflineDownloadService.create_download(payload, current_user)
return success(data)

View File

@@ -1,4 +1,10 @@
from .service import PermissionService
from .matcher import PathMatcher
from .decorator import require_path_permission, require_system_permission
__all__ = ["PermissionService", "PathMatcher"]
__all__ = [
"PermissionService",
"PathMatcher",
"require_system_permission",
"require_path_permission",
]

View File

@@ -0,0 +1,103 @@
import inspect
from functools import wraps
from typing import Any, Iterable, Mapping
from fastapi import HTTPException
from .service import PermissionService
def _get_user_id(user: Any) -> int | None:
if user is None:
return None
if isinstance(user, Mapping):
raw = user.get("id") or user.get("user_id")
return int(raw) if isinstance(raw, int) else None
value = getattr(user, "id", None) or getattr(user, "user_id", None)
return int(value) if isinstance(value, int) else None
def _resolve_expr(bound_args: Mapping[str, Any], expr: str) -> Any:
parts = [p for p in (expr or "").split(".") if p]
if not parts:
return None
cur: Any = bound_args.get(parts[0])
for part in parts[1:]:
if cur is None:
return None
if isinstance(cur, Mapping):
cur = cur.get(part)
else:
cur = getattr(cur, part, None)
return cur
def require_system_permission(permission_code: str, *, user_kw: str = "current_user"):
"""
在 endpoint 内部执行系统/适配器权限校验。
设计目标:
- 保持和当前“在函数体内手写 require_*”一致的行为:失败会被外层 @audit 捕获记录
- 不依赖 FastAPI dependencies避免权限失败发生在 endpoint 之外)
"""
def decorator(func):
@wraps(func)
async def wrapper(*args, **kwargs):
bound = inspect.signature(func).bind_partial(*args, **kwargs)
bound.apply_defaults()
user_id = _get_user_id(bound.arguments.get(user_kw))
if user_id is None:
raise HTTPException(status_code=401, detail="Unauthorized")
await PermissionService.require_system_permission(user_id, permission_code)
result = func(*args, **kwargs)
if inspect.isawaitable(result):
result = await result
return result
return wrapper
return decorator
def require_path_permission(action: str, path_expr: str, *, user_kw: str = "current_user"):
"""
在 endpoint 内部执行路径权限校验。
path_expr 支持:
- "full_path"
- "body.src" / "body.dst"
- "payload.paths"list[str] 会逐个检查)
"""
def decorator(func):
@wraps(func)
async def wrapper(*args, **kwargs):
bound = inspect.signature(func).bind_partial(*args, **kwargs)
bound.apply_defaults()
user_id = _get_user_id(bound.arguments.get(user_kw))
if user_id is None:
raise HTTPException(status_code=401, detail="Unauthorized")
value = _resolve_expr(bound.arguments, path_expr)
paths: Iterable[Any]
if isinstance(value, (list, tuple, set)):
paths = value
else:
paths = [value]
for path in paths:
if path is None:
raise HTTPException(status_code=400, detail="Missing path")
await PermissionService.require_path_permission(user_id, str(path), action)
result = func(*args, **kwargs)
if inspect.isawaitable(result):
result = await result
return result
return wrapper
return decorator

View File

@@ -2,12 +2,15 @@
插件管理 API 路由
"""
from typing import List
from typing import Annotated, List
from fastapi import APIRouter, File, Request, UploadFile
from fastapi import APIRouter, Depends, File, Request, UploadFile
from fastapi.responses import FileResponse
from domain.audit import AuditAction, audit
from domain.auth import User, get_current_active_user
from domain.permission import require_system_permission
from domain.permission.types import SystemPermission
from .service import PluginService
from .types import (
PluginInstallResult,
@@ -22,7 +25,12 @@ router = APIRouter(prefix="/api/plugins", tags=["plugins"])
@router.post("/install", response_model=PluginInstallResult)
@audit(action=AuditAction.CREATE, description="安装插件包")
async def install_plugin(request: Request, file: UploadFile = File(...)):
@require_system_permission(SystemPermission.ROLE_MANAGE)
async def install_plugin(
request: Request,
current_user: Annotated[User, Depends(get_current_active_user)],
file: UploadFile = File(...),
):
"""
安装 .foxpkg 插件包
@@ -37,14 +45,21 @@ async def install_plugin(request: Request, file: UploadFile = File(...)):
@router.get("", response_model=List[PluginOut])
@audit(action=AuditAction.READ, description="获取插件列表")
async def list_plugins(request: Request):
async def list_plugins(
request: Request,
current_user: Annotated[User, Depends(get_current_active_user)],
):
"""获取已安装的插件列表"""
return await PluginService.list_plugins()
@router.get("/{key_or_id}", response_model=PluginOut)
@audit(action=AuditAction.READ, description="获取插件详情")
async def get_plugin(request: Request, key_or_id: str):
async def get_plugin(
request: Request,
key_or_id: str,
current_user: Annotated[User, Depends(get_current_active_user)],
):
"""获取单个插件详情"""
return await PluginService.get_plugin(key_or_id)
@@ -54,7 +69,12 @@ async def get_plugin(request: Request, key_or_id: str):
@router.delete("/{key_or_id}")
@audit(action=AuditAction.DELETE, description="卸载插件")
async def delete_plugin(request: Request, key_or_id: str):
@require_system_permission(SystemPermission.ROLE_MANAGE)
async def delete_plugin(
request: Request,
key_or_id: str,
current_user: Annotated[User, Depends(get_current_active_user)],
):
"""卸载插件"""
await PluginService.delete(key_or_id)
return {"code": 0, "msg": "ok"}

View File

@@ -5,6 +5,12 @@ from fastapi import APIRouter, Body, Depends, Request
from api.response import success
from domain.audit import AuditAction, audit
from domain.auth import User, get_current_active_user
from domain.permission import require_path_permission
from domain.permission import require_system_permission
from domain.permission.service import PermissionService
from domain.permission.types import PathAction
from domain.permission.types import SystemPermission
from domain.processors.registry import get_config_schema
from .service import ProcessorService
from .types import (
ProcessDirectoryRequest,
@@ -31,11 +37,18 @@ async def list_processors(
description="处理单个文件",
body_fields=["path", "processor_type", "save_to", "overwrite"],
)
@require_path_permission(PathAction.READ, "req.path")
async def process_file_with_processor(
request: Request,
current_user: Annotated[User, Depends(get_current_active_user)],
req: ProcessRequest = Body(...),
):
meta = get_config_schema(req.processor_type) or {}
if meta.get("produces_file"):
if req.overwrite:
await PermissionService.require_path_permission(current_user.id, req.path, PathAction.WRITE)
elif req.save_to:
await PermissionService.require_path_permission(current_user.id, req.save_to, PathAction.WRITE)
data = await ProcessorService.process_file(req)
return success(data)
@@ -46,17 +59,22 @@ async def process_file_with_processor(
description="批量处理目录",
body_fields=["path", "processor_type", "overwrite", "max_depth", "suffix"],
)
@require_path_permission(PathAction.READ, "req.path")
async def process_directory_with_processor(
request: Request,
current_user: Annotated[User, Depends(get_current_active_user)],
req: ProcessDirectoryRequest = Body(...),
):
meta = get_config_schema(req.processor_type) or {}
if meta.get("produces_file"):
await PermissionService.require_path_permission(current_user.id, req.path, PathAction.WRITE)
data = await ProcessorService.process_directory(req)
return success(data)
@router.get("/source/{processor_type}")
@audit(action=AuditAction.READ, description="获取处理器源码")
@require_system_permission(SystemPermission.ROLE_MANAGE)
async def get_processor_source(
request: Request,
processor_type: str,
@@ -68,6 +86,7 @@ async def get_processor_source(
@router.put("/source/{processor_type}")
@audit(action=AuditAction.UPDATE, description="更新处理器源码")
@require_system_permission(SystemPermission.ROLE_MANAGE)
async def update_processor_source(
request: Request,
processor_type: str,
@@ -80,6 +99,7 @@ async def update_processor_source(
@router.post("/reload")
@audit(action=AuditAction.UPDATE, description="重载处理器模块")
@require_system_permission(SystemPermission.ROLE_MANAGE)
async def reload_processor_modules(
request: Request,
current_user: Annotated[User, Depends(get_current_active_user)],

View File

@@ -4,7 +4,7 @@ from fastapi import APIRouter, Depends
from domain.auth.service import get_current_active_user
from domain.auth.types import User
from domain.permission.service import PermissionService
from domain.permission import require_system_permission
from domain.permission.types import PathRuleCreate, PathRuleInfo, SystemPermission
from domain.user.service import UserService
from domain.user.types import UserInfo
@@ -16,127 +16,104 @@ router = APIRouter(prefix="/api", tags=["role"])
@router.get("/roles", response_model=list[RoleInfo])
@require_system_permission(SystemPermission.ROLE_MANAGE)
async def list_roles(
current_user: Annotated[User, Depends(get_current_active_user)]
) -> list[RoleInfo]:
await PermissionService.require_system_permission(
current_user.id, SystemPermission.ROLE_MANAGE
)
return await RoleService.get_all_roles()
@router.get("/roles/{role_id}", response_model=RoleDetail)
@require_system_permission(SystemPermission.ROLE_MANAGE)
async def get_role(
role_id: int,
current_user: Annotated[User, Depends(get_current_active_user)],
) -> RoleDetail:
await PermissionService.require_system_permission(
current_user.id, SystemPermission.ROLE_MANAGE
)
return await RoleService.get_role(role_id)
@router.get("/roles/{role_id}/users", response_model=list[UserInfo])
@require_system_permission(SystemPermission.ROLE_MANAGE)
async def list_role_users(
role_id: int,
current_user: Annotated[User, Depends(get_current_active_user)],
) -> list[UserInfo]:
await PermissionService.require_system_permission(
current_user.id, SystemPermission.ROLE_MANAGE
)
return await UserService.get_users_by_role(role_id)
@router.post("/roles", response_model=RoleInfo)
@require_system_permission(SystemPermission.ROLE_MANAGE)
async def create_role(
data: RoleCreate,
current_user: Annotated[User, Depends(get_current_active_user)],
) -> RoleInfo:
await PermissionService.require_system_permission(
current_user.id, SystemPermission.ROLE_MANAGE
)
return await RoleService.create_role(data)
@router.put("/roles/{role_id}", response_model=RoleInfo)
@require_system_permission(SystemPermission.ROLE_MANAGE)
async def update_role(
role_id: int,
data: RoleUpdate,
current_user: Annotated[User, Depends(get_current_active_user)],
) -> RoleInfo:
await PermissionService.require_system_permission(
current_user.id, SystemPermission.ROLE_MANAGE
)
return await RoleService.update_role(role_id, data)
@router.delete("/roles/{role_id}")
@require_system_permission(SystemPermission.ROLE_MANAGE)
async def delete_role(
role_id: int,
current_user: Annotated[User, Depends(get_current_active_user)],
) -> dict:
await PermissionService.require_system_permission(
current_user.id, SystemPermission.ROLE_MANAGE
)
await RoleService.delete_role(role_id)
return {"success": True}
@router.post("/roles/{role_id}/permissions", response_model=list[str])
@require_system_permission(SystemPermission.ROLE_MANAGE)
async def set_role_permissions(
role_id: int,
data: RolePermissionsUpdate,
current_user: Annotated[User, Depends(get_current_active_user)],
) -> list[str]:
await PermissionService.require_system_permission(
current_user.id, SystemPermission.ROLE_MANAGE
)
return await RoleService.set_role_permissions(role_id, data.permission_codes)
@router.get("/roles/{role_id}/path-rules", response_model=list[PathRuleInfo])
@require_system_permission(SystemPermission.ROLE_MANAGE)
async def get_role_path_rules(
role_id: int,
current_user: Annotated[User, Depends(get_current_active_user)],
) -> list[PathRuleInfo]:
await PermissionService.require_system_permission(
current_user.id, SystemPermission.ROLE_MANAGE
)
return await RoleService.get_role_path_rules(role_id)
@router.post("/roles/{role_id}/path-rules", response_model=PathRuleInfo)
@require_system_permission(SystemPermission.ROLE_MANAGE)
async def add_path_rule(
role_id: int,
data: PathRuleCreate,
current_user: Annotated[User, Depends(get_current_active_user)],
) -> PathRuleInfo:
await PermissionService.require_system_permission(
current_user.id, SystemPermission.ROLE_MANAGE
)
return await RoleService.add_path_rule(role_id, data)
@router.put("/path-rules/{rule_id}", response_model=PathRuleInfo)
@require_system_permission(SystemPermission.ROLE_MANAGE)
async def update_path_rule(
rule_id: int,
data: PathRuleCreate,
current_user: Annotated[User, Depends(get_current_active_user)],
) -> PathRuleInfo:
await PermissionService.require_system_permission(
current_user.id, SystemPermission.ROLE_MANAGE
)
return await RoleService.update_path_rule(rule_id, data)
@router.delete("/path-rules/{rule_id}")
@require_system_permission(SystemPermission.ROLE_MANAGE)
async def delete_path_rule(
rule_id: int,
current_user: Annotated[User, Depends(get_current_active_user)],
) -> dict:
await PermissionService.require_system_permission(
current_user.id, SystemPermission.ROLE_MANAGE
)
await RoleService.delete_path_rule(rule_id)
return {"success": True}

View File

@@ -5,6 +5,8 @@ from fastapi import APIRouter, Depends, Request
from api.response import success
from domain.audit import AuditAction, audit
from domain.auth import User, get_current_active_user
from domain.permission import require_path_permission
from domain.permission.types import PathAction
from .service import ShareService
from .types import (
ShareCreate,
@@ -24,6 +26,7 @@ router = APIRouter(prefix="/api/shares", tags=["Share - Management"])
description="创建分享链接",
body_fields=["name", "paths", "expires_in_days", "access_type"],
)
@require_path_permission(PathAction.SHARE, "payload.paths")
async def create_share(
request: Request,
payload: ShareCreate,

View File

@@ -4,7 +4,7 @@ from fastapi import APIRouter, Depends
from domain.auth.service import get_current_active_user
from domain.auth.types import User
from domain.permission.service import PermissionService
from domain.permission import require_system_permission
from domain.permission.types import SystemPermission
from .service import UserService
@@ -14,81 +14,66 @@ router = APIRouter(prefix="/api", tags=["user"])
@router.get("/users", response_model=list[UserInfo])
@require_system_permission(SystemPermission.USER_LIST)
async def list_users(
current_user: Annotated[User, Depends(get_current_active_user)]
) -> list[UserInfo]:
await PermissionService.require_system_permission(
current_user.id, SystemPermission.USER_LIST
)
return await UserService.get_all_users()
@router.get("/users/{user_id}", response_model=UserDetail)
@require_system_permission(SystemPermission.USER_LIST)
async def get_user(
user_id: int,
current_user: Annotated[User, Depends(get_current_active_user)],
) -> UserDetail:
await PermissionService.require_system_permission(
current_user.id, SystemPermission.USER_LIST
)
return await UserService.get_user(user_id)
@router.post("/users", response_model=UserDetail)
@require_system_permission(SystemPermission.USER_CREATE)
async def create_user(
data: UserCreate,
current_user: Annotated[User, Depends(get_current_active_user)],
) -> UserDetail:
await PermissionService.require_system_permission(
current_user.id, SystemPermission.USER_CREATE
)
return await UserService.create_user(data, current_user.id)
@router.put("/users/{user_id}", response_model=UserDetail)
@require_system_permission(SystemPermission.USER_EDIT)
async def update_user(
user_id: int,
data: UserUpdate,
current_user: Annotated[User, Depends(get_current_active_user)],
) -> UserDetail:
await PermissionService.require_system_permission(
current_user.id, SystemPermission.USER_EDIT
)
return await UserService.update_user(user_id, data, current_user.id)
@router.delete("/users/{user_id}")
@require_system_permission(SystemPermission.USER_DELETE)
async def delete_user(
user_id: int,
current_user: Annotated[User, Depends(get_current_active_user)],
) -> dict:
await PermissionService.require_system_permission(
current_user.id, SystemPermission.USER_DELETE
)
await UserService.delete_user(user_id, current_user.id)
return {"success": True}
@router.post("/users/{user_id}/roles", response_model=list[str])
@require_system_permission(SystemPermission.USER_EDIT)
async def set_user_roles(
user_id: int,
data: UserRoleAssign,
current_user: Annotated[User, Depends(get_current_active_user)],
) -> list[str]:
await PermissionService.require_system_permission(
current_user.id, SystemPermission.USER_EDIT
)
return await UserService.set_user_roles(user_id, data.role_ids)
@router.delete("/users/{user_id}/roles/{role_id}", response_model=list[str])
@require_system_permission(SystemPermission.USER_EDIT)
async def remove_user_role(
user_id: int,
role_id: int,
current_user: Annotated[User, Depends(get_current_active_user)],
) -> list[str]:
await PermissionService.require_system_permission(
current_user.id, SystemPermission.USER_EDIT
)
return await UserService.remove_user_role(user_id, role_id)

View File

@@ -5,7 +5,7 @@ from fastapi import APIRouter, Depends, File, Query, Request, UploadFile
from api.response import success
from domain.audit import AuditAction, audit
from domain.auth import User, get_current_active_user
from domain.permission.service import PermissionService
from domain.permission import require_path_permission
from domain.permission.types import PathAction
from .service import VirtualFSService
from .types import MkdirRequest, MoveRequest
@@ -15,12 +15,12 @@ router = APIRouter(prefix="/api/fs", tags=["virtual-fs"])
@router.get("/file/{full_path:path}")
@audit(action=AuditAction.DOWNLOAD, description="获取文件")
@require_path_permission(PathAction.READ, "full_path")
async def get_file(
full_path: str,
request: Request,
current_user: Annotated[User, Depends(get_current_active_user)],
):
await PermissionService.require_path_permission(current_user.id, full_path, PathAction.READ)
return await VirtualFSService.serve_file(full_path, request.headers.get("Range"))
@@ -47,13 +47,13 @@ async def stream_endpoint(
@router.get("/temp-link/{full_path:path}")
@audit(action=AuditAction.SHARE, description="创建临时链接")
@require_path_permission(PathAction.READ, "full_path")
async def get_temp_link(
full_path: str,
request: Request,
current_user: Annotated[User, Depends(get_current_active_user)],
expires_in: int = Query(3600, description="有效时间(秒), 0或负数表示永久"),
):
await PermissionService.require_path_permission(current_user.id, full_path, PathAction.READ)
data = await VirtualFSService.create_temp_link(full_path, expires_in)
return success(data)
@@ -79,25 +79,25 @@ async def access_public_file_with_name(
@router.get("/stat/{full_path:path}")
@audit(action=AuditAction.READ, description="查看文件信息")
@require_path_permission(PathAction.READ, "full_path")
async def get_file_stat(
full_path: str,
request: Request,
current_user: Annotated[User, Depends(get_current_active_user)],
):
await PermissionService.require_path_permission(current_user.id, full_path, PathAction.READ)
stat = await VirtualFSService.stat(full_path)
return success(stat)
@router.post("/file/{full_path:path}")
@audit(action=AuditAction.UPLOAD, description="上传文件")
@require_path_permission(PathAction.WRITE, "full_path")
async def put_file(
request: Request,
current_user: Annotated[User, Depends(get_current_active_user)],
full_path: str,
file: UploadFile = File(...),
):
await PermissionService.require_path_permission(current_user.id, full_path, PathAction.WRITE)
data = await file.read()
result = await VirtualFSService.write_uploaded_file(full_path, data)
return success(result)
@@ -105,62 +105,60 @@ async def put_file(
@router.post("/mkdir")
@audit(action=AuditAction.CREATE, description="创建目录", body_fields=["path"])
@require_path_permission(PathAction.WRITE, "body.path")
async def api_mkdir(
request: Request,
current_user: Annotated[User, Depends(get_current_active_user)],
body: MkdirRequest,
):
await PermissionService.require_path_permission(current_user.id, body.path, PathAction.WRITE)
result = await VirtualFSService.mkdir(body.path)
return success(result)
@router.post("/move")
@audit(action=AuditAction.UPDATE, description="移动路径", body_fields=["src", "dst"])
@require_path_permission(PathAction.WRITE, "body.dst")
@require_path_permission(PathAction.DELETE, "body.src")
async def api_move(
request: Request,
current_user: Annotated[User, Depends(get_current_active_user)],
body: MoveRequest,
overwrite: bool = Query(False, description="是否允许覆盖已存在目标"),
):
# 移动需要源路径的删除权限和目标路径的写权限
await PermissionService.require_path_permission(current_user.id, body.src, PathAction.DELETE)
await PermissionService.require_path_permission(current_user.id, body.dst, PathAction.WRITE)
result = await VirtualFSService.move(body.src, body.dst, overwrite)
return success(result)
@router.post("/rename")
@audit(action=AuditAction.UPDATE, description="重命名路径", body_fields=["src", "dst"])
@require_path_permission(PathAction.WRITE, "body.src")
async def api_rename(
request: Request,
current_user: Annotated[User, Depends(get_current_active_user)],
body: MoveRequest,
overwrite: bool = Query(False, description="是否允许覆盖已存在目标"),
):
# 重命名需要写权限
await PermissionService.require_path_permission(current_user.id, body.src, PathAction.WRITE)
result = await VirtualFSService.rename(body.src, body.dst, overwrite)
return success(result)
@router.post("/copy")
@audit(action=AuditAction.CREATE, description="复制路径", body_fields=["src", "dst"])
@require_path_permission(PathAction.WRITE, "body.dst")
@require_path_permission(PathAction.READ, "body.src")
async def api_copy(
request: Request,
current_user: Annotated[User, Depends(get_current_active_user)],
body: MoveRequest,
overwrite: bool = Query(False, description="是否覆盖已存在目标"),
):
# 复制需要源路径的读权限和目标路径的写权限
await PermissionService.require_path_permission(current_user.id, body.src, PathAction.READ)
await PermissionService.require_path_permission(current_user.id, body.dst, PathAction.WRITE)
result = await VirtualFSService.copy(body.src, body.dst, overwrite)
return success(result)
@router.post("/upload/{full_path:path}")
@audit(action=AuditAction.UPLOAD, description="流式上传文件")
@require_path_permission(PathAction.WRITE, "full_path")
async def upload_stream(
request: Request,
current_user: Annotated[User, Depends(get_current_active_user)],
@@ -169,13 +167,13 @@ async def upload_stream(
overwrite: bool = Query(True, description="是否覆盖已存在文件"),
chunk_size: int = Query(1024 * 1024, ge=8 * 1024, le=8 * 1024 * 1024, description="单次读取块大小"),
):
await PermissionService.require_path_permission(current_user.id, full_path, PathAction.WRITE)
result = await VirtualFSService.upload_stream_from_upload_file(full_path, file, chunk_size, overwrite)
return success(result)
@router.get("/{full_path:path}")
@audit(action=AuditAction.READ, description="浏览目录")
@require_path_permission(PathAction.READ, "full_path")
async def browse_fs(
request: Request,
current_user: Annotated[User, Depends(get_current_active_user)],
@@ -185,7 +183,6 @@ async def browse_fs(
sort_by: str = Query("name", description="按字段排序: name, size, mtime"),
sort_order: str = Query("asc", description="排序顺序: asc, desc"),
):
await PermissionService.require_path_permission(current_user.id, full_path, PathAction.READ)
data = await VirtualFSService.list_directory_with_permission(
full_path, current_user.id, page_num, page_size, sort_by, sort_order
)
@@ -194,12 +191,12 @@ async def browse_fs(
@router.delete("/{full_path:path}")
@audit(action=AuditAction.DELETE, description="删除路径")
@require_path_permission(PathAction.DELETE, "full_path")
async def api_delete(
request: Request,
current_user: Annotated[User, Depends(get_current_active_user)],
full_path: str,
):
await PermissionService.require_path_permission(current_user.id, full_path, PathAction.DELETE)
result = await VirtualFSService.delete(full_path)
return success(result)

View File

@@ -11,6 +11,8 @@ import xml.etree.ElementTree as ET
from domain.audit import AuditAction, audit
from domain.auth import AuthService, User, UserInDB
from domain.config import ConfigService
from domain.permission.service import PermissionService
from domain.permission.types import PathAction
from domain.virtual_fs import VirtualFSService
@@ -65,11 +67,26 @@ async def _get_basic_user(request: Request) -> User:
if not user_or_false:
raise HTTPException(401, detail="Invalid credentials", headers={"WWW-Authenticate": "Basic realm=webdav"})
u: UserInDB = user_or_false
return User(id=u.id, username=u.username, email=u.email, full_name=u.full_name, disabled=u.disabled)
return User(
id=u.id,
username=u.username,
email=u.email,
full_name=u.full_name,
disabled=u.disabled,
is_admin=u.is_admin,
)
elif scheme_lower == "bearer":
if not param:
raise HTTPException(401, detail="Invalid Bearer token")
return User(id=0, username="bearer", email=None, full_name=None, disabled=False)
u = await AuthService.get_current_user(param)
return User(
id=u.id,
username=u.username,
email=u.email,
full_name=u.full_name,
disabled=u.disabled,
is_admin=u.is_admin,
)
else:
raise HTTPException(401, detail="Unsupported auth", headers={"WWW-Authenticate": "Basic realm=webdav"})
@@ -155,6 +172,8 @@ async def propfind(
user: User = Depends(_get_basic_user),
):
full_path = _normalize_fs_path(path)
if full_path != "/":
await PermissionService.require_path_permission(user.id, full_path, PathAction.READ)
depth = request.headers.get("Depth", "1").lower()
if depth not in ("0", "1", "infinity"):
depth = "1"
@@ -195,7 +214,9 @@ async def propfind(
if depth in ("1", "infinity"):
try:
listing = await VirtualFSService.list_virtual_dir(full_path, page_num=1, page_size=1000)
listing = await VirtualFSService.list_virtual_dir_with_permission(
full_path, user.id, page_num=1, page_size=1000
)
for ent in (listing.get("items") or []):
is_dir = bool(ent.get("is_dir"))
name = ent.get("name")
@@ -223,6 +244,8 @@ async def dav_get(
user: User = Depends(_get_basic_user),
):
full_path = _normalize_fs_path(path)
if full_path != "/":
await PermissionService.require_path_permission(user.id, full_path, PathAction.READ)
range_header = request.headers.get("Range")
return await VirtualFSService.stream_file(full_path, range_header)
@@ -236,6 +259,8 @@ async def dav_head(
user: User = Depends(_get_basic_user),
):
full_path = _normalize_fs_path(path)
if full_path != "/":
await PermissionService.require_path_permission(user.id, full_path, PathAction.READ)
try:
st = await VirtualFSService.stat_file(full_path)
except FileNotFoundError:
@@ -264,6 +289,7 @@ async def dav_put(
user: User = Depends(_get_basic_user),
):
full_path = _normalize_fs_path(path)
await PermissionService.require_path_permission(user.id, full_path, PathAction.WRITE)
async def body_iter():
async for chunk in request.stream():
if chunk:
@@ -281,6 +307,7 @@ async def dav_delete(
user: User = Depends(_get_basic_user),
):
full_path = _normalize_fs_path(path)
await PermissionService.require_path_permission(user.id, full_path, PathAction.DELETE)
await VirtualFSService.delete_path(full_path)
return Response(status_code=204, headers=_dav_headers())
@@ -294,6 +321,7 @@ async def dav_mkcol(
user: User = Depends(_get_basic_user),
):
full_path = _normalize_fs_path(path)
await PermissionService.require_path_permission(user.id, full_path, PathAction.WRITE)
await VirtualFSService.make_dir(full_path)
return Response(status_code=201, headers=_dav_headers())
@@ -322,6 +350,8 @@ async def dav_move(
dest_header = request.headers.get("Destination")
dst = _parse_destination(dest_header or "")
overwrite = request.headers.get("Overwrite", "T").upper() != "F"
await PermissionService.require_path_permission(user.id, full_src, PathAction.DELETE)
await PermissionService.require_path_permission(user.id, dst, PathAction.WRITE)
await VirtualFSService.move_path(full_src, dst, overwrite=overwrite)
return Response(status_code=204, headers=_dav_headers())
@@ -338,5 +368,7 @@ async def dav_copy(
dest_header = request.headers.get("Destination")
dst = _parse_destination(dest_header or "")
overwrite = request.headers.get("Overwrite", "T").upper() != "F"
await PermissionService.require_path_permission(user.id, full_src, PathAction.READ)
await PermissionService.require_path_permission(user.id, dst, PathAction.WRITE)
await VirtualFSService.copy_path(full_src, dst, overwrite=overwrite)
return Response(status_code=201 if not overwrite else 204, headers=_dav_headers())

View File

@@ -2,6 +2,8 @@ from fastapi import APIRouter, Depends, Query
from api.response import success
from domain.auth import User, get_current_active_user
from domain.permission.service import PermissionService
from domain.permission.types import PathAction
from .search_service import VirtualFSSearchService
router = APIRouter(prefix="/api/fs/search", tags=["search"])
@@ -24,4 +26,14 @@ async def search_files(
page_size = max(min(page_size, 100), 1)
data = await VirtualFSSearchService.search(q, top_k, mode, page, page_size)
items = data.get("items") if isinstance(data, dict) else None
if isinstance(items, list) and items:
filtered = []
for item in items:
path = getattr(item, "path", None)
if not path:
continue
if await PermissionService.check_path_permission(user.id, str(path), PathAction.READ):
filtered.append(item)
data["items"] = filtered
return success(data)