# 权限系统 `konabot.common.permsys`

本文档面向维护者，说明 `konabot/common/permsys` 模块的职责、数据模型、权限解析规则，以及在插件中接入的推荐方式。

## 模块目标

`permsys` 提供了一套简单的、可继承的权限系统，用于回答两个问题：

1. 某个事件对应的主体是谁。
2. 该主体是否拥有某项权限。

它适合处理 bot 内部的功能开关、管理权限、平台级授权等场景。

当前模块由以下几部分组成：

- `konabot/common/permsys/__init__.py`
  - 暴露 `PermManager`、`DepPermManager`、`require_permission`
  - 负责数据库初始化、启动迁移、超级管理员默认授权
  - 提供 `register_default_allow_permission()` 用于注册“启动时默认放行”的权限键
- `konabot/common/permsys/entity.py`
  - 定义 `PermEntity`
  - 将事件转换为可查询的实体链
- `konabot/common/permsys/repo.py`
  - 封装 SQLite 读写
- `konabot/common/permsys/migrates/`
  - 存放迁移 SQL
- `konabot/common/permsys/sql/`
  - 存放查询与更新 SQL

## 核心概念

### 1. `PermEntity`

`PermEntity` 是权限系统中的最小主体标识：

```python
PermEntity(platform: str, entity_type: str, external_id: str)
```

示例：

- `PermEntity("sys", "global", "global")`
- `PermEntity("ob11", "group", "123456")`
- `PermEntity("ob11", "user", "987654")`

其中：

- `platform` 表示来源平台，如 `sys`、`ob11`、`discord`
- `entity_type` 表示主体类型，如 `global`、`group`、`user`
- `external_id` 表示平台侧的外部标识

### 2. 实体链

权限判断不是只看单个实体，而是看一条“实体链”。

以 `get_entity_chain_of_entity()` 为例，传入一个具体实体时，返回的链为：

```python
[
    PermEntity(platform, entity_type, external_id),
    PermEntity(platform, "global", "global"),
    PermEntity("sys", "global", "global"),
]
```

这意味着权限会优先读取更具体的主体，再回退到平台全局，最后回退到系统全局。

`get_entity_chain(event)` 则会根据事件类型自动构造链。例如：

- OneBot V11 群消息：用户 -> 群 -> 平台全局 -> 系统全局
- OneBot V11 私聊：用户 -> 平台全局 -> 系统全局
- Discord 频道消息：用户/频道/服务器 -> 平台全局 -> 系统全局
- Console：控制台用户/频道 -> 平台全局 -> 系统全局

注意：当前 `entity.py` 中的具体链顺序与字段命名应以实现为准；修改这里时要评估现有权限继承是否会被破坏。

### 3. 权限键

权限键使用点分结构，例如：

- `admin`
- `plugin.weather`
- `plugin.weather.use`

检查时会自动做前缀回退。以 `plugin.weather.use` 为例，查询顺序是：

1. `plugin.weather.use`
2. `plugin.weather`
3. `plugin`
4. `*`

因此，`*` 可以看作兜底总权限。

## 权限解析规则

`PermManager.check_has_permission_info()` 的逻辑可以概括为：

1. 先把输入转换成实体链。
2. 对权限键做逐级回退，同时追加 `*`。
3. 在数据库中批量查出链上所有实体、所有候选键的显式记录。
4. 按“实体越具体越优先、权限键越具体越优先”的顺序，返回第一条命中的记录。

若没有任何显式记录：

- `check_has_permission_info()` 返回 `None`
- `check_has_permission()` 返回 `False`

这表示本系统默认是“未授权即拒绝”。

## 数据存储

模块使用 SQLite，默认数据库文件位于：

- `data/perm.sqlite3`

启动时会执行迁移：

- `create_startup()` 在 NoneBot 启动事件中调用 `execute_migration()`

权限值支持三态：

- `True`：显式允许
- `False`：显式拒绝
- `None`：删除/清空该层的显式设置，让判断重新回退到继承链

`repo.py` 中的 `update_perm_info()` 会将这个三态直接写入数据库。

## 超级管理员注入

在启动阶段，`create_startup()` 会读取 `konabot.common.nb.is_admin.cfg.admin_qq_account`，并为这些 QQ 账号写入：

```python
PermEntity("ob11", "user", str(account)), "*", True
```

也就是说，配置中的超级管理员会直接拥有全部权限。

此外，模块也支持插件在导入阶段通过 `register_default_allow_permission("some.key")` 注册默认放行的权限键；这些键会在启动时被写入到：

```python
PermEntity("sys", "global", "global"), "some.key", True
```

这适合“默认所有人可用，但仍希望后续能被权限系统单独关闭”的功能。

这属于启动时自动灌入的保底策略，不依赖手工授权命令。

## 在插件中使用

### 1. 直接做权限检查

```python
from konabot.common.permsys import DepPermManager


async def handler(pm: DepPermManager, event):
    ok = await pm.check_has_permission(event, "plugin.example.use")
    if not ok:
        return
```

适合需要在处理流程中动态决定权限键的场景。

### 2. 挂到 Rule 上做准入控制

```python
from nonebot_plugin_alconna import Alconna, on_alconna
from konabot.common.permsys import require_permission


cmd = on_alconna(
    Alconna("example"),
    rule=require_permission("plugin.example.use"),
)
```

适合命令入口明确、未通过时直接拦截的场景。

### 3. 更新权限

```python
from konabot.common.permsys import DepPermManager
from konabot.common.permsys.entity import PermEntity


await pm.update_permission(
    PermEntity("ob11", "group", "123456"),
    "plugin.example.use",
    True,
)
```

建议只在专门的管理插件中开放写权限，避免普通功能插件到处分散改表。

## `perm_manage` 插件与本模块的关系

`konabot/plugins/perm_manage/__init__.py` 是本模块当前的管理入口，提供：

- `konaperm list`：列出实体链上已有的显式权限记录
- `konaperm get`：查看某个权限最终命中的记录
- `konaperm set`：写入 allow/deny/null

这个插件本身使用 `require_permission("admin")` 保护，因此只有拥有 `admin` 权限的主体才能管理权限。

## 接入建议

### 权限键命名

建议使用稳定、可扩展的分层键名：

- 推荐：`plugin.xxx`、`plugin.xxx.action`
- 不推荐：含糊的单词或临时字符串

这样才能利用前缀回退机制做批量授权。

### 输入安全

虽然这个项目偏内部使用，但权限键、实体类型、外部 ID 仍然应视为不可信输入：

- 不要把聊天输入直接拼到 SQL 中
- 不要让任意用户可随意构造高权限写入
- 对可写命令至少做权限保护和必要校验

### 改动兼容性

以下改动都可能影响全局权限行为，修改前应充分评估：

- 更改实体链顺序
- 更改默认兜底键 `*` 的语义
- 更改 `None` 的处理方式
- 更改启动时超级管理员注入逻辑

## 调试建议

- 先用 `konaperm get ...` 确认某个权限最终命中了哪一层
- 再用 `konaperm list ...` 查看该实体链上有哪些显式记录
- 若表现异常，检查是否是更上层实体或更宽泛权限键提前命中

## 相关文件

- `konabot/common/permsys/__init__.py`
- `konabot/common/permsys/entity.py`
- `konabot/common/permsys/repo.py`
- `konabot/plugins/perm_manage/__init__.py`
