SCP 與 ACL 如何組合
Scoped Constraint Policy(SCP)不是另一種 grant。ACL 先解析 principal 對 table、row、column 的權限;SCP 再加上一個不可被較寬 grant 移除的資料安全 floor。
三個互補問題
| Layer | 問題 | Principal / context | 設定位置 |
|---|---|---|---|
| ACL | 這個 user/client 能讀寫哪些 rows 與 columns? | User、department、chatroom audience、client | Permissions grants、defaults、column ACL |
channel SCP | 這位呼叫者是透過哪些 room 被授權的,列是否落在他們的合併 scope? | 呼叫者解析出的 channel scope;department table only | channel rule + chatroom grant scope_values |
invariant SCP | 這個 row shape 對任何 writer 都合法嗎? | Principal-blind | invariant rule |
較寬的 can_read: all 不會越過 enforcing SCP:floor 是在 ACL ladder 之後 AND 上去的,所以 can_read: all 的非 manager 一樣被 floor 限制。該張表的 manager 則豁免於 floor,而 manager 身分永遠是逐表解析、不是整個 session 一次決定,因此不是全域 bypass。
Channel:呼叫者的 scope 如何解析
Channel flat form 把某個 link 欄位的 target IDs 與呼叫者解析出的 channel scope 做比較。完整設定需要兩部分:
- 在 department table 上,以
chatroomPermissions.grant為每個 internal room 建立 permissions 與非空scope_values。 - 在同一 table 的完整 rules 清單中加入 flat
channelrule,先observe,確認後再enforce。
scope = EXEMPT 呼叫者是這張表的 manager
= ⋃ 呼叫者所屬、且對這張表持有 internal grant 的
每個「存活」房間的 scope_values (REST)
= session 所在房間的 scope_values (agent session)
= DENY 上述集合為空
READ effective ACL 允許 row
AND scope ≠ DENY
AND ( read_op "subset" : targets ⊆ scope
AND(require_present ⇒ targets ≠ ∅)
| read_op "overlaps" : targets ∩ scope ≠ ∅ )
WRITE effective ACL 允許這次寫入
AND scope ≠ DENY
AND pre-image targets ⊆ scope AND post-image targets ⊆ scope
AND(require_present ⇒ targets ≠ ∅)Policy-form channel 使用規則內的固定值與 tree,是 table-wide 條件;room scope_values 不會自動注入 tree。若兩者都需要,使用兩條 channel rules,全部 AND-composed。
沒有任何 REST 路由接受房間。?acting_chatroom_id 已於 2026-07-29 從全部 57 個 custom-table 操作移除,現在是被靜默忽略而非被拒絕,400 acting_room_required / 403 acting_room_unsatisfiable 這條階梯也一併消失。聯集為空的呼叫者會拿到 200 零列、單列讀取拿到 uniform 404,只有寫入才會拿到 403 scp_scope_undeclared。完整的 union 契約、精確的拒絕 body,以及 REST 與 agent 的不對稱,由 channel 頁面 定義。
read_op:放寬讀取,但不放寬寫入
Flat channel rule 可以帶 read_op,它只決定讀取 lane 與 scope_values 的比較方式:
read_op | 何時可見 | 儲存形式 |
|---|---|---|
subset(預設) | 該列所有 link target 都在呼叫者解析出的 scope 內 | 正規化為不存在的 key,rules 清單的 GET 永遠不會回 read_op: "subset" |
overlaps | 任一 link target 在呼叫者解析出的 scope 內 | 原樣儲存並回傳 |
這就是調撥單的形狀:同時連到倉庫 A 與 B 的單據,對只涵蓋其中一邊的房間也變成可讀。
在 overlaps 之下,「看得到」與「改得動」會分岔。 不論 read_op 為何,寫入 floor 永遠要求完整子集合。只持有倉庫 A 的房間可以列出連到 {A, B} 的調撥單,但儲存時會被 403、detail.error = "scp_out_of_scope" 拒絕。前端若假設「可見即可編輯」,就會做出一顆永遠失敗的編輯按鈕;請把這個 403 當成政策判定,不要重試。
還有三項限制:
- 沒有
require_present: true就不允許overlaps(否則400)。若require_present為 false,沒有 link 的列可以通過寫入 floor(∅ ⊆ scope 恆真)卻無法 overlap 任何房間,等於建立出一列「寫得進去但所有房間都看不到」的資料,連建立者自己也找不到,只有 table manager 找得到。與其產生這種孤兒列,不如在 authoring 時直接拒絕這個組合。 read_op只屬於 flat form。與policy一起送出,或送在invariantrule 上,都是400。- 在
overlaps之下,讀取 lane 只產生一個 overlap 條件,不會另外加 presence 條件:∅ 不與任何集合相交,所以沒有 link 的列本來就不可見。require_present只在寫入 floor 上發揮作用,用來堵住 subset 的 ∅ ⊆ scope 恆真漏洞。
透過 overlaps 變成可見的列會暴露整列內容——link cell、link 展開,以及來源是 scope 外 target 的 lookup / rollup 欄位。可見列上的 scope 外 target 沒有逐欄遮蔽機制。要遮蔽敏感資料,請改為治理目標表本身,或使用 column ACL。
Link leaf 逐字帶過
在 policy tree 內,link_membership 與 link_target leaf 會被 IaC walker 逐字複製:不翻譯,也不往下走訪。這對 link_target 特別重要——它的 target 子樹指的是被連結表的欄位,單表 walker 絕不能拿它去解析本表。因此含這種 leaf 的規則現在能 export → plan → apply 完全一致地往返;leaf 自己的 link 仍會在 apply 時對本表真實的 link 欄位驗證,逐字帶過不會存下無法解析的內容。
在 policy 之外,link 就是一個普通 key。單純夾帶這個 key 的管理行——api_call 的 body、invoke_command 的 input、json 欄位的值,或任何剛好叫做「link」、當成 view filters map 鍵的欄位——不再被誤判成 SCP leaf。
Observe 這個 kill switch
CUSTOM_TABLE_SCP_V13_OBSERVE 是營運端的開關,不是租戶設定。開啟時,所有受治理的表都會被強制進入 observe 模式,讓營運人員在事故中不必走 rules PUT(那需要 table mutex 與逐表 moderator 權限)就能先解除影響。它是這一類 SCP 開關中唯一還存在的一個;沒有 per-tenant 版本,表自己的 enforcement 值仍然是正常的控制方式。
若某張表的 channel rules 全都在 observe 模式,其讀取行為與未受治理的表完全相同:讀取不會被 floor 限制,缺少 binding 也不會拋錯,只會留下 log。
Invariant:所有 carriers 的 post-image
Invariant 沒有 room 或 grant scope。它檢查 create、update、restore、revert 的 post-image,包含 user API、callback、trigger action 與 approval replay。這正是它可以與 triggers 共存的原因;相反地,channel 需要解析出房間 scope,而 trigger 沒有可供解析的 principal,因此伺服器在 authoring 時就雙向拒絕這個組合。
Policy tree 的 and / or / not / if、scalar、link membership 與 link target grammar,請直接依 invariant 規則頁面;不要在 ACL client 另做一份 evaluator。
Read 與 write 結果
observe 會評估與產生 preview/diagnostic evidence,但不拒絕或隱藏。enforce 時:
- List/query 只看得到同時通過 ACL row policy 與 channel read floor 的 rows。
- Single-record read 對不可見 row 採 uniform not-found posture。
- Write 必須同時通過 ACL edit/column checks、channel pre/post constraints,以及 invariant post-image。
- Approval 只延後落地,不凍結舊 policy;核准套用時仍以當時有效 floor 判斷。
拒絕一律是 403,body 可被程式判讀。請以 detail.error 分支,不要用 HTTP status:
{
"detail": {
"error": "scp_out_of_scope",
"message": "this write targets records outside your channel's scope",
"table_id": "22222222-2222-4222-8222-222222222222",
"rule_id": "55555555-5555-4555-8555-555555555555"
}
}寫入 floor 與 invariant 的拒絕會附上 table_id 與 rule_id,UI 因此能指名是哪一條規則擋下的。每個代碼有各自的修復方式,把所有 403 對應成同一句話就會失去它:
detail.error | 意義 | 修復方式 |
|---|---|---|
scp_scope_undeclared | 呼叫者的聯集為空:他所屬的存活房間,沒有一個在這張表上宣告了 scope_values。REST 上訊息是 "no chatroom of yours declares a scope on this table";在釘選房間的 agent 或 trusted lane 上則是 "chatroom {id} has no declared scope on this table" | 由 moderator 為呼叫者的房間授予非空 scope_values。前端沒有任何可重試的動作 |
scp_out_of_scope | 聯集非空,但沒有涵蓋目標 | 放寬 scope_values,或改寫 scope 內的列。不要重試同一個請求 |
scp_invariant_violation | 是列的形狀問題,不是房間問題 | 修正該列 |
scp_link_required | 寫入者漏了受治理的 link | 補上 |
scp_binding_missing | 完全沒有 carrier——這是接線 bug,不是呼叫者的錯。REST 永遠帶著 bare principal,因此從 REST 不可能觸發;它只為「派送時沒帶 channel context 的背景工作」保留 | 回報問題;呼叫者改不了任何東西 |
請注意這份清單裡沒有的東西:沒有任何一句叫呼叫者指定房間,因為沒有任何 REST 路由接受房間。
寫入時若 link target 被目標表的 channel 治理隱藏,現在會被歸因,而不是被吞掉:回 403 {"error": "scp_out_of_scope", "message": "the link target exists but is outside your channel scope"};目標表的 floor 解析為 deny 時則回 403 {"error": "scp_scope_undeclared", "message": "no channel scope is declared for you on the link target's table (grant your room scope_values, or act through a room that has them)"}。真的不存在的 target,以及被 row ACL 隱藏的 target,仍然回 uniform 404——no-oracle 規則沒有改變。兩則訊息現在都與 lane 無關,不再提「acting room」。
SCP policy leaf 是全表條件,只接受literal值:$me、$today 這類 row policy token 在那裡會被 400 拒絕。Floor 的設計本來就與檢視者無關,token 在該 lane 沒有定義好的 principal。見 row policy tokens。
ACL row policy 的 link_target leaf 是另一回事,只是形狀剛好一樣,兩者有兩點差異要分清楚。**存活判斷:**row policy 的 leaf 只算未刪除的被連結列(目標進垃圾桶,授權即刻消失),而 SCP channel rule 的 target 比對刻意不做存活 join,避免軟刪除變成洗掉治理範圍的手段。**IaC ref:**SCP 的 link_membership / link_target leaf 由 IaC walker 逐字帶過,row policy 的 link_target ref 則會雙向翻譯(見 IaC grants)。被連結那張表的 SCP channel floor 會以 on_missing_binding="deny" 一併 AND 進 row policy 的 leaf,因此受治理的目標永遠不會「透過」某人的 grant 變成可讀。
設定前先讀 channel 與 invariant,再以 rules.preview 觀察既有資料。互動式設定可使用流程精靈。