Files
dy/backend/auth/permissions.py
T
2026-08-07 17:51:57 +08:00

379 lines
12 KiB
Python

"""Fixed permission catalog: menus, button actions, and data scope.
UI only selects from this list; new codes must be added in code.
"""
from __future__ import annotations
from typing import Any
# ---------------------------------------------------------------------------
# Menu visibility
# ---------------------------------------------------------------------------
MENU_DASHBOARD = "menu.dashboard"
MENU_ACCOUNTS = "menu.accounts"
MENU_MESSAGES = "menu.messages"
MENU_RULES = "menu.rules"
MENU_LOGS = "menu.logs"
MENU_RECEIVED_MESSAGES = "menu.received_messages"
MENU_SYSTEM_LOGS = "menu.system_logs"
MENU_DOWNLOAD = "menu.download"
MENU_HELP = "menu.help"
MENU_USERS = "menu.users"
MENU_ROLES = "menu.roles"
MENU_SETTINGS = "menu.settings"
MENU_DESKTOP_UPDATE = "menu.desktop_update"
MENU_PAYMENT_SETTINGS = "menu.payment_settings"
MENU_PAYMENT_ORDERS = "menu.payment_orders"
# ---------------------------------------------------------------------------
# Button / action permissions
# ---------------------------------------------------------------------------
# Accounts (granular). Legacy ``accounts.write`` expands to the full set.
ACCOUNTS_CREATE = "accounts.create"
ACCOUNTS_UPDATE = "accounts.update"
ACCOUNTS_DELETE = "accounts.delete"
ACCOUNTS_START = "accounts.start"
ACCOUNTS_STOP = "accounts.stop"
ACCOUNTS_COOKIE = "accounts.cookie"
ACCOUNTS_WRITE = "accounts.write" # legacy bundle
MESSAGES_WRITE = "messages.write"
RULES_WRITE = "rules.write"
LINK_CARDS_WRITE = "link_cards.write"
LOGS_READ = "logs.read"
RECEIVED_MESSAGES_READ = "received_messages.read"
SYSTEM_LOGS_READ = "system_logs.read"
SYSTEM_LOGS_CLEAR = "system_logs.clear"
USERS_MANAGE = "users.manage"
ROLES_MANAGE = "roles.manage"
SETTINGS_MANAGE = "settings.manage"
SETTINGS_DATABASE = "settings.database"
DESKTOP_MANAGE = "desktop.manage"
PAYMENTS_MANAGE = "payments.manage"
ORDERS_READ = "orders.read"
ORDERS_CREATE = "orders.create"
# ---------------------------------------------------------------------------
# Data scope
# ---------------------------------------------------------------------------
# Without this (and without is_admin), users only see their own data.
DATA_SCOPE_ALL = "data.scope_all"
# ---------------------------------------------------------------------------
# Bundles / aliases expanded on save & when checking permissions
# ---------------------------------------------------------------------------
ACCOUNTS_WRITE_GRANULAR: tuple[str, ...] = (
ACCOUNTS_CREATE,
ACCOUNTS_UPDATE,
ACCOUNTS_DELETE,
ACCOUNTS_START,
ACCOUNTS_STOP,
ACCOUNTS_COOKIE,
)
LEGACY_BUNDLES: dict[str, tuple[str, ...]] = {
ACCOUNTS_WRITE: ACCOUNTS_WRITE_GRANULAR,
}
ALL_PERMISSIONS: tuple[str, ...] = (
MENU_DASHBOARD,
MENU_ACCOUNTS,
MENU_MESSAGES,
MENU_RULES,
MENU_LOGS,
MENU_RECEIVED_MESSAGES,
MENU_SYSTEM_LOGS,
MENU_DOWNLOAD,
MENU_HELP,
MENU_USERS,
MENU_ROLES,
MENU_SETTINGS,
MENU_DESKTOP_UPDATE,
MENU_PAYMENT_SETTINGS,
MENU_PAYMENT_ORDERS,
ACCOUNTS_CREATE,
ACCOUNTS_UPDATE,
ACCOUNTS_DELETE,
ACCOUNTS_START,
ACCOUNTS_STOP,
ACCOUNTS_COOKIE,
ACCOUNTS_WRITE,
MESSAGES_WRITE,
RULES_WRITE,
LINK_CARDS_WRITE,
LOGS_READ,
RECEIVED_MESSAGES_READ,
SYSTEM_LOGS_READ,
SYSTEM_LOGS_CLEAR,
USERS_MANAGE,
ROLES_MANAGE,
SETTINGS_MANAGE,
SETTINGS_DATABASE,
DESKTOP_MANAGE,
PAYMENTS_MANAGE,
ORDERS_READ,
ORDERS_CREATE,
DATA_SCOPE_ALL,
)
PERMISSION_SET = frozenset(ALL_PERMISSIONS)
WRITE_PERMISSIONS = frozenset(
{
ACCOUNTS_WRITE,
*ACCOUNTS_WRITE_GRANULAR,
MESSAGES_WRITE,
RULES_WRITE,
LINK_CARDS_WRITE,
}
)
_PERMISSION_META: dict[str, dict[str, str]] = {
MENU_DASHBOARD: {"group": "menu", "label": "数据概览"},
MENU_ACCOUNTS: {"group": "menu", "label": "账号管理"},
MENU_MESSAGES: {"group": "menu", "label": "私信收发"},
MENU_RULES: {"group": "menu", "label": "自动回复规则"},
MENU_LOGS: {"group": "menu", "label": "回复日志面板"},
MENU_RECEIVED_MESSAGES: {"group": "menu", "label": "接收消息日志"},
MENU_SYSTEM_LOGS: {"group": "menu", "label": "系统诊断日志"},
MENU_DOWNLOAD: {"group": "menu", "label": "软件下载"},
MENU_HELP: {"group": "menu", "label": "帮助中心"},
MENU_USERS: {"group": "menu", "label": "用户管理"},
MENU_ROLES: {"group": "menu", "label": "角色设定"},
MENU_SETTINGS: {"group": "menu", "label": "系统设置"},
MENU_DESKTOP_UPDATE: {"group": "menu", "label": "桌面端升级"},
MENU_PAYMENT_SETTINGS: {"group": "menu", "label": "支付配置"},
MENU_PAYMENT_ORDERS: {"group": "menu", "label": "我的订单"},
ACCOUNTS_CREATE: {"group": "action", "label": "新增抖音账号"},
ACCOUNTS_UPDATE: {"group": "action", "label": "编辑账号信息"},
ACCOUNTS_DELETE: {"group": "action", "label": "删除账号"},
ACCOUNTS_START: {"group": "action", "label": "启动托管 / 批量启动"},
ACCOUNTS_STOP: {"group": "action", "label": "停止托管"},
ACCOUNTS_COOKIE: {"group": "action", "label": "查看/修改 Cookie 与凭证"},
ACCOUNTS_WRITE: {"group": "action", "label": "账号全部写操作(兼容旧版,等同下列细项)"},
MESSAGES_WRITE: {"group": "action", "label": "发送私信 / 队列立即发送"},
RULES_WRITE: {"group": "action", "label": "编辑自动回复规则"},
LINK_CARDS_WRITE: {"group": "action", "label": "上传素材 / 生成链接卡片"},
LOGS_READ: {"group": "action", "label": "查看回复日志"},
RECEIVED_MESSAGES_READ: {"group": "action", "label": "查看接收消息日志"},
SYSTEM_LOGS_READ: {"group": "action", "label": "查看系统诊断日志"},
SYSTEM_LOGS_CLEAR: {"group": "action", "label": "清空系统诊断日志"},
USERS_MANAGE: {"group": "action", "label": "管理用户"},
ROLES_MANAGE: {"group": "action", "label": "管理角色"},
SETTINGS_MANAGE: {"group": "action", "label": "管理系统设置"},
SETTINGS_DATABASE: {"group": "action", "label": "管理数据库配置与迁移"},
DESKTOP_MANAGE: {"group": "action", "label": "管理桌面端升级"},
PAYMENTS_MANAGE: {"group": "action", "label": "管理支付配置与全部订单"},
ORDERS_READ: {"group": "action", "label": "查看我的订单"},
ORDERS_CREATE: {"group": "action", "label": "购买额度 / 创建订单"},
DATA_SCOPE_ALL: {"group": "data", "label": "查看全部用户数据(全局数据范围)"},
}
OPERATOR_PERMISSIONS: tuple[str, ...] = (
MENU_DASHBOARD,
MENU_ACCOUNTS,
MENU_MESSAGES,
MENU_RULES,
MENU_LOGS,
MENU_RECEIVED_MESSAGES,
MENU_DOWNLOAD,
MENU_HELP,
MENU_PAYMENT_ORDERS,
*ACCOUNTS_WRITE_GRANULAR,
MESSAGES_WRITE,
RULES_WRITE,
LINK_CARDS_WRITE,
LOGS_READ,
RECEIVED_MESSAGES_READ,
ORDERS_READ,
ORDERS_CREATE,
)
VIEWER_PERMISSIONS: tuple[str, ...] = (
MENU_DASHBOARD,
MENU_ACCOUNTS,
MENU_MESSAGES,
MENU_RULES,
MENU_LOGS,
MENU_RECEIVED_MESSAGES,
MENU_DOWNLOAD,
MENU_HELP,
LOGS_READ,
RECEIVED_MESSAGES_READ,
)
def normalize_permissions(codes: list[str] | tuple[str, ...] | None) -> list[str]:
if not codes:
return []
seen: set[str] = set()
result: list[str] = []
for code in codes:
value = str(code or "").strip()
if not value or value not in PERMISSION_SET or value in seen:
continue
seen.add(value)
result.append(value)
return result
def expand_legacy_bundles(codes: set[str]) -> set[str]:
"""Expand legacy bundle codes into granular permissions."""
expanded = set(codes)
for bundle, parts in LEGACY_BUNDLES.items():
if bundle in expanded:
expanded.update(parts)
return expanded
# Menu → required action. Selecting a menu always grants the action.
MENU_REQUIRED_ACTIONS: dict[str, str | tuple[str, ...]] = {
MENU_USERS: USERS_MANAGE,
MENU_ROLES: ROLES_MANAGE,
MENU_SETTINGS: SETTINGS_MANAGE,
MENU_DESKTOP_UPDATE: DESKTOP_MANAGE,
MENU_PAYMENT_SETTINGS: PAYMENTS_MANAGE,
MENU_PAYMENT_ORDERS: (ORDERS_READ, ORDERS_CREATE),
MENU_LOGS: LOGS_READ,
MENU_RECEIVED_MESSAGES: RECEIVED_MESSAGES_READ,
MENU_SYSTEM_LOGS: SYSTEM_LOGS_READ,
MENU_ACCOUNTS: (), # page access only; buttons are separate
MENU_MESSAGES: (),
MENU_RULES: (),
}
# Action → primary menu only.
ACTION_PRIMARY_MENU: dict[str, str] = {
USERS_MANAGE: MENU_USERS,
ROLES_MANAGE: MENU_ROLES,
SETTINGS_MANAGE: MENU_SETTINGS,
SETTINGS_DATABASE: MENU_SETTINGS,
DESKTOP_MANAGE: MENU_DESKTOP_UPDATE,
PAYMENTS_MANAGE: MENU_PAYMENT_SETTINGS,
ORDERS_READ: MENU_PAYMENT_ORDERS,
ORDERS_CREATE: MENU_PAYMENT_ORDERS,
LOGS_READ: MENU_LOGS,
RECEIVED_MESSAGES_READ: MENU_RECEIVED_MESSAGES,
SYSTEM_LOGS_READ: MENU_SYSTEM_LOGS,
SYSTEM_LOGS_CLEAR: MENU_SYSTEM_LOGS,
ACCOUNTS_CREATE: MENU_ACCOUNTS,
ACCOUNTS_UPDATE: MENU_ACCOUNTS,
ACCOUNTS_DELETE: MENU_ACCOUNTS,
ACCOUNTS_START: MENU_ACCOUNTS,
ACCOUNTS_STOP: MENU_ACCOUNTS,
ACCOUNTS_COOKIE: MENU_ACCOUNTS,
ACCOUNTS_WRITE: MENU_ACCOUNTS,
MESSAGES_WRITE: MENU_MESSAGES,
RULES_WRITE: MENU_RULES,
LINK_CARDS_WRITE: MENU_RULES,
}
def _iter_required_actions(menu: str) -> tuple[str, ...]:
raw = MENU_REQUIRED_ACTIONS.get(menu)
if raw is None:
return ()
if isinstance(raw, str):
return (raw,)
return tuple(raw)
# Catalog / UI pairs (menu → first required action for checkbox hints).
MENU_ACTION_PAIRS: tuple[tuple[str, str], ...] = tuple(
(menu, actions[0])
for menu, actions in (
(m, _iter_required_actions(m)) for m in MENU_REQUIRED_ACTIONS
)
if actions
)
def expand_paired_permissions(codes: list[str] | tuple[str, ...] | None) -> list[str]:
"""Normalize, expand legacy bundles, and auto-complete menu/action pairs."""
selected = expand_legacy_bundles(set(normalize_permissions(codes)))
for menu in list(selected):
for action in _iter_required_actions(menu):
selected.add(action)
for action, menu in ACTION_PRIMARY_MENU.items():
if action in selected:
selected.add(menu)
# If every granular account write is present, keep the legacy bundle flag.
if all(code in selected for code in ACCOUNTS_WRITE_GRANULAR):
selected.add(ACCOUNTS_WRITE)
return [code for code in ALL_PERMISSIONS if code in selected]
def permission_implies(held: set[str], needed: str) -> bool:
"""Whether a held permission set satisfies ``needed`` (incl. legacy bundles)."""
if needed in held:
return True
for bundle, parts in LEGACY_BUNDLES.items():
if needed in parts and bundle in held:
return True
return False
def permission_catalog() -> dict[str, Any]:
menus = []
actions = []
data = []
meta_by_code: dict[str, dict[str, str]] = {}
for code in ALL_PERMISSIONS:
meta = _PERMISSION_META[code]
item = {"code": code, "label": meta["label"]}
meta_by_code[code] = meta
group = meta["group"]
if group == "menu":
menus.append(item)
elif group == "data":
data.append(item)
else:
actions.append(item)
# Reverse index: menu → child action codes (preserve ALL_PERMISSIONS order).
children_by_menu: dict[str, list[dict[str, str]]] = {m["code"]: [] for m in menus}
for code in ALL_PERMISSIONS:
if meta_by_code[code]["group"] != "action":
continue
parent = ACTION_PRIMARY_MENU.get(code)
if parent and parent in children_by_menu:
children_by_menu[parent].append(
{"code": code, "label": meta_by_code[code]["label"]}
)
tree: list[dict[str, Any]] = []
for menu in menus:
node: dict[str, Any] = {
"code": menu["code"],
"label": menu["label"],
"kind": "menu",
"children": children_by_menu.get(menu["code"], []),
}
tree.append(node)
if data:
tree.append(
{
"code": "__group.data__",
"label": "数据权限",
"kind": "group",
"children": [
{"code": item["code"], "label": item["label"]} for item in data
],
}
)
return {
"menus": menus,
"actions": actions,
"data": data,
"tree": tree,
"pairs": [
{"menu": menu, "action": action} for menu, action in MENU_ACTION_PAIRS
],
}