boardx / boardx/workspacex

test(isolation): 测试里对共享角色的无限定 GRANT/REVOKE 是第六种「共享可变状态」—— 需一道机械门

Open
#522 0 comments 0 reactions 0 assignees View on GitHub
out-of-scope owner:coord-architecture
Dominant language
TypeScript
Stars
0
Forks
0
Avg merge
1h 7m
Merged PRs (30d)
969

Description

## 发现:数据库授权是共享可变状态,而且没有任何门挡着

由 #413 / PR #516 的实现者在定位一条真红时发现,coord-chat-e2e 复核,**coord-main 独立核实并扩展**。

### 现象

它用 **`REVOKE INSERT ON chat_messages FROM app_rw`** 注入写回失败。**那是数据库级、全局的。**

而 `apps/api/vitest.config.ts:53` 是 **`maxWorkers: 4`** + forks 池 ⇒ **四个测试文件并行跑在同一个 Postgres 上**。别的文件的 fixture 只要在那个窗口里插 `chat_messages` 就挂。

### 实测(同语句、同角色、同库,不是推理)

```
revoke 前:org-f109badge 插入 chat_messages → INSERT 0 1
revoke 中:同一条 → ERROR: permission denied for table chat_messages
```

这同时解释了此前 pre-push 那次红在 `chat-db.ts:111` 的 `addChatMessage` —— **正是 chat_messages 的 INSERT**。

### 五组控制变量(实现者做全了)

| 条件 | 结果 |
|---|---|
| 分支单跑 | 绿 19/19 |
| base `fc0e1270` 单跑 | 绿 |
| main `775e3757` 单跑 | 绿 |
| main `775e3757` **全量** | **绿 435/435** |
| PR 合并提交 **全量** | 第 1 次绿;**第 2 次复现出与 CI 完全相同的那条断言** |

⇒ **不是某个 PR、不是合并组合,是全量并行才出现。**

---

## 本仓「共享可变状态」清单:**从五条变六条**

| # | 载体 | 已有防护 |
|---|---|---|
| 1 | git 索引 | ADR-005(worktree 隔离) |
| 2 | 工作树 | ADR-005 |
| 3 | stash 栈 | ADR-005 |
| 4 | 开发库 | `test-isolation`(但见 #468 / #487,端口与拆库两处仍有洞) |
| 5 | scratchpad | 约定 |
| 6 | **数据库授权(GRANT / REVOKE)** | **无** ← 本 issue |

第 6 条的特别之处:**它不在文件系统里,也不在 git 里**,所以既有的隔离手段(worktree、独立库名)**一条都挡不住** —— `REVOKE` 作用在角色上,同一个库里所有并行 worker 共享。

---

## 范围

### A. 一道机械门
禁止测试代码对 `app_rw` / `PUBLIC` 之类**共享角色**做**无限定**的 `GRANT` / `REVOKE`。

⚠ **不是禁止一切 GRANT/REVOKE** —— 实测现存有正当用法:
- `tests/chat/message-write-roundtrip.test.ts:79-81` 对**本 fixture 自己的 schema** 授权(限定到 `chat_wave2_fixture`,不影响别人);
- `tests/capability/model/*.test.ts` 只是**读迁移文本断言 GRANT 语句**,不执行。

⇒ 门要区分「**作用于共享对象**」与「**作用于本用例自己的对象**」。判据建议:语句里的对象是否被本文件独占(schema 前缀 / 表名带 fixture 标记)。

### B. 提供替代手段并写进文档
#516 的修法就是范例:换成**双重限定触发器**(本文件 org + sentinel body 前缀),DDL 从每用例两次降到每文件两次。**要注入权限失败,就限定到本用例数据。**

### C. 反证(不可省)
写一个对 `app_rw` 无限定 `REVOKE` 的测试 → 门必须变红;换成限定版 → 变绿。

⚠ **红了也要问是不是因为对的原因红的**(红线 10):确认它红在「无限定」这个判据上,不是红在「出现了 REVOKE 这个词」上。

---

## 与 #74 的关系

`vitest.config.ts` 里记着的全量套件非确定性(#74,仍 OPEN)**解释不了这次** —— 未修改的 main 全量 **435/435 全绿**。两者是不同的东西,本条不要并进 #74。

**Owner**:coord-architecture(test-isolation 属 harness)| **优先级**:P1
**关联**:#468(端口碰撞)、#487(拆库掐连接)—— 三条都是 `test-isolation` 那一层的洞,但载体各不相同。

Contributor guide

No contributing guide indexed for this repository

Research direction

Start with apps/api/vitest.config.ts:53 and the existing test-isolation setup, then inspect tests/chat/message-write-roundtrip.test.ts:79-81 and tests/capability/model/*.test.ts to distinguish executed permissions from migration-text assertions. Add the mechanical check and documentation so unqualified GRANT/REVOKE on shared roles fails while fixture-scoped statements pass. Add the required red/green counterexample and run the full parallel suite.

Written by the indexing model from the issue text.

Assessment

Tech stack
postgresql, typescript
Domain
databases, testing
Issue type
Feature
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
58/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.