refactor: remove Permission model and update related code to use permission codes

This commit is contained in:
shiyu
2026-02-09 11:15:01 +08:00
parent c5e4b3ef43
commit 103beb7dad
9 changed files with 35 additions and 243 deletions

View File

@@ -3,9 +3,7 @@ from fastapi import HTTPException
from models.database import (
UserAccount,
Role,
UserRole,
Permission,
RolePermission,
PathRule,
)
@@ -144,13 +142,8 @@ class PermissionService:
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
role_id__in=role_ids, permission_code=permission_code
).first()
return role_permission is not None
@@ -180,12 +173,12 @@ class PermissionService:
# 超级管理员拥有所有权限
if user.is_admin:
all_permissions = await Permission.all()
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=[p.code for p in all_permissions],
permissions=all_permission_codes,
path_rules=[
PathRuleInfo(
id=r.id,
@@ -210,10 +203,8 @@ class PermissionService:
# 获取权限
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))
role_permissions = await RolePermission.filter(role_id__in=role_ids)
permissions = sorted(set(rp.permission_code for rp in role_permissions))
# 获取路径规则
path_rules = []
@@ -245,16 +236,14 @@ class PermissionService:
@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,
code=item["code"],
name=item["name"],
category=item["category"],
description=item.get("description"),
)
for p in permissions
for item in PERMISSION_DEFINITIONS
]
@classmethod

View File

@@ -49,7 +49,6 @@ PERMISSION_DEFINITIONS = [
# Pydantic 模型
class PermissionInfo(BaseModel):
id: int
code: str
name: str
category: str

View File

@@ -1,9 +1,9 @@
from typing import List
from fastapi import HTTPException
from models.database import Role, RolePermission, Permission, PathRule, UserRole
from models.database import Role, RolePermission, PathRule, UserRole
from domain.permission.service import PermissionService
from domain.permission.types import PathRuleCreate, PathRuleInfo
from domain.permission.types import PathRuleCreate, PathRuleInfo, PERMISSION_DEFINITIONS
from .types import RoleInfo, RoleDetail, RoleCreate, RoleUpdate, SystemRoles
@@ -33,10 +33,8 @@ class RoleService:
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]
role_permissions = await RolePermission.filter(role_id=role_id)
permissions = sorted(set(rp.permission_code for rp in role_permissions))
# 获取路径规则数量
path_rules_count = await PathRule.filter(role_id=role_id).count()
@@ -126,12 +124,8 @@ class RoleService:
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())
all_permission_codes = {item["code"] for item in PERMISSION_DEFINITIONS}
invalid_codes = set(permission_codes) - all_permission_codes
if invalid_codes:
raise HTTPException(400, detail=f"无效的权限代码: {', '.join(invalid_codes)}")
@@ -142,13 +136,13 @@ class RoleService:
for code in permission_codes:
await RolePermission.create(
role_id=role_id,
permission_id=permission_map[code],
permission_code=code,
)
# 清除权限缓存
PermissionService.clear_cache()
return permission_codes
return list(permission_codes)
@classmethod
async def get_role_path_rules(cls, role_id: int) -> List[PathRuleInfo]:
@@ -292,36 +286,3 @@ class RoleService:
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,
)

View File

@@ -53,7 +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)
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)

View File

@@ -21,6 +21,7 @@ from middleware.exception_handler import (
import httpx
from dotenv import load_dotenv
from domain.tasks import task_queue_service, task_scheduler
from domain.role.service import RoleService
load_dotenv()
@@ -66,6 +67,7 @@ async def lifespan(app: FastAPI):
os.makedirs("data/db", exist_ok=True)
os.makedirs("data/plugins", exist_ok=True)
await init_db()
await RoleService.ensure_system_roles()
await runtime_registry.refresh()
await ConfigService.set("APP_VERSION", VERSION)
await task_queue_service.start_worker()

View File

@@ -3,7 +3,6 @@ from .database import (
UserAccount,
Role,
UserRole,
Permission,
RolePermission,
PathRule,
)
@@ -13,7 +12,6 @@ __all__ = [
"UserAccount",
"Role",
"UserRole",
"Permission",
"RolePermission",
"PathRule",
]

View File

@@ -62,19 +62,6 @@ class UserRole(Model):
unique_together = (("user", "role"),)
class Permission(Model):
"""权限定义表"""
id = fields.IntField(pk=True)
code = fields.CharField(max_length=50, unique=True) # 权限代码
name = fields.CharField(max_length=100) # 权限名称
category = fields.CharField(max_length=50) # 分类system/adapter/file
description = fields.CharField(max_length=255, null=True)
class Meta:
table = "permissions"
class RolePermission(Model):
"""角色-权限关联表"""
@@ -82,13 +69,11 @@ class RolePermission(Model):
role: fields.ForeignKeyRelation[Role] = fields.ForeignKeyField(
"models.Role", related_name="role_permissions", on_delete=fields.CASCADE
)
permission: fields.ForeignKeyRelation[Permission] = fields.ForeignKeyField(
"models.Permission", related_name="permission_roles", on_delete=fields.CASCADE
)
permission_code = fields.CharField(max_length=50)
class Meta:
table = "role_permissions"
unique_together = (("role", "permission"),)
unique_together = (("role", "permission_code"),)
class PathRule(Model):

View File

@@ -10,17 +10,12 @@ import sys
from pathlib import Path
PROJECT_ROOT = Path(__file__).resolve().parents[1]
DEFAULT_DB_PATH = PROJECT_ROOT / "data/db/db.sqlite3"
if str(PROJECT_ROOT) not in sys.path:
sys.path.insert(0, str(PROJECT_ROOT))
from domain.config import VERSION
from domain.auth import get_password_hash
from domain.permission.types import PERMISSION_DEFINITIONS
from domain.role.types import SystemRoles
def _project_root() -> Path:
return PROJECT_ROOT
def _supports_color() -> bool:
@@ -67,10 +62,6 @@ def _print_banner() -> None:
print(f"{title}\n", file=sys.stderr)
def _default_db_path() -> Path:
return _project_root() / "data/db/db.sqlite3"
def _gen_password(length: int) -> str:
alphabet = string.ascii_letters + string.digits
return "".join(secrets.choice(alphabet) for _ in range(length))
@@ -78,19 +69,17 @@ def _gen_password(length: int) -> str:
def _find_user(conn: sqlite3.Connection, username_or_email: str) -> tuple[int, str] | None:
cursor = conn.cursor()
cursor.execute("SELECT id, username FROM user WHERE username = ?", (username_or_email,))
row = cursor.fetchone()
if row:
return int(row[0]), str(row[1])
cursor.execute("SELECT id, username FROM user WHERE email = ?", (username_or_email,))
row = cursor.fetchone()
if row:
return int(row[0]), str(row[1])
normalized = username_or_email.strip().lower()
candidates = [
("username", username_or_email),
("email", username_or_email),
]
if normalized and normalized != username_or_email:
cursor.execute("SELECT id, username FROM user WHERE email = ?", (normalized,))
candidates.append(("email", normalized))
for field, value in candidates:
cursor.execute(f"SELECT id, username FROM user WHERE {field} = ?", (value,))
row = cursor.fetchone()
if row:
return int(row[0]), str(row[1])
@@ -99,7 +88,7 @@ def _find_user(conn: sqlite3.Connection, username_or_email: str) -> tuple[int, s
def _cmd_reset_password(args: argparse.Namespace) -> int:
db_path = Path(args.db).expanduser() if args.db else _default_db_path()
db_path = Path(args.db).expanduser() if args.db else DEFAULT_DB_PATH
if args.random:
password = _gen_password(args.length)
@@ -108,8 +97,7 @@ def _cmd_reset_password(args: argparse.Namespace) -> int:
hashed_password = get_password_hash(password)
conn = sqlite3.connect(str(db_path))
try:
with sqlite3.connect(str(db_path)) as conn:
user = _find_user(conn, args.username_or_email)
if not user:
print(f"用户不存在: {args.username_or_email}", file=sys.stderr)
@@ -120,8 +108,6 @@ def _cmd_reset_password(args: argparse.Namespace) -> int:
(hashed_password, user_id),
)
conn.commit()
finally:
conn.close()
if args.random:
print(password)
@@ -129,129 +115,6 @@ def _cmd_reset_password(args: argparse.Namespace) -> int:
return 0
def _cmd_init_rbac(args: argparse.Namespace) -> int:
db_path = Path(args.db).expanduser() if args.db else _default_db_path()
role_definitions = [
{
"name": SystemRoles.ADMIN,
"description": "管理员角色,拥有所有系统和适配器权限",
},
{
"name": SystemRoles.USER,
"description": "普通用户角色,需要管理员配置路径权限",
},
{
"name": SystemRoles.VIEWER,
"description": "只读用户角色,仅可查看文件",
},
]
conn = sqlite3.connect(str(db_path))
try:
conn.execute("PRAGMA foreign_keys = ON")
cursor = conn.cursor()
try:
cursor.execute("SELECT 1 FROM permissions LIMIT 1")
cursor.execute("SELECT 1 FROM roles LIMIT 1")
cursor.execute("SELECT 1 FROM role_permissions LIMIT 1")
cursor.execute("SELECT 1 FROM path_rules LIMIT 1")
except sqlite3.OperationalError as exc:
print(f"数据库未初始化(缺少表)。请先启动一次服务生成表。{exc}", file=sys.stderr)
return 1
# upsert permissions
for perm in PERMISSION_DEFINITIONS:
cursor.execute(
"""
INSERT INTO permissions (code, name, category, description)
VALUES (?, ?, ?, ?)
ON CONFLICT(code) DO UPDATE SET
name = excluded.name,
category = excluded.category,
description = excluded.description
""",
(
perm["code"],
perm["name"],
perm["category"],
perm.get("description"),
),
)
# upsert roles
for role in role_definitions:
cursor.execute(
"""
INSERT INTO roles (name, description, is_system)
VALUES (?, ?, 1)
ON CONFLICT(name) DO UPDATE SET
description = excluded.description,
is_system = 1
""",
(role["name"], role["description"]),
)
# grant all permissions to Admin role
cursor.execute("SELECT id FROM roles WHERE name = ?", (SystemRoles.ADMIN,))
admin_row = cursor.fetchone()
if not admin_row:
print("初始化失败:未找到 Admin 角色", file=sys.stderr)
return 1
admin_role_id = int(admin_row[0])
cursor.execute("DELETE FROM role_permissions WHERE role_id = ?", (admin_role_id,))
cursor.execute("SELECT id FROM permissions")
permission_ids = [int(row[0]) for row in cursor.fetchall()]
cursor.executemany(
"INSERT INTO role_permissions (role_id, permission_id) VALUES (?, ?)",
[(admin_role_id, pid) for pid in permission_ids],
)
# ensure Admin has full access path rule
cursor.execute(
"SELECT id FROM path_rules WHERE role_id = ? AND path_pattern = ? LIMIT 1",
(admin_role_id, "/**"),
)
existing_rule = cursor.fetchone()
if existing_rule:
cursor.execute(
"""
UPDATE path_rules
SET is_regex = 0,
can_read = 1,
can_write = 1,
can_delete = 1,
can_share = 1,
priority = 100
WHERE id = ?
""",
(int(existing_rule[0]),),
)
else:
cursor.execute(
"""
INSERT INTO path_rules (
role_id, path_pattern, is_regex,
can_read, can_write, can_delete, can_share,
priority
)
VALUES (?, ?, 0, 1, 1, 1, 1, 100)
""",
(admin_role_id, "/**"),
)
conn.commit()
finally:
conn.close()
print(f"已初始化权限: {len(PERMISSION_DEFINITIONS)}", file=sys.stderr)
print("已补齐内置角色: Admin / User / Viewer", file=sys.stderr)
print("已为 Admin 角色授予全部权限并设置 /** 全路径规则", file=sys.stderr)
return 0
def _build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(prog="foxel")
subparsers = parser.add_subparsers(dest="command", required=True)
@@ -264,10 +127,6 @@ def _build_parser() -> argparse.ArgumentParser:
reset_password.add_argument("--db", help="sqlite db 路径(默认 data/db/db.sqlite3")
reset_password.set_defaults(func=_cmd_reset_password)
init_rbac = subparsers.add_parser("init-rbac", help="初始化权限与内置角色")
init_rbac.add_argument("--db", help="sqlite db 路径(默认 data/db/db.sqlite3")
init_rbac.set_defaults(func=_cmd_init_rbac)
return parser

View File

@@ -2,7 +2,6 @@ import request from './client';
import type { PathRuleInfo } from './roles';
export interface PermissionInfo {
id: number;
code: string;
name: string;
category: string;