Effective permissions 與 permissions/me
Effective permission 是針對「目前 user + table」解析出的單一 server-side context。它不是把所有 grants 做 union:下面這道階梯在第一個命中的來源就停下,只有 chatroom grant 那一階會跨多列摺疊。理解解析順序,才能預測 revoke、shared chatroom 與 defaults 的效果。
User resolution 順序
- Table manager / moderator:full access,
hidden_columns為空。 - Explicit user grant。
- 符合
user.department_id的 department grant。 - Chatroom grant。在 REST 上會合併使用者所有存活聊天室的
internalgrant;在 agent session 內則改為釘選 session 所在的房間。 - Table
default_permissions,僅限原本就在 scope 內的 user。 - System fallback;out-of-scope caller 沒有 explicit share 時 fail closed。
前一個 direct grant 命中後,後面不再一般性合併。唯一特別規則是 chatroom share:如果 user 本來就在 table scope 內,server 會把 room grant 與 defaults/system baseline 按 read、insert、edit 三個 axis 比較,room membership 只能放寬、不能讓 insider 被較窄 grant 降權。filtered 與 own 同級,平手保留 baseline。
第 4 步在 REST 上合併,在 agent session 內釘選
第 4 步過去只挑一列得勝的授權列——先比等級,再以 granted_at、chatroom_id 打破平手——其餘房間的列全部丟棄。透過房間 A 拿到北區、透過房間 B 拿到南區的使用者,只會看到其中一半,而決定的依據是他無從觀察的 tie-break。自 2026-07-29 起,REST 把它們全部摺疊:
| 軸 | 各列如何摺疊 |
|---|---|
can_read / can_edit | 取各列的最大等級。filtered 與 own 在階梯上同分,摺疊時確定性地偏好 filtered——own 列會消失在更高的 filtered 或 all 列之下,因為這套語法沒有可以拿來聯集的 created_by 述詞 |
can_insert | 各列取 OR |
read_filter / edit_filter | 有貢獻的 filtered 分支。只有一個相異 filter 時逐字沿用;多個時成為 {"or": [arms]}——如今就是一般的 row-policy 節點(見 grants),完全相同的 filter 會去重回單一分支 |
visible_columns | 分兩種情況——見下方 |
{"or": [...]} 曾經只由這次摺疊產生。PR #1127 之後它就是一般的 row-policy 文法:grant 寫入 payload 上的 read_filter 與 edit_filter 接受任意 and / or / not 樹(含 link_target leaf),摺疊出的聯集也只是其中一棵樹。permissions/me 不回傳 filter,所以這個聯集在 wire 上看不見——你只能從回來的列看出它。執行時是帶極性的 fail-closed:空的分支、帶有未解析 $me/$today token 的分支、或找不到 link 的 link_target,在 or 底下會被丟掉而其餘分支照常生效,在 and / not 底下則讓該組永不命中,因此某個房間壞掉的 filter 不會把另一個房間合法授權的列一起遮掉,也永遠不會反轉成「全部列」。只有存活的房間有貢獻:軟刪除一個聊天室後,下一次請求起,它的 grant 與 channel scope_values 都不再進入任何成員的解析。
visible_columns 可能收窄到比任一房間所授權的還少。摺疊會為每一列有貢獻的授權列算出一個涵蓋鍵——(can_read, read_filter, can_edit, edit_filter, can_insert)。當所有有貢獻的列涵蓋完全相同時,白名單取聯集,而其中只要有一列沒有白名單,就等於整個解除欄位上限。當涵蓋不同時——相異的 filter 分支,或等級混合——白名單改為取交集,沒有宣告白名單的列視為全集而退出。理由是列與欄的相關性:房間 B 的欄位不能出現在只有房間 A 授權的列上。而且同一份白名單也管寫入,因此在這裡授寬既是讀取洩漏、也是寫入擴權,所以授窄是刻意選擇的安全近似。結果是:同時在兩個房間、但 row filter 不同的使用者,看到的欄位可能比任一房間單獨宣告的還少。
Agent session 改為釘選單一房間。在 agent session 的 toolkit 裡,部門層級的表不做任何摺疊——session 所在聊天室的授權列既是釘選也是天花板。第 4 步只看那個房間的授權列;該房沒有授權列就是無權存取,不管使用者是誰、表管理者也一樣;房間列再與 user-centric 結果逐軸取交集(等級取低、can_insert 取 AND、row filter 合取、欄位白名單取交集),所以你自己較窄的限制仍然生效。兩個組合會 fail-closed 成 none 而不是悄悄丟掉 filter:own × filtered,以及非正規的 stored filter 形狀。理由在於渲染目標:agent 的回答落在共享聊天室裡,因此永遠不能夾帶這個房間沒被授權的資料——即使發問的人自己在別處拿得更多。chatroom 與 company 層級的表沒有授權列、不受此限;external client 本來就是單房;commands toolkit 維持 user-centric。
兩條規則合起來的後果是:同一個身分在兩個介面上合法地得到不同答案,而且 REST 呼叫者已經無法重現 agent 的切片——沒有任何房間參數可以拿來釘選。同時在 A、B 兩房的成員,走 REST 列出 A ∪ B,在房間 A 的 session 內只列出 A。「API 給我 4 列,為什麼機器人只說 2 列?」是預期行為,不是 bug。
同一個釘選也會縮小 principal SET
acting room 不只挑出授權列,也會縮小 principal 欄位 row policy 解析出的 $me 與 $me.department SET 的 room 腿。兩個 token 共用同一個釘住 helper,所以優先序完全一致,兩頁的敘述也必須一致。依序是:
ScpDeny載體——trigger 執行或 public callback 寫入——最優先,直接完全沒有 room 腿,即使旁邊同時傳了 acting room 也一樣。使用者撰寫的介面不論實際在哪個房間執行,都拿不到 room 腿。- 其次是明確傳入的 acting room(agent toolkit 與 command executor 會傳)。
- 再其次是已 stash 的
ScpBinding,釘到它的房間——這正是讓「忘了傳房間的 roomed lane」不會安靜地放寬到呼叫者所屬全部房間的保險。 - 都沒有時該 lane 不釘住,套用 REST 語意:呼叫者所屬(
$me)或屬於其部門($me.department)的所有存活、同公司房間。
明確的 acting room 與已 stash 的 binding 指向不同房間屬於程式錯誤,伺服器的回答是沒有 room 腿加上一則 error log,而不是挑一個。被釘住的房間仍必須通過未釘住查詢的每一項檢查——存活、公司錨點,再加上 $me 的成員身分或 $me.department 的部門檢查——所以釘住永遠只會減少。$me.department 的 users 腿則完全不受釘選影響:它就是該部門所有存活、同公司的 user,成員身分對它根本不是判斷條件。
Combined-grant walkthrough
假設一位非 manager:
- 沒有 user 或 department grant。
- Table defaults 是
read: none、insert: false、edit: none。 - 他加入的 internal chatroom grant 是 read
filtered(北區)、inserttrue、editfiltered(草稿),allowlist 包含name、region、status、cost。 - Table
column_acl把cost設為 managers-only;live schema 還有internal_note。
Server 選到 chatroom grant,並與 in-scope baseline 合併:
{
"can_read": "filtered",
"read_filter": {
"and": [
{ "column": "col_aaaaaaaa_aaaa_4aaa_8aaa_aaaaaaaaaaaa", "op": "eq", "value": "北區" }
]
},
"can_insert": true,
"can_edit": "filtered",
"edit_filter": {
"and": [
{ "column": "col_bbbbbbbb_bbbb_4bbb_8bbb_bbbbbbbbbbbb", "op": "eq", "value": "草稿" }
]
},
"hidden_columns": [
"col_dddddddd_dddd_4ddd_8ddd_dddddddddddd",
"col_eeeeeeee_eeee_4eee_8eee_eeeeeeeeeeee"
],
"is_manager": false
}這是 CRUD 內部使用的 effective context:read query 只回北區 rows,edit 同時要求 pre/post-image 都是草稿;cost 來自 table hide-map,internal_note 來自 allowlist complement,兩者取 union。
permissions/me 的實際 wire response
呼叫:
curl \
"$BASE_URL/private/module/custom_tables/department/11111111-1111-4111-8111-111111111111/tables/22222222-2222-4222-8222-222222222222/permissions/me" \
-H "Authorization: Bearer $TOKEN"目前公開 response model 只回四個欄位:
{
"can_read": "filtered",
"can_insert": true,
"can_edit": "filtered",
"is_manager": false
}Important
read_filter、edit_filter、hidden_columns是 server-side effective context,不是目前permissions/me的 response fields。前端不得假設能從此端點取得 policy 內容,也不要自行重算授權。以實際 list/get response 中可見的 rows 與 fields 為準。
這個區分很重要:permissions/me 適合決定「顯示新增按鈕嗎」或「標示 read level」,不是把 filter DSL 下放給 client。精確 endpoint model 見 permissions.me。
當儲存的 policy 帶有 token 時,解析出來的 read_filter 還會綁定當次檢視者的身分。$me、$me.department、$today、$now 就是在這個 resolver 的出口被代換——在 can_edit → can_read 拉齊之後,而且永遠不會寫回 grant——因此同一份設定會讓兩位使用者合法地看到不同的列與不同的總數。在 principal 欄位上,兩個身分 token 各自變成一個 tagged cell 的 SET 而不是單一 scalar id,這正是一筆 grant 能同時涵蓋業務的 TS 帳號、LINE 身分與所屬房間的原因。見 row policy tokens。
Revoke 會回報使用者落到哪一層
移除 per-user grant 不等於「回落到 default」。Explicit grant 會覆蓋該使用者原本會解析到的結果,不論 level 高低,因此移除它可能放寬、收窄或不變;落點是上面那份順序中的下一個命中者:department grant → 合併後的 internal chatroom grants → table default_permissions → system fallback。
因此三種 scope 的 revoke 都回傳 RevokePermissionResponse——刪除後才解析出的 effective_permissions、widened_access 布林值,以及只在權限被放寬時才出現的 warning:
{
"message": "Permissions revoked successfully",
"effective_permissions": {
"can_read": "all",
"can_insert": false,
"can_edit": "none",
"is_manager": false
},
"widened_access": true,
"warning": "Revoking this grant WIDENED the user's access on can_read: the access they fall back to is broader than the grant that was removed."
}widened_access 是在 ordinal ladder none = 0 < filtered = 1 == own = 1 < all = 2 上逐 axis 比較的結果。由於 filtered 與 own 同級,把一個 filtered 範圍換成另一個會回報 false,但可見的列可能完全不同——不論旗標為何,revoke 後都要重新載入資料。Warning 只描述效果、不指明原因:resolver 不回傳 provenance,較寬的 level 可能來自 department grant、chatroom grant 或 table default,UI 不應宣稱是哪一個。
只有單一使用者的 revoke 帶這份回報。Bulk permissions lane 只回計數,department、client、chatroom grant 的 revoke 也只回一般成功訊息,提供這些操作的 UI 必須自行重新呼叫 permissions/me。
完整 request 形狀與周邊 grant 模型見 Grant 類型與 row policy。
前端驗證流程
- 載入
permissions/me,用四欄摘要調整可操作控制項。 - 載入 records;server 已套
read_filter與 hidden set,不補欄位、不把缺列解讀成資料不存在於全域。 - Write 收到 403/404 時重新載入 permissions 與 record;grant、ownership、filter 或 SCP binding 都可能已變更。
- 管理員修改或 revoke grant 後,重新載入整張表。不要自行假設方向,請讀 revoke 的回應:移除一筆收窄用的 grant 會讓使用者看到更多列,而平移的情況會回報
widened_access: false,可見的列卻可能全部改變。
欄位 union 的細節見 column visibility,row policy shape 見 grants,每次請求的代換規則見 row policy tokens。若表上有 channel/invariant,effective ACL 後仍會套 SCP。