mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-08-15 11:04:12 +08:00
fix(auth): support nested feature permissions (#6310)
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user