Skip to Content
核心概念JSONL IaC可攜式身分代號

可攜身分 token

原始的 user、department、client 或 chatroom ID 都綁定特定環境。在身分 token 出現之前,只要文件指名了某個 principal,它就被釘死在匯出它的那套安裝上:ref 可攜,principal 不可攜。$user: / $dept: / $smc: / $room: 補上了這個缺口。凡是過去必須填環境專屬 ID 的位置,現在都能改填 token;它們在 plan 與 apply 時以公司為錨點解析並 fail-closed;export 也會輸出 token——因此一份 export 可以進 Git 審查,再套用到另一個 scope,不必動手改 ID。

四種 token 家族

Token指名對象自然鍵
$user:<username>使用者Username,在公司內唯一
$dept:<department name>部門部門名稱,在公司內唯一
$smc:<platform>:<platform_user_id>社群媒體用戶公司底下各 chatroom 內的 (platform, platform user id)
$room:<chatroom name>聊天室聊天室名稱——不唯一
$room:<department name>/<chatroom name>聊天室以部門限定的聊天室名稱

Token 主體是 1 到 150 個字元的自由文字,可以包含空白與冒號,因為真實的部門與聊天室名稱本來就會有。唯一被排除的是 JSONL 本身就禁止的換行。因此 $dept:Front Desk$smc:line:U8f2c1b 都是合法寫法。

Chatroom.name 完全沒有唯一性限制,所以在多數租戶裡,裸寫 $room:<name> 都是真的有歧義。請把 $room:<department name>/<chatroom name> 當成預設寫法,裸名只是捷徑,而不是反過來。

各介面接受哪些家族

介面可用 token
grantprincipal.idprincipal.type 相符的那一種:user$user:department$dept:client$smc:chatroom$room:
insight_selectionchatroom只有 $room:——填 $user:$dept:$smc: 會在 parse 階段被拒
record 中 principal 欄位的 data celluser 欄位填 $user:social_client 欄位填 $smc:principal 欄位可填 $user:$smc:$room:——principal cell 會存成帶標籤形式(user:<id>smc:<id>room:<id>)。任何 cell 裡的 $dept: 都是逐行 kind 錯誤
trigger 中 principal 欄位的 when predicate 值usersocial_client 欄位填 $user:$smc:(解析成裸 ID);principal 欄位填 $user:$smc:$room:(解析成帶標籤 cell)
型別為 identity:user / identity:social_media_clientinvoke_command 輸入$user:$smc:
tablespec.moderators只有 $user:$dept:$smc:$room: 會在 parse 階段被拒
目標表自然鍵為 usersocial_clientprincipal 欄位的 link cell與該自然鍵欄位相符的那一種:user 鍵用 $user:social_client 鍵用 $smc:principal 鍵可用 $user:$smc:$room:。查詢用的值就是目標列實際儲存的形式:舊型別自然鍵是裸 ID,principal 自然鍵是帶標籤 cell

Moderator 使用下列只允許 user 的窄版 pattern。原始 user ID 仍可供同環境往返使用;可攜形式則是 $user:<username>

^([a-z0-9][a-z0-9_.-]{0,35}|\$user:[^\r\n]{1,150})$

Moderator 清單最多 50 項,而且是 full-set PUT 語意。Token 會在 plan 依文件所屬公司解析、在 apply 再解析一次;last-applied state 只儲存 canonical user ID,authored token 永不持久化。

放寬後的 principal.id 契約

IacGrantPrincipal.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 相符;前綴交錯是 per-line 錯誤,而不是去解析成另一種 principal。

insight_selection.chatroom 使用同一組 pattern 中僅限 $room: 的窄版:

^([a-z0-9][a-z0-9_.-]{0,35}|\$room:[^\r\n]{1,150})$

解析以公司為錨點並 fail-closed

每個 token 都在 diff 執行之前,對文件 scope 所屬的公司解析;apply 也會依 live state 重驗帶 token 的 mutation。唯一的例外是 principal 欄位在同一份文件較早處宣告的 record 行:plan 階段無法翻譯它,因此整行會帶著 warning 延後,token 改由 executor 在 apply 時解析——見 record line kind。沒有跨租戶 fallback,也沒有「最接近的比對」。找不到、有歧義、已軟刪除、跨租戶——四種情況都讓該行失敗,都不會靜默略過該行,也不會挑第一個候選。

解析發生在衍生 ref 建立之前,所以 grant:{type}:{id}insight.{uuid} 一定帶著解析後的原始 ID。這就是為什麼一份文件可以從原始 ID 改寫成 token 而收斂為 noop,而不是新增一筆重複 grant:兩種寫法會落在同一列 state。

當 token 無法解析時,plan action 仍然需要一個 ref,而 IacPlanAction.ref 有 pattern 限制且不接受 $。伺服器會代入 <prefix>unresolved-<8 個十六進位字元> 形式的佔位符,例如 insight.unresolved-1a2b3c4d。請把它當成錯誤標籤,絕不是資源身分——出錯的 token 就寫在 error detail 裡。

語法錯誤

括號裡的提示標示的是該通道自己的文法。Cell 通道——record cell、trigger when 值、invoke_command 輸入、link 自然鍵,以及 table.spec.moderators——提供三種前綴;grant 通道——grant.principal.idinsight_selection.chatroom——提供四種,因為 $dept: 只用於 grant。

malformed principal token '$user:': empty username (expected '$user:<username>', '$smc:<platform>:<platform_user_id>', '$room:<chatroom name>' or '$room:<department name>/<chatroom name>') malformed principal token '$smc:line': needs platform AND platform_user_id (expected '$user:<username>', '$smc:<platform>:<platform_user_id>', '$room:<chatroom name>' or '$room:<department name>/<chatroom name>') malformed principal token '$room:': empty chatroom name (expected '$user:<username>', '$smc:<platform>:<platform_user_id>', '$room:<chatroom name>' or '$room:<department name>/<chatroom name>') unrecognized principal token '$team:sales' (expected '$user:<username>', '$smc:<platform>:<platform_user_id>', '$room:<chatroom name>' or '$room:<department name>/<chatroom name>') malformed principal token '$dept:': empty department name (expected '$user:<username>', '$dept:<department name>', '$smc:<platform>:<platform_user_id>', '$room:<chatroom name>' or '$room:<department name>/<chatroom name>') principal token '$smc:line:U123' does not match the column's type: a user column expects $user:<...> or a raw id principal token '$dept:Sales' does not match the column's type: a principal column expects $user:<...> or $smc:<...> or $room:<...> or a raw id

最後兩則是 kind 閘門。它在任何查詢之前就執行,所以 token 帶的名稱不必存在;它列出的期待值就是該欄位或該 grant 自己的前綴集合,以 or 串接——usersocial_client 各一種、principal 三種、每種 grant 型別一種。Cell 裡的 $dept: 一律是這則錯誤:只有直接呼叫 cell 解析器才會回報成 unrecognized principal token '$dept:Sales' (…)

解析錯誤

principal token '$user:bob': no user with username 'bob' in this company principal token '$user:bob': user 'bob' is deleted principal token '$dept:Sales': no department named 'Sales' in this company principal token '$dept:Sales': department 'Sales' is deleted principal token '$room:Support': no live chatroom named 'Support' in this company principal token '$room:Support': ambiguous — 2 live chatrooms are named 'Support' (['<id-a>', '<id-b>']); qualify it as '$room:<department name>/Support' principal token '$room:Support Desk/Tier 1': no live chatroom named 'Tier 1' in department 'Support Desk' in this company principal token '$room:Support Desk/Tier 1': ambiguous — 2 live chatrooms match (['<id-a>', '<id-b>']) principal token '$smc:line:U123': no social client with platform user id 'U123' on platform 'line' in this company principal token '$smc:line:U123': ambiguous — 2 social clients match, in chatrooms ['<id-a>', '<id-b>'] cannot resolve this scope's company for principal token resolution

其中兩點值得強調。軟刪除的使用者或部門會得到明確錯誤,而不是「找不到」——離職會讓新的 apply 大聲失敗,而不是靜默改綁到別人。另外,$smc: 的歧義是正常的租戶樣貌,不是防禦性分支:social client 的唯一性是以 chatroom 為單位,所以同一個 (platform, platform user id) 出現在同公司的兩個 chatroom 時,永遠無法被無歧義地 token 化。軟刪除的聊天室則是第一條規則的例外:兩個聊天室分支都在 SQL 裡過濾存活狀態,所以被墓碑化的房間回報的是上面那則「找不到」,永遠不會是 is deleted

Trigger predicate 內的 token 會加上位置前綴:

trigger when column 'assignee': principal token '$user:bob': user 'bob' is deleted

Token 與 plan_hash

解析後的 token 綁定會以 (principal, type, authored, resolved) 指紋折入 plan_hash——但只有 grant principalinsight_selection.chatroom 會綁定。record cell、trigger when 值與 link 自然鍵裡的 token 走的是同一組 helper,卻永遠不會折入 hash,所以改掉一位使用者或一個聊天室的名稱,並不會重新蓋章一份 token 只出現在 cell 裡的 plan。就會綁定的那兩者而言,由此得到兩個都很關鍵的結果:

  • 有釘住 plan_hash 時,審查與 apply 之間的改名會重新蓋章,該次 apply 會被 409 plan_stale 拒絕。這是名稱易主之後唯一的保護。
  • 沒有 plan_hash 時,apply 會依設計綁到當下持有該名稱的人。省略這個 query parameter,現在放棄的東西比以前更多。

Export 會輸出 token,但不是一律如此

Export 會把原始 principal ID 反向對應成 token,依種類批次查詢並限定租戶。principal cell 會先被解析,再用它自己的標籤去對應的表查——user: 查使用者表、smc: 查 social client 表、room: 查聊天室表;usersocial_client cell、grant principal 與 insight_selection.chatroom 則直接用裸 ID 查。當 token 會出錯或不可用時,export 刻意退回已儲存的值:principal 欄位退回原始帶標籤 cell(user:<id>smc:<id>room:<id>),其他位置退回原始 UUID。

  • 裸名重複的 chatroom 會退回限定形式 $room:<department name>/<chatroom name>。若兩種形式都模稜兩可,就完全不輸出 token。
  • 名稱本身含有 token 分隔字元 / 的 chatroom 維持原樣;裸名重複、且其部門名稱也含有 / 的 chatroom 同樣維持原樣,因為限定形式會被切錯。$dept: grant principal 不受影響:部門名稱是整串比對,所以 $dept:Sales/EMEA 會被輸出,也解析得回來。
  • 被軟刪除或懸空的 chatroom 不在聊天室對應表裡,因此保持原樣——這正是讓該匯出重新 plan 得到 0 差異、而不會靜默改綁到另一個存活房間的原因。
  • (platform, platform user id) 在公司內對到多列的 social client 永遠不會被 token 化,因為那個 token 套用不了。
  • 懸空、跨租戶或 username 空白的 ID 以原樣匯出。
  • 軟刪除的使用者仍會被 token 化,好讓重新 apply 在解析器大聲失敗,而不是把一個死掉的原始 ID 帶下去。

因此,export 的可攜程度取決於該租戶名稱有多唯一。Export 裡出現原始 ID 或原始帶標籤 cell 是正確行為,不是 bug——但那也正是套用到別處之前必須人工修正的那幾行。同一個未變動系統的兩次匯出是位元相同的;帶著這些退路的匯出,在原本的 scope 重新 plan 依然是 0 差異。

$row.<column ref> 樣板 token

第二層可攜性與前者無關,處理的是 rule 與 trigger spec 內的樣板。Store 存的是 $row.<col_hex>,文件寫的是 $row.<column ref>。Export 把內部 key 改寫成 ref,plan/apply 再把 ref 翻回內部 key。

以下欄位會帶 $row. 樣板:

  • api_call action 的 urlheadersbody
  • notifysend_channel_message action 的訊息
  • line_flex 字串
  • invoke_command 的輸入值,於觸發當下依觸發列渲染

$row. 的翻譯刻意不是 fail-closed。無法解析的 token 會原樣保留,好讓舊有以內部 key 撰寫的文件、以及只是「看起來像 token」的自由文字繼續運作。因此 api_call body 或 notify 訊息裡打錯的 column ref 會靜默通過 plan,並在觸發時渲染成字面文字。伺服器 plan 抓不到它;實際觸發一次才會。

各種 $ 語法互不相干

不要把某一種語法的 token 帶進另一種。它們長得像,但毫無共通之處:

語法Token使用位置
IaC 身分 token$user:$dept:$smc:$room:只在 authored IaC 文件
ACL row-policy token$me$me.department$today$today±Nd$nowGrant 與 client-access 的 read_filteredit_filter predicate
Command DSL 參照$input$ctx$row$rel$new$itemCommand definition

此外還有三種位置專屬的 $ 語法——link 比對的 $self、trigger 樣板的 $row、rule 參照的 $row. / $same——同樣各自獨立;row policy token 列出了全部六種。

Grant filter 永遠不接受 $input。Command definition 永遠不接受把 $dept: 當 filter 值。而 IaC 身分 token 只屬於 IaC:把 $user:alice 送進 REST record API 會被當成原始 ID,並得到 Unknown or inaccessible user ids: ['$user:alice']

Public-read 的 read_filter 只取 row-policy 語法中不含主體的子集,詳見 public_read

動手試試

IaC 工作台寫一個 department grant,principal{"type": "department", "id": "$dept:Front Desk"},再把前綴改成 $user:,確認本機 parser 接受該 pattern、而伺服器 plan 會回報種類不符。執行唯讀 plan,並確認 grant action 的 ref 帶的是解析後的 UUID,而不是 token。

Last updated on