Skip to Content

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、clientPermissions grants、defaults、column ACL
channel SCP這位呼叫者是透過哪些 room 被授權的,列是否落在他們的合併 scope?呼叫者解析出的 channel scope;department table onlychannel rule + chatroom grant scope_values
invariant SCP這個 row shape 對任何 writer 都合法嗎?Principal-blindinvariant 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 做比較。完整設定需要兩部分:

  1. 在 department table 上,以 chatroomPermissions.grant 為每個 internal room 建立 permissions 與非空 scope_values
  2. 在同一 table 的完整 rules 清單中加入 flat channel rule,先 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,它只決定讀取 lanescope_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} 的調撥單,但儲存時會被 403detail.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 一起送出,或送在 invariant rule 上,都是 400
  • overlaps 之下,讀取 lane 只產生一個 overlap 條件,不會另外加 presence 條件:∅ 不與任何集合相交,所以沒有 link 的列本來就不可見。require_present 只在寫入 floor 上發揮作用,用來堵住 subset 的 ∅ ⊆ scope 恆真漏洞。

透過 overlaps 變成可見的列會暴露整列內容——link cell、link 展開,以及來源是 scope 外 target 的 lookup / rollup 欄位。可見列上的 scope 外 target 沒有逐欄遮蔽機制。要遮蔽敏感資料,請改為治理目標表本身,或使用 column ACL

policy tree 內,link_membershiplink_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_idrule_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 變成可讀。

設定前先讀 channelinvariant,再以 rules.preview 觀察既有資料。互動式設定可使用流程精靈

Last updated on