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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,10 @@
from .service import PermissionService from .service import PermissionService
from .matcher import PathMatcher 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 路由 插件管理 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 fastapi.responses import FileResponse
from domain.audit import AuditAction, audit 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 .service import PluginService
from .types import ( from .types import (
PluginInstallResult, PluginInstallResult,
@@ -22,7 +25,12 @@ router = APIRouter(prefix="/api/plugins", tags=["plugins"])
@router.post("/install", response_model=PluginInstallResult) @router.post("/install", response_model=PluginInstallResult)
@audit(action=AuditAction.CREATE, description="安装插件包") @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 插件包 安装 .foxpkg 插件包
@@ -37,14 +45,21 @@ async def install_plugin(request: Request, file: UploadFile = File(...)):
@router.get("", response_model=List[PluginOut]) @router.get("", response_model=List[PluginOut])
@audit(action=AuditAction.READ, description="获取插件列表") @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() return await PluginService.list_plugins()
@router.get("/{key_or_id}", response_model=PluginOut) @router.get("/{key_or_id}", response_model=PluginOut)
@audit(action=AuditAction.READ, description="获取插件详情") @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) 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}") @router.delete("/{key_or_id}")
@audit(action=AuditAction.DELETE, description="卸载插件") @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) await PluginService.delete(key_or_id)
return {"code": 0, "msg": "ok"} return {"code": 0, "msg": "ok"}

View File

@@ -5,6 +5,12 @@ from fastapi import APIRouter, Body, Depends, Request
from api.response import success from api.response import success
from domain.audit import AuditAction, audit from domain.audit import AuditAction, audit
from domain.auth import User, get_current_active_user 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 .service import ProcessorService
from .types import ( from .types import (
ProcessDirectoryRequest, ProcessDirectoryRequest,
@@ -31,11 +37,18 @@ async def list_processors(
description="处理单个文件", description="处理单个文件",
body_fields=["path", "processor_type", "save_to", "overwrite"], body_fields=["path", "processor_type", "save_to", "overwrite"],
) )
@require_path_permission(PathAction.READ, "req.path")
async def process_file_with_processor( async def process_file_with_processor(
request: Request, request: Request,
current_user: Annotated[User, Depends(get_current_active_user)], current_user: Annotated[User, Depends(get_current_active_user)],
req: ProcessRequest = Body(...), 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) data = await ProcessorService.process_file(req)
return success(data) return success(data)
@@ -46,17 +59,22 @@ async def process_file_with_processor(
description="批量处理目录", description="批量处理目录",
body_fields=["path", "processor_type", "overwrite", "max_depth", "suffix"], body_fields=["path", "processor_type", "overwrite", "max_depth", "suffix"],
) )
@require_path_permission(PathAction.READ, "req.path")
async def process_directory_with_processor( async def process_directory_with_processor(
request: Request, request: Request,
current_user: Annotated[User, Depends(get_current_active_user)], current_user: Annotated[User, Depends(get_current_active_user)],
req: ProcessDirectoryRequest = Body(...), 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) data = await ProcessorService.process_directory(req)
return success(data) return success(data)
@router.get("/source/{processor_type}") @router.get("/source/{processor_type}")
@audit(action=AuditAction.READ, description="获取处理器源码") @audit(action=AuditAction.READ, description="获取处理器源码")
@require_system_permission(SystemPermission.ROLE_MANAGE)
async def get_processor_source( async def get_processor_source(
request: Request, request: Request,
processor_type: str, processor_type: str,
@@ -68,6 +86,7 @@ async def get_processor_source(
@router.put("/source/{processor_type}") @router.put("/source/{processor_type}")
@audit(action=AuditAction.UPDATE, description="更新处理器源码") @audit(action=AuditAction.UPDATE, description="更新处理器源码")
@require_system_permission(SystemPermission.ROLE_MANAGE)
async def update_processor_source( async def update_processor_source(
request: Request, request: Request,
processor_type: str, processor_type: str,
@@ -80,6 +99,7 @@ async def update_processor_source(
@router.post("/reload") @router.post("/reload")
@audit(action=AuditAction.UPDATE, description="重载处理器模块") @audit(action=AuditAction.UPDATE, description="重载处理器模块")
@require_system_permission(SystemPermission.ROLE_MANAGE)
async def reload_processor_modules( async def reload_processor_modules(
request: Request, request: Request,
current_user: Annotated[User, Depends(get_current_active_user)], 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.service import get_current_active_user
from domain.auth.types import 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.permission.types import PathRuleCreate, PathRuleInfo, SystemPermission
from domain.user.service import UserService from domain.user.service import UserService
from domain.user.types import UserInfo from domain.user.types import UserInfo
@@ -16,127 +16,104 @@ router = APIRouter(prefix="/api", tags=["role"])
@router.get("/roles", response_model=list[RoleInfo]) @router.get("/roles", response_model=list[RoleInfo])
@require_system_permission(SystemPermission.ROLE_MANAGE)
async def list_roles( async def list_roles(
current_user: Annotated[User, Depends(get_current_active_user)] current_user: Annotated[User, Depends(get_current_active_user)]
) -> list[RoleInfo]: ) -> list[RoleInfo]:
await PermissionService.require_system_permission(
current_user.id, SystemPermission.ROLE_MANAGE
)
return await RoleService.get_all_roles() return await RoleService.get_all_roles()
@router.get("/roles/{role_id}", response_model=RoleDetail) @router.get("/roles/{role_id}", response_model=RoleDetail)
@require_system_permission(SystemPermission.ROLE_MANAGE)
async def get_role( async def get_role(
role_id: int, role_id: int,
current_user: Annotated[User, Depends(get_current_active_user)], current_user: Annotated[User, Depends(get_current_active_user)],
) -> RoleDetail: ) -> RoleDetail:
await PermissionService.require_system_permission(
current_user.id, SystemPermission.ROLE_MANAGE
)
return await RoleService.get_role(role_id) return await RoleService.get_role(role_id)
@router.get("/roles/{role_id}/users", response_model=list[UserInfo]) @router.get("/roles/{role_id}/users", response_model=list[UserInfo])
@require_system_permission(SystemPermission.ROLE_MANAGE)
async def list_role_users( async def list_role_users(
role_id: int, role_id: int,
current_user: Annotated[User, Depends(get_current_active_user)], current_user: Annotated[User, Depends(get_current_active_user)],
) -> list[UserInfo]: ) -> list[UserInfo]:
await PermissionService.require_system_permission(
current_user.id, SystemPermission.ROLE_MANAGE
)
return await UserService.get_users_by_role(role_id) return await UserService.get_users_by_role(role_id)
@router.post("/roles", response_model=RoleInfo) @router.post("/roles", response_model=RoleInfo)
@require_system_permission(SystemPermission.ROLE_MANAGE)
async def create_role( async def create_role(
data: RoleCreate, data: RoleCreate,
current_user: Annotated[User, Depends(get_current_active_user)], current_user: Annotated[User, Depends(get_current_active_user)],
) -> RoleInfo: ) -> RoleInfo:
await PermissionService.require_system_permission(
current_user.id, SystemPermission.ROLE_MANAGE
)
return await RoleService.create_role(data) return await RoleService.create_role(data)
@router.put("/roles/{role_id}", response_model=RoleInfo) @router.put("/roles/{role_id}", response_model=RoleInfo)
@require_system_permission(SystemPermission.ROLE_MANAGE)
async def update_role( async def update_role(
role_id: int, role_id: int,
data: RoleUpdate, data: RoleUpdate,
current_user: Annotated[User, Depends(get_current_active_user)], current_user: Annotated[User, Depends(get_current_active_user)],
) -> RoleInfo: ) -> RoleInfo:
await PermissionService.require_system_permission(
current_user.id, SystemPermission.ROLE_MANAGE
)
return await RoleService.update_role(role_id, data) return await RoleService.update_role(role_id, data)
@router.delete("/roles/{role_id}") @router.delete("/roles/{role_id}")
@require_system_permission(SystemPermission.ROLE_MANAGE)
async def delete_role( async def delete_role(
role_id: int, role_id: int,
current_user: Annotated[User, Depends(get_current_active_user)], current_user: Annotated[User, Depends(get_current_active_user)],
) -> dict: ) -> dict:
await PermissionService.require_system_permission(
current_user.id, SystemPermission.ROLE_MANAGE
)
await RoleService.delete_role(role_id) await RoleService.delete_role(role_id)
return {"success": True} return {"success": True}
@router.post("/roles/{role_id}/permissions", response_model=list[str]) @router.post("/roles/{role_id}/permissions", response_model=list[str])
@require_system_permission(SystemPermission.ROLE_MANAGE)
async def set_role_permissions( async def set_role_permissions(
role_id: int, role_id: int,
data: RolePermissionsUpdate, data: RolePermissionsUpdate,
current_user: Annotated[User, Depends(get_current_active_user)], current_user: Annotated[User, Depends(get_current_active_user)],
) -> list[str]: ) -> list[str]:
await PermissionService.require_system_permission(
current_user.id, SystemPermission.ROLE_MANAGE
)
return await RoleService.set_role_permissions(role_id, data.permission_codes) return await RoleService.set_role_permissions(role_id, data.permission_codes)
@router.get("/roles/{role_id}/path-rules", response_model=list[PathRuleInfo]) @router.get("/roles/{role_id}/path-rules", response_model=list[PathRuleInfo])
@require_system_permission(SystemPermission.ROLE_MANAGE)
async def get_role_path_rules( async def get_role_path_rules(
role_id: int, role_id: int,
current_user: Annotated[User, Depends(get_current_active_user)], current_user: Annotated[User, Depends(get_current_active_user)],
) -> list[PathRuleInfo]: ) -> list[PathRuleInfo]:
await PermissionService.require_system_permission(
current_user.id, SystemPermission.ROLE_MANAGE
)
return await RoleService.get_role_path_rules(role_id) return await RoleService.get_role_path_rules(role_id)
@router.post("/roles/{role_id}/path-rules", response_model=PathRuleInfo) @router.post("/roles/{role_id}/path-rules", response_model=PathRuleInfo)
@require_system_permission(SystemPermission.ROLE_MANAGE)
async def add_path_rule( async def add_path_rule(
role_id: int, role_id: int,
data: PathRuleCreate, data: PathRuleCreate,
current_user: Annotated[User, Depends(get_current_active_user)], current_user: Annotated[User, Depends(get_current_active_user)],
) -> PathRuleInfo: ) -> PathRuleInfo:
await PermissionService.require_system_permission(
current_user.id, SystemPermission.ROLE_MANAGE
)
return await RoleService.add_path_rule(role_id, data) return await RoleService.add_path_rule(role_id, data)
@router.put("/path-rules/{rule_id}", response_model=PathRuleInfo) @router.put("/path-rules/{rule_id}", response_model=PathRuleInfo)
@require_system_permission(SystemPermission.ROLE_MANAGE)
async def update_path_rule( async def update_path_rule(
rule_id: int, rule_id: int,
data: PathRuleCreate, data: PathRuleCreate,
current_user: Annotated[User, Depends(get_current_active_user)], current_user: Annotated[User, Depends(get_current_active_user)],
) -> PathRuleInfo: ) -> PathRuleInfo:
await PermissionService.require_system_permission(
current_user.id, SystemPermission.ROLE_MANAGE
)
return await RoleService.update_path_rule(rule_id, data) return await RoleService.update_path_rule(rule_id, data)
@router.delete("/path-rules/{rule_id}") @router.delete("/path-rules/{rule_id}")
@require_system_permission(SystemPermission.ROLE_MANAGE)
async def delete_path_rule( async def delete_path_rule(
rule_id: int, rule_id: int,
current_user: Annotated[User, Depends(get_current_active_user)], current_user: Annotated[User, Depends(get_current_active_user)],
) -> dict: ) -> dict:
await PermissionService.require_system_permission(
current_user.id, SystemPermission.ROLE_MANAGE
)
await RoleService.delete_path_rule(rule_id) await RoleService.delete_path_rule(rule_id)
return {"success": True} return {"success": True}

View File

@@ -5,6 +5,8 @@ from fastapi import APIRouter, Depends, Request
from api.response import success from api.response import success
from domain.audit import AuditAction, audit from domain.audit import AuditAction, audit
from domain.auth import User, get_current_active_user 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 .service import ShareService
from .types import ( from .types import (
ShareCreate, ShareCreate,
@@ -24,6 +26,7 @@ router = APIRouter(prefix="/api/shares", tags=["Share - Management"])
description="创建分享链接", description="创建分享链接",
body_fields=["name", "paths", "expires_in_days", "access_type"], body_fields=["name", "paths", "expires_in_days", "access_type"],
) )
@require_path_permission(PathAction.SHARE, "payload.paths")
async def create_share( async def create_share(
request: Request, request: Request,
payload: ShareCreate, 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.service import get_current_active_user
from domain.auth.types import 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 domain.permission.types import SystemPermission
from .service import UserService from .service import UserService
@@ -14,81 +14,66 @@ router = APIRouter(prefix="/api", tags=["user"])
@router.get("/users", response_model=list[UserInfo]) @router.get("/users", response_model=list[UserInfo])
@require_system_permission(SystemPermission.USER_LIST)
async def list_users( async def list_users(
current_user: Annotated[User, Depends(get_current_active_user)] current_user: Annotated[User, Depends(get_current_active_user)]
) -> list[UserInfo]: ) -> list[UserInfo]:
await PermissionService.require_system_permission(
current_user.id, SystemPermission.USER_LIST
)
return await UserService.get_all_users() return await UserService.get_all_users()
@router.get("/users/{user_id}", response_model=UserDetail) @router.get("/users/{user_id}", response_model=UserDetail)
@require_system_permission(SystemPermission.USER_LIST)
async def get_user( async def get_user(
user_id: int, user_id: int,
current_user: Annotated[User, Depends(get_current_active_user)], current_user: Annotated[User, Depends(get_current_active_user)],
) -> UserDetail: ) -> UserDetail:
await PermissionService.require_system_permission(
current_user.id, SystemPermission.USER_LIST
)
return await UserService.get_user(user_id) return await UserService.get_user(user_id)
@router.post("/users", response_model=UserDetail) @router.post("/users", response_model=UserDetail)
@require_system_permission(SystemPermission.USER_CREATE)
async def create_user( async def create_user(
data: UserCreate, data: UserCreate,
current_user: Annotated[User, Depends(get_current_active_user)], current_user: Annotated[User, Depends(get_current_active_user)],
) -> UserDetail: ) -> UserDetail:
await PermissionService.require_system_permission(
current_user.id, SystemPermission.USER_CREATE
)
return await UserService.create_user(data, current_user.id) return await UserService.create_user(data, current_user.id)
@router.put("/users/{user_id}", response_model=UserDetail) @router.put("/users/{user_id}", response_model=UserDetail)
@require_system_permission(SystemPermission.USER_EDIT)
async def update_user( async def update_user(
user_id: int, user_id: int,
data: UserUpdate, data: UserUpdate,
current_user: Annotated[User, Depends(get_current_active_user)], current_user: Annotated[User, Depends(get_current_active_user)],
) -> UserDetail: ) -> UserDetail:
await PermissionService.require_system_permission(
current_user.id, SystemPermission.USER_EDIT
)
return await UserService.update_user(user_id, data, current_user.id) return await UserService.update_user(user_id, data, current_user.id)
@router.delete("/users/{user_id}") @router.delete("/users/{user_id}")
@require_system_permission(SystemPermission.USER_DELETE)
async def delete_user( async def delete_user(
user_id: int, user_id: int,
current_user: Annotated[User, Depends(get_current_active_user)], current_user: Annotated[User, Depends(get_current_active_user)],
) -> dict: ) -> dict:
await PermissionService.require_system_permission(
current_user.id, SystemPermission.USER_DELETE
)
await UserService.delete_user(user_id, current_user.id) await UserService.delete_user(user_id, current_user.id)
return {"success": True} return {"success": True}
@router.post("/users/{user_id}/roles", response_model=list[str]) @router.post("/users/{user_id}/roles", response_model=list[str])
@require_system_permission(SystemPermission.USER_EDIT)
async def set_user_roles( async def set_user_roles(
user_id: int, user_id: int,
data: UserRoleAssign, data: UserRoleAssign,
current_user: Annotated[User, Depends(get_current_active_user)], current_user: Annotated[User, Depends(get_current_active_user)],
) -> list[str]: ) -> list[str]:
await PermissionService.require_system_permission(
current_user.id, SystemPermission.USER_EDIT
)
return await UserService.set_user_roles(user_id, data.role_ids) return await UserService.set_user_roles(user_id, data.role_ids)
@router.delete("/users/{user_id}/roles/{role_id}", response_model=list[str]) @router.delete("/users/{user_id}/roles/{role_id}", response_model=list[str])
@require_system_permission(SystemPermission.USER_EDIT)
async def remove_user_role( async def remove_user_role(
user_id: int, user_id: int,
role_id: int, role_id: int,
current_user: Annotated[User, Depends(get_current_active_user)], current_user: Annotated[User, Depends(get_current_active_user)],
) -> list[str]: ) -> list[str]:
await PermissionService.require_system_permission(
current_user.id, SystemPermission.USER_EDIT
)
return await UserService.remove_user_role(user_id, role_id) 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 api.response import success
from domain.audit import AuditAction, audit from domain.audit import AuditAction, audit
from domain.auth import User, get_current_active_user 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 domain.permission.types import PathAction
from .service import VirtualFSService from .service import VirtualFSService
from .types import MkdirRequest, MoveRequest from .types import MkdirRequest, MoveRequest
@@ -15,12 +15,12 @@ router = APIRouter(prefix="/api/fs", tags=["virtual-fs"])
@router.get("/file/{full_path:path}") @router.get("/file/{full_path:path}")
@audit(action=AuditAction.DOWNLOAD, description="获取文件") @audit(action=AuditAction.DOWNLOAD, description="获取文件")
@require_path_permission(PathAction.READ, "full_path")
async def get_file( async def get_file(
full_path: str, full_path: str,
request: Request, request: Request,
current_user: Annotated[User, Depends(get_current_active_user)], 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")) 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}") @router.get("/temp-link/{full_path:path}")
@audit(action=AuditAction.SHARE, description="创建临时链接") @audit(action=AuditAction.SHARE, description="创建临时链接")
@require_path_permission(PathAction.READ, "full_path")
async def get_temp_link( async def get_temp_link(
full_path: str, full_path: str,
request: Request, request: Request,
current_user: Annotated[User, Depends(get_current_active_user)], current_user: Annotated[User, Depends(get_current_active_user)],
expires_in: int = Query(3600, description="有效时间(秒), 0或负数表示永久"), 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) data = await VirtualFSService.create_temp_link(full_path, expires_in)
return success(data) return success(data)
@@ -79,25 +79,25 @@ async def access_public_file_with_name(
@router.get("/stat/{full_path:path}") @router.get("/stat/{full_path:path}")
@audit(action=AuditAction.READ, description="查看文件信息") @audit(action=AuditAction.READ, description="查看文件信息")
@require_path_permission(PathAction.READ, "full_path")
async def get_file_stat( async def get_file_stat(
full_path: str, full_path: str,
request: Request, request: Request,
current_user: Annotated[User, Depends(get_current_active_user)], 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) stat = await VirtualFSService.stat(full_path)
return success(stat) return success(stat)
@router.post("/file/{full_path:path}") @router.post("/file/{full_path:path}")
@audit(action=AuditAction.UPLOAD, description="上传文件") @audit(action=AuditAction.UPLOAD, description="上传文件")
@require_path_permission(PathAction.WRITE, "full_path")
async def put_file( async def put_file(
request: Request, request: Request,
current_user: Annotated[User, Depends(get_current_active_user)], current_user: Annotated[User, Depends(get_current_active_user)],
full_path: str, full_path: str,
file: UploadFile = File(...), file: UploadFile = File(...),
): ):
await PermissionService.require_path_permission(current_user.id, full_path, PathAction.WRITE)
data = await file.read() data = await file.read()
result = await VirtualFSService.write_uploaded_file(full_path, data) result = await VirtualFSService.write_uploaded_file(full_path, data)
return success(result) return success(result)
@@ -105,62 +105,60 @@ async def put_file(
@router.post("/mkdir") @router.post("/mkdir")
@audit(action=AuditAction.CREATE, description="创建目录", body_fields=["path"]) @audit(action=AuditAction.CREATE, description="创建目录", body_fields=["path"])
@require_path_permission(PathAction.WRITE, "body.path")
async def api_mkdir( async def api_mkdir(
request: Request, request: Request,
current_user: Annotated[User, Depends(get_current_active_user)], current_user: Annotated[User, Depends(get_current_active_user)],
body: MkdirRequest, body: MkdirRequest,
): ):
await PermissionService.require_path_permission(current_user.id, body.path, PathAction.WRITE)
result = await VirtualFSService.mkdir(body.path) result = await VirtualFSService.mkdir(body.path)
return success(result) return success(result)
@router.post("/move") @router.post("/move")
@audit(action=AuditAction.UPDATE, description="移动路径", body_fields=["src", "dst"]) @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( async def api_move(
request: Request, request: Request,
current_user: Annotated[User, Depends(get_current_active_user)], current_user: Annotated[User, Depends(get_current_active_user)],
body: MoveRequest, body: MoveRequest,
overwrite: bool = Query(False, description="是否允许覆盖已存在目标"), 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) result = await VirtualFSService.move(body.src, body.dst, overwrite)
return success(result) return success(result)
@router.post("/rename") @router.post("/rename")
@audit(action=AuditAction.UPDATE, description="重命名路径", body_fields=["src", "dst"]) @audit(action=AuditAction.UPDATE, description="重命名路径", body_fields=["src", "dst"])
@require_path_permission(PathAction.WRITE, "body.src")
async def api_rename( async def api_rename(
request: Request, request: Request,
current_user: Annotated[User, Depends(get_current_active_user)], current_user: Annotated[User, Depends(get_current_active_user)],
body: MoveRequest, body: MoveRequest,
overwrite: bool = Query(False, description="是否允许覆盖已存在目标"), 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) result = await VirtualFSService.rename(body.src, body.dst, overwrite)
return success(result) return success(result)
@router.post("/copy") @router.post("/copy")
@audit(action=AuditAction.CREATE, description="复制路径", body_fields=["src", "dst"]) @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( async def api_copy(
request: Request, request: Request,
current_user: Annotated[User, Depends(get_current_active_user)], current_user: Annotated[User, Depends(get_current_active_user)],
body: MoveRequest, body: MoveRequest,
overwrite: bool = Query(False, description="是否覆盖已存在目标"), 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) result = await VirtualFSService.copy(body.src, body.dst, overwrite)
return success(result) return success(result)
@router.post("/upload/{full_path:path}") @router.post("/upload/{full_path:path}")
@audit(action=AuditAction.UPLOAD, description="流式上传文件") @audit(action=AuditAction.UPLOAD, description="流式上传文件")
@require_path_permission(PathAction.WRITE, "full_path")
async def upload_stream( async def upload_stream(
request: Request, request: Request,
current_user: Annotated[User, Depends(get_current_active_user)], current_user: Annotated[User, Depends(get_current_active_user)],
@@ -169,13 +167,13 @@ async def upload_stream(
overwrite: bool = Query(True, description="是否覆盖已存在文件"), overwrite: bool = Query(True, description="是否覆盖已存在文件"),
chunk_size: int = Query(1024 * 1024, ge=8 * 1024, le=8 * 1024 * 1024, 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) result = await VirtualFSService.upload_stream_from_upload_file(full_path, file, chunk_size, overwrite)
return success(result) return success(result)
@router.get("/{full_path:path}") @router.get("/{full_path:path}")
@audit(action=AuditAction.READ, description="浏览目录") @audit(action=AuditAction.READ, description="浏览目录")
@require_path_permission(PathAction.READ, "full_path")
async def browse_fs( async def browse_fs(
request: Request, request: Request,
current_user: Annotated[User, Depends(get_current_active_user)], 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_by: str = Query("name", description="按字段排序: name, size, mtime"),
sort_order: str = Query("asc", description="排序顺序: asc, desc"), 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( data = await VirtualFSService.list_directory_with_permission(
full_path, current_user.id, page_num, page_size, sort_by, sort_order 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}") @router.delete("/{full_path:path}")
@audit(action=AuditAction.DELETE, description="删除路径") @audit(action=AuditAction.DELETE, description="删除路径")
@require_path_permission(PathAction.DELETE, "full_path")
async def api_delete( async def api_delete(
request: Request, request: Request,
current_user: Annotated[User, Depends(get_current_active_user)], current_user: Annotated[User, Depends(get_current_active_user)],
full_path: str, full_path: str,
): ):
await PermissionService.require_path_permission(current_user.id, full_path, PathAction.DELETE)
result = await VirtualFSService.delete(full_path) result = await VirtualFSService.delete(full_path)
return success(result) return success(result)

View File

@@ -11,6 +11,8 @@ import xml.etree.ElementTree as ET
from domain.audit import AuditAction, audit from domain.audit import AuditAction, audit
from domain.auth import AuthService, User, UserInDB from domain.auth import AuthService, User, UserInDB
from domain.config import ConfigService from domain.config import ConfigService
from domain.permission.service import PermissionService
from domain.permission.types import PathAction
from domain.virtual_fs import VirtualFSService from domain.virtual_fs import VirtualFSService
@@ -65,11 +67,26 @@ async def _get_basic_user(request: Request) -> User:
if not user_or_false: if not user_or_false:
raise HTTPException(401, detail="Invalid credentials", headers={"WWW-Authenticate": "Basic realm=webdav"}) raise HTTPException(401, detail="Invalid credentials", headers={"WWW-Authenticate": "Basic realm=webdav"})
u: UserInDB = user_or_false 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": elif scheme_lower == "bearer":
if not param: if not param:
raise HTTPException(401, detail="Invalid Bearer token") 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: else:
raise HTTPException(401, detail="Unsupported auth", headers={"WWW-Authenticate": "Basic realm=webdav"}) 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), user: User = Depends(_get_basic_user),
): ):
full_path = _normalize_fs_path(path) 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() depth = request.headers.get("Depth", "1").lower()
if depth not in ("0", "1", "infinity"): if depth not in ("0", "1", "infinity"):
depth = "1" depth = "1"
@@ -195,7 +214,9 @@ async def propfind(
if depth in ("1", "infinity"): if depth in ("1", "infinity"):
try: 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 []): for ent in (listing.get("items") or []):
is_dir = bool(ent.get("is_dir")) is_dir = bool(ent.get("is_dir"))
name = ent.get("name") name = ent.get("name")
@@ -223,6 +244,8 @@ async def dav_get(
user: User = Depends(_get_basic_user), user: User = Depends(_get_basic_user),
): ):
full_path = _normalize_fs_path(path) 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") range_header = request.headers.get("Range")
return await VirtualFSService.stream_file(full_path, range_header) return await VirtualFSService.stream_file(full_path, range_header)
@@ -236,6 +259,8 @@ async def dav_head(
user: User = Depends(_get_basic_user), user: User = Depends(_get_basic_user),
): ):
full_path = _normalize_fs_path(path) full_path = _normalize_fs_path(path)
if full_path != "/":
await PermissionService.require_path_permission(user.id, full_path, PathAction.READ)
try: try:
st = await VirtualFSService.stat_file(full_path) st = await VirtualFSService.stat_file(full_path)
except FileNotFoundError: except FileNotFoundError:
@@ -264,6 +289,7 @@ async def dav_put(
user: User = Depends(_get_basic_user), user: User = Depends(_get_basic_user),
): ):
full_path = _normalize_fs_path(path) full_path = _normalize_fs_path(path)
await PermissionService.require_path_permission(user.id, full_path, PathAction.WRITE)
async def body_iter(): async def body_iter():
async for chunk in request.stream(): async for chunk in request.stream():
if chunk: if chunk:
@@ -281,6 +307,7 @@ async def dav_delete(
user: User = Depends(_get_basic_user), user: User = Depends(_get_basic_user),
): ):
full_path = _normalize_fs_path(path) full_path = _normalize_fs_path(path)
await PermissionService.require_path_permission(user.id, full_path, PathAction.DELETE)
await VirtualFSService.delete_path(full_path) await VirtualFSService.delete_path(full_path)
return Response(status_code=204, headers=_dav_headers()) return Response(status_code=204, headers=_dav_headers())
@@ -294,6 +321,7 @@ async def dav_mkcol(
user: User = Depends(_get_basic_user), user: User = Depends(_get_basic_user),
): ):
full_path = _normalize_fs_path(path) full_path = _normalize_fs_path(path)
await PermissionService.require_path_permission(user.id, full_path, PathAction.WRITE)
await VirtualFSService.make_dir(full_path) await VirtualFSService.make_dir(full_path)
return Response(status_code=201, headers=_dav_headers()) return Response(status_code=201, headers=_dav_headers())
@@ -322,6 +350,8 @@ async def dav_move(
dest_header = request.headers.get("Destination") dest_header = request.headers.get("Destination")
dst = _parse_destination(dest_header or "") dst = _parse_destination(dest_header or "")
overwrite = request.headers.get("Overwrite", "T").upper() != "F" 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) await VirtualFSService.move_path(full_src, dst, overwrite=overwrite)
return Response(status_code=204, headers=_dav_headers()) return Response(status_code=204, headers=_dav_headers())
@@ -338,5 +368,7 @@ async def dav_copy(
dest_header = request.headers.get("Destination") dest_header = request.headers.get("Destination")
dst = _parse_destination(dest_header or "") dst = _parse_destination(dest_header or "")
overwrite = request.headers.get("Overwrite", "T").upper() != "F" 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) await VirtualFSService.copy_path(full_src, dst, overwrite=overwrite)
return Response(status_code=201 if not overwrite else 204, headers=_dav_headers()) 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 api.response import success
from domain.auth import User, get_current_active_user 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 from .search_service import VirtualFSSearchService
router = APIRouter(prefix="/api/fs/search", tags=["search"]) router = APIRouter(prefix="/api/fs/search", tags=["search"])
@@ -24,4 +26,14 @@ async def search_files(
page_size = max(min(page_size, 100), 1) page_size = max(min(page_size, 100), 1)
data = await VirtualFSSearchService.search(q, top_k, mode, page, page_size) 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) return success(data)