自訂資料表 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。
這些層決定誰可以寫入;它們無法打開 write_policy 為 commands_only 的資料表。在這類資料表上,通過這些層的呼叫者(包括 manager)對 Command 以外的每一筆紀錄寫入,仍會以 403 governed_context_required 被拒絕。見 action policy。
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,或公司 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 前綴,而被授權的人通常不知道是哪個部門或哪個房間分享給他。現在每份清單都用 by-id 路由同一套 resolver 逐表過濾,垃圾桶中的表一律不出現:
tables.readable——GET .../department/{department_id}/readable-tables——對同公司任何使用者列出該部門中解析後can_read不是none的現行資料表。一般成員就是用它列出自己部門的表:部門的tables.list仍只開放給管理者(403 Insufficient permissions for department-level access.)。GET .../chatroom/{chatroom_id}/tables只列出你握有某種權利(讀或編輯、can_insert,或管理權)的房間資料表;GET .../chatroom/{chatroom_id}/tables/shared也會略過你完全沒有權利的分享表。
另外兩條沒有 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,那張表不會出現在任何 REST 清單上——company 清單是角色門檻,而 shared-with-me 無條件排除 company scope。那一張請用名稱解析——或者在 agent 端用 MCP tables_list 工具(scope_kind: "company")列出,它讓同公司的任何使用者進門並逐表過濾。
上表中每一種 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 的成員,或是房間門檻放行的非成員(房間所屬部門中 role >= CAN_MANAGE_DEPARTMENT 的管理者,或對其他部門房間而言 role >= CAN_MANAGE_COMPANY 的公司管理者);不必先取得 table grant |
| Tenant | 已通過該 department/company 的 tenant 身分邊界;不等於 moderator |
| Metadata read | 已通過資料表門檻,且對這張表握有某種權利——管理者或版主、can_read 或 can_edit 不是 none,或有 can_insert。同公司但沒有任何權利的呼叫者會收到 404 {"detail": "CustomTable Not found"},與 id 不存在完全相同 |
| Moderator | CustomTableModeratorRequired:明確的版主列(聊天室資料表的建立者在建表時會得到一列)、房間建立者、房間或資料表所屬部門的管理者,或公司管理者。可管理 schema、rules、triggers、設定,以及部門/用戶端/聊天室 grants。個別使用者 grant 路由用的是較窄的集合(CustomTableGrantRequired:明確版主;聊天室資料表另加房間建立者或房間管理者;部門資料表另加該部門的管理者或公司管理者;公司資料表另加公司管理者),所以在房間的資料表上通過 Moderator 的部門或公司管理者,仍無法管理它的個別使用者 grant |
這兩個角色門檻是伺服器設定。CAN_MANAGE_DEPARTMENT(預設 2)定義部門管理者:角色達到它的使用者,在自己部門的每張資料表、以及該部門各房間的資料表上都會通過 Moderator,部門資料表的 GET .../moderators 也會把他們列為預設版主。CAN_MANAGE_COMPANY(預設 3)定義公司管理者,他們在公司內每張資料表上都會通過 Moderator。被 Moderator 拒絕的呼叫者會收到 403,detail 依 scope 而不同——聊天室資料表:來自房間所屬部門以外是 Insufficient permissions. Required: table moderator, chatroom creator, or management rights.,來自該部門內是 Insufficient permissions. Required: table moderator, chatroom creator, or department management rights.;部門資料表:來自其他部門是 Insufficient permissions. Required: table moderator, department manager, or company management rights.,來自本部門是 Insufficient permissions. Required: table moderator or department management rights.;公司資料表:Insufficient permissions. Required: table moderator or company management rights.。id 不存在回 404 CustomTable Not found——在垃圾桶中的也一樣,資料表還原與 purge 路由除外——聊天室資料表的房間已刪除時則回 404 Chatroom Not found;自後端 5.21.0 起,其他公司的資料表也回同樣的 404 CustomTable Not found,不再是 403 Insufficient permissions. (Different Company)(見其他公司的 id 回應與不存在相同)。GET .../triggers 會先跑 Metadata read,所以對這張表沒有任何權利的同公司呼叫者拿到的是該閘門的 404,不會看到上述任何 403。
不同 endpoint family 的 dependency 並不完全一致。不要從某一個成功呼叫推論其他端點也可用。
踢人與離開會關上這扇門。 POST /private/chatrooms/kick/{chatroom_id} 或 POST /private/chatrooms/leave/{chatroom_id} 回 200 之後,成員關係與 collector/manager/social media manager 三種角色列都已消失,前成員的存取會翻成: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 列也一併刪除。若前成員握有 member-independent grant,或本身就過得了 CAN_MANAGE_DEPARTMENT / CAN_MANAGE_COMPANY 的角色 fallback,存取權仍然成立。
各 scope 決策表
| 操作 family | Chatroom | Department | Company |
|---|---|---|---|
| 新增欄位 | Moderator | Moderator | Moderator |
Lifecycle diagnosis 與其報告(POST .../diagnosis、GET .../migrations/{migration_id}/diagnosis-results) | Moderator | Moderator | Moderator |
By-id 資料表中繼資料(詳細資料、/columns、/permissions/me、/moderators、/rules、/staged-changes、/migration/status) | Joined,再 Metadata read(/rules 與 /staged-changes 只有 Metadata read;沒有 /moderators 路由) | Metadata read | Metadata read |
讀取觸發器設定(GET .../triggers) | Metadata read,再 Moderator | Metadata read,再 Moderator | Metadata read,再 Moderator |
| Resolve links | Joined | Tenant | Any authenticated user |
Schema 寫入與診斷報告現在在每個 scope 都是 Moderator:在 #1748 之前,部門表新增欄位與公司 scope 的診斷只需通過資料表門檻。診斷報告內含每筆不合格資料列的原始 record_data,所以讀取、資料列與分享授權都不算數。Resolve links 是這張表中唯一仍依 scope 而不同的 family(匯入的 extract/status 也不同,見範圍);端點內部仍會套用目標可見性,解析出的資料也不構成額外授權。
這些 family 的精確 endpoints 是 columns.create、lifecycle.diagnosis、lifecycle.migrationDiagnosis、tables.get 與 records.linksResolve。前端 route guard 應按「scope + endpoint」建表,不要只做一個全域 isModerator 判斷。
安全的前端策略
- 進入表格時呼叫
permissions.me,取得五欄摘要——can_read、can_insert、can_edit、is_manager與visible_columns(null= 所有欄位,[]= 沒有欄位)。在這裡收到404代表你對這張表沒有任何權利:把它藏起來。 - 依實際 endpoint privilege 顯示控制項;任何 403——以及你對某張表沒有任何權利時 by-id 中繼資料讀取回的、或大多數資料表 id 路由對其他公司的資料表回的
404 CustomTable Not found——都視為伺服器的最新決定。 - 不在 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。