mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-05 23:47:41 +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 pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
from app.schemas.user import UserPermissions
|
||||||
|
|
||||||
|
|
||||||
class MfaChallenge(BaseModel):
|
class MfaChallenge(BaseModel):
|
||||||
"""密码认证通过后需要继续完成的二次验证信息。"""
|
"""密码认证通过后需要继续完成的二次验证信息。"""
|
||||||
@@ -28,7 +30,7 @@ class Token(BaseModel):
|
|||||||
# 权限级别
|
# 权限级别
|
||||||
level: int = 1
|
level: int = 1
|
||||||
# 详细权限
|
# 详细权限
|
||||||
permissions: Optional[dict[str, bool]] = Field(default_factory=dict)
|
permissions: Optional[UserPermissions] = Field(default_factory=dict)
|
||||||
# 是否显示配置向导
|
# 是否显示配置向导
|
||||||
wizard: Optional[bool] = None
|
wizard: Optional[bool] = None
|
||||||
|
|
||||||
|
|||||||
+15
-3
@@ -1,10 +1,22 @@
|
|||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from pydantic import BaseModel, Field, ConfigDict
|
from pydantic import BaseModel, Field, ConfigDict
|
||||||
|
from typing_extensions import TypedDict
|
||||||
|
|
||||||
from app.schemas.common import JsonData
|
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
|
# Shared properties
|
||||||
class UserBase(BaseModel):
|
class UserBase(BaseModel):
|
||||||
"""用户公共资料、权限和个性化设置。"""
|
"""用户公共资料、权限和个性化设置。"""
|
||||||
@@ -22,7 +34,7 @@ class UserBase(BaseModel):
|
|||||||
# 是否开启二次验证
|
# 是否开启二次验证
|
||||||
is_otp: Optional[bool] = False
|
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)
|
settings: Optional[dict[str, JsonData]] = Field(default_factory=dict)
|
||||||
|
|
||||||
@@ -37,7 +49,7 @@ class UserCreate(UserBase):
|
|||||||
email: Optional[str] = None
|
email: Optional[str] = None
|
||||||
password: Optional[str] = None
|
password: Optional[str] = None
|
||||||
settings: Optional[dict[str, JsonData]] = Field(default_factory=dict)
|
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
|
# Properties to receive via API on update
|
||||||
@@ -49,7 +61,7 @@ class UserUpdate(UserBase):
|
|||||||
email: Optional[str] = None
|
email: Optional[str] = None
|
||||||
password: Optional[str] = None
|
password: Optional[str] = None
|
||||||
settings: Optional[dict[str, JsonData]] = Field(default_factory=dict)
|
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):
|
class UserInDBBase(UserBase):
|
||||||
|
|||||||
@@ -174,7 +174,7 @@ def test_login_sets_resource_token_cookie(monkeypatch):
|
|||||||
name=username,
|
name=username,
|
||||||
is_superuser=False,
|
is_superuser=False,
|
||||||
avatar="",
|
avatar="",
|
||||||
permissions={"discovery": True},
|
permissions={"discovery": True, "features": {}},
|
||||||
)
|
)
|
||||||
|
|
||||||
class FakeSystemConfigOper:
|
class FakeSystemConfigOper:
|
||||||
@@ -198,7 +198,7 @@ def test_login_sets_resource_token_cookie(monkeypatch):
|
|||||||
)
|
)
|
||||||
|
|
||||||
assert token.user_id == 1
|
assert token.user_id == 1
|
||||||
assert token.permissions == {"discovery": True}
|
assert token.permissions == {"discovery": True, "features": {}}
|
||||||
assert "set-cookie" in response.headers
|
assert "set-cookie" in response.headers
|
||||||
|
|
||||||
resource_cookie = response.headers["set-cookie"].split("=", 1)[1].split(";", 1)[0]
|
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.tmdb import TmdbEpisode
|
||||||
from app.schemas.token import Token
|
from app.schemas.token import Token
|
||||||
from app.schemas.types import MediaSource
|
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:
|
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():
|
def test_permissions_and_extension_json_keep_values_but_have_explicit_schemas():
|
||||||
"""权限映射应为布尔值,扩展数据应通过递归 JSON Schema 展示合法类型。"""
|
"""权限分类和功能映射应保持原值,扩展数据应展示合法 JSON 类型。"""
|
||||||
permissions = {"manage": True, "search": False}
|
permissions = {
|
||||||
|
"manage": True,
|
||||||
|
"search": False,
|
||||||
|
"features": {"search.resource": False},
|
||||||
|
}
|
||||||
token = Token(
|
token = Token(
|
||||||
access_token="token",
|
access_token="token",
|
||||||
token_type="bearer",
|
token_type="bearer",
|
||||||
@@ -95,6 +99,8 @@ def test_permissions_and_extension_json_keep_values_but_have_explicit_schemas():
|
|||||||
permissions=permissions,
|
permissions=permissions,
|
||||||
settings={"nickname": "测试", "layout": {"dense": True}},
|
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": "首次发布"})
|
plugin = Plugin(history={"v1.0.0": "首次发布"})
|
||||||
dashboard = PluginDashboard(
|
dashboard = PluginDashboard(
|
||||||
attrs={"class": ["pa-2", {"active": True}]},
|
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 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 user.model_dump()["settings"]["layout"] == {"dense": True}
|
||||||
assert plugin.model_dump()["history"] == {"v1.0.0": "首次发布"}
|
assert plugin.model_dump()["history"] == {"v1.0.0": "首次发布"}
|
||||||
assert dashboard.model_dump()["elements"][0]["component"] == "VAlert"
|
assert dashboard.model_dump()["elements"][0]["component"] == "VAlert"
|
||||||
|
|
||||||
user_schema = User.model_json_schema()
|
user_schema = User.model_json_schema()
|
||||||
permission_schema = _nonnull_branch(user_schema["properties"]["permissions"])
|
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"])
|
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")
|
assert settings_schema["additionalProperties"]["$ref"].endswith("/JsonData")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user