"""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 ], }