From 230705f689706231367a54cfe96f8aa6175c222d Mon Sep 17 00:00:00 2001 From: passthem Date: Sat, 7 Mar 2026 17:35:59 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E6=88=90=E6=9D=83=E9=99=90=E7=B3=BB?= =?UTF-8?q?=E7=BB=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .drone.yml | 10 +-- bot.py | 5 +- konabot/common/permsys/__init__.py | 64 ++++++++++---- konabot/common/permsys/entity.py | 8 ++ konabot/common/permsys/repo.py | 58 ++++++++++++ konabot/plugins/perm_manage/__init__.py | 112 ++++++++++++++++++++++++ 6 files changed, 232 insertions(+), 25 deletions(-) create mode 100644 konabot/plugins/perm_manage/__init__.py diff --git a/.drone.yml b/.drone.yml index 65f065e..87b9f28 100644 --- a/.drone.yml +++ b/.drone.yml @@ -30,7 +30,7 @@ steps: volumes: - name: docker-socket path: /var/run/docker.sock - - name: 在容器中测试插件加载 + - name: 在容器中进行若干测试 image: docker:dind privileged: true volumes: @@ -38,14 +38,8 @@ steps: path: /var/run/docker.sock commands: - docker run --rm gitea.service.jazzwhom.top/mttu-developers/konabot:nightly-${DRONE_COMMIT_SHA} python scripts/test_plugin_load.py - - name: 在容器中测试 Playwright 工作正常 - image: docker:dind - privileged: true - volumes: - - name: docker-socket - path: /var/run/docker.sock - commands: - docker run --rm gitea.service.jazzwhom.top/mttu-developers/konabot:nightly-${DRONE_COMMIT_SHA} python scripts/test_playwright.py + - docker run --rm gitea.service.jazzwhom.top/mttu-developers/konabot:nightly-${DRONE_COMMIT_SHA} python -m pytest --cov-report term-missing:skip-covered - name: 发送构建结果到 ntfy image: parrazam/drone-ntfy when: diff --git a/bot.py b/bot.py index d57ea9a..446a4fe 100644 --- a/bot.py +++ b/bot.py @@ -7,7 +7,6 @@ from nonebot.adapters.discord import Adapter as DiscordAdapter from nonebot.adapters.minecraft import Adapter as MinecraftAdapter from nonebot.adapters.onebot.v11 import Adapter as OnebotAdapter -from konabot.common import permsys from konabot.common.log import init_logger from konabot.common.nb.exc import BotExceptionMessage from konabot.common.path import LOG_PATH @@ -57,11 +56,13 @@ def main(): nonebot.load_plugins("konabot/plugins") nonebot.load_plugin("nonebot_plugin_analysis_bilibili") + from konabot.common import permsys + permsys.create_startup() # 注册关闭钩子 @driver.on_shutdown - async def shutdown_handler(): + async def _(): # 关闭全局数据库管理器 db_manager = get_global_db_manager() await db_manager.close_all_connections() diff --git a/konabot/common/permsys/__init__.py b/konabot/common/permsys/__init__.py index 037b09b..7f5b8b7 100644 --- a/konabot/common/permsys/__init__.py +++ b/konabot/common/permsys/__init__.py @@ -1,7 +1,12 @@ +from typing import Annotated import nonebot from nonebot.adapters import Event +from nonebot.params import Depends +from nonebot.rule import Rule from konabot.common.database import DatabaseManager +from konabot.common.nb.is_admin import cfg +from konabot.common.pager import PagerQuery from konabot.common.path import DATA_PATH from konabot.common.permsys.entity import PermEntity, get_entity_chain from konabot.common.permsys.migrates import execute_migration @@ -11,18 +16,23 @@ from konabot.common.permsys.repo import PermRepo db = DatabaseManager(DATA_PATH / "perm.sqlite3") +_EntityLike = Event | PermEntity | list[PermEntity] + + +async def _to_entity_chain(el: _EntityLike): + if isinstance(el, Event): + return await get_entity_chain(el) # pragma: no cover + if isinstance(el, PermEntity): + return [el] + return el + + class PermManager: def __init__(self, db: DatabaseManager) -> None: self.db = db - async def check_has_permission( - self, entities: Event | PermEntity | list[PermEntity], key: str - ) -> bool: - if isinstance(entities, Event): - entities = await get_entity_chain(entities) # pragma: no cover - if isinstance(entities, PermEntity): - entities = [entities] - + async def check_has_permission_info(self, entities: _EntityLike, key: str): + entities = await _to_entity_chain(entities) key = key.removesuffix("*").removesuffix(".") key_split = key.split(".") key_split = [s for s in key_split if len(s) > 0] @@ -32,24 +42,31 @@ class PermManager: async with self.db.get_conn() as conn: repo = PermRepo(conn) - # for entity in entities: - # for k in keys: - # perm = await repo.get_perm_info(entity, k) - # if perm is not None: - # return perm data = await repo.get_perm_info_batch(entities, keys) for entity in entities: for k in keys: p = data.get((entity, k)) if p is not None: - return p - return False + return (entity, k, p) + return None + + async def check_has_permission(self, entities: _EntityLike, key: str) -> bool: + res = await self.check_has_permission_info(entities, key) + if res is None: + return False + return res[2] async def update_permission(self, entity: PermEntity, key: str, perm: bool | None): async with self.db.get_conn() as conn: repo = PermRepo(conn) await repo.update_perm_info(entity, key, perm) + async def list_permission(self, entities: _EntityLike, query: PagerQuery): + entities = await _to_entity_chain(entities) + async with self.db.get_conn() as conn: + repo = PermRepo(conn) + return await repo.list_perm_info_batch(entities, query) + def perm_manager(_db: DatabaseManager | None = None) -> PermManager: # pragma: no cover if _db is None: @@ -64,7 +81,24 @@ def create_startup(): # pragma: no cover async def _(): async with db.get_conn() as conn: await execute_migration(conn) + pm = perm_manager(db) + for account in cfg.admin_qq_account: + # ^ 这里的是超级管理员!!用环境变量定义的。 + # 咕嘿嘿嘿!!!夺取全部权限!!! + await pm.update_permission( + PermEntity("ob11", "user", str(account)), "*", True + ) @driver.on_shutdown async def _(): await db.close_all_connections() + + +DepPermManager = Annotated[PermManager, Depends(perm_manager)] + + +def require_permission(perm: str) -> Rule: # pragma: no cover + async def check_permission(event: Event, pm: DepPermManager) -> bool: + return await pm.check_has_permission(event, perm) + + return Rule(check_permission) diff --git a/konabot/common/permsys/entity.py b/konabot/common/permsys/entity.py index 481dc1e..2f1f5f7 100644 --- a/konabot/common/permsys/entity.py +++ b/konabot/common/permsys/entity.py @@ -21,6 +21,14 @@ class PermEntity: external_id: str +def get_entity_chain_of_entity(entity: PermEntity) -> list[PermEntity]: + return [ + PermEntity("sys", "global", "global"), + PermEntity(entity.platform, "global", "global"), + entity, + ][::-1] + + async def get_entity_chain(event: Event) -> list[PermEntity]: # pragma: no cover entities = [PermEntity("sys", "global", "global")] diff --git a/konabot/common/permsys/repo.py b/konabot/common/permsys/repo.py index dd1d738..f8a294a 100644 --- a/konabot/common/permsys/repo.py +++ b/konabot/common/permsys/repo.py @@ -1,8 +1,11 @@ from dataclasses import dataclass +import math from pathlib import Path import aiosqlite +from konabot.common.pager import PagerQuery, PagerResult + from .entity import PermEntity @@ -178,3 +181,58 @@ class PermRepo: rows = await cursor.fetchall() return {(entity_ids[row[0]], row[1]): bool(row[2]) for row in rows} + + async def list_perm_info_batch( + self, entities: list[PermEntity], pager: PagerQuery + ) -> PagerResult[tuple[PermEntity, str, bool]]: + """批量获取某个实体的权限信息 + + Args: + entities: PermEntity 列表 + pager: PagerQuery 对象,即分页要求 + + Returns: + 字典,键是 PermEntity,值是权限条目和布尔的元组,过滤掉所有空值 + """ + entity_to_id = await self.get_entity_id_batch(entities) + id_to_entity = {v: k for k, v in entity_to_id.items()} + ordered_ids = [entity_to_id[e] for e in entities if e in entity_to_id] + + placeholders = ", ".join("?" * len(ordered_ids)) + order_by_cases = " ".join([f"WHEN ? THEN {i}" for i in range(len(ordered_ids))]) + + pagecount_sql = f"SELECT COUNT(*) FROM perm_info WHERE entity_id IN ({placeholders}) AND value IS NOT NULL;" + count_cursor = await self.conn.execute(pagecount_sql, tuple(ordered_ids)) + total_count = (await count_cursor.fetchone() or (0,))[0] + + sql = f""" + SELECT entity_id, config_key, value + FROM perm_info + WHERE entity_id IN ({placeholders}) + AND value IS NOT NULL + ORDER BY + (CASE entity_id {order_by_cases} END) ASC, + config_key ASC + LIMIT ? + OFFSET ?; + """ + + params = ( + tuple(ordered_ids) + + tuple(ordered_ids) + + ( + pager.page_size, + (pager.page_index - 1) * pager.page_size, + ) + ) + cursor = await self.conn.execute(sql, params) + rows = await cursor.fetchall() + + # return {entity_ids[row[0]]: (row[1], bool(row[2])) for row in rows} + return PagerResult( + data=[(id_to_entity[row[0]], row[1], row[2]) for row in rows], + success=True, + message="", + page_count=math.ceil(total_count / pager.page_size), + query=pager, + ) diff --git a/konabot/plugins/perm_manage/__init__.py b/konabot/plugins/perm_manage/__init__.py new file mode 100644 index 0000000..fe3ea6f --- /dev/null +++ b/konabot/plugins/perm_manage/__init__.py @@ -0,0 +1,112 @@ +from typing import Annotated +from nonebot.adapters import Event +from nonebot.params import Depends +from nonebot_plugin_alconna import Alconna, Args, Subcommand, UniMessage, on_alconna +from konabot.common.pager import PagerQuery +from konabot.common.permsys import DepPermManager, require_permission +from konabot.common.permsys.entity import PermEntity, get_entity_chain_of_entity + + +cmd = on_alconna( + Alconna( + "konaperm", + Subcommand( + "list", + Args["platform", str], + Args["entity_type", str], + Args["external_id", str], + Args["page?", int], + ), + Subcommand( + "get", + Args["platform", str], + Args["entity_type", str], + Args["external_id", str], + Args["perm", str], + ), + Subcommand( + "set", + Args["platform", str], + Args["entity_type", str], + Args["external_id", str], + Args["perm", str], + Args["val", str], + ), + ), + rule=require_permission("admin"), +) + + +async def _get_perm_entity_chain(platform: str, entity_type: str, external_id: str): + return get_entity_chain_of_entity(PermEntity(platform, entity_type, external_id)) + + +_DepEntityChain = Annotated[list[PermEntity], Depends(_get_perm_entity_chain)] + + +def make_formatter(parent: PermEntity): + def _formatter(d: tuple[PermEntity, str, bool]): + permmark = {True: "[✅ ALLOW] ", False: "[❌ DENY] "}[d[2]] + inheritmark = "" + if parent != d[0]: + inheritmark = ( + f"[继承自 {d[0].platform}.{d[0].entity_type}.{d[0].external_id}] " + ) + return f"{permmark}{inheritmark}{d[1]}" + + return _formatter + + +@cmd.assign("list") +async def list_permisison( + pm: DepPermManager, + ec: _DepEntityChain, + event: Event, + page: int = 1, +): + pq = PagerQuery(page, 10) + data = await pm.list_permission(ec, pq) + msg = data.to_unimessage(make_formatter(ec[0])) + await msg.send(event) + + +@cmd.assign("get") +async def get_permisison( + pm: DepPermManager, + ec: _DepEntityChain, + perm: str, + event: Event, +): + data = await pm.check_has_permission_info(ec, perm) + + obj_s = f"{ec[0].platform}.{ec[0].entity_type}.{ec[0].external_id}" + + if data is None: + await UniMessage.text(f"对象 {obj_s} 无 {perm} 权限记录").send(event) + return + pe, k, p = data + inheritmark = "" + if ec[0] != pe or k != perm: + inheritmark = ( + f"继承自 {pe.platform}.{pe.entity_type}.{pe.external_id} 对 {k} 的设置," + ) + await UniMessage.text(f"{inheritmark}对象 {obj_s} 对 {perm} 的权限为 {p}").send( + event + ) + + +@cmd.assign("set") +async def set_permisison( + pm: DepPermManager, + ec: _DepEntityChain, + perm: str, + val: str, + event: Event, +): + if any(i == val.lower() for i in ("y", "yes", "allow", "true", "t")): + await pm.update_permission(ec[0], perm, True) + if any(i == val.lower() for i in ("n", "no", "deny", "false", "f")): + await pm.update_permission(ec[0], perm, False) + if any(i == val.lower() for i in ("null", "none")): + await pm.update_permission(ec[0], perm, None) + await get_permisison(pm, ec, perm, event)