Skip to Content
核心概念權限與 ACLEffective permissions

Effective permissions 與 permissions/me

Effective permission 是針對「目前 user + table」解析出的單一 server-side context。它不是把所有 grants 做 union:下面這道階梯在第一個命中的來源就停下,只有 chatroom grant 那一階會跨多列摺疊。理解解析順序,才能預測 revoke、shared chatroom 與 defaults 的效果。

User resolution 順序

  1. Table manager / moderator:full access,hidden_columns 為空。
  2. Explicit user grant。
  3. 符合 user.department_id 的 department grant。
  4. Chatroom grant。在 REST 上會合併使用者所有存活聊天室的 internal grant;在 agent session 內則改為釘選 session 所在的房間。
  5. Table default_permissions,僅限原本就在 scope 內的 user。
  6. 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 降權。filteredown 同級,平手保留 baseline。

第 4 步在 REST 上合併,在 agent session 內釘選

第 4 步過去只挑一列得勝的授權列——先比等級,再以 granted_atchatroom_id 打破平手——其餘房間的列全部丟棄。透過房間 A 拿到北區、透過房間 B 拿到南區的使用者,只會看到其中一半,而決定的依據是他無從觀察的 tie-break。自 2026-07-29 起,REST 把它們全部摺疊:

各列如何摺疊
can_read / can_edit取各列的最大等級。filteredown 在階梯上同分,摺疊時確定性地偏好 filtered——own 列會消失在更高的 filteredall 列之下,因為這套語法沒有可以拿來聯集的 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_filteredit_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,所以優先序完全一致,兩頁的敘述也必須一致。依序是:

  1. ScpDeny 載體——trigger 執行或 public callback 寫入——最優先,直接完全沒有 room 腿,即使旁邊同時傳了 acting room 也一樣。使用者撰寫的介面不論實際在哪個房間執行,都拿不到 room 腿。
  2. 其次是明確傳入的 acting room(agent toolkit 與 command executor 會傳)。
  3. 再其次是已 stash 的 ScpBinding,釘到它的房間——這正是讓「忘了傳房間的 roomed lane」不會安靜地放寬到呼叫者所屬全部房間的保險。
  4. 都沒有時該 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: noneinsert: falseedit: none
  • 他加入的 internal chatroom grant 是 read filtered(北區)、insert true、edit filtered(草稿),allowlist 包含 nameregionstatuscost
  • Table column_aclcost 設為 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_filteredit_filterhidden_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_editcan_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_permissionswidened_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 比較的結果。由於 filteredown 同級,把一個 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

前端驗證流程

  1. 載入 permissions/me,用四欄摘要調整可操作控制項。
  2. 載入 records;server 已套 read_filter 與 hidden set,不補欄位、不把缺列解讀成資料不存在於全域。
  3. Write 收到 403/404 時重新載入 permissions 與 record;grant、ownership、filter 或 SCP binding 都可能已變更。
  4. 管理員修改或 revoke grant 後,重新載入整張表。不要自行假設方向,請讀 revoke 的回應:移除一筆收窄用的 grant 會讓使用者看到更多列,而平移的情況會回報 widened_access: false,可見的列卻可能全部改變。

欄位 union 的細節見 column visibility,row policy shape 見 grants,每次請求的代換規則見 row policy tokens。若表上有 channel/invariant,effective ACL 後仍會套 SCP

Last updated on