fix(auth): support nested feature permissions (#6310)

This commit is contained in:
InfinityPacer
2026-08-14 10:21:27 +08:00
committed by GitHub
parent 666e110693
commit 006b6f6318
4 changed files with 40 additions and 10 deletions

View File

@@ -2,6 +2,8 @@ from typing import Optional
from pydantic import BaseModel, Field
from app.schemas.user import UserPermissions
class MfaChallenge(BaseModel):
"""密码认证通过后需要继续完成的二次验证信息。"""
@@ -28,7 +30,7 @@ class Token(BaseModel):
# 权限级别
level: int = 1
# 详细权限
permissions: Optional[dict[str, bool]] = Field(default_factory=dict)
permissions: Optional[UserPermissions] = Field(default_factory=dict)
# 是否显示配置向导
wizard: Optional[bool] = None

View File

@@ -1,10 +1,22 @@
from typing import Optional
from pydantic import BaseModel, Field, ConfigDict
from typing_extensions import TypedDict
from app.schemas.common import JsonData
class UserPermissions(TypedDict, total=False):
"""用户分类权限及可动态扩展的功能级权限。"""
discovery: bool # 发现功能分类权限
search: bool # 资源搜索分类权限
subscribe: bool # 订阅管理分类权限
manage: bool # 系统管理分类权限
admin: bool # 管理员入口标识,实际授权仍由超级用户身份决定
features: dict[str, bool] # 功能键到启用状态的映射
# Shared properties
class UserBase(BaseModel):
"""用户公共资料、权限和个性化设置。"""
@@ -22,7 +34,7 @@ class UserBase(BaseModel):
# 是否开启二次验证
is_otp: Optional[bool] = False
# 权限
permissions: Optional[dict[str, bool]] = Field(default_factory=dict)
permissions: Optional[UserPermissions] = Field(default_factory=dict)
# 个性化设置
settings: Optional[dict[str, JsonData]] = Field(default_factory=dict)
@@ -37,7 +49,7 @@ class UserCreate(UserBase):
email: Optional[str] = None
password: Optional[str] = None
settings: Optional[dict[str, JsonData]] = Field(default_factory=dict)
permissions: Optional[dict[str, bool]] = Field(default_factory=dict)
permissions: Optional[UserPermissions] = Field(default_factory=dict)
# Properties to receive via API on update
@@ -49,7 +61,7 @@ class UserUpdate(UserBase):
email: Optional[str] = None
password: Optional[str] = None
settings: Optional[dict[str, JsonData]] = Field(default_factory=dict)
permissions: Optional[dict[str, bool]] = Field(default_factory=dict)
permissions: Optional[UserPermissions] = Field(default_factory=dict)
class UserInDBBase(UserBase):

View File

@@ -174,7 +174,7 @@ def test_login_sets_resource_token_cookie(monkeypatch):
name=username,
is_superuser=False,
avatar="",
permissions={"discovery": True},
permissions={"discovery": True, "features": {}},
)
class FakeSystemConfigOper:
@@ -198,7 +198,7 @@ def test_login_sets_resource_token_cookie(monkeypatch):
)
assert token.user_id == 1
assert token.permissions == {"discovery": True}
assert token.permissions == {"discovery": True, "features": {}}
assert "set-cookie" in response.headers
resource_cookie = response.headers["set-cookie"].split("=", 1)[1].split(";", 1)[0]

View File

@@ -10,7 +10,7 @@ from app.schemas.subscribe import Subscribe
from app.schemas.tmdb import TmdbEpisode
from app.schemas.token import Token
from app.schemas.types import MediaSource
from app.schemas.user import User
from app.schemas.user import User, UserCreate, UserUpdate
def _nonnull_branch(schema: dict) -> dict:
@@ -80,8 +80,12 @@ def test_site_and_subscribe_legacy_values_remain_compatible():
def test_permissions_and_extension_json_keep_values_but_have_explicit_schemas():
"""权限映射应为布尔值,扩展数据应通过递归 JSON Schema 展示合法类型。"""
permissions = {"manage": True, "search": False}
"""权限分类和功能映射应保持原值,扩展数据应展示合法 JSON 类型。"""
permissions = {
"manage": True,
"search": False,
"features": {"search.resource": False},
}
token = Token(
access_token="token",
token_type="bearer",
@@ -95,6 +99,8 @@ def test_permissions_and_extension_json_keep_values_but_have_explicit_schemas():
permissions=permissions,
settings={"nickname": "测试", "layout": {"dense": True}},
)
user_create = UserCreate(name="created", permissions={"features": {}})
user_update = UserUpdate(id=1, name="updated", permissions=permissions)
plugin = Plugin(history={"v1.0.0": "首次发布"})
dashboard = PluginDashboard(
attrs={"class": ["pa-2", {"active": True}]},
@@ -103,14 +109,24 @@ def test_permissions_and_extension_json_keep_values_but_have_explicit_schemas():
)
assert token.model_dump()["permissions"] == permissions
assert user.model_dump()["permissions"] == permissions
assert user_create.model_dump()["permissions"] == {"features": {}}
assert user_update.model_dump()["permissions"] == permissions
assert user.model_dump()["settings"]["layout"] == {"dense": True}
assert plugin.model_dump()["history"] == {"v1.0.0": "首次发布"}
assert dashboard.model_dump()["elements"][0]["component"] == "VAlert"
user_schema = User.model_json_schema()
permission_schema = _nonnull_branch(user_schema["properties"]["permissions"])
permission_schema = user_schema["$defs"][permission_schema["$ref"].rsplit("/", 1)[-1]]
settings_schema = _nonnull_branch(user_schema["properties"]["settings"])
assert permission_schema["additionalProperties"] == {"type": "boolean"}
assert permission_schema["properties"]["manage"] == {
"title": "Manage",
"type": "boolean",
}
assert permission_schema["properties"]["features"]["additionalProperties"] == {
"type": "boolean"
}
assert settings_schema["additionalProperties"]["$ref"].endswith("/JsonData")