mirror of
https://github.com/DrizzleTime/Foxel.git
synced 2026-06-08 17:09:59 +08:00
feat: add user and role management pages with API integration
- Implemented user management functionality in UsersPage including user creation, editing, deletion, and role assignment. - Added role management functionality in RolesPage with role creation, editing, deletion, and path rule management. - Created users API for handling user-related operations. - Created roles API for handling role-related operations. - Integrated permissions handling in both user and role management. - Enhanced UI with Ant Design components for better user experience.
This commit is contained in:
@@ -140,6 +140,7 @@ class AuthService:
|
||||
email=user.email,
|
||||
full_name=user.full_name,
|
||||
disabled=user.disabled,
|
||||
is_admin=user.is_admin,
|
||||
hashed_password=user.hashed_password,
|
||||
)
|
||||
return None
|
||||
@@ -166,12 +167,14 @@ class AuthService:
|
||||
if exists:
|
||||
raise HTTPException(status_code=400, detail="用户名已存在")
|
||||
hashed = cls.get_password_hash(payload.password)
|
||||
# 第一个用户自动成为超级管理员
|
||||
user = await UserAccount.create(
|
||||
username=payload.username,
|
||||
email=payload.email,
|
||||
full_name=payload.full_name,
|
||||
hashed_password=hashed,
|
||||
disabled=False,
|
||||
is_admin=True, # 第一个用户是超级管理员
|
||||
)
|
||||
return user
|
||||
|
||||
@@ -195,6 +198,13 @@ class AuthService:
|
||||
detail="用户名或密码错误",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
|
||||
# 更新最后登录时间
|
||||
db_user = await UserAccount.get_or_none(id=user.id)
|
||||
if db_user:
|
||||
db_user.last_login = _now()
|
||||
await db_user.save(update_fields=["last_login"])
|
||||
|
||||
access_token_expires = timedelta(minutes=cls.access_token_expire_minutes)
|
||||
access_token = await cls.create_access_token(
|
||||
data={"sub": user.username}, expires_delta=access_token_expires
|
||||
@@ -212,6 +222,7 @@ class AuthService:
|
||||
"email": getattr(user, "email", None),
|
||||
"full_name": getattr(user, "full_name", None),
|
||||
"gravatar_url": gravatar_url,
|
||||
"is_admin": getattr(user, "is_admin", False),
|
||||
}
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -16,6 +16,7 @@ class User(BaseModel):
|
||||
email: str | None = None
|
||||
full_name: str | None = None
|
||||
disabled: bool | None = None
|
||||
is_admin: bool = False
|
||||
|
||||
|
||||
class UserInDB(User):
|
||||
|
||||
4
domain/permission/__init__.py
Normal file
4
domain/permission/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
||||
from .service import PermissionService
|
||||
from .matcher import PathMatcher
|
||||
|
||||
__all__ = ["PermissionService", "PathMatcher"]
|
||||
41
domain/permission/api.py
Normal file
41
domain/permission/api.py
Normal file
@@ -0,0 +1,41 @@
|
||||
from typing import Annotated
|
||||
from fastapi import APIRouter, Depends
|
||||
|
||||
from domain.auth.service import get_current_active_user
|
||||
from domain.auth.types import User
|
||||
from .service import PermissionService
|
||||
from .types import (
|
||||
PathPermissionCheck,
|
||||
PathPermissionResult,
|
||||
UserPermissions,
|
||||
PermissionInfo,
|
||||
)
|
||||
|
||||
router = APIRouter(prefix="/api", tags=["permissions"])
|
||||
|
||||
|
||||
@router.get("/permissions", response_model=list[PermissionInfo])
|
||||
async def get_all_permissions(
|
||||
current_user: Annotated[User, Depends(get_current_active_user)]
|
||||
) -> list[PermissionInfo]:
|
||||
"""获取所有权限定义"""
|
||||
return await PermissionService.get_all_permissions()
|
||||
|
||||
|
||||
@router.get("/me/permissions", response_model=UserPermissions)
|
||||
async def get_my_permissions(
|
||||
current_user: Annotated[User, Depends(get_current_active_user)]
|
||||
) -> UserPermissions:
|
||||
"""获取当前用户的有效权限"""
|
||||
return await PermissionService.get_user_permissions(current_user.id)
|
||||
|
||||
|
||||
@router.post("/me/check-path", response_model=PathPermissionResult)
|
||||
async def check_path_permission(
|
||||
data: PathPermissionCheck,
|
||||
current_user: Annotated[User, Depends(get_current_active_user)],
|
||||
) -> PathPermissionResult:
|
||||
"""检查当前用户对某路径的权限"""
|
||||
return await PermissionService.check_path_permission_detailed(
|
||||
current_user.id, data.path, data.action
|
||||
)
|
||||
158
domain/permission/matcher.py
Normal file
158
domain/permission/matcher.py
Normal file
@@ -0,0 +1,158 @@
|
||||
import re
|
||||
import fnmatch
|
||||
from functools import lru_cache
|
||||
|
||||
|
||||
class PathMatcher:
|
||||
"""路径匹配器,支持精确匹配、通配符匹配和正则匹配"""
|
||||
|
||||
@classmethod
|
||||
def normalize_path(cls, path: str) -> str:
|
||||
"""规范化路径"""
|
||||
if not path:
|
||||
return "/"
|
||||
# 确保以 / 开头
|
||||
if not path.startswith("/"):
|
||||
path = "/" + path
|
||||
# 移除末尾的 /(除了根路径)
|
||||
if path != "/" and path.endswith("/"):
|
||||
path = path.rstrip("/")
|
||||
return path
|
||||
|
||||
@classmethod
|
||||
def get_parent_path(cls, path: str) -> str | None:
|
||||
"""获取父目录路径"""
|
||||
path = cls.normalize_path(path)
|
||||
if path == "/":
|
||||
return None
|
||||
parent = "/".join(path.rsplit("/", 1)[:-1])
|
||||
return parent if parent else "/"
|
||||
|
||||
@classmethod
|
||||
def match_pattern(cls, path: str, pattern: str, is_regex: bool = False) -> bool:
|
||||
"""
|
||||
匹配路径和模式
|
||||
|
||||
Args:
|
||||
path: 要匹配的路径
|
||||
pattern: 匹配模式
|
||||
is_regex: 是否为正则表达式
|
||||
|
||||
Returns:
|
||||
是否匹配
|
||||
"""
|
||||
path = cls.normalize_path(path)
|
||||
pattern = cls.normalize_path(pattern)
|
||||
|
||||
if is_regex:
|
||||
return cls._match_regex(path, pattern)
|
||||
else:
|
||||
return cls._match_glob(path, pattern)
|
||||
|
||||
@classmethod
|
||||
def _match_regex(cls, path: str, pattern: str) -> bool:
|
||||
"""正则表达式匹配"""
|
||||
try:
|
||||
# 限制正则表达式的复杂度,防止 ReDoS 攻击
|
||||
if len(pattern) > 500:
|
||||
return False
|
||||
regex = re.compile(pattern)
|
||||
return bool(regex.match(path))
|
||||
except re.error:
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def _match_glob(cls, path: str, pattern: str) -> bool:
|
||||
"""
|
||||
通配符匹配
|
||||
|
||||
支持的语法:
|
||||
- * : 匹配单层目录中的任意字符
|
||||
- ** : 匹配任意层级目录
|
||||
- ? : 匹配单个字符
|
||||
"""
|
||||
# 精确匹配
|
||||
if pattern == path:
|
||||
return True
|
||||
|
||||
# 处理 ** 通配符
|
||||
if "**" in pattern:
|
||||
return cls._match_double_star(path, pattern)
|
||||
|
||||
# 使用 fnmatch 进行标准通配符匹配
|
||||
return fnmatch.fnmatch(path, pattern)
|
||||
|
||||
@classmethod
|
||||
def _match_double_star(cls, path: str, pattern: str) -> bool:
|
||||
"""处理 ** 通配符匹配"""
|
||||
# 将 ** 替换为特殊标记
|
||||
parts = pattern.split("**")
|
||||
|
||||
if len(parts) == 2:
|
||||
prefix, suffix = parts
|
||||
# 移除 prefix 末尾的 / 和 suffix 开头的 /
|
||||
prefix = prefix.rstrip("/") if prefix else ""
|
||||
suffix = suffix.lstrip("/") if suffix else ""
|
||||
|
||||
# 检查前缀匹配
|
||||
if prefix and not path.startswith(prefix):
|
||||
return False
|
||||
|
||||
# 如果没有后缀,只需要前缀匹配
|
||||
if not suffix:
|
||||
return True
|
||||
|
||||
# 检查后缀匹配
|
||||
remaining = path[len(prefix):].lstrip("/") if prefix else path.lstrip("/")
|
||||
|
||||
# 后缀可以出现在任意位置
|
||||
if "*" in suffix or "?" in suffix:
|
||||
# 后缀包含通配符,逐层检查
|
||||
path_parts = remaining.split("/")
|
||||
suffix_parts = suffix.split("/")
|
||||
|
||||
# 简化处理:检查路径的最后几层是否与后缀匹配
|
||||
if len(path_parts) >= len(suffix_parts):
|
||||
tail = "/".join(path_parts[-len(suffix_parts):])
|
||||
return fnmatch.fnmatch(tail, suffix)
|
||||
return False
|
||||
else:
|
||||
# 后缀是精确字符串
|
||||
return remaining.endswith(suffix) or ("/" + suffix) in remaining or remaining == suffix
|
||||
|
||||
# 多个 ** 的情况,使用简化匹配
|
||||
regex_pattern = pattern.replace("**", ".*").replace("*", "[^/]*").replace("?", ".")
|
||||
try:
|
||||
return bool(re.match(f"^{regex_pattern}$", path))
|
||||
except re.error:
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def get_pattern_specificity(cls, pattern: str, is_regex: bool = False) -> int:
|
||||
"""
|
||||
计算模式的具体程度(用于优先级排序)
|
||||
|
||||
返回值越大表示模式越具体
|
||||
"""
|
||||
pattern = cls.normalize_path(pattern)
|
||||
|
||||
if is_regex:
|
||||
# 正则表达式具体程度较低
|
||||
return len(pattern) // 2
|
||||
|
||||
# 精确路径最具体
|
||||
if "*" not in pattern and "?" not in pattern:
|
||||
return len(pattern) * 10
|
||||
|
||||
# 计算非通配符部分的长度
|
||||
specificity = 0
|
||||
parts = pattern.split("/")
|
||||
for part in parts:
|
||||
if part == "**":
|
||||
specificity += 1
|
||||
elif "*" in part or "?" in part:
|
||||
specificity += 5
|
||||
else:
|
||||
specificity += 10
|
||||
|
||||
return specificity
|
||||
351
domain/permission/service.py
Normal file
351
domain/permission/service.py
Normal file
@@ -0,0 +1,351 @@
|
||||
from typing import List, Optional
|
||||
from fastapi import HTTPException
|
||||
|
||||
from models.database import (
|
||||
UserAccount,
|
||||
Role,
|
||||
UserRole,
|
||||
Permission,
|
||||
RolePermission,
|
||||
PathRule,
|
||||
)
|
||||
from .matcher import PathMatcher
|
||||
from .types import (
|
||||
PathAction,
|
||||
PathRuleInfo,
|
||||
PathPermissionResult,
|
||||
UserPermissions,
|
||||
PermissionInfo,
|
||||
PERMISSION_DEFINITIONS,
|
||||
)
|
||||
|
||||
|
||||
class PermissionService:
|
||||
"""权限检查服务"""
|
||||
|
||||
# 权限检查结果缓存(简单的内存缓存)
|
||||
_cache: dict[str, tuple[bool, float]] = {}
|
||||
_cache_ttl = 300 # 5分钟缓存
|
||||
|
||||
@classmethod
|
||||
async def check_path_permission(
|
||||
cls, user_id: int, path: str, action: str
|
||||
) -> bool:
|
||||
"""
|
||||
检查用户对路径的操作权限
|
||||
|
||||
Args:
|
||||
user_id: 用户ID
|
||||
path: 要检查的路径
|
||||
action: 操作类型 (read/write/delete/share)
|
||||
|
||||
Returns:
|
||||
是否有权限
|
||||
"""
|
||||
import time
|
||||
|
||||
# 检查缓存
|
||||
cache_key = f"{user_id}:{path}:{action}"
|
||||
if cache_key in cls._cache:
|
||||
result, timestamp = cls._cache[cache_key]
|
||||
if time.time() - timestamp < cls._cache_ttl:
|
||||
return result
|
||||
|
||||
# 获取用户
|
||||
user = await UserAccount.get_or_none(id=user_id)
|
||||
if not user:
|
||||
return False
|
||||
|
||||
# 超级管理员直接放行
|
||||
if user.is_admin:
|
||||
cls._cache[cache_key] = (True, time.time())
|
||||
return True
|
||||
|
||||
# 获取用户所有角色
|
||||
user_roles = await UserRole.filter(user_id=user_id).prefetch_related("role")
|
||||
role_ids = [ur.role_id for ur in user_roles]
|
||||
|
||||
if not role_ids:
|
||||
cls._cache[cache_key] = (False, time.time())
|
||||
return False
|
||||
|
||||
# 获取所有角色的路径规则
|
||||
path_rules = await PathRule.filter(role_id__in=role_ids).order_by("-priority")
|
||||
|
||||
# 规范化路径
|
||||
normalized_path = PathMatcher.normalize_path(path)
|
||||
|
||||
# 按优先级和具体程度匹配
|
||||
result = cls._match_path_rules(normalized_path, action, list(path_rules))
|
||||
|
||||
# 如果没有匹配到规则,检查父目录(继承)
|
||||
if result is None:
|
||||
parent_path = PathMatcher.get_parent_path(normalized_path)
|
||||
if parent_path:
|
||||
result = await cls.check_path_permission(user_id, parent_path, action)
|
||||
else:
|
||||
result = False # 默认拒绝
|
||||
|
||||
cls._cache[cache_key] = (result, time.time())
|
||||
return result
|
||||
|
||||
@classmethod
|
||||
def _match_path_rules(
|
||||
cls, path: str, action: str, rules: List[PathRule]
|
||||
) -> Optional[bool]:
|
||||
"""
|
||||
匹配路径规则
|
||||
|
||||
Returns:
|
||||
True/False 表示明确的权限结果,None 表示没有匹配到规则
|
||||
"""
|
||||
# 按优先级和具体程度排序
|
||||
sorted_rules = sorted(
|
||||
rules,
|
||||
key=lambda r: (
|
||||
r.priority,
|
||||
PathMatcher.get_pattern_specificity(r.path_pattern, r.is_regex),
|
||||
),
|
||||
reverse=True,
|
||||
)
|
||||
|
||||
for rule in sorted_rules:
|
||||
if PathMatcher.match_pattern(path, rule.path_pattern, rule.is_regex):
|
||||
# 匹配到规则,检查具体操作权限
|
||||
if action == PathAction.READ:
|
||||
return rule.can_read
|
||||
elif action == PathAction.WRITE:
|
||||
return rule.can_write
|
||||
elif action == PathAction.DELETE:
|
||||
return rule.can_delete
|
||||
elif action == PathAction.SHARE:
|
||||
return rule.can_share
|
||||
else:
|
||||
return False
|
||||
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
async def check_system_permission(cls, user_id: int, permission_code: str) -> bool:
|
||||
"""检查用户的系统/适配器权限"""
|
||||
# 获取用户
|
||||
user = await UserAccount.get_or_none(id=user_id)
|
||||
if not user:
|
||||
return False
|
||||
|
||||
# 超级管理员直接放行
|
||||
if user.is_admin:
|
||||
return True
|
||||
|
||||
# 获取用户所有角色
|
||||
user_roles = await UserRole.filter(user_id=user_id)
|
||||
role_ids = [ur.role_id for ur in user_roles]
|
||||
|
||||
if not role_ids:
|
||||
return False
|
||||
|
||||
# 检查角色是否有该权限
|
||||
permission = await Permission.get_or_none(code=permission_code)
|
||||
if not permission:
|
||||
return False
|
||||
|
||||
role_permission = await RolePermission.filter(
|
||||
role_id__in=role_ids, permission_id=permission.id
|
||||
).first()
|
||||
|
||||
return role_permission is not None
|
||||
|
||||
@classmethod
|
||||
async def require_path_permission(
|
||||
cls, user_id: int, path: str, action: str
|
||||
) -> None:
|
||||
"""要求用户具有路径权限,否则抛出 403"""
|
||||
if not await cls.check_path_permission(user_id, path, action):
|
||||
raise HTTPException(403, detail=f"没有权限执行此操作: {action}")
|
||||
|
||||
@classmethod
|
||||
async def require_system_permission(
|
||||
cls, user_id: int, permission_code: str
|
||||
) -> None:
|
||||
"""要求用户具有系统权限,否则抛出 403"""
|
||||
if not await cls.check_system_permission(user_id, permission_code):
|
||||
raise HTTPException(403, detail=f"没有权限: {permission_code}")
|
||||
|
||||
@classmethod
|
||||
async def get_user_permissions(cls, user_id: int) -> UserPermissions:
|
||||
"""获取用户的所有权限"""
|
||||
user = await UserAccount.get_or_none(id=user_id)
|
||||
if not user:
|
||||
raise HTTPException(404, detail="用户不存在")
|
||||
|
||||
# 超级管理员拥有所有权限
|
||||
if user.is_admin:
|
||||
all_permissions = await Permission.all()
|
||||
all_path_rules = await PathRule.all()
|
||||
return UserPermissions(
|
||||
user_id=user_id,
|
||||
is_admin=True,
|
||||
permissions=[p.code for p in all_permissions],
|
||||
path_rules=[
|
||||
PathRuleInfo(
|
||||
id=r.id,
|
||||
role_id=r.role_id,
|
||||
path_pattern=r.path_pattern,
|
||||
is_regex=r.is_regex,
|
||||
can_read=r.can_read,
|
||||
can_write=r.can_write,
|
||||
can_delete=r.can_delete,
|
||||
can_share=r.can_share,
|
||||
priority=r.priority,
|
||||
created_at=r.created_at,
|
||||
)
|
||||
for r in all_path_rules
|
||||
],
|
||||
)
|
||||
|
||||
# 获取用户角色
|
||||
user_roles = await UserRole.filter(user_id=user_id)
|
||||
role_ids = [ur.role_id for ur in user_roles]
|
||||
|
||||
# 获取权限
|
||||
permissions = []
|
||||
if role_ids:
|
||||
role_permissions = await RolePermission.filter(
|
||||
role_id__in=role_ids
|
||||
).prefetch_related("permission")
|
||||
permissions = list(set(rp.permission.code for rp in role_permissions))
|
||||
|
||||
# 获取路径规则
|
||||
path_rules = []
|
||||
if role_ids:
|
||||
rules = await PathRule.filter(role_id__in=role_ids)
|
||||
path_rules = [
|
||||
PathRuleInfo(
|
||||
id=r.id,
|
||||
role_id=r.role_id,
|
||||
path_pattern=r.path_pattern,
|
||||
is_regex=r.is_regex,
|
||||
can_read=r.can_read,
|
||||
can_write=r.can_write,
|
||||
can_delete=r.can_delete,
|
||||
can_share=r.can_share,
|
||||
priority=r.priority,
|
||||
created_at=r.created_at,
|
||||
)
|
||||
for r in rules
|
||||
]
|
||||
|
||||
return UserPermissions(
|
||||
user_id=user_id,
|
||||
is_admin=False,
|
||||
permissions=permissions,
|
||||
path_rules=path_rules,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
async def get_all_permissions(cls) -> List[PermissionInfo]:
|
||||
"""获取所有权限定义"""
|
||||
permissions = await Permission.all()
|
||||
return [
|
||||
PermissionInfo(
|
||||
id=p.id,
|
||||
code=p.code,
|
||||
name=p.name,
|
||||
category=p.category,
|
||||
description=p.description,
|
||||
)
|
||||
for p in permissions
|
||||
]
|
||||
|
||||
@classmethod
|
||||
async def check_path_permission_detailed(
|
||||
cls, user_id: int, path: str, action: str
|
||||
) -> PathPermissionResult:
|
||||
"""检查路径权限并返回详细结果"""
|
||||
user = await UserAccount.get_or_none(id=user_id)
|
||||
if not user:
|
||||
return PathPermissionResult(path=path, action=action, allowed=False)
|
||||
|
||||
# 超级管理员
|
||||
if user.is_admin:
|
||||
return PathPermissionResult(path=path, action=action, allowed=True)
|
||||
|
||||
# 获取用户角色
|
||||
user_roles = await UserRole.filter(user_id=user_id)
|
||||
role_ids = [ur.role_id for ur in user_roles]
|
||||
|
||||
if not role_ids:
|
||||
return PathPermissionResult(path=path, action=action, allowed=False)
|
||||
|
||||
# 获取路径规则
|
||||
path_rules = await PathRule.filter(role_id__in=role_ids).order_by("-priority")
|
||||
normalized_path = PathMatcher.normalize_path(path)
|
||||
|
||||
# 查找匹配的规则
|
||||
matched_rule = None
|
||||
for rule in sorted(
|
||||
path_rules,
|
||||
key=lambda r: (
|
||||
r.priority,
|
||||
PathMatcher.get_pattern_specificity(r.path_pattern, r.is_regex),
|
||||
),
|
||||
reverse=True,
|
||||
):
|
||||
if PathMatcher.match_pattern(
|
||||
normalized_path, rule.path_pattern, rule.is_regex
|
||||
):
|
||||
matched_rule = rule
|
||||
break
|
||||
|
||||
# 检查权限
|
||||
allowed = False
|
||||
if matched_rule:
|
||||
if action == PathAction.READ:
|
||||
allowed = matched_rule.can_read
|
||||
elif action == PathAction.WRITE:
|
||||
allowed = matched_rule.can_write
|
||||
elif action == PathAction.DELETE:
|
||||
allowed = matched_rule.can_delete
|
||||
elif action == PathAction.SHARE:
|
||||
allowed = matched_rule.can_share
|
||||
|
||||
rule_info = None
|
||||
if matched_rule:
|
||||
rule_info = PathRuleInfo(
|
||||
id=matched_rule.id,
|
||||
role_id=matched_rule.role_id,
|
||||
path_pattern=matched_rule.path_pattern,
|
||||
is_regex=matched_rule.is_regex,
|
||||
can_read=matched_rule.can_read,
|
||||
can_write=matched_rule.can_write,
|
||||
can_delete=matched_rule.can_delete,
|
||||
can_share=matched_rule.can_share,
|
||||
priority=matched_rule.priority,
|
||||
created_at=matched_rule.created_at,
|
||||
)
|
||||
|
||||
return PathPermissionResult(
|
||||
path=path, action=action, allowed=allowed, matched_rule=rule_info
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def clear_cache(cls, user_id: int | None = None) -> None:
|
||||
"""清除权限缓存"""
|
||||
if user_id is None:
|
||||
cls._cache.clear()
|
||||
else:
|
||||
# 清除特定用户的缓存
|
||||
keys_to_delete = [k for k in cls._cache if k.startswith(f"{user_id}:")]
|
||||
for k in keys_to_delete:
|
||||
del cls._cache[k]
|
||||
|
||||
@classmethod
|
||||
async def filter_paths_by_permission(
|
||||
cls, user_id: int, paths: List[str], action: str
|
||||
) -> List[str]:
|
||||
"""过滤出用户有权限的路径列表"""
|
||||
result = []
|
||||
for path in paths:
|
||||
if await cls.check_path_permission(user_id, path, action):
|
||||
result.append(path)
|
||||
return result
|
||||
108
domain/permission/types.py
Normal file
108
domain/permission/types.py
Normal file
@@ -0,0 +1,108 @@
|
||||
from pydantic import BaseModel
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
# 权限操作类型
|
||||
class PathAction:
|
||||
READ = "read"
|
||||
WRITE = "write"
|
||||
DELETE = "delete"
|
||||
SHARE = "share"
|
||||
|
||||
|
||||
# 系统权限代码
|
||||
class SystemPermission:
|
||||
USER_CREATE = "system.user.create"
|
||||
USER_EDIT = "system.user.edit"
|
||||
USER_DELETE = "system.user.delete"
|
||||
USER_LIST = "system.user.list"
|
||||
ROLE_MANAGE = "system.role.manage"
|
||||
CONFIG_EDIT = "system.config.edit"
|
||||
AUDIT_VIEW = "system.audit.view"
|
||||
|
||||
|
||||
# 适配器权限代码
|
||||
class AdapterPermission:
|
||||
CREATE = "adapter.create"
|
||||
EDIT = "adapter.edit"
|
||||
DELETE = "adapter.delete"
|
||||
LIST = "adapter.list"
|
||||
|
||||
|
||||
# 所有权限定义
|
||||
PERMISSION_DEFINITIONS = [
|
||||
# 系统权限
|
||||
{"code": SystemPermission.USER_CREATE, "name": "创建用户", "category": "system", "description": "允许创建新用户"},
|
||||
{"code": SystemPermission.USER_EDIT, "name": "编辑用户", "category": "system", "description": "允许编辑用户信息"},
|
||||
{"code": SystemPermission.USER_DELETE, "name": "删除用户", "category": "system", "description": "允许删除用户"},
|
||||
{"code": SystemPermission.USER_LIST, "name": "查看用户列表", "category": "system", "description": "允许查看用户列表"},
|
||||
{"code": SystemPermission.ROLE_MANAGE, "name": "管理角色和权限", "category": "system", "description": "允许管理角色和权限配置"},
|
||||
{"code": SystemPermission.CONFIG_EDIT, "name": "修改系统配置", "category": "system", "description": "允许修改系统配置"},
|
||||
{"code": SystemPermission.AUDIT_VIEW, "name": "查看审计日志", "category": "system", "description": "允许查看审计日志"},
|
||||
# 适配器权限
|
||||
{"code": AdapterPermission.CREATE, "name": "创建存储适配器", "category": "adapter", "description": "允许创建存储适配器"},
|
||||
{"code": AdapterPermission.EDIT, "name": "编辑存储适配器", "category": "adapter", "description": "允许编辑存储适配器"},
|
||||
{"code": AdapterPermission.DELETE, "name": "删除存储适配器", "category": "adapter", "description": "允许删除存储适配器"},
|
||||
{"code": AdapterPermission.LIST, "name": "查看存储适配器列表", "category": "adapter", "description": "允许查看存储适配器列表"},
|
||||
]
|
||||
|
||||
|
||||
# Pydantic 模型
|
||||
class PermissionInfo(BaseModel):
|
||||
id: int
|
||||
code: str
|
||||
name: str
|
||||
category: str
|
||||
description: str | None = None
|
||||
|
||||
|
||||
class PathRuleInfo(BaseModel):
|
||||
id: int
|
||||
role_id: int
|
||||
path_pattern: str
|
||||
is_regex: bool
|
||||
can_read: bool
|
||||
can_write: bool
|
||||
can_delete: bool
|
||||
can_share: bool
|
||||
priority: int
|
||||
created_at: datetime
|
||||
|
||||
|
||||
class PathRuleCreate(BaseModel):
|
||||
path_pattern: str
|
||||
is_regex: bool = False
|
||||
can_read: bool = True
|
||||
can_write: bool = False
|
||||
can_delete: bool = False
|
||||
can_share: bool = False
|
||||
priority: int = 0
|
||||
|
||||
|
||||
class PathRuleUpdate(BaseModel):
|
||||
path_pattern: str | None = None
|
||||
is_regex: bool | None = None
|
||||
can_read: bool | None = None
|
||||
can_write: bool | None = None
|
||||
can_delete: bool | None = None
|
||||
can_share: bool | None = None
|
||||
priority: int | None = None
|
||||
|
||||
|
||||
class PathPermissionCheck(BaseModel):
|
||||
path: str
|
||||
action: str
|
||||
|
||||
|
||||
class PathPermissionResult(BaseModel):
|
||||
path: str
|
||||
action: str
|
||||
allowed: bool
|
||||
matched_rule: PathRuleInfo | None = None
|
||||
|
||||
|
||||
class UserPermissions(BaseModel):
|
||||
user_id: int
|
||||
is_admin: bool
|
||||
permissions: list[str] # 系统/适配器权限代码列表
|
||||
path_rules: list[PathRuleInfo] # 路径权限规则
|
||||
3
domain/role/__init__.py
Normal file
3
domain/role/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from .service import RoleService
|
||||
|
||||
__all__ = ["RoleService"]
|
||||
142
domain/role/api.py
Normal file
142
domain/role/api.py
Normal file
@@ -0,0 +1,142 @@
|
||||
from typing import Annotated
|
||||
|
||||
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.types import PathRuleCreate, PathRuleInfo, SystemPermission
|
||||
from domain.user.service import UserService
|
||||
from domain.user.types import UserInfo
|
||||
|
||||
from .service import RoleService
|
||||
from .types import RoleCreate, RoleDetail, RoleInfo, RolePermissionsUpdate, RoleUpdate
|
||||
|
||||
router = APIRouter(prefix="/api", tags=["role"])
|
||||
|
||||
|
||||
@router.get("/roles", response_model=list[RoleInfo])
|
||||
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)
|
||||
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])
|
||||
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)
|
||||
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)
|
||||
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}")
|
||||
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])
|
||||
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])
|
||||
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)
|
||||
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)
|
||||
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}")
|
||||
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}
|
||||
|
||||
327
domain/role/service.py
Normal file
327
domain/role/service.py
Normal file
@@ -0,0 +1,327 @@
|
||||
from typing import List
|
||||
from fastapi import HTTPException
|
||||
|
||||
from models.database import Role, RolePermission, Permission, PathRule, UserRole
|
||||
from domain.permission.service import PermissionService
|
||||
from domain.permission.types import PathRuleCreate, PathRuleInfo
|
||||
from .types import RoleInfo, RoleDetail, RoleCreate, RoleUpdate, SystemRoles
|
||||
|
||||
|
||||
class RoleService:
|
||||
"""角色管理服务"""
|
||||
|
||||
@classmethod
|
||||
async def get_all_roles(cls) -> List[RoleInfo]:
|
||||
"""获取所有角色"""
|
||||
roles = await Role.all().order_by("id")
|
||||
return [
|
||||
RoleInfo(
|
||||
id=r.id,
|
||||
name=r.name,
|
||||
description=r.description,
|
||||
is_system=r.is_system,
|
||||
created_at=r.created_at,
|
||||
)
|
||||
for r in roles
|
||||
]
|
||||
|
||||
@classmethod
|
||||
async def get_role(cls, role_id: int) -> RoleDetail:
|
||||
"""获取角色详情"""
|
||||
role = await Role.get_or_none(id=role_id)
|
||||
if not role:
|
||||
raise HTTPException(404, detail="角色不存在")
|
||||
|
||||
# 获取权限
|
||||
role_permissions = await RolePermission.filter(role_id=role_id).prefetch_related(
|
||||
"permission"
|
||||
)
|
||||
permissions = [rp.permission.code for rp in role_permissions]
|
||||
|
||||
# 获取路径规则数量
|
||||
path_rules_count = await PathRule.filter(role_id=role_id).count()
|
||||
|
||||
return RoleDetail(
|
||||
id=role.id,
|
||||
name=role.name,
|
||||
description=role.description,
|
||||
is_system=role.is_system,
|
||||
created_at=role.created_at,
|
||||
permissions=permissions,
|
||||
path_rules_count=path_rules_count,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
async def create_role(cls, data: RoleCreate) -> RoleInfo:
|
||||
"""创建角色"""
|
||||
# 检查名称是否已存在
|
||||
existing = await Role.get_or_none(name=data.name)
|
||||
if existing:
|
||||
raise HTTPException(400, detail="角色名称已存在")
|
||||
|
||||
role = await Role.create(
|
||||
name=data.name,
|
||||
description=data.description,
|
||||
is_system=False,
|
||||
)
|
||||
|
||||
return RoleInfo(
|
||||
id=role.id,
|
||||
name=role.name,
|
||||
description=role.description,
|
||||
is_system=role.is_system,
|
||||
created_at=role.created_at,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
async def update_role(cls, role_id: int, data: RoleUpdate) -> RoleInfo:
|
||||
"""更新角色"""
|
||||
role = await Role.get_or_none(id=role_id)
|
||||
if not role:
|
||||
raise HTTPException(404, detail="角色不存在")
|
||||
|
||||
if data.name is not None:
|
||||
# 检查名称是否与其他角色冲突
|
||||
existing = await Role.filter(name=data.name).exclude(id=role_id).first()
|
||||
if existing:
|
||||
raise HTTPException(400, detail="角色名称已存在")
|
||||
role.name = data.name
|
||||
|
||||
if data.description is not None:
|
||||
role.description = data.description
|
||||
|
||||
await role.save()
|
||||
|
||||
return RoleInfo(
|
||||
id=role.id,
|
||||
name=role.name,
|
||||
description=role.description,
|
||||
is_system=role.is_system,
|
||||
created_at=role.created_at,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
async def delete_role(cls, role_id: int) -> None:
|
||||
"""删除角色"""
|
||||
role = await Role.get_or_none(id=role_id)
|
||||
if not role:
|
||||
raise HTTPException(404, detail="角色不存在")
|
||||
|
||||
if role.is_system:
|
||||
raise HTTPException(400, detail="系统内置角色不可删除")
|
||||
|
||||
# 检查是否有用户使用此角色
|
||||
user_count = await UserRole.filter(role_id=role_id).count()
|
||||
if user_count > 0:
|
||||
raise HTTPException(400, detail=f"有 {user_count} 个用户正在使用此角色,无法删除")
|
||||
|
||||
await role.delete()
|
||||
# 清除权限缓存
|
||||
PermissionService.clear_cache()
|
||||
|
||||
@classmethod
|
||||
async def set_role_permissions(cls, role_id: int, permission_codes: List[str]) -> List[str]:
|
||||
"""设置角色的权限"""
|
||||
role = await Role.get_or_none(id=role_id)
|
||||
if not role:
|
||||
raise HTTPException(404, detail="角色不存在")
|
||||
|
||||
# 获取权限ID
|
||||
permissions = await Permission.filter(code__in=permission_codes)
|
||||
permission_map = {p.code: p.id for p in permissions}
|
||||
|
||||
# 验证所有权限代码
|
||||
invalid_codes = set(permission_codes) - set(permission_map.keys())
|
||||
if invalid_codes:
|
||||
raise HTTPException(400, detail=f"无效的权限代码: {', '.join(invalid_codes)}")
|
||||
|
||||
# 删除现有权限
|
||||
await RolePermission.filter(role_id=role_id).delete()
|
||||
|
||||
# 添加新权限
|
||||
for code in permission_codes:
|
||||
await RolePermission.create(
|
||||
role_id=role_id,
|
||||
permission_id=permission_map[code],
|
||||
)
|
||||
|
||||
# 清除权限缓存
|
||||
PermissionService.clear_cache()
|
||||
|
||||
return permission_codes
|
||||
|
||||
@classmethod
|
||||
async def get_role_path_rules(cls, role_id: int) -> List[PathRuleInfo]:
|
||||
"""获取角色的路径规则"""
|
||||
role = await Role.get_or_none(id=role_id)
|
||||
if not role:
|
||||
raise HTTPException(404, detail="角色不存在")
|
||||
|
||||
rules = await PathRule.filter(role_id=role_id).order_by("-priority", "id")
|
||||
return [
|
||||
PathRuleInfo(
|
||||
id=r.id,
|
||||
role_id=r.role_id,
|
||||
path_pattern=r.path_pattern,
|
||||
is_regex=r.is_regex,
|
||||
can_read=r.can_read,
|
||||
can_write=r.can_write,
|
||||
can_delete=r.can_delete,
|
||||
can_share=r.can_share,
|
||||
priority=r.priority,
|
||||
created_at=r.created_at,
|
||||
)
|
||||
for r in rules
|
||||
]
|
||||
|
||||
@classmethod
|
||||
async def add_path_rule(cls, role_id: int, data: PathRuleCreate) -> PathRuleInfo:
|
||||
"""添加路径规则"""
|
||||
role = await Role.get_or_none(id=role_id)
|
||||
if not role:
|
||||
raise HTTPException(404, detail="角色不存在")
|
||||
|
||||
# 验证路径模式
|
||||
if data.is_regex:
|
||||
import re
|
||||
try:
|
||||
re.compile(data.path_pattern)
|
||||
except re.error as e:
|
||||
raise HTTPException(400, detail=f"无效的正则表达式: {e}")
|
||||
|
||||
rule = await PathRule.create(
|
||||
role_id=role_id,
|
||||
path_pattern=data.path_pattern,
|
||||
is_regex=data.is_regex,
|
||||
can_read=data.can_read,
|
||||
can_write=data.can_write,
|
||||
can_delete=data.can_delete,
|
||||
can_share=data.can_share,
|
||||
priority=data.priority,
|
||||
)
|
||||
|
||||
# 清除权限缓存
|
||||
PermissionService.clear_cache()
|
||||
|
||||
return PathRuleInfo(
|
||||
id=rule.id,
|
||||
role_id=rule.role_id,
|
||||
path_pattern=rule.path_pattern,
|
||||
is_regex=rule.is_regex,
|
||||
can_read=rule.can_read,
|
||||
can_write=rule.can_write,
|
||||
can_delete=rule.can_delete,
|
||||
can_share=rule.can_share,
|
||||
priority=rule.priority,
|
||||
created_at=rule.created_at,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
async def update_path_rule(cls, rule_id: int, data: PathRuleCreate) -> PathRuleInfo:
|
||||
"""更新路径规则"""
|
||||
rule = await PathRule.get_or_none(id=rule_id)
|
||||
if not rule:
|
||||
raise HTTPException(404, detail="路径规则不存在")
|
||||
|
||||
# 验证路径模式
|
||||
if data.is_regex:
|
||||
import re
|
||||
try:
|
||||
re.compile(data.path_pattern)
|
||||
except re.error as e:
|
||||
raise HTTPException(400, detail=f"无效的正则表达式: {e}")
|
||||
|
||||
rule.path_pattern = data.path_pattern
|
||||
rule.is_regex = data.is_regex
|
||||
rule.can_read = data.can_read
|
||||
rule.can_write = data.can_write
|
||||
rule.can_delete = data.can_delete
|
||||
rule.can_share = data.can_share
|
||||
rule.priority = data.priority
|
||||
await rule.save()
|
||||
|
||||
# 清除权限缓存
|
||||
PermissionService.clear_cache()
|
||||
|
||||
return PathRuleInfo(
|
||||
id=rule.id,
|
||||
role_id=rule.role_id,
|
||||
path_pattern=rule.path_pattern,
|
||||
is_regex=rule.is_regex,
|
||||
can_read=rule.can_read,
|
||||
can_write=rule.can_write,
|
||||
can_delete=rule.can_delete,
|
||||
can_share=rule.can_share,
|
||||
priority=rule.priority,
|
||||
created_at=rule.created_at,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
async def delete_path_rule(cls, rule_id: int) -> None:
|
||||
"""删除路径规则"""
|
||||
rule = await PathRule.get_or_none(id=rule_id)
|
||||
if not rule:
|
||||
raise HTTPException(404, detail="路径规则不存在")
|
||||
|
||||
await rule.delete()
|
||||
# 清除权限缓存
|
||||
PermissionService.clear_cache()
|
||||
|
||||
@classmethod
|
||||
async def ensure_system_roles(cls) -> None:
|
||||
"""确保系统内置角色存在"""
|
||||
system_roles = [
|
||||
{
|
||||
"name": SystemRoles.ADMIN,
|
||||
"description": "管理员角色,拥有所有系统和适配器权限",
|
||||
"is_system": True,
|
||||
},
|
||||
{
|
||||
"name": SystemRoles.USER,
|
||||
"description": "普通用户角色,需要管理员配置路径权限",
|
||||
"is_system": True,
|
||||
},
|
||||
{
|
||||
"name": SystemRoles.VIEWER,
|
||||
"description": "只读用户角色,仅可查看文件",
|
||||
"is_system": True,
|
||||
},
|
||||
]
|
||||
|
||||
for role_data in system_roles:
|
||||
existing = await Role.get_or_none(name=role_data["name"])
|
||||
if not existing:
|
||||
await Role.create(**role_data)
|
||||
|
||||
@classmethod
|
||||
async def setup_admin_role_permissions(cls) -> None:
|
||||
"""为管理员角色设置所有权限"""
|
||||
admin_role = await Role.get_or_none(name=SystemRoles.ADMIN)
|
||||
if not admin_role:
|
||||
return
|
||||
|
||||
# 获取所有权限
|
||||
all_permissions = await Permission.all()
|
||||
|
||||
# 清除现有权限
|
||||
await RolePermission.filter(role_id=admin_role.id).delete()
|
||||
|
||||
# 添加所有权限
|
||||
for perm in all_permissions:
|
||||
await RolePermission.create(role_id=admin_role.id, permission_id=perm.id)
|
||||
|
||||
# 添加全路径访问规则
|
||||
existing_rule = await PathRule.filter(
|
||||
role_id=admin_role.id, path_pattern="/**"
|
||||
).first()
|
||||
if not existing_rule:
|
||||
await PathRule.create(
|
||||
role_id=admin_role.id,
|
||||
path_pattern="/**",
|
||||
is_regex=False,
|
||||
can_read=True,
|
||||
can_write=True,
|
||||
can_delete=True,
|
||||
can_share=True,
|
||||
priority=100,
|
||||
)
|
||||
36
domain/role/types.py
Normal file
36
domain/role/types.py
Normal file
@@ -0,0 +1,36 @@
|
||||
from pydantic import BaseModel
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class RoleInfo(BaseModel):
|
||||
id: int
|
||||
name: str
|
||||
description: str | None = None
|
||||
is_system: bool
|
||||
created_at: datetime
|
||||
|
||||
|
||||
class RoleDetail(RoleInfo):
|
||||
permissions: list[str] # 权限代码列表
|
||||
path_rules_count: int
|
||||
|
||||
|
||||
class RoleCreate(BaseModel):
|
||||
name: str
|
||||
description: str | None = None
|
||||
|
||||
|
||||
class RoleUpdate(BaseModel):
|
||||
name: str | None = None
|
||||
description: str | None = None
|
||||
|
||||
|
||||
class RolePermissionsUpdate(BaseModel):
|
||||
permission_codes: list[str]
|
||||
|
||||
|
||||
# 预置角色名称
|
||||
class SystemRoles:
|
||||
ADMIN = "Admin"
|
||||
USER = "User"
|
||||
VIEWER = "Viewer"
|
||||
4
domain/user/__init__.py
Normal file
4
domain/user/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
||||
from .service import UserService
|
||||
|
||||
__all__ = ["UserService"]
|
||||
|
||||
94
domain/user/api.py
Normal file
94
domain/user/api.py
Normal file
@@ -0,0 +1,94 @@
|
||||
from typing import Annotated
|
||||
|
||||
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.types import SystemPermission
|
||||
|
||||
from .service import UserService
|
||||
from .types import UserCreate, UserDetail, UserInfo, UserRoleAssign, UserUpdate
|
||||
|
||||
router = APIRouter(prefix="/api", tags=["user"])
|
||||
|
||||
|
||||
@router.get("/users", response_model=list[UserInfo])
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
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}")
|
||||
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])
|
||||
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])
|
||||
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)
|
||||
|
||||
190
domain/user/service.py
Normal file
190
domain/user/service.py
Normal file
@@ -0,0 +1,190 @@
|
||||
from typing import List
|
||||
|
||||
from fastapi import HTTPException
|
||||
|
||||
from domain.auth.service import AuthService
|
||||
from domain.permission.service import PermissionService
|
||||
from models.database import Role, UserAccount, UserRole
|
||||
|
||||
from .types import UserCreate, UserDetail, UserInfo, UserUpdate
|
||||
|
||||
|
||||
class UserService:
|
||||
"""用户管理服务"""
|
||||
|
||||
@classmethod
|
||||
async def get_all_users(cls) -> List[UserInfo]:
|
||||
users = await UserAccount.all().order_by("id")
|
||||
return [
|
||||
UserInfo(
|
||||
id=u.id,
|
||||
username=u.username,
|
||||
email=u.email,
|
||||
full_name=u.full_name,
|
||||
disabled=u.disabled,
|
||||
is_admin=u.is_admin,
|
||||
created_at=u.created_at,
|
||||
last_login=u.last_login,
|
||||
)
|
||||
for u in users
|
||||
]
|
||||
|
||||
@classmethod
|
||||
async def get_user(cls, user_id: int) -> UserDetail:
|
||||
user = await UserAccount.get_or_none(id=user_id).prefetch_related("created_by")
|
||||
if not user:
|
||||
raise HTTPException(404, detail="用户不存在")
|
||||
|
||||
user_roles = await UserRole.filter(user_id=user_id).prefetch_related("role")
|
||||
roles = [ur.role.name for ur in user_roles]
|
||||
|
||||
created_by_username = None
|
||||
if user.created_by_id:
|
||||
creator = await UserAccount.get_or_none(id=user.created_by_id)
|
||||
if creator:
|
||||
created_by_username = creator.username
|
||||
|
||||
return UserDetail(
|
||||
id=user.id,
|
||||
username=user.username,
|
||||
email=user.email,
|
||||
full_name=user.full_name,
|
||||
disabled=user.disabled,
|
||||
is_admin=user.is_admin,
|
||||
created_at=user.created_at,
|
||||
last_login=user.last_login,
|
||||
roles=roles,
|
||||
created_by_username=created_by_username,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
async def get_users_by_role(cls, role_id: int) -> List[UserInfo]:
|
||||
role = await Role.get_or_none(id=role_id)
|
||||
if not role:
|
||||
raise HTTPException(404, detail="角色不存在")
|
||||
|
||||
user_roles = await UserRole.filter(role_id=role_id).prefetch_related("user")
|
||||
users = [ur.user for ur in user_roles if ur.user]
|
||||
users.sort(key=lambda u: u.id)
|
||||
return [
|
||||
UserInfo(
|
||||
id=u.id,
|
||||
username=u.username,
|
||||
email=u.email,
|
||||
full_name=u.full_name,
|
||||
disabled=u.disabled,
|
||||
is_admin=u.is_admin,
|
||||
created_at=u.created_at,
|
||||
last_login=u.last_login,
|
||||
)
|
||||
for u in users
|
||||
]
|
||||
|
||||
@classmethod
|
||||
async def create_user(cls, data: UserCreate, creator_id: int) -> UserDetail:
|
||||
existing = await UserAccount.get_or_none(username=data.username)
|
||||
if existing:
|
||||
raise HTTPException(400, detail="用户名已存在")
|
||||
|
||||
if data.email:
|
||||
existing_email = await UserAccount.get_or_none(email=data.email)
|
||||
if existing_email:
|
||||
raise HTTPException(400, detail="邮箱已被使用")
|
||||
|
||||
hashed_password = AuthService.get_password_hash(data.password)
|
||||
user = await UserAccount.create(
|
||||
username=data.username,
|
||||
email=data.email,
|
||||
full_name=data.full_name,
|
||||
hashed_password=hashed_password,
|
||||
disabled=data.disabled,
|
||||
is_admin=data.is_admin,
|
||||
created_by_id=creator_id,
|
||||
)
|
||||
|
||||
if data.role_ids:
|
||||
for role_id in data.role_ids:
|
||||
role = await Role.get_or_none(id=role_id)
|
||||
if role:
|
||||
await UserRole.create(user_id=user.id, role_id=role_id)
|
||||
|
||||
return await cls.get_user(user.id)
|
||||
|
||||
@classmethod
|
||||
async def update_user(cls, user_id: int, data: UserUpdate, operator_id: int) -> UserDetail:
|
||||
user = await UserAccount.get_or_none(id=user_id)
|
||||
if not user:
|
||||
raise HTTPException(404, detail="用户不存在")
|
||||
|
||||
if data.is_admin is not None and user_id == operator_id:
|
||||
raise HTTPException(400, detail="不能修改自己的管理员状态")
|
||||
|
||||
if data.email is not None:
|
||||
existing = await UserAccount.filter(email=data.email).exclude(id=user_id).first()
|
||||
if existing:
|
||||
raise HTTPException(400, detail="邮箱已被使用")
|
||||
user.email = data.email
|
||||
|
||||
if data.full_name is not None:
|
||||
user.full_name = data.full_name
|
||||
|
||||
if data.password is not None:
|
||||
user.hashed_password = AuthService.get_password_hash(data.password)
|
||||
|
||||
if data.is_admin is not None:
|
||||
user.is_admin = data.is_admin
|
||||
|
||||
if data.disabled is not None:
|
||||
if user_id == operator_id and data.disabled:
|
||||
raise HTTPException(400, detail="不能禁用自己")
|
||||
user.disabled = data.disabled
|
||||
|
||||
await user.save()
|
||||
|
||||
PermissionService.clear_cache(user_id)
|
||||
return await cls.get_user(user_id)
|
||||
|
||||
@classmethod
|
||||
async def delete_user(cls, user_id: int, operator_id: int) -> None:
|
||||
if user_id == operator_id:
|
||||
raise HTTPException(400, detail="不能删除自己")
|
||||
|
||||
user = await UserAccount.get_or_none(id=user_id)
|
||||
if not user:
|
||||
raise HTTPException(404, detail="用户不存在")
|
||||
|
||||
await UserRole.filter(user_id=user_id).delete()
|
||||
await user.delete()
|
||||
PermissionService.clear_cache(user_id)
|
||||
|
||||
@classmethod
|
||||
async def set_user_roles(cls, user_id: int, role_ids: List[int]) -> List[str]:
|
||||
user = await UserAccount.get_or_none(id=user_id)
|
||||
if not user:
|
||||
raise HTTPException(404, detail="用户不存在")
|
||||
|
||||
roles = await Role.filter(id__in=role_ids)
|
||||
valid_role_ids = {r.id for r in roles}
|
||||
invalid_ids = set(role_ids) - valid_role_ids
|
||||
if invalid_ids:
|
||||
raise HTTPException(400, detail=f"无效的角色ID: {invalid_ids}")
|
||||
|
||||
await UserRole.filter(user_id=user_id).delete()
|
||||
for role_id in role_ids:
|
||||
await UserRole.create(user_id=user_id, role_id=role_id)
|
||||
|
||||
PermissionService.clear_cache(user_id)
|
||||
return [r.name for r in roles if r.id in role_ids]
|
||||
|
||||
@classmethod
|
||||
async def remove_user_role(cls, user_id: int, role_id: int) -> List[str]:
|
||||
user = await UserAccount.get_or_none(id=user_id)
|
||||
if not user:
|
||||
raise HTTPException(404, detail="用户不存在")
|
||||
|
||||
await UserRole.filter(user_id=user_id, role_id=role_id).delete()
|
||||
PermissionService.clear_cache(user_id)
|
||||
|
||||
user_roles = await UserRole.filter(user_id=user_id).prefetch_related("role")
|
||||
return [ur.role.name for ur in user_roles]
|
||||
|
||||
42
domain/user/types.py
Normal file
42
domain/user/types.py
Normal file
@@ -0,0 +1,42 @@
|
||||
from datetime import datetime
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class UserInfo(BaseModel):
|
||||
id: int
|
||||
username: str
|
||||
email: str | None = None
|
||||
full_name: str | None = None
|
||||
disabled: bool
|
||||
is_admin: bool
|
||||
created_at: datetime
|
||||
last_login: datetime | None = None
|
||||
|
||||
|
||||
class UserDetail(UserInfo):
|
||||
roles: list[str]
|
||||
created_by_username: str | None = None
|
||||
|
||||
|
||||
class UserCreate(BaseModel):
|
||||
username: str
|
||||
password: str
|
||||
email: str | None = None
|
||||
full_name: str | None = None
|
||||
is_admin: bool = False
|
||||
disabled: bool = False
|
||||
role_ids: list[int] = []
|
||||
|
||||
|
||||
class UserUpdate(BaseModel):
|
||||
email: str | None = None
|
||||
full_name: str | None = None
|
||||
password: str | None = None
|
||||
is_admin: bool | None = None
|
||||
disabled: bool | None = None
|
||||
|
||||
|
||||
class UserRoleAssign(BaseModel):
|
||||
role_ids: list[int]
|
||||
|
||||
@@ -5,6 +5,8 @@ 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.types import PathAction
|
||||
from .service import VirtualFSService
|
||||
from .types import MkdirRequest, MoveRequest
|
||||
|
||||
@@ -18,6 +20,7 @@ async def get_file(
|
||||
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"))
|
||||
|
||||
|
||||
@@ -50,6 +53,7 @@ async def get_temp_link(
|
||||
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.SHARE)
|
||||
data = await VirtualFSService.create_temp_link(full_path, expires_in)
|
||||
return success(data)
|
||||
|
||||
@@ -80,6 +84,7 @@ async def get_file_stat(
|
||||
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)
|
||||
|
||||
@@ -92,6 +97,7 @@ async def put_file(
|
||||
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)
|
||||
@@ -104,6 +110,7 @@ async def api_mkdir(
|
||||
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)
|
||||
|
||||
@@ -116,6 +123,9 @@ async def api_move(
|
||||
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)
|
||||
|
||||
@@ -128,6 +138,8 @@ async def api_rename(
|
||||
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)
|
||||
|
||||
@@ -140,6 +152,9 @@ async def api_copy(
|
||||
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)
|
||||
|
||||
@@ -154,6 +169,7 @@ 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)
|
||||
|
||||
@@ -169,7 +185,10 @@ async def browse_fs(
|
||||
sort_by: str = Query("name", description="按字段排序: name, size, mtime"),
|
||||
sort_order: str = Query("asc", description="排序顺序: asc, desc"),
|
||||
):
|
||||
data = await VirtualFSService.list_directory(full_path, page_num, page_size, sort_by, sort_order)
|
||||
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
|
||||
)
|
||||
return success(data)
|
||||
|
||||
|
||||
@@ -180,6 +199,7 @@ async def api_delete(
|
||||
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)
|
||||
|
||||
@@ -194,5 +214,8 @@ async def root_listing(
|
||||
sort_by: str = Query("name", description="按字段排序: name, size, mtime"),
|
||||
sort_order: str = Query("asc", description="排序顺序: asc, desc"),
|
||||
):
|
||||
data = await VirtualFSService.list_directory("/", page_num, page_size, sort_by, sort_order)
|
||||
# 根目录不需要权限检查,但需要过滤无权限的子目录
|
||||
data = await VirtualFSService.list_directory_with_permission(
|
||||
"/", current_user.id, page_num, page_size, sort_by, sort_order
|
||||
)
|
||||
return success(data)
|
||||
|
||||
@@ -5,6 +5,8 @@ from fastapi import HTTPException
|
||||
from api.response import page
|
||||
from domain.adapters import runtime_registry
|
||||
from domain.ai import FILE_COLLECTION_NAME, VECTOR_COLLECTION_NAME, VectorDBService
|
||||
from domain.permission.service import PermissionService
|
||||
from domain.permission.types import PathAction
|
||||
from .thumbnail import is_image_filename, is_video_filename
|
||||
from models import StorageAdapter
|
||||
|
||||
@@ -245,3 +247,54 @@ class VirtualFSListingMixin(VirtualFSResolverMixin):
|
||||
info["vector_index"] = vector_index
|
||||
|
||||
return info
|
||||
|
||||
@classmethod
|
||||
async def list_virtual_dir_with_permission(
|
||||
cls,
|
||||
path: str,
|
||||
user_id: int,
|
||||
page_num: int = 1,
|
||||
page_size: int = 50,
|
||||
sort_by: str = "name",
|
||||
sort_order: str = "asc",
|
||||
) -> Dict:
|
||||
"""
|
||||
带权限过滤的目录列表
|
||||
|
||||
过滤掉用户没有读取权限的条目
|
||||
"""
|
||||
# 首先获取完整的目录列表
|
||||
result = await cls.list_virtual_dir(path, page_num, page_size, sort_by, sort_order)
|
||||
|
||||
# 检查用户是否是管理员(管理员可以看到所有内容)
|
||||
from models.database import UserAccount
|
||||
user = await UserAccount.get_or_none(id=user_id)
|
||||
if user and user.is_admin:
|
||||
return result
|
||||
|
||||
# 过滤无权限的条目
|
||||
items = result.get("items", [])
|
||||
if not items:
|
||||
return result
|
||||
|
||||
norm = cls._normalize_path(path).rstrip("/") or "/"
|
||||
filtered_items = []
|
||||
|
||||
for item in items:
|
||||
item_name = item.get("name", "")
|
||||
if norm == "/":
|
||||
item_path = f"/{item_name}"
|
||||
else:
|
||||
item_path = f"{norm}/{item_name}"
|
||||
|
||||
# 检查用户是否有读取权限
|
||||
has_permission = await PermissionService.check_path_permission(
|
||||
user_id, item_path, PathAction.READ
|
||||
)
|
||||
if has_permission:
|
||||
filtered_items.append(item)
|
||||
|
||||
# 更新结果
|
||||
result["items"] = filtered_items
|
||||
|
||||
return result
|
||||
|
||||
@@ -18,4 +18,40 @@ class VirtualFSService(
|
||||
VirtualFSResolverMixin,
|
||||
VirtualFSCommonMixin,
|
||||
):
|
||||
pass
|
||||
@classmethod
|
||||
async def list_directory(
|
||||
cls,
|
||||
path: str,
|
||||
page_num: int = 1,
|
||||
page_size: int = 50,
|
||||
sort_by: str = "name",
|
||||
sort_order: str = "asc",
|
||||
):
|
||||
"""列出目录内容"""
|
||||
return await cls.list_virtual_dir(path, page_num, page_size, sort_by, sort_order)
|
||||
|
||||
@classmethod
|
||||
async def list_directory_with_permission(
|
||||
cls,
|
||||
path: str,
|
||||
user_id: int,
|
||||
page_num: int = 1,
|
||||
page_size: int = 50,
|
||||
sort_by: str = "name",
|
||||
sort_order: str = "asc",
|
||||
):
|
||||
"""列出目录内容(带权限过滤)"""
|
||||
full_path = cls._normalize_path(path).rstrip("/") or "/"
|
||||
result = await cls.list_virtual_dir_with_permission(
|
||||
full_path, user_id, page_num, page_size, sort_by, sort_order
|
||||
)
|
||||
return {
|
||||
"path": full_path,
|
||||
"entries": result.get("items", []) if isinstance(result, dict) else [],
|
||||
"pagination": {
|
||||
"total": result.get("total", 0) if isinstance(result, dict) else 0,
|
||||
"page": result.get("page", page_num) if isinstance(result, dict) else page_num,
|
||||
"page_size": result.get("page_size", page_size) if isinstance(result, dict) else page_size,
|
||||
"pages": result.get("pages", 0) if isinstance(result, dict) else 0,
|
||||
},
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user