自訂資料表 ACL
ACL 決定 principal 能否存取一張表,以及可讀、可新增、可編輯哪些 rows 和 columns。前端可以用權限摘要調整控制項,但真正的授權永遠在伺服器端:隱藏按鈕不等於阻止 API 呼叫。
四層判斷
端點所需 privilege
→ effective grant(manager / user / department / chatroom / default)
→ row access(none / own / filtered / all)
→ column visibility(column_acl + visible_columns)
→ 若有 SCP,再套 acting-room / invariant floorGrant 的解析不是把所有來源任意聯集。一般情況先命中的 grant 勝出;chatroom share 對原本就在 table scope 內的使用者有特別的「只能放寬 baseline」規則。完整步驟見 effective permissions。
Grant matrix
| 來源 | Chatroom table | Department table | Company table | 管理端點與用途 |
|---|---|---|---|---|
| User grant | ✓ | ✓ | ✓ | permissions.grant,指定 user |
| Department grant | ✓ | ✓ | ✓ | departmentPermissions 只適用 chatroom table;另一組 departmentTableGrants 可把 department 或 company table 分享給一個 department,受贈方路徑段名為 {target_department_id} |
| Chatroom grant | — | ✓ | — | Department table 分享給 room;每個 internal / external audience 各自一列,可帶 SCP scope_values |
| Client / CRM grant | ✓ | ✓ | ✓ | clientPermissions 在三種 table scope 都能指定 social-media client 的 external access |
| Client permission request | ✓ | ✓ | — | Client 可申請存取——包含分享進其聊天室的部門表;由審核者(明確 moderator、部門或公司 manager)approve / reject,approve 會產生 explicit client grant |
| Passphrase access | ✓ | ✓ | ✓ | clientAccess 在三種 table scope 都可設定共用 passphrase 與套用的 client permissions |
| Explicit table moderator | scope manager 已隱含 | ✓ | ✓ | Department / company table 可增刪 moderator;manager 一律 full implicit access。指派錨定在該表的 effective company,因此 moderator 不必屬於擁有它的部門 |
| Table defaults | ✓ | ✓ | ✓ | 只對 in-scope internal users做 fallback——唯一例外是帶著 default_permissions.audience: "company" 的部門表,它讓同公司的每個人都算 in-scope 並為他們開門 |
完整端點清單見 grant 類型。Table-level column hide map 另由 columnAcl.update 管理。
在 department-scoped table 上,grant 只回答了一半:by-id 路由前面那道 membership door 有自己的一份鑰匙清單,而它以前比 resolver 的短。見部門表的門由誰打開。
找到別人分享給你的表
每一條 table LIST 路由都帶 scope 前綴,而被授權的人通常不知道是哪個部門或哪個房間分享給他。兩條沒有 scope 的路由補上這個缺口,而且刻意互補:
tables.sharedWithMe列出從你的 scope 之外送到你手上的東西——per-user grant、你所屬部門的 grant、透過你所屬活房間的 internal chatroom grant,或分享給全部部門的部門表——並附上放行的sources與你解析後的權限。你自己部門的表、company scope 的表、你所屬房間的表都被刻意排除。tables.resolveByName把精確表名轉成 id,範圍涵蓋 company 與 department scope,逐表由 resolver 把關,而不是用角色把關。
兩者都不是你 scope 清單的超集,而且有一個缺口兩者都補不上:角色低於 CAN_MANAGE_COMPANY 的呼叫者若在 company scope 表上握有 per-user grant,那張表不會出現在任何清單上——company 清單是角色門檻,而 discovery 無條件排除 company scope。那一張只能用名稱解析。
上表中每一種 row policy 介面都用同一棵布林樹——and / or / not 包住 predicate 與 link_target leaf——並接受每次請求解析的 tokens——$me、$me.department、$today、$today±Nd、$now——以原字串儲存,並在伺服器解析當下 principal 時代換。在 principal 欄位上,兩個身分 token 解析成 tagged user: / smc: / room: cell 的 SET,而不是單一 scalar id;在 agent session 裡,acting room 縮小該 SET 的 room 腿的方式,與它縮小 chatroom grant 那一步完全相同——見 effective permissions。這些介面都不在任何 feature flag 之後:row policies、column ACL、visible_columns、chatroom share grants 與 SCP floors 一律啟用,這裡的端點也不會再回「requires CUSTOM_TABLE_…」之類的錯誤。
權限等級詞彙
| 名稱 | 意義 |
|---|---|
| Joined | 已加入該 chatroom;不必先取得 table grant |
| Tenant | 已通過該 department/company 的 tenant 身分邊界;不等於 moderator |
| Access | 已通過這張表的 effective access dependency |
| Moderator | Table creator、適用的 scope manager,或 explicit table moderator;可管理表設定與 grants |
不同 endpoint family 的 dependency 並不完全一致。不要從某一個成功呼叫推論其他端點也可用。
踢人與離開現在真的關得上門。 POST /private/chatrooms/kick/{chatroom_id} 與 POST /private/chatrooms/leave/{chatroom_id} 過去回 200,卻沒有把成員關係本身寫進去:暫存的移除在同一個請求裡被後續的鎖定重讀覆蓋掉,user_chatroom_association 那一列因此存活,被踢的人在該房間的自訂資料表路由上照樣拿 200。正式環境曾經連踢四次,成員還是列在那裡。現在這些暫存的移除會在那些重讀之前先 flush,所以 200 之後,成員關係與 collector/manager/social media manager 三種角色列在新的 session 上也確實消失,前成員的存取會翻成:table-id 資料列路由回 403 {"detail":"Not a member of this chatroom."},GET .../chatroom/{chatroom_id}/tables 回 403 {"detail":"Insufficient permissions, not a member (Required department manager)."}(或公司管理者版本的字串)。他的 explicit grant 列與 moderator 列一直都刪得掉——那些是 raw delete——所以這個修正針對的正是成員關係本身。若前成員握有 member-independent grant,或本身就過得了 CAN_MANAGE_DEPARTMENT / CAN_MANAGE_COMPANY 的角色 fallback,存取權仍然成立。
Chatroom scope 決策表
| 操作 family | 最低 privilege | 為什麼 |
|---|---|---|
| 新增欄位 | Moderator | Schema mutation 只交給 table manager |
| Lifecycle diagnosis | Moderator | Chatroom 診斷可能揭露修復資訊 |
| Resolve links | Joined | 只需 room membership;目標資料仍由 endpoint 內部可見性限制 |
Department scope 決策表
| 操作 family | 最低 privilege | 與其他 scope 的差異 |
|---|---|---|
| 新增欄位 | Access | 這裡不是 Moderator;已取得 table access 即可 |
| Lifecycle diagnosis | Moderator | 這裡比 company 嚴格 |
| Resolve links | Tenant | 不要求先有這張表的 Access dependency |
Company scope 決策表
| 操作 family | 最低 privilege | 與其他 scope 的差異 |
|---|---|---|
| 新增欄位 | Moderator | 與 chatroom 相同,與 department 不同 |
| Lifecycle diagnosis | Access | 與 chatroom / department 的 Moderator 不同 |
| Resolve links | Any authenticated user | 只驗證登入;資料解析仍不應被當成額外授權 |
這三個 family 的精確 endpoints 是 columns.create、lifecycle.diagnosis、records.linksResolve。前端 route guard 應按「scope + endpoint」建表,不要只做一個全域 isModerator 判斷。
安全的前端策略
- 進入表格時呼叫
permissions.me,取得公開的四欄摘要。 - 依實際 endpoint privilege 顯示控制項;任何 403 都視為伺服器的最新決定。
- 不在 client 重算 row filter 或 hidden columns;只呈現伺服器回傳的 rows / fields。
- Grant 變更或 revoke 後重新抓取摘要與資料。Revoke 只是移除一個來源、讓使用者落到下一層;revoke 的回應會回報結果權限,並在這次移除放寬了使用者的存取範圍時標示出來。
Row filters 見 grants,每次請求的 $me / $today 詞彙與空值 cell 規則見 row policy tokens,兩種欄位機制見 column visibility,SCP floor 見 SCP 與 ACL。