mirror of
https://github.com/DrizzleTime/Foxel.git
synced 2026-05-11 18:10:10 +08:00
409 lines
13 KiB
Python
409 lines
13 KiB
Python
from dataclasses import dataclass
|
||
from typing import List, Optional
|
||
from fastapi import HTTPException
|
||
|
||
from models.database import (
|
||
UserAccount,
|
||
UserRole,
|
||
RolePermission,
|
||
PathRule,
|
||
)
|
||
from .matcher import PathMatcher
|
||
from .types import (
|
||
PathAction,
|
||
PathRuleInfo,
|
||
PathPermissionResult,
|
||
UserPermissions,
|
||
PermissionInfo,
|
||
PERMISSION_DEFINITIONS,
|
||
)
|
||
|
||
@dataclass(slots=True)
|
||
class PermissionContext:
|
||
exists: bool
|
||
is_admin: bool
|
||
path_rules: List[PathRule]
|
||
|
||
|
||
class PermissionService:
|
||
"""权限检查服务"""
|
||
|
||
# 权限检查结果缓存(简单的内存缓存)
|
||
_cache: dict[str, tuple[bool, float]] = {}
|
||
_context_cache: dict[int, tuple[PermissionContext, float]] = {}
|
||
_cache_ttl = 300 # 5分钟缓存
|
||
|
||
@classmethod
|
||
def _now(cls) -> float:
|
||
import time
|
||
|
||
return time.time()
|
||
|
||
@classmethod
|
||
def _is_cache_valid(cls, timestamp: float) -> bool:
|
||
return cls._now() - timestamp < cls._cache_ttl
|
||
|
||
@classmethod
|
||
def _get_cached_result(cls, cache_key: str) -> Optional[bool]:
|
||
cached = cls._cache.get(cache_key)
|
||
if not cached:
|
||
return None
|
||
result, timestamp = cached
|
||
if cls._is_cache_valid(timestamp):
|
||
return result
|
||
cls._cache.pop(cache_key, None)
|
||
return None
|
||
|
||
@classmethod
|
||
def _sort_path_rules(cls, rules: List[PathRule]) -> List[PathRule]:
|
||
return sorted(
|
||
rules,
|
||
key=lambda r: (
|
||
r.priority,
|
||
PathMatcher.get_pattern_specificity(r.path_pattern, r.is_regex),
|
||
),
|
||
reverse=True,
|
||
)
|
||
|
||
@classmethod
|
||
def _match_sorted_path_rules(
|
||
cls, path: str, action: str, sorted_rules: List[PathRule]
|
||
) -> Optional[bool]:
|
||
for rule in sorted_rules:
|
||
if PathMatcher.match_pattern(path, rule.path_pattern, rule.is_regex):
|
||
if action == PathAction.READ:
|
||
return rule.can_read
|
||
if action == PathAction.WRITE:
|
||
return rule.can_write
|
||
if action == PathAction.DELETE:
|
||
return rule.can_delete
|
||
if action == PathAction.SHARE:
|
||
return rule.can_share
|
||
return False
|
||
return None
|
||
|
||
@classmethod
|
||
async def _get_permission_context(cls, user_id: int) -> PermissionContext:
|
||
cached = cls._context_cache.get(user_id)
|
||
if cached:
|
||
context, timestamp = cached
|
||
if cls._is_cache_valid(timestamp):
|
||
return context
|
||
cls._context_cache.pop(user_id, None)
|
||
|
||
user = await UserAccount.get_or_none(id=user_id)
|
||
if not user:
|
||
context = PermissionContext(exists=False, is_admin=False, path_rules=[])
|
||
cls._context_cache[user_id] = (context, cls._now())
|
||
return context
|
||
|
||
if user.is_admin:
|
||
context = PermissionContext(exists=True, is_admin=True, path_rules=[])
|
||
cls._context_cache[user_id] = (context, cls._now())
|
||
return context
|
||
|
||
user_roles = await UserRole.filter(user_id=user_id)
|
||
role_ids = [ur.role_id for ur in user_roles]
|
||
if not role_ids:
|
||
context = PermissionContext(exists=True, is_admin=False, path_rules=[])
|
||
cls._context_cache[user_id] = (context, cls._now())
|
||
return context
|
||
|
||
path_rules = await PathRule.filter(role_id__in=role_ids)
|
||
context = PermissionContext(
|
||
exists=True,
|
||
is_admin=False,
|
||
path_rules=cls._sort_path_rules(list(path_rules)),
|
||
)
|
||
cls._context_cache[user_id] = (context, cls._now())
|
||
return context
|
||
|
||
@classmethod
|
||
def _check_path_permission_with_context(
|
||
cls,
|
||
user_id: int,
|
||
normalized_path: str,
|
||
action: str,
|
||
context: PermissionContext,
|
||
) -> bool:
|
||
if not context.exists:
|
||
return False
|
||
if context.is_admin:
|
||
return True
|
||
|
||
checked_cache_keys: List[str] = []
|
||
current_path = normalized_path
|
||
|
||
while True:
|
||
cache_key = f"{user_id}:{current_path}:{action}"
|
||
cached_result = cls._get_cached_result(cache_key)
|
||
if cached_result is not None:
|
||
result = cached_result
|
||
break
|
||
|
||
checked_cache_keys.append(cache_key)
|
||
result = cls._match_sorted_path_rules(current_path, action, context.path_rules)
|
||
if result is not None:
|
||
break
|
||
|
||
parent_path = PathMatcher.get_parent_path(current_path)
|
||
if not parent_path:
|
||
result = False
|
||
break
|
||
current_path = parent_path
|
||
|
||
timestamp = cls._now()
|
||
for cache_key in checked_cache_keys:
|
||
cls._cache[cache_key] = (result, timestamp)
|
||
return result
|
||
|
||
@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:
|
||
是否有权限
|
||
"""
|
||
normalized_path = PathMatcher.normalize_path(path)
|
||
cache_key = f"{user_id}:{normalized_path}:{action}"
|
||
cached_result = cls._get_cached_result(cache_key)
|
||
if cached_result is not None:
|
||
return cached_result
|
||
|
||
context = await cls._get_permission_context(user_id)
|
||
result = cls._check_path_permission_with_context(user_id, normalized_path, action, context)
|
||
cls._cache[cache_key] = (result, cls._now())
|
||
return result
|
||
|
||
@classmethod
|
||
def _match_path_rules(
|
||
cls, path: str, action: str, rules: List[PathRule]
|
||
) -> Optional[bool]:
|
||
"""
|
||
匹配路径规则
|
||
|
||
Returns:
|
||
True/False 表示明确的权限结果,None 表示没有匹配到规则
|
||
"""
|
||
return cls._match_sorted_path_rules(path, action, cls._sort_path_rules(rules))
|
||
|
||
@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
|
||
|
||
role_permission = await RolePermission.filter(
|
||
role_id__in=role_ids, permission_code=permission_code
|
||
).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_permission_codes = [item["code"] for item in PERMISSION_DEFINITIONS]
|
||
all_path_rules = await PathRule.all()
|
||
return UserPermissions(
|
||
user_id=user_id,
|
||
is_admin=True,
|
||
permissions=all_permission_codes,
|
||
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)
|
||
permissions = sorted(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]:
|
||
"""获取所有权限定义"""
|
||
return [
|
||
PermissionInfo(
|
||
code=item["code"],
|
||
name=item["name"],
|
||
category=item["category"],
|
||
description=item.get("description"),
|
||
)
|
||
for item in PERMISSION_DEFINITIONS
|
||
]
|
||
|
||
@classmethod
|
||
async def check_path_permission_detailed(
|
||
cls, user_id: int, path: str, action: str
|
||
) -> PathPermissionResult:
|
||
"""检查路径权限并返回详细结果"""
|
||
context = await cls._get_permission_context(user_id)
|
||
if not context.exists:
|
||
return PathPermissionResult(path=path, action=action, allowed=False)
|
||
|
||
if context.is_admin:
|
||
return PathPermissionResult(path=path, action=action, allowed=True)
|
||
|
||
if not context.path_rules:
|
||
return PathPermissionResult(path=path, action=action, allowed=False)
|
||
|
||
normalized_path = PathMatcher.normalize_path(path)
|
||
|
||
matched_rule = None
|
||
for rule in context.path_rules:
|
||
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()
|
||
cls._context_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]
|
||
cls._context_cache.pop(user_id, None)
|
||
|
||
@classmethod
|
||
async def filter_paths_by_permission(
|
||
cls, user_id: int, paths: List[str], action: str
|
||
) -> List[str]:
|
||
"""过滤出用户有权限的路径列表"""
|
||
if not paths:
|
||
return []
|
||
|
||
context = await cls._get_permission_context(user_id)
|
||
if not context.exists:
|
||
return []
|
||
if context.is_admin:
|
||
return list(paths)
|
||
|
||
result = []
|
||
for path in paths:
|
||
normalized_path = PathMatcher.normalize_path(path)
|
||
if cls._check_path_permission_with_context(user_id, normalized_path, action, context):
|
||
result.append(path)
|
||
return result
|