mirror of
https://github.com/DrizzleTime/Foxel.git
synced 2026-05-06 18:22:44 +08:00
feat: add permission decorator to enhance API access control
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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)"),
|
||||
|
||||
@@ -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"),
|
||||
):
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
|
||||
103
domain/permission/decorator.py
Normal file
103
domain/permission/decorator.py
Normal 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
|
||||
|
||||
@@ -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"}
|
||||
|
||||
@@ -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)],
|
||||
|
||||
@@ -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}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user