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:
shiyu
2026-01-30 15:59:22 +08:00
parent 4a2e01196d
commit e6ab01ef9d
33 changed files with 3462 additions and 10 deletions

View File

@@ -0,0 +1,4 @@
from .service import PermissionService
from .matcher import PathMatcher
__all__ = ["PermissionService", "PathMatcher"]

41
domain/permission/api.py Normal file
View 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
)

View 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

View 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
View 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] # 路径权限规则