IaC 權限授予
grant 把表格權限當成 IaC resource 管理。它用 principal 的穩定種類與 ID 表達「誰」,以 spec 表達「能做什麼」,並以 table/column refs 表達「在哪張表、哪些欄位與資料列」。撤銷同樣是可審查的 state: "absent",不是把整行偷偷移除。
Grant Line 心智模型
{"kind":"grant","table":"orders","principal":{"type":"department","id":"support"},"spec":{"can_read":"filtered","can_insert":false,"can_edit":"none","visible_columns":["order_no","status"],"read_filter":{"and":[{"column":"status","op":"eq","value":"open"}]}}}table 是本文件中的 table ref。Grant 不另有作者指定的 ref;principal identity 會形成穩定 grant identity。state 預設為 present,spec 在 Pydantic model 中可選,absent 不得帶 spec。新 grant 若省略 spec,寫入預設是 can_read: "none"、can_insert: false、can_edit: "none";既有 grant 的未宣告欄位保留 live 值。實務上應明寫安全意圖,避免讀者猜測預設。
Principal 種類
principal.type | principal.id | audience 規則 |
|---|---|---|
user | 同公司 user 的穩定 ID | 禁止提供 |
department | department ID | 禁止提供 |
client | 由同公司鏈結可解析的 client ID | 禁止提供 |
chatroom | chatroom ID;table 必須具備可用的 department scope | 必填,只能是 internal 或 external |
只有 chatroom principal 可以帶 audience;「audience iff chatroom」是結構契約,不是稍後的授權建議。
principal.id:原始 ID 或可攜 token
principal.id 現在是 1 到 160 個字元,且符合:
^([a-z0-9][a-z0-9_.-]{0,35}|\$(user|dept|smc|room):[^\r\n]{1,150})$這是兩個並列選項,不是把單一 pattern 放寬:
- 原始 ID 仍遵守 token 出現前的契約——最多 36 個字元、不含冒號與空白,這樣衍生的
{table}.grant:{type}:{id}ref 才會合法。原始 ID 綁定環境。 - 可攜身分 token 以公司內唯一的自然鍵指名 principal。它的主體是 1 到 150 個字元的自由文字,因此含空白與冒號的部門、聊天室名稱都寫得出來。
前綴必須與 type 相符:user → $user:、department → $dept:、client → $smc:、chatroom → $room:。前綴交錯是 per-line 錯誤,而不是去解析成另一種 principal。
{"kind":"grant","table":"orders","principal":{"type":"department","id":"$dept:Front Desk"},"spec":{"can_read":"all","can_insert":false,"can_edit":"none"}}解析以公司為錨點並 fail-closed:找不到、有歧義、已軟刪除、跨租戶都會讓該行失敗。它發生在衍生 grant:{type}:{id} ref 建立之前,因此指向同一 principal 的 token 文件與原始 ID 文件會落在同一列 state——把文件從 ID 改寫成 token 會收斂為 noop,而不是新增一筆重複 grant。
解析後的 token 綁定會折入 plan_hash。有釘住 hash 時,審查與 apply 之間的改名會得到 409 plan_stale;沒有釘住時,apply 會綁到當下持有該名稱的人。完整語法與錯誤目錄請見可攜身分 token。
精確 Spec 欄位
Grant spec 只接受以下欄位:
| 欄位 | 型別與值 | 意義 |
|---|---|---|
can_read | "none"、"own"、"all"、"filtered" 或 null | 資料列讀取範圍;明確 null 會正規化為 none |
can_insert | boolean 或 null | 是否可新增;明確 null 會正規化為 false |
can_edit | "none"、"own"、"all"、"filtered" 或 null | 資料列編輯範圍;明確 null 會正規化為 none |
visible_columns | column refs 陣列或 null | 可見欄位;每個 token 會翻成 live internal key |
read_filter | row policy object 或 null | can_read: "filtered" 時必填 |
edit_filter | row policy object 或 null | can_edit: "filtered" 時必填 |
scope_values | 字串陣列或 null | channel/chatroom scope governance;明確 null 表示 deny-all scope |
省略欄位與明確 null 不同。省略代表本次宣告不接管該 live grant 欄位;明確 null 依上表正規化或清除。
Filtered Grant 與 Row Policy
filtered 必須與對應 filter 成對;非 filtered 模式禁止保留該 filter:
can_read "filtered" requires a read_filter
read_filter is only valid when can_read is "filtered"
can_edit "filtered" requires an edit_filter
edit_filter is only valid when can_edit is "filtered"Row policy 的根可以是任一種布林節點:{"and": [node, ...]}、{"or": [node, ...]}、{"not": node}、predicate leaf,或 link_target leaf。舊的 {"and": [predicates]} 只是其中一棵合法的樹,行為完全不變。群組不可為空;深度上限 5、每條 policy 最多 24 個 predicate、最多 6 個 link_target leaf。
{"or":[{"column":"status","op":"eq","value":"open"},{"and":[{"column":"region","op":"in","value":["tw","jp"]},{"not":{"column":"status","op":"eq","value":"closed"}}]}]}Predicate 中的 column 使用同一 table 的 column ref。Policy 只應指向可持久化的 scalar columns;computed 或不支援的欄位型別會在伺服器 plan 驗證時被拒絕。Plan 會把 refs 解析成 live internal keys,並對 unresolved token 失敗關閉。
link_target 的 ref 橫跨兩張表
link_target leaf 帶著兩個 ref 命名空間的參照,executor、differ 與 export 都遵守這個區分:
{"kind":"grant","table":"deals","principal":{"type":"department","id":"$dept:Sales"},"spec":{"can_read":"filtered","can_insert":false,"can_edit":"none","read_filter":{"link":"assignee","quantifier":"any","target":{"column":"ts_account","op":"eq","value":"$me"}}}}link是本行table上的 column ref,且必須指向link型別欄位。target內每個column都是被連結那張表的 ref,透過該 link 的 target table 解析——絕不會對到近端表。- Export 會把兩者都反解。當被連結的表不在匯出選取範圍內(已刪除或跨公司)時,該節點會降級而不是讓整份匯出失敗:
link仍然反解,target子樹則原樣輸出被連結表的 internalcol_<hex>key,被連結的 table id 會列在X-IaC-Dangling-Link-Targetsresponse header。重新套用一份匯出之前,先讀那個 header。
Predicate 值中的 row-policy token
read_filter 與 edit_filter 的 predicate 值還接受完整的 ACL row-policy token 詞彙;若把 filter 只當成單純的 column/op/value 條件來讀,很容易漏掉這件事:
| Token | 解析為 |
|---|---|
$me | 操作主體自己的 ID——在 principal 欄位上,改為它所代表的 tagged cell SET |
$me.department | 操作主體的部門 ID——在 principal 欄位上,改為該部門成員與房間的 SET |
$today | 以 Asia/Taipei 為日界的今天 |
$today+Nd/$today-Nd | 今天之後或之前 N 天,N 為 1 到 730 |
$now | 目前的 Asia/Taipei 分鐘 |
{"and":[{"column":"owner","op":"eq","value":"$me"},{"column":"due_on","op":"lte","value":"$today+7d"}]}link_target 的 target 內的 token 是對被連結那張表的欄位型別配對,不是本表的。這套語法與 command DSL 互不相干:grant filter 永遠不接受 $input、$ctx 或 $row。它也比 public-read 的語法更寬——後者去掉了 $me 與 $me.department,因為公開 token 沒有操作主體。跨介面複製 filter 之前,請先看 public_read。
欄位可見性
visible_columns 也是 refs,不是 display names 或 internal keys。例如 "order_no" 必須在 orders table 中解析為一個 column line/state ref。未知 ref 會得到:
unresolved visible_columns ref 'order_no': unknown column ref 'orders.order_no'同一規則適用於 read_filter 與 edit_filter 內的 column token。不要用看似相同的顯示名稱取代 ref。
Chatroom Grant 與 scope_values
Chatroom principal 必須選定 audience: "internal" 或 "external",讓相同 chatroom identity 的內外受眾不混在一起。伺服器還會確認 table 的 department scope 與 chatroom/company 關係。
scope_values 用於 channel-aware 的範圍限制。省略時保留既有治理設定;字串陣列明確列出允許 scopes;null 是 deny-all,不是「全部允許」。把 audience、row policy 與 scope values 一起納入 plan review。
安全撤銷
以相同 table 與 principal 宣告 absent,不帶 spec:
{"kind":"grant","table":"orders","principal":{"type":"department","id":"support"},"state":"absent"}Plan 應顯示 delete,讓審查者確認被撤銷的 principal。若只是從文件刪掉 grant line,IaC 不會把省略推導為 revoke。
動手試試
在 IaC 工作台建立一個 department filtered grant,故意移除 read_filter 觀察本機錯誤,再修正並對測試 scope 執行唯讀 plan。完整 grant endpoint 契約請參考權限 API。