權限與存取控制
解析實際存取權、管理使用者與用戶端授權,並掌握各範圍實際提供的權限端點。
Scope matrix:departmentPermissions 只適用 chatroom table;departmentTableGrants 適用 department 與 company table;clientPermissions 與 clientAccess 適用三種 scope;chatroomPermissions 仍是 department-table-to-room 的獨立介面。路徑名稱相似,不代表可以把某個 family 擴張到其他 scope。
同一份解析後的權限也決定探索結果:成員讀得到的表會出現在 GET …/department/{department_id}/readable-tables;同公司但對某張表沒有任何權利的呼叫者,在它的 by-id 中繼資料讀取(包括 permissions/me 與 moderators)上會收到資料表不存在的 404。
/private/module/custom_tables/{scope}/tables/{table_id}/permissions分別回傳資料表的隱含管理者,以及逐使用者設定的明確授權。
適合用來製作存取管理畫面,或稽核哪些人可以操作這張資料表。
- 聊天室
/private/module/custom_tables/chatroom/{chatroom_id}/tables/{table_id}/permissions - 部門
/private/module/custom_tables/department/{department_id}/tables/{table_id}/permissions - 公司
/private/module/custom_tables/company/tables/{table_id}/permissions
範圍
| 範圍 | 是否提供 | 驗證與權限 |
|---|---|---|
| 聊天室 | 可使用 | CustomTableGrantRequired |
| 部門 | 可使用 | CustomTableGrantRequired |
| 公司 | 可使用 | CustomTableGrantRequired |
參數
| 欄位 | 位置 | 型別 | 必填 | 說明 |
|---|---|---|---|---|
table_id | path | uuid | 是 | 要列出使用者權限的資料表 UUID。 |
回應結構
TablePermissionsResponse| 欄位 | 型別 | 必填 | 說明 |
|---|---|---|---|
explicit_grants | ExplicitGrantEntry[] | 是 | Users with explicit per-user permission grants |
implicit_managers | ImplicitManagerEntry[] | 是 | Users with implicit full access (creators and moderators) |
範例
列出管理者與明確授權
{
"pathParams": {
"chatroom_id": "11111111-1111-4111-8111-111111111111",
"table_id": "22222222-2222-4222-8222-222222222222"
}
}{
"implicit_managers": [
{
"id": "55555555-5555-4555-8555-555555555555",
"created_at": "2026-07-19T02:00:00",
"expired_at": "2027-07-19T02:00:00",
"photo_url": null,
"enabled": true,
"is_verified": true,
"username": "catalog.manager",
"email": "manager@example.invalid",
"nickname": "林主管",
"department_id": "33333333-3333-4333-8333-333333333333",
"department_name": "營運部",
"role": "creator"
}
],
"explicit_grants": [
{
"id": "44444444-4444-4444-8444-444444444444",
"created_at": "2026-07-19T02:00:00",
"expired_at": "2027-07-19T02:00:00",
"photo_url": null,
"enabled": true,
"is_verified": true,
"username": "catalog.member",
"email": "member@example.invalid",
"nickname": "陳小安",
"department_id": "33333333-3333-4333-8333-333333333333",
"department_name": "營運部",
"can_read": "filtered",
"can_insert": true,
"can_edit": "own",
"read_filter": {
"column": "col_88888888_8888_4888_8888_888888888888",
"op": "eq",
"value": "北區"
},
"edit_filter": null,
"visible_columns": [
"col_88888888_8888_4888_8888_888888888888"
],
"granted_by": {
"id": "55555555-5555-4555-8555-555555555555",
"created_at": "2026-07-19T02:00:00",
"expired_at": "2027-07-19T02:00:00",
"photo_url": null,
"enabled": true,
"is_verified": true,
"username": "catalog.manager",
"email": "manager@example.invalid",
"nickname": "林主管",
"department_id": "33333333-3333-4333-8333-333333333333",
"department_name": "營運部"
},
"granted_at": "2026-07-19T02:05:00"
}
]
}錯誤
驗證錯誤範例列出識別欄位;部署中的驗證器也可能回傳 input 與 ctx。錯誤訊息 URL 的版本由建置決定,因此使用 pydantic-version 佔位符。
| 狀態 | 發生條件 | 錯誤本文 |
|---|---|---|
| 403 | 呼叫者不是資料表管理者:明確指定的版主;聊天室資料表則是聊天室建立者或聊天室管理者;部門資料表則是所屬部門的部門管理者(role ≥ 2)或同公司的公司管理者(role ≥ 3);公司資料表則是公司管理者。資料表 id 不存在或已在垃圾桶時也回這個 403——這道門檻從不回 404。 | |
| 404 | 呼叫者管理這張表,但它不在路徑指定的聊天室或部門中。company 路由不傳入 scope,因此不會回這個錯誤。 | |
/private/module/custom_tables/{scope}/tables/{table_id}/permissions/me解析呼叫者對單一資料表的最終讀取、新增、編輯與管理能力,以及他看得到的欄位鍵值(`visible_columns`)。
適合依實際權限啟用介面操作,不必從角色自行猜測。
- 聊天室
/private/module/custom_tables/chatroom/{chatroom_id}/tables/{table_id}/permissions/me - 部門
/private/module/custom_tables/department/{department_id}/tables/{table_id}/permissions/me - 公司
/private/module/custom_tables/company/tables/{table_id}/permissions/me
範圍
| 範圍 | 是否提供 | 驗證與權限 |
|---|---|---|
| 聊天室 | 可使用 | ChatRoomJoinedRequired + ChatroomTablePermissionsReadRequired |
| 部門 | 可使用 | DepartmentTableMetadataReadRequired |
| 公司 | 可使用 | CompanyTableMetadataReadRequired |
參數
| 欄位 | 位置 | 型別 | 必填 | 說明 |
|---|---|---|---|---|
table_id | path | uuid | 是 | 要解析呼叫者權限的資料表 UUID。 |
回應結構
MyPermissionsResponse| 欄位 | 型別 | 必填 | 說明 |
|---|---|---|---|
can_edit | "none" | "own" | "all" | "filtered" | 是 | Effective edit/delete access: "none" | "own" | "all" | "filtered" (v1.2 §11) |
can_insert | boolean | 是 | Whether the user can insert records |
can_read | "none" | "own" | "all" | "filtered" | 是 | Effective read access: "none" | "own" | "all" | "filtered" (v1.2 §11) |
is_manager | boolean | 是 | Whether the user is a table manager (implicit full access) |
visible_columns | string[] | null | 是 | Effective live internal column keys in schema order; null = all columns, [] = none. This is a column limit, not a read or write grant. |
範例
解析目前使用者的資料表權限
{
"pathParams": {
"chatroom_id": "11111111-1111-4111-8111-111111111111",
"table_id": "22222222-2222-4222-8222-222222222222"
}
}{
"can_read": "all",
"can_insert": true,
"can_edit": "all",
"is_manager": true,
"visible_columns": null
}錯誤
驗證錯誤範例列出識別欄位;部署中的驗證器也可能回傳 input 與 ctx。錯誤訊息 URL 的版本由建置決定,因此使用 pydantic-version 佔位符。
| 狀態 | 發生條件 | 錯誤本文 |
|---|---|---|
| 404 | Chatroom 路由:資料表不存在、已在垃圾桶、屬於其他聊天室、屬於其他公司(自後端 5.21.0 起),或你對它完全沒有權利。聊天室門檻會先執行,並保留自己的 `404 Chatroom Not found` 與 403。 | |
| 404 | Department 或 company 路由:資料表不存在或已在垃圾桶、屬於其他 scope 或(自後端 5.21.0 起)其他公司,或你是同公司但對它沒有任何權利的呼叫者(包括被門檻拒絕者)。以上全部回應得與 id 不存在完全相同。 | |
/private/module/custom_tables/{scope}/tables/{table_id}/permissions建立或取代某位使用者的明確讀取、新增、編輯、篩選與可見欄位設定。
某位使用者需要不同於預設值或共用範圍授權時使用。
- 聊天室
/private/module/custom_tables/chatroom/{chatroom_id}/tables/{table_id}/permissions - 部門
/private/module/custom_tables/department/{department_id}/tables/{table_id}/permissions - 公司
/private/module/custom_tables/company/tables/{table_id}/permissions
範圍
| 範圍 | 是否提供 | 驗證與權限 |
|---|---|---|
| 聊天室 | 可使用 | CustomTableGrantRequired |
| 部門 | 可使用 | CustomTableGrantRequired |
| 公司 | 可使用 | CustomTableGrantRequired |
參數
| 欄位 | 位置 | 型別 | 必填 | 說明 |
|---|---|---|---|---|
table_id | path | uuid | 是 | 要加入明確授權的資料表 UUID。 |
請求結構
GrantPermissionPayload| 欄位 | 型別 | 必填 | 說明 |
|---|---|---|---|
can_edit | "none" | "own" | "all" | "filtered" | 否 | Edit/delete access: "none" | "own" | "all" | "filtered" (v1.2 §11) |
can_insert | boolean | 否 | Whether the user can insert records |
can_read | "none" | "own" | "all" | "filtered" | 否 | Read access: "none" | "own" | "all" | "filtered" (v1.2 §11) |
edit_filter | object | null | 否 | v1.2 §11 row policy — required when can_edit="filtered". A row policy is a boolean tree over scalar predicates: a node is {"and": [node, ...]} (every child holds), {"or": [node, ...]} (at least one holds), {"not": node} (strict complement) or a predicate {"column": "col_<hex>", "op": "eq|neq|gt|gte|lt|lte|in|contains|is_null|is_not_null", "value": ...}. Groups are non-empty; a node carries exactly its kind's keys; the root may be any node (the legacy {"and": [preds]} is one such tree). Caps: depth <= 5 (a bare predicate is depth 1), <= 24 predicates, <= 100 entries per "in" list (caps bind AUTHORED policies; the server may compose wider resolved trees). Predicates reference internal column keys of stored scalar columns only. An empty/unset cell fails every value op and matches only is_null, so {"not": {col eq X}} INCLUDES blank cells while {col neq X} excludes them. Unresolvable tokens and malformed nodes always NARROW the row set (never-match under "and"/"not", dropped under "or"), never widen it. A node may also be a link_target leaf {"link": "col_<hex> of a LINK column of this table", "quantifier": "any"|"all", "target": node, "require_present": bool} — it follows the link ONE hop and evaluates "target" (any row-policy node over the LINKED table's stored scalar columns; no link node inside it: 1-hop) against the linked rows: "any" = at least one linked row matches, "all" = every linked row matches (vacuously true when none — require_present MUST be stated on "all" and must be absent on "any"). Only LIVE (not trashed) linked rows count and the linked table's SCP channel floor applies; the linked table's own row/column ACL does NOT. Tokens inside "target" resolve against the LINKED column's type ("$me" on its user column = rows assigned to me). Caps: <= 6 link leaves; a target costs one depth level and its predicates count toward the 24. A {"link", "op", "value"} membership leaf is NOT accepted — write link_target with a target {"column": "id", "op": "in", "value": [...]}. Example: {"or": [{"column": "col_a1", "op": "eq", "value": "TW"}, {"not": {"column": "col_b2", "op": "eq", "value": "$me"}}]}. Predicate values may use per-request tokens (stored RAW, substituted at permission-resolution time): "$me" = the acting principal's own id (user id; client id for social clients; the linked user's id for agent-platform clients) — ops eq/neq or as an element of an "in" list, on user/social_client/principal/string/text columns; on a principal column "$me" is a SET — the caller's own user:<id> plus room:<id> for every live chatroom they belong to (REST) or the acting chatroom only inside an agent / command session; social clients match smc:<id> only; a public-read token never matches — compiled as an "in" over tagged cells ("neq" excludes blank cells); "$me.department" = the acting user's department id — string/text columns; on a principal column it is a SET: user:<id> for every live user of that department plus room:<id> for every live chatroom of that department (the acting chatroom only inside an agent / command session, and only if it belongs to the department) — matches NO rows for clients; "$today"/"$today+Nd"/"$today-Nd" (N 1..730, day boundary of the company business timezone when configured, otherwise Asia/Taipei) — date columns with eq/neq/gt/gte/lt/lte, datetime columns with gt/gte/lt/lte only (an eq day is unsatisfiable — author gte "$today" AND lt "$today+1d"), never inside "in" lists; "$now" = the current wall-clock instant of the company business timezone when configured, otherwise Asia/Taipei, at minute precision — datetime columns with gt/gte/lt/lte only (never eq/neq, never a date column — use "$today" there — never inside an "in" list). Any other "$"-prefixed value is rejected (reserved). Grant endpoints echo the RAW token, never a resolved literal. Unlike "own" (fixed to the record creator), "$me" may target any legal column — e.g. an assigned-owner column. A token whose principal kind never matches the column (a user-audience "$me" on a social_client column) is legal but matches nothing. |
read_filter | object | null | 否 | v1.2 §11 row policy — an and/or/not tree of {"column": col_<hex>, "op": ..., "value": ...} predicates; required when can_read="filtered". A row policy is a boolean tree over scalar predicates: a node is {"and": [node, ...]} (every child holds), {"or": [node, ...]} (at least one holds), {"not": node} (strict complement) or a predicate {"column": "col_<hex>", "op": "eq|neq|gt|gte|lt|lte|in|contains|is_null|is_not_null", "value": ...}. Groups are non-empty; a node carries exactly its kind's keys; the root may be any node (the legacy {"and": [preds]} is one such tree). Caps: depth <= 5 (a bare predicate is depth 1), <= 24 predicates, <= 100 entries per "in" list (caps bind AUTHORED policies; the server may compose wider resolved trees). Predicates reference internal column keys of stored scalar columns only. An empty/unset cell fails every value op and matches only is_null, so {"not": {col eq X}} INCLUDES blank cells while {col neq X} excludes them. Unresolvable tokens and malformed nodes always NARROW the row set (never-match under "and"/"not", dropped under "or"), never widen it. A node may also be a link_target leaf {"link": "col_<hex> of a LINK column of this table", "quantifier": "any"|"all", "target": node, "require_present": bool} — it follows the link ONE hop and evaluates "target" (any row-policy node over the LINKED table's stored scalar columns; no link node inside it: 1-hop) against the linked rows: "any" = at least one linked row matches, "all" = every linked row matches (vacuously true when none — require_present MUST be stated on "all" and must be absent on "any"). Only LIVE (not trashed) linked rows count and the linked table's SCP channel floor applies; the linked table's own row/column ACL does NOT. Tokens inside "target" resolve against the LINKED column's type ("$me" on its user column = rows assigned to me). Caps: <= 6 link leaves; a target costs one depth level and its predicates count toward the 24. A {"link", "op", "value"} membership leaf is NOT accepted — write link_target with a target {"column": "id", "op": "in", "value": [...]}. Example: {"or": [{"column": "col_a1", "op": "eq", "value": "TW"}, {"not": {"column": "col_b2", "op": "eq", "value": "$me"}}]}. Predicate values may use per-request tokens (stored RAW, substituted at permission-resolution time): "$me" = the acting principal's own id (user id; client id for social clients; the linked user's id for agent-platform clients) — ops eq/neq or as an element of an "in" list, on user/social_client/principal/string/text columns; on a principal column "$me" is a SET — the caller's own user:<id> plus room:<id> for every live chatroom they belong to (REST) or the acting chatroom only inside an agent / command session; social clients match smc:<id> only; a public-read token never matches — compiled as an "in" over tagged cells ("neq" excludes blank cells); "$me.department" = the acting user's department id — string/text columns; on a principal column it is a SET: user:<id> for every live user of that department plus room:<id> for every live chatroom of that department (the acting chatroom only inside an agent / command session, and only if it belongs to the department) — matches NO rows for clients; "$today"/"$today+Nd"/"$today-Nd" (N 1..730, day boundary of the company business timezone when configured, otherwise Asia/Taipei) — date columns with eq/neq/gt/gte/lt/lte, datetime columns with gt/gte/lt/lte only (an eq day is unsatisfiable — author gte "$today" AND lt "$today+1d"), never inside "in" lists; "$now" = the current wall-clock instant of the company business timezone when configured, otherwise Asia/Taipei, at minute precision — datetime columns with gt/gte/lt/lte only (never eq/neq, never a date column — use "$today" there — never inside an "in" list). Any other "$"-prefixed value is rejected (reserved). Grant endpoints echo the RAW token, never a resolved literal. Unlike "own" (fixed to the record creator), "$me" may target any legal column — e.g. an assigned-owner column. A token whose principal kind never matches the column (a user-audience "$me" on a social_client column) is legal but matches nothing. |
user_id | string | 是 | Target user UUID |
visible_columns | string[] | null | 否 | Column allowlist: INTERNAL column keys (col_<hex>) visible under this grant; all other columns are hidden (read strip + write reject + formula/export/view/history scoping). null/omitted = all columns (v1.2 column_acl still applies on top). Empty list rejected (422); unknown keys rejected (422). |
回應結構
SuccessResponse| 欄位 | 型別 | 必填 | 說明 |
|---|---|---|---|
message | string | 否 | 未指定 |
範例
授予完整讀取與僅可編輯自己資料的權限
{
"pathParams": {
"chatroom_id": "11111111-1111-4111-8111-111111111111",
"table_id": "22222222-2222-4222-8222-222222222222"
},
"body": {
"user_id": "44444444-4444-4444-8444-444444444444",
"can_read": "all",
"can_insert": true,
"can_edit": "own",
"visible_columns": [
"col_88888888_8888_4888_8888_888888888888"
]
}
}{
"message": "Permissions updated successfully"
}錯誤
驗證錯誤範例列出識別欄位;部署中的驗證器也可能回傳 input 與 ctx。錯誤訊息 URL 的版本由建置決定,因此使用 pydantic-version 佔位符。
| 狀態 | 發生條件 | 錯誤本文 |
|---|---|---|
| 400 | 目標使用者已是隱含管理者,不能再建立重複的明確授權(如上所示)。更早執行的兩項檢查同樣回 400:目標使用者 id 不存在(`Users not found: ['<user_id>']`),或屬於其他公司(`Users not in the same company: ['<user_id>']`)。 | |
| 403 | 呼叫者不是資料表管理者:明確指定的版主;聊天室資料表則是聊天室建立者或聊天室管理者;部門資料表則是所屬部門的部門管理者(role ≥ 2)或同公司的公司管理者(role ≥ 3);公司資料表則是公司管理者。資料表 id 不存在或已在垃圾桶時也回這個 403——這道門檻從不回 404。 | |
| 404 | 呼叫者管理這張表,但它不在路徑指定的聊天室或部門中。company 路由不傳入 scope,因此不會回這個錯誤。 | |
| 409 | 兩筆寫入爭用同一批資料列,InnoDB 中止了這一筆。交易已回滾,授權內容維持不變——直接重送請求即可。 | |
| 422 | 權限值、篩選條件或可見欄位不符合這張資料表的規格。 | |
/private/module/custom_tables/{scope}/tables/{table_id}/permissions/bulk在單一交易中授予或撤銷多筆明確使用者權限。
適合同步成員資料,或製作多人權限編輯器。
- 聊天室
/private/module/custom_tables/chatroom/{chatroom_id}/tables/{table_id}/permissions/bulk - 部門
/private/module/custom_tables/department/{department_id}/tables/{table_id}/permissions/bulk - 公司
/private/module/custom_tables/company/tables/{table_id}/permissions/bulk
範圍
| 範圍 | 是否提供 | 驗證與權限 |
|---|---|---|
| 聊天室 | 可使用 | CustomTableGrantRequired |
| 部門 | 可使用 | CustomTableGrantRequired |
| 公司 | 可使用 | CustomTableGrantRequired |
參數
| 欄位 | 位置 | 型別 | 必填 | 說明 |
|---|---|---|---|---|
table_id | path | uuid | 是 | 要批次變更明確授權的資料表 UUID。 |
請求結構
BulkPermissionPayload| 欄位 | 型別 | 必填 | 說明 |
|---|---|---|---|
actions | BulkPermissionAction[] | 是 | List of grant/revoke actions (max 100 per request) |
回應結構
BulkPermissionResponse| 欄位 | 型別 | 必填 | 說明 |
|---|---|---|---|
granted | integer | 是 | Number of users granted/updated permissions |
message | string | 否 | 未指定 |
revoked | integer | 是 | Number of users whose permissions were revoked |
範例
以批次操作授予一位使用者權限
{
"pathParams": {
"chatroom_id": "11111111-1111-4111-8111-111111111111",
"table_id": "22222222-2222-4222-8222-222222222222"
},
"body": {
"actions": [
{
"action": "grant",
"user_id": "44444444-4444-4444-8444-444444444444",
"can_read": "own",
"can_insert": true,
"can_edit": "own"
}
]
}
}{
"message": "Bulk permissions updated successfully",
"granted": 1,
"revoked": 0
}錯誤
驗證錯誤範例列出識別欄位;部署中的驗證器也可能回傳 input 與 ctx。錯誤訊息 URL 的版本由建置決定,因此使用 pydantic-version 佔位符。
| 狀態 | 發生條件 | 錯誤本文 |
|---|---|---|
| 400 | 有一或多位目標使用者不存在(`Users not found: [...]`)或屬於其他公司(`Users not in the same company: [...]`)。bulk 不會檢查目標是否為資料表管理者:對管理者的授權動作也會被接受。 | |
| 403 | 呼叫者不是資料表管理者:明確指定的版主;聊天室資料表則是聊天室建立者或聊天室管理者;部門資料表則是所屬部門的部門管理者(role ≥ 2)或同公司的公司管理者(role ≥ 3);公司資料表則是公司管理者。資料表 id 不存在或已在垃圾桶時也回這個 403——這道門檻從不回 404。 | |
| 404 | 呼叫者管理這張表,但它不在路徑指定的聊天室或部門中。company 路由不傳入 scope,因此不會回這個錯誤。 | |
| 422 | 某個 grant 動作的 row policy 不合法(多則文法訊息以 `; ` 串接,如上所示),或 `visible_columns` 含有不是現行欄位的鍵(`visible_columns references unknown columns: [...]`)。在使用者檢查之後逐一檢查 grant 動作;整批都不會套用。 | |
/private/module/custom_tables/{scope}/tables/{table_id}/permissions/{user_id}刪除使用者的明確 grant,使後續有效權限回到管理者、部門、聊天室或資料表預設值。
用來移除單一使用者的覆寫、讓他回到繼承的存取權,而不是建立明確的拒絕。
- 聊天室
/private/module/custom_tables/chatroom/{chatroom_id}/tables/{table_id}/permissions/{user_id} - 部門
/private/module/custom_tables/department/{department_id}/tables/{table_id}/permissions/{user_id} - 公司
/private/module/custom_tables/company/tables/{table_id}/permissions/{user_id}
範圍
| 範圍 | 是否提供 | 驗證與權限 |
|---|---|---|
| 聊天室 | 可使用 | CustomTableGrantRequired |
| 部門 | 可使用 | CustomTableGrantRequired |
| 公司 | 可使用 | CustomTableGrantRequired |
參數
| 欄位 | 位置 | 型別 | 必填 | 說明 |
|---|---|---|---|---|
table_id | path | uuid | 是 | 擁有這筆明確授權的資料表 UUID。 |
user_id | path | uuid | 是 | 要移除明確授權的使用者 UUID。 |
回應結構
RevokePermissionResponse| 欄位 | 型別 | 必填 | 說明 |
|---|---|---|---|
effective_permissions | MyPermissionsResponse | 是 | The user's resulting effective permissions on this table, computed AFTER the revoke |
message | string | 是 | Human-readable result message |
warning | string | null | 否 | Present only when widened_access is true; names the widened axis/axes and the table's default permission floor as the cause |
widened_access | boolean | 是 | True when the revoke increased the user's effective access on can_read, can_insert, or can_edit relative to the grant just removed |
範例
移除一筆明確使用者授權
{
"pathParams": {
"chatroom_id": "11111111-1111-4111-8111-111111111111",
"table_id": "22222222-2222-4222-8222-222222222222",
"user_id": "44444444-4444-4444-8444-444444444444"
}
}{
"message": "Permissions revoked successfully",
"effective_permissions": {
"can_read": "all",
"can_insert": false,
"can_edit": "none",
"is_manager": false,
"visible_columns": null
},
"widened_access": false,
"warning": null
}錯誤
驗證錯誤範例列出識別欄位;部署中的驗證器也可能回傳 input 與 ctx。錯誤訊息 URL 的版本由建置決定,因此使用 pydantic-version 佔位符。
| 狀態 | 發生條件 | 錯誤本文 |
|---|---|---|
| 404 | 此使用者在這張資料表沒有明確權限列。 | |
| 403 | 呼叫者不是資料表管理者:明確指定的版主;聊天室資料表則是聊天室建立者或聊天室管理者;部門資料表則是所屬部門的部門管理者(role ≥ 2)或同公司的公司管理者(role ≥ 3);公司資料表則是公司管理者。資料表 id 不存在或已在垃圾桶時也回這個 403——這道門檻從不回 404。 | |
| 404 | Chatroom 或 department 路由:資料表不在路徑指定的 scope 內。Company 路由:資料表已在垃圾桶、是聊天室範圍的表(沒有 company id——請透過 chatroom 路由撤銷),或屬於目標使用者以外的公司;目標使用者 id 不存在時也會落在這裡。 | |
/private/module/custom_tables/{scope}/tables/{table_id}/default-permissions取代當管理者或明確授權都未命中時所使用的後備資料列權限。
用來設定範圍內、沒有管理角色也沒有使用者或部門授權者的基準權限。範圍內且另有聊天室授權的使用者,仍以這個基準為下限:每個軸向只有在授權嚴格更寬時才以授權為準。僅限聊天室資料表:它也涵蓋該聊天室自己的、沒有用戶端授權的社群用戶端(agent 平台用戶端會以其連結的使用者身分解析)。範圍外的使用者,以及部門或公司資料表的社群用戶端,永遠不會套用預設權限。
- 聊天室
/private/module/custom_tables/chatroom/{chatroom_id}/tables/{table_id}/default-permissions - 部門
/private/module/custom_tables/department/{department_id}/tables/{table_id}/default-permissions - 公司
/private/module/custom_tables/company/tables/{table_id}/default-permissions
範圍
| 範圍 | 是否提供 | 驗證與權限 |
|---|---|---|
| 聊天室 | 可使用 | CustomTableModeratorRequired |
| 部門 | 可使用 | CustomTableModeratorRequired |
| 公司 | 可使用 | CustomTableModeratorRequired |
參數
| 欄位 | 位置 | 型別 | 必填 | 說明 |
|---|---|---|---|---|
table_id | path | uuid | 是 | 要更新後備權限的資料表 UUID。 |
請求結構
DefaultPermissionsPayload| 欄位 | 型別 | 必填 | 說明 |
|---|---|---|---|
audience | "scope" | "company" | 否 | Who these defaults (and the system fallback) apply to. "scope" (default) = the table's own scope members, current behavior. "company" — DEPARTMENT-scoped tables only (422 elsewhere) — shares the table to ALL departments: every same-company user resolves these defaults and passes the membership door. Per-department grant rows still override per department (narrower or wider), and per-user grants override those. PATCH replaces the whole default_permissions object, so omitting audience on a later call reverts the share to "scope". |
can_edit | "none" | "own" | "all" | "filtered" | 否 | Edit/delete access level: "none" | "own" | "all" | "filtered" (v1.2 §11, requires edit_filter) |
can_insert | boolean | 否 | Whether users can insert new records |
can_read | "none" | "own" | "all" | "filtered" | 否 | Read access level: "none" | "own" (only own records) | "all" | "filtered" (v1.2 §11, requires read_filter) |
edit_filter | object | null | 否 | v1.2 §11 row policy — REQUIRED when can_edit="filtered". Token grammar is identical to read_filter: A row policy is a boolean tree over scalar predicates: a node is {"and": [node, ...]} (every child holds), {"or": [node, ...]} (at least one holds), {"not": node} (strict complement) or a predicate {"column": "col_<hex>", "op": "eq|neq|gt|gte|lt|lte|in|contains|is_null|is_not_null", "value": ...}. Groups are non-empty; a node carries exactly its kind's keys; the root may be any node (the legacy {"and": [preds]} is one such tree). Caps: depth <= 5 (a bare predicate is depth 1), <= 24 predicates, <= 100 entries per "in" list (caps bind AUTHORED policies; the server may compose wider resolved trees). Predicates reference internal column keys of stored scalar columns only. An empty/unset cell fails every value op and matches only is_null, so {"not": {col eq X}} INCLUDES blank cells while {col neq X} excludes them. Unresolvable tokens and malformed nodes always NARROW the row set (never-match under "and"/"not", dropped under "or"), never widen it. A node may also be a link_target leaf {"link": "col_<hex> of a LINK column of this table", "quantifier": "any"|"all", "target": node, "require_present": bool} — it follows the link ONE hop and evaluates "target" (any row-policy node over the LINKED table's stored scalar columns; no link node inside it: 1-hop) against the linked rows: "any" = at least one linked row matches, "all" = every linked row matches (vacuously true when none — require_present MUST be stated on "all" and must be absent on "any"). Only LIVE (not trashed) linked rows count and the linked table's SCP channel floor applies; the linked table's own row/column ACL does NOT. Tokens inside "target" resolve against the LINKED column's type ("$me" on its user column = rows assigned to me). Caps: <= 6 link leaves; a target costs one depth level and its predicates count toward the 24. A {"link", "op", "value"} membership leaf is NOT accepted — write link_target with a target {"column": "id", "op": "in", "value": [...]}. Example: {"or": [{"column": "col_a1", "op": "eq", "value": "TW"}, {"not": {"column": "col_b2", "op": "eq", "value": "$me"}}]}. Predicate values may use per-request tokens (stored RAW, substituted at permission-resolution time): "$me" = the acting principal's own id (user id; client id for social clients; the linked user's id for agent-platform clients) — ops eq/neq or as an element of an "in" list, on user/social_client/principal/string/text columns; on a principal column "$me" is a SET — the caller's own user:<id> plus room:<id> for every live chatroom they belong to (REST) or the acting chatroom only inside an agent / command session; social clients match smc:<id> only; a public-read token never matches — compiled as an "in" over tagged cells ("neq" excludes blank cells); "$me.department" = the acting user's department id — string/text columns; on a principal column it is a SET: user:<id> for every live user of that department plus room:<id> for every live chatroom of that department (the acting chatroom only inside an agent / command session, and only if it belongs to the department) — matches NO rows for clients; "$today"/"$today+Nd"/"$today-Nd" (N 1..730, day boundary of the company business timezone when configured, otherwise Asia/Taipei) — date columns with eq/neq/gt/gte/lt/lte, datetime columns with gt/gte/lt/lte only (an eq day is unsatisfiable — author gte "$today" AND lt "$today+1d"), never inside "in" lists; "$now" = the current wall-clock instant of the company business timezone when configured, otherwise Asia/Taipei, at minute precision — datetime columns with gt/gte/lt/lte only (never eq/neq, never a date column — use "$today" there — never inside an "in" list). Any other "$"-prefixed value is rejected (reserved). Grant endpoints echo the RAW token, never a resolved literal. Unlike "own" (fixed to the record creator), "$me" may target any legal column — e.g. an assigned-owner column. A token whose principal kind never matches the column (a user-audience "$me" on a social_client column) is legal but matches nothing. |
read_filter | object | null | 否 | v1.2 §11 row policy — an and/or/not tree of {"column": col_<hex>, "op": ..., "value": ...} predicates; REQUIRED when can_read="filtered". A row policy is a boolean tree over scalar predicates: a node is {"and": [node, ...]} (every child holds), {"or": [node, ...]} (at least one holds), {"not": node} (strict complement) or a predicate {"column": "col_<hex>", "op": "eq|neq|gt|gte|lt|lte|in|contains|is_null|is_not_null", "value": ...}. Groups are non-empty; a node carries exactly its kind's keys; the root may be any node (the legacy {"and": [preds]} is one such tree). Caps: depth <= 5 (a bare predicate is depth 1), <= 24 predicates, <= 100 entries per "in" list (caps bind AUTHORED policies; the server may compose wider resolved trees). Predicates reference internal column keys of stored scalar columns only. An empty/unset cell fails every value op and matches only is_null, so {"not": {col eq X}} INCLUDES blank cells while {col neq X} excludes them. Unresolvable tokens and malformed nodes always NARROW the row set (never-match under "and"/"not", dropped under "or"), never widen it. A node may also be a link_target leaf {"link": "col_<hex> of a LINK column of this table", "quantifier": "any"|"all", "target": node, "require_present": bool} — it follows the link ONE hop and evaluates "target" (any row-policy node over the LINKED table's stored scalar columns; no link node inside it: 1-hop) against the linked rows: "any" = at least one linked row matches, "all" = every linked row matches (vacuously true when none — require_present MUST be stated on "all" and must be absent on "any"). Only LIVE (not trashed) linked rows count and the linked table's SCP channel floor applies; the linked table's own row/column ACL does NOT. Tokens inside "target" resolve against the LINKED column's type ("$me" on its user column = rows assigned to me). Caps: <= 6 link leaves; a target costs one depth level and its predicates count toward the 24. A {"link", "op", "value"} membership leaf is NOT accepted — write link_target with a target {"column": "id", "op": "in", "value": [...]}. Example: {"or": [{"column": "col_a1", "op": "eq", "value": "TW"}, {"not": {"column": "col_b2", "op": "eq", "value": "$me"}}]}. Predicate values may use per-request tokens (stored RAW, substituted at permission-resolution time): "$me" = the acting principal's own id (user id; client id for social clients; the linked user's id for agent-platform clients) — ops eq/neq or as an element of an "in" list, on user/social_client/principal/string/text columns; on a principal column "$me" is a SET — the caller's own user:<id> plus room:<id> for every live chatroom they belong to (REST) or the acting chatroom only inside an agent / command session; social clients match smc:<id> only; a public-read token never matches — compiled as an "in" over tagged cells ("neq" excludes blank cells); "$me.department" = the acting user's department id — string/text columns; on a principal column it is a SET: user:<id> for every live user of that department plus room:<id> for every live chatroom of that department (the acting chatroom only inside an agent / command session, and only if it belongs to the department) — matches NO rows for clients; "$today"/"$today+Nd"/"$today-Nd" (N 1..730, day boundary of the company business timezone when configured, otherwise Asia/Taipei) — date columns with eq/neq/gt/gte/lt/lte, datetime columns with gt/gte/lt/lte only (an eq day is unsatisfiable — author gte "$today" AND lt "$today+1d"), never inside "in" lists; "$now" = the current wall-clock instant of the company business timezone when configured, otherwise Asia/Taipei, at minute precision — datetime columns with gt/gte/lt/lte only (never eq/neq, never a date column — use "$today" there — never inside an "in" list). Any other "$"-prefixed value is rejected (reserved). Grant endpoints echo the RAW token, never a resolved literal. Unlike "own" (fixed to the record creator), "$me" may target any legal column — e.g. an assigned-owner column. A token whose principal kind never matches the column (a user-audience "$me" on a social_client column) is legal but matches nothing. |
回應結構
SuccessResponse| 欄位 | 型別 | 必填 | 說明 |
|---|---|---|---|
message | string | 否 | 未指定 |
範例
開放讀取,但預設不允許寫入
{
"pathParams": {
"chatroom_id": "11111111-1111-4111-8111-111111111111",
"table_id": "22222222-2222-4222-8222-222222222222"
},
"body": {
"can_read": "all",
"can_insert": false,
"can_edit": "none"
}
}{
"message": "Default permissions updated successfully"
}把部門表分享給公司裡的每一個部門
{
"pathParams": {
"department_id": "33333333-3333-4333-8333-333333333333",
"table_id": "22222222-2222-4222-8222-222222222222"
},
"body": {
"can_read": "all",
"can_insert": false,
"can_edit": "none",
"audience": "company"
}
}{
"message": "Default permissions updated successfully"
}錯誤
驗證錯誤範例列出識別欄位;部署中的驗證器也可能回傳 input 與 ctx。錯誤訊息 URL 的版本由建置決定,因此使用 pydantic-version 佔位符。
| 狀態 | 發生條件 | 錯誤本文 |
|---|---|---|
| 404 | 資料表 id 不存在或已在垃圾桶(版主權限相依);在 chatroom 路由上,資料表不在路徑指定的聊天室中也是這個錯誤。 | |
| 404 | Department 或 company 路由:呼叫者擔任版主、但屬於其他 scope 的現行資料表。 | |
| 409 | settings 合併在伺服器自身處理之後仍輸掉資料表列鎖。沒有任何內容被合併,儲存的預設權限維持原狀——重送 PATCH;拿到 200 才代表合併真的成立。 | |
| 422 | 受篩選的權限層級缺少必要的篩選條件,或其他權限值不合法。 | |
| 422 | 在 chatroom 或 company scope 的表上送了 `audience: "company"`。detail 就是這一整句。不在 `{"scope","company"}` 內的值根本到不了這道閘——`audience` 是 `Literal`,Pydantic 會先以一般的 422 驗證 body 擋下。 | |
/private/module/custom_tables/{scope}/tables/{table_id}/column-acl取代疊加在資料列權限之上的欄位讀取限制。
適合在不拆分資料表的情況下,對非管理者隱藏敏感欄位。
- 聊天室
/private/module/custom_tables/chatroom/{chatroom_id}/tables/{table_id}/column-acl - 部門
/private/module/custom_tables/department/{department_id}/tables/{table_id}/column-acl - 公司
/private/module/custom_tables/company/tables/{table_id}/column-acl
範圍
| 範圍 | 是否提供 | 驗證與權限 |
|---|---|---|
| 聊天室 | 可使用 | CustomTableModeratorRequired |
| 部門 | 可使用 | CustomTableModeratorRequired |
| 公司 | 可使用 | CustomTableModeratorRequired |
參數
| 欄位 | 位置 | 型別 | 必填 | 說明 |
|---|---|---|---|---|
table_id | path | uuid | 是 | 要取代欄位 ACL 的資料表 UUID。 |
請求結構
ColumnAclPayload| 欄位 | 型別 | 必填 | 說明 |
|---|---|---|---|
column_acl | Record<string, Record<string, string>> | 否 | {"col_<hex>": {"read": "all"|"managers"}} — hide a column from non-managers. Internal keys (renames do not break it). Empty = clear. |
回應結構
SuccessResponse| 欄位 | 型別 | 必填 | 說明 |
|---|---|---|---|
message | string | 否 | 未指定 |
範例
將單一欄位限制為僅管理者可讀
{
"pathParams": {
"chatroom_id": "11111111-1111-4111-8111-111111111111",
"table_id": "22222222-2222-4222-8222-222222222222"
},
"body": {
"column_acl": {
"col_88888888_8888_4888_8888_888888888888": {
"read": "managers"
}
}
}
}{
"message": "Column ACL updated successfully"
}錯誤
驗證錯誤範例列出識別欄位;部署中的驗證器也可能回傳 input 與 ctx。錯誤訊息 URL 的版本由建置決定,因此使用 pydantic-version 佔位符。
| 狀態 | 發生條件 | 錯誤本文 |
|---|---|---|
| 404 | 資料表 id 不存在或已在垃圾桶(版主權限相依);在 chatroom 路由上,資料表不在路徑指定的聊天室中也是這個錯誤。 | |
| 404 | Department 或 company 路由:呼叫者擔任版主、但屬於其他 scope 的現行資料表。 | |
| 409 | settings 合併在伺服器自身處理之後仍輸掉資料表列鎖。儲存的欄位 ACL 維持原狀——你送出的收緊「沒有」生效;重送 PATCH 直到拿到 200。 | |
| 422 | ACL 引用了不存在的欄位內部 ID,或規則無效。 | |
/private/module/custom_tables/{scope}/tables/{table_id}/department-permissions列出附加在聊天室資料表上的部門共用授權。
適合稽核哪些部門可繼承聊天室資料表的存取權。
- 聊天室
/private/module/custom_tables/chatroom/{chatroom_id}/tables/{table_id}/department-permissions
範圍
| 範圍 | 是否提供 | 驗證與權限 |
|---|---|---|
| 聊天室 | 可使用 | CustomTableModeratorRequired |
| 部門 | 未提供 | 不適用 |
| 公司 | 未提供 | 不適用 |
參數
| 欄位 | 位置 | 型別 | 必填 | 說明 |
|---|---|---|---|---|
table_id | path | uuid | 是 | 要列出部門授權的聊天室資料表 UUID。 |
回應結構
DepartmentPermissionListResponse| 欄位 | 型別 | 必填 | 說明 |
|---|---|---|---|
departments | DepartmentPermissionResponse[] | 是 | 未指定 |
範例
列出獲授權使用聊天室資料表的部門
{
"pathParams": {
"chatroom_id": "11111111-1111-4111-8111-111111111111",
"table_id": "22222222-2222-4222-8222-222222222222"
}
}{
"departments": [
{
"department_id": "33333333-3333-4333-8333-333333333333",
"department_name": "營運部",
"can_read": "filtered",
"can_insert": true,
"can_edit": "own",
"read_filter": {
"column": "col_88888888_8888_4888_8888_888888888888",
"op": "eq",
"value": "北區"
},
"edit_filter": null,
"visible_columns": null,
"granted_by": {
"id": "55555555-5555-4555-8555-555555555555",
"created_at": "2026-07-19T02:00:00",
"expired_at": "2027-07-19T02:00:00",
"photo_url": null,
"enabled": true,
"is_verified": true,
"username": "catalog.manager",
"email": "manager@example.invalid",
"nickname": "林主管",
"department_id": "33333333-3333-4333-8333-333333333333",
"department_name": "營運部"
},
"granted_at": "2026-07-19T02:05:00"
}
]
}錯誤
驗證錯誤範例列出識別欄位;部署中的驗證器也可能回傳 input 與 ctx。錯誤訊息 URL 的版本由建置決定,因此使用 pydantic-version 佔位符。
| 狀態 | 發生條件 | 錯誤本文 |
|---|---|---|
| 404 | 資料表 id 不存在或已在垃圾桶(版主權限相依),或資料表所屬聊天室已被刪除(`Chatroom Not found`)。此路由不會檢查資料表是否屬於路徑中的聊天室:呼叫者在其他聊天室擔任版主的資料表,其授權會被列出,而不是拒絕。 | |
/private/module/custom_tables/{scope}/tables/{table_id}/department-permissions/{department_id}在聊天室資料表建立或取代一筆部門共用權限。
同一部門的成員需要對聊天室資料表共用基礎權限時使用。
- 聊天室
/private/module/custom_tables/chatroom/{chatroom_id}/tables/{table_id}/department-permissions/{department_id}
範圍
| 範圍 | 是否提供 | 驗證與權限 |
|---|---|---|
| 聊天室 | 可使用 | CustomTableModeratorRequired |
| 部門 | 未提供 | 不適用 |
| 公司 | 未提供 | 不適用 |
參數
| 欄位 | 位置 | 型別 | 必填 | 說明 |
|---|---|---|---|---|
table_id | path | uuid | 是 | 要加入授權的聊天室資料表 UUID。 |
department_id | path | uuid | 是 | 要取得存取權的目標部門 UUID,與聊天室範圍 ID 不同。 |
請求結構
DepartmentPermissionPayload| 欄位 | 型別 | 必填 | 說明 |
|---|---|---|---|
can_edit | "none" | "own" | "all" | "filtered" | 否 | Edit access: "none" | "own" | "all" | "filtered" |
can_insert | boolean | 否 | Insert permission |
can_read | "none" | "own" | "all" | "filtered" | 否 | Read access: "none" | "own" | "all" | "filtered" |
edit_filter | object | null | 否 | v1.2 §11 row policy — required when can_edit="filtered". Token grammar is identical to read_filter: A row policy is a boolean tree over scalar predicates: a node is {"and": [node, ...]} (every child holds), {"or": [node, ...]} (at least one holds), {"not": node} (strict complement) or a predicate {"column": "col_<hex>", "op": "eq|neq|gt|gte|lt|lte|in|contains|is_null|is_not_null", "value": ...}. Groups are non-empty; a node carries exactly its kind's keys; the root may be any node (the legacy {"and": [preds]} is one such tree). Caps: depth <= 5 (a bare predicate is depth 1), <= 24 predicates, <= 100 entries per "in" list (caps bind AUTHORED policies; the server may compose wider resolved trees). Predicates reference internal column keys of stored scalar columns only. An empty/unset cell fails every value op and matches only is_null, so {"not": {col eq X}} INCLUDES blank cells while {col neq X} excludes them. Unresolvable tokens and malformed nodes always NARROW the row set (never-match under "and"/"not", dropped under "or"), never widen it. A node may also be a link_target leaf {"link": "col_<hex> of a LINK column of this table", "quantifier": "any"|"all", "target": node, "require_present": bool} — it follows the link ONE hop and evaluates "target" (any row-policy node over the LINKED table's stored scalar columns; no link node inside it: 1-hop) against the linked rows: "any" = at least one linked row matches, "all" = every linked row matches (vacuously true when none — require_present MUST be stated on "all" and must be absent on "any"). Only LIVE (not trashed) linked rows count and the linked table's SCP channel floor applies; the linked table's own row/column ACL does NOT. Tokens inside "target" resolve against the LINKED column's type ("$me" on its user column = rows assigned to me). Caps: <= 6 link leaves; a target costs one depth level and its predicates count toward the 24. A {"link", "op", "value"} membership leaf is NOT accepted — write link_target with a target {"column": "id", "op": "in", "value": [...]}. Example: {"or": [{"column": "col_a1", "op": "eq", "value": "TW"}, {"not": {"column": "col_b2", "op": "eq", "value": "$me"}}]}. Predicate values may use per-request tokens (stored RAW, substituted at permission-resolution time): "$me" = the acting principal's own id (user id; client id for social clients; the linked user's id for agent-platform clients) — ops eq/neq or as an element of an "in" list, on user/social_client/principal/string/text columns; on a principal column "$me" is a SET — the caller's own user:<id> plus room:<id> for every live chatroom they belong to (REST) or the acting chatroom only inside an agent / command session; social clients match smc:<id> only; a public-read token never matches — compiled as an "in" over tagged cells ("neq" excludes blank cells); "$me.department" = the acting user's department id — string/text columns; on a principal column it is a SET: user:<id> for every live user of that department plus room:<id> for every live chatroom of that department (the acting chatroom only inside an agent / command session, and only if it belongs to the department) — matches NO rows for clients; "$today"/"$today+Nd"/"$today-Nd" (N 1..730, day boundary of the company business timezone when configured, otherwise Asia/Taipei) — date columns with eq/neq/gt/gte/lt/lte, datetime columns with gt/gte/lt/lte only (an eq day is unsatisfiable — author gte "$today" AND lt "$today+1d"), never inside "in" lists; "$now" = the current wall-clock instant of the company business timezone when configured, otherwise Asia/Taipei, at minute precision — datetime columns with gt/gte/lt/lte only (never eq/neq, never a date column — use "$today" there — never inside an "in" list). Any other "$"-prefixed value is rejected (reserved). Grant endpoints echo the RAW token, never a resolved literal. Unlike "own" (fixed to the record creator), "$me" may target any legal column — e.g. an assigned-owner column. A token whose principal kind never matches the column (a user-audience "$me" on a social_client column) is legal but matches nothing. |
read_filter | object | null | 否 | v1.2 §11 row policy — required when can_read="filtered". A row policy is a boolean tree over scalar predicates: a node is {"and": [node, ...]} (every child holds), {"or": [node, ...]} (at least one holds), {"not": node} (strict complement) or a predicate {"column": "col_<hex>", "op": "eq|neq|gt|gte|lt|lte|in|contains|is_null|is_not_null", "value": ...}. Groups are non-empty; a node carries exactly its kind's keys; the root may be any node (the legacy {"and": [preds]} is one such tree). Caps: depth <= 5 (a bare predicate is depth 1), <= 24 predicates, <= 100 entries per "in" list (caps bind AUTHORED policies; the server may compose wider resolved trees). Predicates reference internal column keys of stored scalar columns only. An empty/unset cell fails every value op and matches only is_null, so {"not": {col eq X}} INCLUDES blank cells while {col neq X} excludes them. Unresolvable tokens and malformed nodes always NARROW the row set (never-match under "and"/"not", dropped under "or"), never widen it. A node may also be a link_target leaf {"link": "col_<hex> of a LINK column of this table", "quantifier": "any"|"all", "target": node, "require_present": bool} — it follows the link ONE hop and evaluates "target" (any row-policy node over the LINKED table's stored scalar columns; no link node inside it: 1-hop) against the linked rows: "any" = at least one linked row matches, "all" = every linked row matches (vacuously true when none — require_present MUST be stated on "all" and must be absent on "any"). Only LIVE (not trashed) linked rows count and the linked table's SCP channel floor applies; the linked table's own row/column ACL does NOT. Tokens inside "target" resolve against the LINKED column's type ("$me" on its user column = rows assigned to me). Caps: <= 6 link leaves; a target costs one depth level and its predicates count toward the 24. A {"link", "op", "value"} membership leaf is NOT accepted — write link_target with a target {"column": "id", "op": "in", "value": [...]}. Example: {"or": [{"column": "col_a1", "op": "eq", "value": "TW"}, {"not": {"column": "col_b2", "op": "eq", "value": "$me"}}]}. Predicate values may use per-request tokens (stored RAW, substituted at permission-resolution time): "$me" = the acting principal's own id (user id; client id for social clients; the linked user's id for agent-platform clients) — ops eq/neq or as an element of an "in" list, on user/social_client/principal/string/text columns; on a principal column "$me" is a SET — the caller's own user:<id> plus room:<id> for every live chatroom they belong to (REST) or the acting chatroom only inside an agent / command session; social clients match smc:<id> only; a public-read token never matches — compiled as an "in" over tagged cells ("neq" excludes blank cells); "$me.department" = the acting user's department id — string/text columns; on a principal column it is a SET: user:<id> for every live user of that department plus room:<id> for every live chatroom of that department (the acting chatroom only inside an agent / command session, and only if it belongs to the department) — matches NO rows for clients; "$today"/"$today+Nd"/"$today-Nd" (N 1..730, day boundary of the company business timezone when configured, otherwise Asia/Taipei) — date columns with eq/neq/gt/gte/lt/lte, datetime columns with gt/gte/lt/lte only (an eq day is unsatisfiable — author gte "$today" AND lt "$today+1d"), never inside "in" lists; "$now" = the current wall-clock instant of the company business timezone when configured, otherwise Asia/Taipei, at minute precision — datetime columns with gt/gte/lt/lte only (never eq/neq, never a date column — use "$today" there — never inside an "in" list). Any other "$"-prefixed value is rejected (reserved). Grant endpoints echo the RAW token, never a resolved literal. Unlike "own" (fixed to the record creator), "$me" may target any legal column — e.g. an assigned-owner column. A token whose principal kind never matches the column (a user-audience "$me" on a social_client column) is legal but matches nothing. |
visible_columns | string[] | null | 否 | Column allowlist: INTERNAL column keys (col_<hex>) visible under this grant; all other columns are hidden (read strip + write reject + formula/export/view/history scoping). null/omitted = all columns (v1.2 column_acl still applies on top). Empty list rejected (422); unknown keys rejected (422). |
回應結構
SuccessResponse| 欄位 | 型別 | 必填 | 說明 |
|---|---|---|---|
message | string | 否 | 未指定 |
範例
授予部門使用聊天室資料表的權限
{
"pathParams": {
"chatroom_id": "11111111-1111-4111-8111-111111111111",
"table_id": "22222222-2222-4222-8222-222222222222",
"department_id": "33333333-3333-4333-8333-333333333333"
},
"body": {
"can_read": "all",
"can_insert": true,
"can_edit": "own",
"visible_columns": [
"col_88888888_8888_4888_8888_888888888888"
]
}
}{
"message": "Department permissions updated successfully"
}錯誤
驗證錯誤範例列出識別欄位;部署中的驗證器也可能回傳 input 與 ctx。錯誤訊息 URL 的版本由建置決定,因此使用 pydantic-version 佔位符。
| 狀態 | 發生條件 | 錯誤本文 |
|---|---|---|
| 404 | 目標部門不在呼叫者的公司中(以單一查詢檢查,因此不存在與其他公司的 id 得到相同回應)。 | |
| 404 | 資料表 id 不存在或已在垃圾桶,或資料表不在路徑指定的聊天室中。 | |
| 422 | 權限、篩選條件或可見欄位無效。 | |
/private/module/custom_tables/{scope}/tables/{table_id}/department-permissions/{department_id}取代聊天室資料表上既有的部門共用權限。
部門授權已存在,且要以 PATCH 明確調整時使用。
- 聊天室
/private/module/custom_tables/chatroom/{chatroom_id}/tables/{table_id}/department-permissions/{department_id}
範圍
| 範圍 | 是否提供 | 驗證與權限 |
|---|---|---|
| 聊天室 | 可使用 | CustomTableModeratorRequired |
| 部門 | 未提供 | 不適用 |
| 公司 | 未提供 | 不適用 |
參數
| 欄位 | 位置 | 型別 | 必填 | 說明 |
|---|---|---|---|---|
table_id | path | uuid | 是 | 擁有該授權的聊天室資料表 UUID。 |
department_id | path | uuid | 是 | 要調整授權的目標部門 UUID。 |
請求結構
DepartmentPermissionPayload| 欄位 | 型別 | 必填 | 說明 |
|---|---|---|---|
can_edit | "none" | "own" | "all" | "filtered" | 否 | Edit access: "none" | "own" | "all" | "filtered" |
can_insert | boolean | 否 | Insert permission |
can_read | "none" | "own" | "all" | "filtered" | 否 | Read access: "none" | "own" | "all" | "filtered" |
edit_filter | object | null | 否 | v1.2 §11 row policy — required when can_edit="filtered". Token grammar is identical to read_filter: A row policy is a boolean tree over scalar predicates: a node is {"and": [node, ...]} (every child holds), {"or": [node, ...]} (at least one holds), {"not": node} (strict complement) or a predicate {"column": "col_<hex>", "op": "eq|neq|gt|gte|lt|lte|in|contains|is_null|is_not_null", "value": ...}. Groups are non-empty; a node carries exactly its kind's keys; the root may be any node (the legacy {"and": [preds]} is one such tree). Caps: depth <= 5 (a bare predicate is depth 1), <= 24 predicates, <= 100 entries per "in" list (caps bind AUTHORED policies; the server may compose wider resolved trees). Predicates reference internal column keys of stored scalar columns only. An empty/unset cell fails every value op and matches only is_null, so {"not": {col eq X}} INCLUDES blank cells while {col neq X} excludes them. Unresolvable tokens and malformed nodes always NARROW the row set (never-match under "and"/"not", dropped under "or"), never widen it. A node may also be a link_target leaf {"link": "col_<hex> of a LINK column of this table", "quantifier": "any"|"all", "target": node, "require_present": bool} — it follows the link ONE hop and evaluates "target" (any row-policy node over the LINKED table's stored scalar columns; no link node inside it: 1-hop) against the linked rows: "any" = at least one linked row matches, "all" = every linked row matches (vacuously true when none — require_present MUST be stated on "all" and must be absent on "any"). Only LIVE (not trashed) linked rows count and the linked table's SCP channel floor applies; the linked table's own row/column ACL does NOT. Tokens inside "target" resolve against the LINKED column's type ("$me" on its user column = rows assigned to me). Caps: <= 6 link leaves; a target costs one depth level and its predicates count toward the 24. A {"link", "op", "value"} membership leaf is NOT accepted — write link_target with a target {"column": "id", "op": "in", "value": [...]}. Example: {"or": [{"column": "col_a1", "op": "eq", "value": "TW"}, {"not": {"column": "col_b2", "op": "eq", "value": "$me"}}]}. Predicate values may use per-request tokens (stored RAW, substituted at permission-resolution time): "$me" = the acting principal's own id (user id; client id for social clients; the linked user's id for agent-platform clients) — ops eq/neq or as an element of an "in" list, on user/social_client/principal/string/text columns; on a principal column "$me" is a SET — the caller's own user:<id> plus room:<id> for every live chatroom they belong to (REST) or the acting chatroom only inside an agent / command session; social clients match smc:<id> only; a public-read token never matches — compiled as an "in" over tagged cells ("neq" excludes blank cells); "$me.department" = the acting user's department id — string/text columns; on a principal column it is a SET: user:<id> for every live user of that department plus room:<id> for every live chatroom of that department (the acting chatroom only inside an agent / command session, and only if it belongs to the department) — matches NO rows for clients; "$today"/"$today+Nd"/"$today-Nd" (N 1..730, day boundary of the company business timezone when configured, otherwise Asia/Taipei) — date columns with eq/neq/gt/gte/lt/lte, datetime columns with gt/gte/lt/lte only (an eq day is unsatisfiable — author gte "$today" AND lt "$today+1d"), never inside "in" lists; "$now" = the current wall-clock instant of the company business timezone when configured, otherwise Asia/Taipei, at minute precision — datetime columns with gt/gte/lt/lte only (never eq/neq, never a date column — use "$today" there — never inside an "in" list). Any other "$"-prefixed value is rejected (reserved). Grant endpoints echo the RAW token, never a resolved literal. Unlike "own" (fixed to the record creator), "$me" may target any legal column — e.g. an assigned-owner column. A token whose principal kind never matches the column (a user-audience "$me" on a social_client column) is legal but matches nothing. |
read_filter | object | null | 否 | v1.2 §11 row policy — required when can_read="filtered". A row policy is a boolean tree over scalar predicates: a node is {"and": [node, ...]} (every child holds), {"or": [node, ...]} (at least one holds), {"not": node} (strict complement) or a predicate {"column": "col_<hex>", "op": "eq|neq|gt|gte|lt|lte|in|contains|is_null|is_not_null", "value": ...}. Groups are non-empty; a node carries exactly its kind's keys; the root may be any node (the legacy {"and": [preds]} is one such tree). Caps: depth <= 5 (a bare predicate is depth 1), <= 24 predicates, <= 100 entries per "in" list (caps bind AUTHORED policies; the server may compose wider resolved trees). Predicates reference internal column keys of stored scalar columns only. An empty/unset cell fails every value op and matches only is_null, so {"not": {col eq X}} INCLUDES blank cells while {col neq X} excludes them. Unresolvable tokens and malformed nodes always NARROW the row set (never-match under "and"/"not", dropped under "or"), never widen it. A node may also be a link_target leaf {"link": "col_<hex> of a LINK column of this table", "quantifier": "any"|"all", "target": node, "require_present": bool} — it follows the link ONE hop and evaluates "target" (any row-policy node over the LINKED table's stored scalar columns; no link node inside it: 1-hop) against the linked rows: "any" = at least one linked row matches, "all" = every linked row matches (vacuously true when none — require_present MUST be stated on "all" and must be absent on "any"). Only LIVE (not trashed) linked rows count and the linked table's SCP channel floor applies; the linked table's own row/column ACL does NOT. Tokens inside "target" resolve against the LINKED column's type ("$me" on its user column = rows assigned to me). Caps: <= 6 link leaves; a target costs one depth level and its predicates count toward the 24. A {"link", "op", "value"} membership leaf is NOT accepted — write link_target with a target {"column": "id", "op": "in", "value": [...]}. Example: {"or": [{"column": "col_a1", "op": "eq", "value": "TW"}, {"not": {"column": "col_b2", "op": "eq", "value": "$me"}}]}. Predicate values may use per-request tokens (stored RAW, substituted at permission-resolution time): "$me" = the acting principal's own id (user id; client id for social clients; the linked user's id for agent-platform clients) — ops eq/neq or as an element of an "in" list, on user/social_client/principal/string/text columns; on a principal column "$me" is a SET — the caller's own user:<id> plus room:<id> for every live chatroom they belong to (REST) or the acting chatroom only inside an agent / command session; social clients match smc:<id> only; a public-read token never matches — compiled as an "in" over tagged cells ("neq" excludes blank cells); "$me.department" = the acting user's department id — string/text columns; on a principal column it is a SET: user:<id> for every live user of that department plus room:<id> for every live chatroom of that department (the acting chatroom only inside an agent / command session, and only if it belongs to the department) — matches NO rows for clients; "$today"/"$today+Nd"/"$today-Nd" (N 1..730, day boundary of the company business timezone when configured, otherwise Asia/Taipei) — date columns with eq/neq/gt/gte/lt/lte, datetime columns with gt/gte/lt/lte only (an eq day is unsatisfiable — author gte "$today" AND lt "$today+1d"), never inside "in" lists; "$now" = the current wall-clock instant of the company business timezone when configured, otherwise Asia/Taipei, at minute precision — datetime columns with gt/gte/lt/lte only (never eq/neq, never a date column — use "$today" there — never inside an "in" list). Any other "$"-prefixed value is rejected (reserved). Grant endpoints echo the RAW token, never a resolved literal. Unlike "own" (fixed to the record creator), "$me" may target any legal column — e.g. an assigned-owner column. A token whose principal kind never matches the column (a user-audience "$me" on a social_client column) is legal but matches nothing. |
visible_columns | string[] | null | 否 | Column allowlist: INTERNAL column keys (col_<hex>) visible under this grant; all other columns are hidden (read strip + write reject + formula/export/view/history scoping). null/omitted = all columns (v1.2 column_acl still applies on top). Empty list rejected (422); unknown keys rejected (422). |
回應結構
SuccessResponse| 欄位 | 型別 | 必填 | 說明 |
|---|---|---|---|
message | string | 否 | 未指定 |
範例
降低現有部門授權
{
"pathParams": {
"chatroom_id": "11111111-1111-4111-8111-111111111111",
"table_id": "22222222-2222-4222-8222-222222222222",
"department_id": "33333333-3333-4333-8333-333333333333"
},
"body": {
"can_read": "own",
"can_insert": false,
"can_edit": "none"
}
}{
"message": "Department permissions updated successfully"
}錯誤
驗證錯誤範例列出識別欄位;部署中的驗證器也可能回傳 input 與 ctx。錯誤訊息 URL 的版本由建置決定,因此使用 pydantic-version 佔位符。
| 狀態 | 發生條件 | 錯誤本文 |
|---|---|---|
| 404 | 部門權限列不存在。 | |
| 422 | 權限、篩選條件或可見欄位無效(在查找授權列之後檢查)。 | |
/private/module/custom_tables/{scope}/tables/{table_id}/department-permissions/{department_id}刪除聊天室資料表上的部門明確 grant;這只移除該權限覆寫,不會刪除部門、資料表或其中資料。
部門不應再受到這筆直接共用授權時使用。
- 聊天室
/private/module/custom_tables/chatroom/{chatroom_id}/tables/{table_id}/department-permissions/{department_id}
範圍
| 範圍 | 是否提供 | 驗證與權限 |
|---|---|---|
| 聊天室 | 可使用 | CustomTableModeratorRequired |
| 部門 | 未提供 | 不適用 |
| 公司 | 未提供 | 不適用 |
參數
| 欄位 | 位置 | 型別 | 必填 | 說明 |
|---|---|---|---|---|
table_id | path | uuid | 是 | 擁有部門授權的聊天室資料表 UUID。 |
department_id | path | uuid | 是 | 要移除授權的目標部門 UUID。 |
回應結構
SuccessResponse| 欄位 | 型別 | 必填 | 說明 |
|---|---|---|---|
message | string | 否 | 未指定 |
範例
移除部門授權
{
"pathParams": {
"chatroom_id": "11111111-1111-4111-8111-111111111111",
"table_id": "22222222-2222-4222-8222-222222222222",
"department_id": "33333333-3333-4333-8333-333333333333"
}
}{
"message": "Department permissions revoked successfully"
}錯誤
驗證錯誤範例列出識別欄位;部署中的驗證器也可能回傳 input 與 ctx。錯誤訊息 URL 的版本由建置決定,因此使用 pydantic-version 佔位符。
| 狀態 | 發生條件 | 錯誤本文 |
|---|---|---|
| 404 | 部門權限列不存在。 | |
/private/module/custom_tables/{scope}/tables/{table_id}/department-permissions回傳 department/company scope 表上所有以「部門」為 principal 的授權列,附部門名稱、授權者與時間;沒有授權時 `departments` 為空陣列。
在放寬或撤銷前,用它稽核哪些部門取得了這張部門或公司資料表。
- 部門
/private/module/custom_tables/department/{department_id}/tables/{table_id}/department-permissions - 公司
/private/module/custom_tables/company/tables/{table_id}/department-permissions
範圍
| 範圍 | 是否提供 | 驗證與權限 |
|---|---|---|
| 聊天室 | 未提供 | 不適用 |
| 部門 | 可使用 | CustomTableModeratorRequired |
| 公司 | 可使用 | CustomTableModeratorRequired |
參數
| 欄位 | 位置 | 型別 | 必填 | 說明 |
|---|---|---|---|---|
table_id | path | uuid | 是 | 路由所指定、仍存活的 department/company scope 資料表 UUID。 |
回應結構
DepartmentPermissionListResponse| 欄位 | 型別 | 必填 | 說明 |
|---|---|---|---|
departments | DepartmentPermissionResponse[] | 是 | 未指定 |
範例
列出部門或公司資料表已授權的部門
{
"pathParams": {
"department_id": "33333333-3333-4333-8333-333333333333",
"table_id": "22222222-2222-4222-8222-222222222222"
}
}{
"departments": [
{
"department_id": "99999999-9999-4999-8999-999999999999",
"department_name": "客服部",
"can_read": "filtered",
"can_insert": true,
"can_edit": "own",
"read_filter": {
"column": "col_88888888_8888_4888_8888_888888888888",
"op": "eq",
"value": "南區"
},
"edit_filter": null,
"visible_columns": [
"col_88888888_8888_4888_8888_888888888888"
],
"granted_by": {
"id": "55555555-5555-4555-8555-555555555555",
"created_at": "2026-07-19T02:00:00",
"expired_at": "2027-07-19T02:00:00",
"photo_url": null,
"enabled": true,
"is_verified": true,
"username": "catalog.manager",
"email": "manager@example.invalid",
"nickname": "林主管",
"department_id": "33333333-3333-4333-8333-333333333333",
"department_name": "營運部"
},
"granted_at": "2026-07-19T02:05:00"
}
]
}錯誤
驗證錯誤範例列出識別欄位;部署中的驗證器也可能回傳 input 與 ctx。錯誤訊息 URL 的版本由建置決定,因此使用 pydantic-version 佔位符。
| 狀態 | 發生條件 | 錯誤本文 |
|---|---|---|
| 404 | 由版主權限相依在 id 不存在、資料表已在垃圾桶,或(自後端 5.21.0 起)資料表屬於其他公司時拋出(`CustomTable Not found`)。它一定先於路由自己的 scope 查詢執行,後者回的是 `Table not found`。 | |
| 404 | 呼叫者擔任版主、但屬於其他 scope 的現行資料表。 | |
/private/module/custom_tables/{scope}/tables/{table_id}/department-permissions/{target_department_id}在 department/company scope table 上為一個部門 upsert 完整 grant row,等同 IaC `grant` 行搭配 `principal.type: "department"`。
用來把部門或公司資料表分享給一個指定的同公司部門。若希望每個部門都繼承存取權、而不是逐部門建立授權列,請改設 `default_permissions.audience: "company"`——僅限部門資料表;公司資料表的預設權限本來就套用到所有同公司使用者,並會以 422 拒絕這個 audience。
- 部門
/private/module/custom_tables/department/{department_id}/tables/{table_id}/department-permissions/{target_department_id} - 公司
/private/module/custom_tables/company/tables/{table_id}/department-permissions/{target_department_id}
範圍
| 範圍 | 是否提供 | 驗證與權限 |
|---|---|---|
| 聊天室 | 未提供 | 不適用 |
| 部門 | 可使用 | CustomTableModeratorRequired |
| 公司 | 可使用 | CustomTableModeratorRequired |
參數
| 欄位 | 位置 | 型別 | 必填 | 說明 |
|---|---|---|---|---|
table_id | path | uuid | 是 | 擁有此授權的 department/company scope table UUID。 |
target_department_id | path | uuid | 是 | 接受授權的同公司部門;在 department route 上,它與 owning scope 的 `department_id` 不同。 |
請求結構
DepartmentPermissionPayload| 欄位 | 型別 | 必填 | 說明 |
|---|---|---|---|
can_edit | "none" | "own" | "all" | "filtered" | 否 | Edit access: "none" | "own" | "all" | "filtered" |
can_insert | boolean | 否 | Insert permission |
can_read | "none" | "own" | "all" | "filtered" | 否 | Read access: "none" | "own" | "all" | "filtered" |
edit_filter | object | null | 否 | v1.2 §11 row policy — required when can_edit="filtered". Token grammar is identical to read_filter: A row policy is a boolean tree over scalar predicates: a node is {"and": [node, ...]} (every child holds), {"or": [node, ...]} (at least one holds), {"not": node} (strict complement) or a predicate {"column": "col_<hex>", "op": "eq|neq|gt|gte|lt|lte|in|contains|is_null|is_not_null", "value": ...}. Groups are non-empty; a node carries exactly its kind's keys; the root may be any node (the legacy {"and": [preds]} is one such tree). Caps: depth <= 5 (a bare predicate is depth 1), <= 24 predicates, <= 100 entries per "in" list (caps bind AUTHORED policies; the server may compose wider resolved trees). Predicates reference internal column keys of stored scalar columns only. An empty/unset cell fails every value op and matches only is_null, so {"not": {col eq X}} INCLUDES blank cells while {col neq X} excludes them. Unresolvable tokens and malformed nodes always NARROW the row set (never-match under "and"/"not", dropped under "or"), never widen it. A node may also be a link_target leaf {"link": "col_<hex> of a LINK column of this table", "quantifier": "any"|"all", "target": node, "require_present": bool} — it follows the link ONE hop and evaluates "target" (any row-policy node over the LINKED table's stored scalar columns; no link node inside it: 1-hop) against the linked rows: "any" = at least one linked row matches, "all" = every linked row matches (vacuously true when none — require_present MUST be stated on "all" and must be absent on "any"). Only LIVE (not trashed) linked rows count and the linked table's SCP channel floor applies; the linked table's own row/column ACL does NOT. Tokens inside "target" resolve against the LINKED column's type ("$me" on its user column = rows assigned to me). Caps: <= 6 link leaves; a target costs one depth level and its predicates count toward the 24. A {"link", "op", "value"} membership leaf is NOT accepted — write link_target with a target {"column": "id", "op": "in", "value": [...]}. Example: {"or": [{"column": "col_a1", "op": "eq", "value": "TW"}, {"not": {"column": "col_b2", "op": "eq", "value": "$me"}}]}. Predicate values may use per-request tokens (stored RAW, substituted at permission-resolution time): "$me" = the acting principal's own id (user id; client id for social clients; the linked user's id for agent-platform clients) — ops eq/neq or as an element of an "in" list, on user/social_client/principal/string/text columns; on a principal column "$me" is a SET — the caller's own user:<id> plus room:<id> for every live chatroom they belong to (REST) or the acting chatroom only inside an agent / command session; social clients match smc:<id> only; a public-read token never matches — compiled as an "in" over tagged cells ("neq" excludes blank cells); "$me.department" = the acting user's department id — string/text columns; on a principal column it is a SET: user:<id> for every live user of that department plus room:<id> for every live chatroom of that department (the acting chatroom only inside an agent / command session, and only if it belongs to the department) — matches NO rows for clients; "$today"/"$today+Nd"/"$today-Nd" (N 1..730, day boundary of the company business timezone when configured, otherwise Asia/Taipei) — date columns with eq/neq/gt/gte/lt/lte, datetime columns with gt/gte/lt/lte only (an eq day is unsatisfiable — author gte "$today" AND lt "$today+1d"), never inside "in" lists; "$now" = the current wall-clock instant of the company business timezone when configured, otherwise Asia/Taipei, at minute precision — datetime columns with gt/gte/lt/lte only (never eq/neq, never a date column — use "$today" there — never inside an "in" list). Any other "$"-prefixed value is rejected (reserved). Grant endpoints echo the RAW token, never a resolved literal. Unlike "own" (fixed to the record creator), "$me" may target any legal column — e.g. an assigned-owner column. A token whose principal kind never matches the column (a user-audience "$me" on a social_client column) is legal but matches nothing. |
read_filter | object | null | 否 | v1.2 §11 row policy — required when can_read="filtered". A row policy is a boolean tree over scalar predicates: a node is {"and": [node, ...]} (every child holds), {"or": [node, ...]} (at least one holds), {"not": node} (strict complement) or a predicate {"column": "col_<hex>", "op": "eq|neq|gt|gte|lt|lte|in|contains|is_null|is_not_null", "value": ...}. Groups are non-empty; a node carries exactly its kind's keys; the root may be any node (the legacy {"and": [preds]} is one such tree). Caps: depth <= 5 (a bare predicate is depth 1), <= 24 predicates, <= 100 entries per "in" list (caps bind AUTHORED policies; the server may compose wider resolved trees). Predicates reference internal column keys of stored scalar columns only. An empty/unset cell fails every value op and matches only is_null, so {"not": {col eq X}} INCLUDES blank cells while {col neq X} excludes them. Unresolvable tokens and malformed nodes always NARROW the row set (never-match under "and"/"not", dropped under "or"), never widen it. A node may also be a link_target leaf {"link": "col_<hex> of a LINK column of this table", "quantifier": "any"|"all", "target": node, "require_present": bool} — it follows the link ONE hop and evaluates "target" (any row-policy node over the LINKED table's stored scalar columns; no link node inside it: 1-hop) against the linked rows: "any" = at least one linked row matches, "all" = every linked row matches (vacuously true when none — require_present MUST be stated on "all" and must be absent on "any"). Only LIVE (not trashed) linked rows count and the linked table's SCP channel floor applies; the linked table's own row/column ACL does NOT. Tokens inside "target" resolve against the LINKED column's type ("$me" on its user column = rows assigned to me). Caps: <= 6 link leaves; a target costs one depth level and its predicates count toward the 24. A {"link", "op", "value"} membership leaf is NOT accepted — write link_target with a target {"column": "id", "op": "in", "value": [...]}. Example: {"or": [{"column": "col_a1", "op": "eq", "value": "TW"}, {"not": {"column": "col_b2", "op": "eq", "value": "$me"}}]}. Predicate values may use per-request tokens (stored RAW, substituted at permission-resolution time): "$me" = the acting principal's own id (user id; client id for social clients; the linked user's id for agent-platform clients) — ops eq/neq or as an element of an "in" list, on user/social_client/principal/string/text columns; on a principal column "$me" is a SET — the caller's own user:<id> plus room:<id> for every live chatroom they belong to (REST) or the acting chatroom only inside an agent / command session; social clients match smc:<id> only; a public-read token never matches — compiled as an "in" over tagged cells ("neq" excludes blank cells); "$me.department" = the acting user's department id — string/text columns; on a principal column it is a SET: user:<id> for every live user of that department plus room:<id> for every live chatroom of that department (the acting chatroom only inside an agent / command session, and only if it belongs to the department) — matches NO rows for clients; "$today"/"$today+Nd"/"$today-Nd" (N 1..730, day boundary of the company business timezone when configured, otherwise Asia/Taipei) — date columns with eq/neq/gt/gte/lt/lte, datetime columns with gt/gte/lt/lte only (an eq day is unsatisfiable — author gte "$today" AND lt "$today+1d"), never inside "in" lists; "$now" = the current wall-clock instant of the company business timezone when configured, otherwise Asia/Taipei, at minute precision — datetime columns with gt/gte/lt/lte only (never eq/neq, never a date column — use "$today" there — never inside an "in" list). Any other "$"-prefixed value is rejected (reserved). Grant endpoints echo the RAW token, never a resolved literal. Unlike "own" (fixed to the record creator), "$me" may target any legal column — e.g. an assigned-owner column. A token whose principal kind never matches the column (a user-audience "$me" on a social_client column) is legal but matches nothing. |
visible_columns | string[] | null | 否 | Column allowlist: INTERNAL column keys (col_<hex>) visible under this grant; all other columns are hidden (read strip + write reject + formula/export/view/history scoping). null/omitted = all columns (v1.2 column_acl still applies on top). Empty list rejected (422); unknown keys rejected (422). |
回應結構
SuccessResponse| 欄位 | 型別 | 必填 | 說明 |
|---|---|---|---|
message | string | 否 | 未指定 |
範例
把部門或公司資料表分享給一個部門,並限制 row 與 column
{
"pathParams": {
"department_id": "33333333-3333-4333-8333-333333333333",
"table_id": "22222222-2222-4222-8222-222222222222",
"target_department_id": "99999999-9999-4999-8999-999999999999"
},
"body": {
"can_read": "filtered",
"read_filter": {
"and": [
{
"column": "col_88888888_8888_4888_8888_888888888888",
"op": "eq",
"value": "已出貨"
}
]
},
"can_insert": false,
"can_edit": "none",
"visible_columns": [
"col_88888888_8888_4888_8888_888888888888"
]
}
}{
"message": "Department permissions updated successfully"
}錯誤
驗證錯誤範例列出識別欄位;部署中的驗證器也可能回傳 input 與 ctx。錯誤訊息 URL 的版本由建置決定,因此使用 pydantic-version 佔位符。
| 狀態 | 發生條件 | 錯誤本文 |
|---|---|---|
| 404 | 資料表 id 不存在或已在垃圾桶(版主權限相依,先於路由的 scope 查詢)。 | |
| 404 | 呼叫者擔任版主、但不在路徑指定的部門或公司 scope 中的現行資料表。 | |
| 404 | 目標部門不存在於呼叫者的公司;POST 與 PATCH 都會執行這項 company-anchored 驗證。 | |
| 422 | `visible_columns` 中有不屬於這張表的欄位。請送 internal `col_<hex>` key;空陣列同樣會被拒絕,`null`/省略代表所有欄位。 | |
| 422 | Row policy 文法:level 是 `filtered` 卻沒給 filter,或在其他 level 上夾帶 filter。與所有 grant 介面同一套規則。 | |
/private/module/custom_tables/{scope}/tables/{table_id}/department-permissions/{target_department_id}取代既有的 department 授權列。Payload 是同一個 `DepartmentPermissionPayload`、語意與 POST 同樣是取代,唯一差別是該列必須已經存在。
當「授權列不存在」應該明確報錯、而不是被建立出來時使用。
- 部門
/private/module/custom_tables/department/{department_id}/tables/{table_id}/department-permissions/{target_department_id} - 公司
/private/module/custom_tables/company/tables/{table_id}/department-permissions/{target_department_id}
範圍
| 範圍 | 是否提供 | 驗證與權限 |
|---|---|---|
| 聊天室 | 未提供 | 不適用 |
| 部門 | 可使用 | CustomTableModeratorRequired |
| 公司 | 可使用 | CustomTableModeratorRequired |
參數
| 欄位 | 位置 | 型別 | 必填 | 說明 |
|---|---|---|---|---|
table_id | path | uuid | 是 | 擁有此授權的 department/company scope table UUID。 |
target_department_id | path | uuid | 是 | 要被取代授權列的部門。 |
請求結構
DepartmentPermissionPayload| 欄位 | 型別 | 必填 | 說明 |
|---|---|---|---|
can_edit | "none" | "own" | "all" | "filtered" | 否 | Edit access: "none" | "own" | "all" | "filtered" |
can_insert | boolean | 否 | Insert permission |
can_read | "none" | "own" | "all" | "filtered" | 否 | Read access: "none" | "own" | "all" | "filtered" |
edit_filter | object | null | 否 | v1.2 §11 row policy — required when can_edit="filtered". Token grammar is identical to read_filter: A row policy is a boolean tree over scalar predicates: a node is {"and": [node, ...]} (every child holds), {"or": [node, ...]} (at least one holds), {"not": node} (strict complement) or a predicate {"column": "col_<hex>", "op": "eq|neq|gt|gte|lt|lte|in|contains|is_null|is_not_null", "value": ...}. Groups are non-empty; a node carries exactly its kind's keys; the root may be any node (the legacy {"and": [preds]} is one such tree). Caps: depth <= 5 (a bare predicate is depth 1), <= 24 predicates, <= 100 entries per "in" list (caps bind AUTHORED policies; the server may compose wider resolved trees). Predicates reference internal column keys of stored scalar columns only. An empty/unset cell fails every value op and matches only is_null, so {"not": {col eq X}} INCLUDES blank cells while {col neq X} excludes them. Unresolvable tokens and malformed nodes always NARROW the row set (never-match under "and"/"not", dropped under "or"), never widen it. A node may also be a link_target leaf {"link": "col_<hex> of a LINK column of this table", "quantifier": "any"|"all", "target": node, "require_present": bool} — it follows the link ONE hop and evaluates "target" (any row-policy node over the LINKED table's stored scalar columns; no link node inside it: 1-hop) against the linked rows: "any" = at least one linked row matches, "all" = every linked row matches (vacuously true when none — require_present MUST be stated on "all" and must be absent on "any"). Only LIVE (not trashed) linked rows count and the linked table's SCP channel floor applies; the linked table's own row/column ACL does NOT. Tokens inside "target" resolve against the LINKED column's type ("$me" on its user column = rows assigned to me). Caps: <= 6 link leaves; a target costs one depth level and its predicates count toward the 24. A {"link", "op", "value"} membership leaf is NOT accepted — write link_target with a target {"column": "id", "op": "in", "value": [...]}. Example: {"or": [{"column": "col_a1", "op": "eq", "value": "TW"}, {"not": {"column": "col_b2", "op": "eq", "value": "$me"}}]}. Predicate values may use per-request tokens (stored RAW, substituted at permission-resolution time): "$me" = the acting principal's own id (user id; client id for social clients; the linked user's id for agent-platform clients) — ops eq/neq or as an element of an "in" list, on user/social_client/principal/string/text columns; on a principal column "$me" is a SET — the caller's own user:<id> plus room:<id> for every live chatroom they belong to (REST) or the acting chatroom only inside an agent / command session; social clients match smc:<id> only; a public-read token never matches — compiled as an "in" over tagged cells ("neq" excludes blank cells); "$me.department" = the acting user's department id — string/text columns; on a principal column it is a SET: user:<id> for every live user of that department plus room:<id> for every live chatroom of that department (the acting chatroom only inside an agent / command session, and only if it belongs to the department) — matches NO rows for clients; "$today"/"$today+Nd"/"$today-Nd" (N 1..730, day boundary of the company business timezone when configured, otherwise Asia/Taipei) — date columns with eq/neq/gt/gte/lt/lte, datetime columns with gt/gte/lt/lte only (an eq day is unsatisfiable — author gte "$today" AND lt "$today+1d"), never inside "in" lists; "$now" = the current wall-clock instant of the company business timezone when configured, otherwise Asia/Taipei, at minute precision — datetime columns with gt/gte/lt/lte only (never eq/neq, never a date column — use "$today" there — never inside an "in" list). Any other "$"-prefixed value is rejected (reserved). Grant endpoints echo the RAW token, never a resolved literal. Unlike "own" (fixed to the record creator), "$me" may target any legal column — e.g. an assigned-owner column. A token whose principal kind never matches the column (a user-audience "$me" on a social_client column) is legal but matches nothing. |
read_filter | object | null | 否 | v1.2 §11 row policy — required when can_read="filtered". A row policy is a boolean tree over scalar predicates: a node is {"and": [node, ...]} (every child holds), {"or": [node, ...]} (at least one holds), {"not": node} (strict complement) or a predicate {"column": "col_<hex>", "op": "eq|neq|gt|gte|lt|lte|in|contains|is_null|is_not_null", "value": ...}. Groups are non-empty; a node carries exactly its kind's keys; the root may be any node (the legacy {"and": [preds]} is one such tree). Caps: depth <= 5 (a bare predicate is depth 1), <= 24 predicates, <= 100 entries per "in" list (caps bind AUTHORED policies; the server may compose wider resolved trees). Predicates reference internal column keys of stored scalar columns only. An empty/unset cell fails every value op and matches only is_null, so {"not": {col eq X}} INCLUDES blank cells while {col neq X} excludes them. Unresolvable tokens and malformed nodes always NARROW the row set (never-match under "and"/"not", dropped under "or"), never widen it. A node may also be a link_target leaf {"link": "col_<hex> of a LINK column of this table", "quantifier": "any"|"all", "target": node, "require_present": bool} — it follows the link ONE hop and evaluates "target" (any row-policy node over the LINKED table's stored scalar columns; no link node inside it: 1-hop) against the linked rows: "any" = at least one linked row matches, "all" = every linked row matches (vacuously true when none — require_present MUST be stated on "all" and must be absent on "any"). Only LIVE (not trashed) linked rows count and the linked table's SCP channel floor applies; the linked table's own row/column ACL does NOT. Tokens inside "target" resolve against the LINKED column's type ("$me" on its user column = rows assigned to me). Caps: <= 6 link leaves; a target costs one depth level and its predicates count toward the 24. A {"link", "op", "value"} membership leaf is NOT accepted — write link_target with a target {"column": "id", "op": "in", "value": [...]}. Example: {"or": [{"column": "col_a1", "op": "eq", "value": "TW"}, {"not": {"column": "col_b2", "op": "eq", "value": "$me"}}]}. Predicate values may use per-request tokens (stored RAW, substituted at permission-resolution time): "$me" = the acting principal's own id (user id; client id for social clients; the linked user's id for agent-platform clients) — ops eq/neq or as an element of an "in" list, on user/social_client/principal/string/text columns; on a principal column "$me" is a SET — the caller's own user:<id> plus room:<id> for every live chatroom they belong to (REST) or the acting chatroom only inside an agent / command session; social clients match smc:<id> only; a public-read token never matches — compiled as an "in" over tagged cells ("neq" excludes blank cells); "$me.department" = the acting user's department id — string/text columns; on a principal column it is a SET: user:<id> for every live user of that department plus room:<id> for every live chatroom of that department (the acting chatroom only inside an agent / command session, and only if it belongs to the department) — matches NO rows for clients; "$today"/"$today+Nd"/"$today-Nd" (N 1..730, day boundary of the company business timezone when configured, otherwise Asia/Taipei) — date columns with eq/neq/gt/gte/lt/lte, datetime columns with gt/gte/lt/lte only (an eq day is unsatisfiable — author gte "$today" AND lt "$today+1d"), never inside "in" lists; "$now" = the current wall-clock instant of the company business timezone when configured, otherwise Asia/Taipei, at minute precision — datetime columns with gt/gte/lt/lte only (never eq/neq, never a date column — use "$today" there — never inside an "in" list). Any other "$"-prefixed value is rejected (reserved). Grant endpoints echo the RAW token, never a resolved literal. Unlike "own" (fixed to the record creator), "$me" may target any legal column — e.g. an assigned-owner column. A token whose principal kind never matches the column (a user-audience "$me" on a social_client column) is legal but matches nothing. |
visible_columns | string[] | null | 否 | Column allowlist: INTERNAL column keys (col_<hex>) visible under this grant; all other columns are hidden (read strip + write reject + formula/export/view/history scoping). null/omitted = all columns (v1.2 column_acl still applies on top). Empty list rejected (422); unknown keys rejected (422). |
回應結構
SuccessResponse| 欄位 | 型別 | 必填 | 說明 |
|---|---|---|---|
message | string | 否 | 未指定 |
範例
把既有的 department 授權收窄成只看自己的列
{
"pathParams": {
"department_id": "33333333-3333-4333-8333-333333333333",
"table_id": "22222222-2222-4222-8222-222222222222",
"target_department_id": "99999999-9999-4999-8999-999999999999"
},
"body": {
"can_read": "own",
"can_insert": false,
"can_edit": "none"
}
}{
"message": "Department permissions updated successfully"
}錯誤
驗證錯誤範例列出識別欄位;部署中的驗證器也可能回傳 input 與 ctx。錯誤訊息 URL 的版本由建置決定,因此使用 pydantic-version 佔位符。
| 狀態 | 發生條件 | 錯誤本文 |
|---|---|---|
| 404 | 資料表 id 不存在或已在垃圾桶(版主權限相依,先於路由的 scope 查詢)。 | |
| 404 | 呼叫者擔任版主、但不在路徑指定的部門或公司 scope 中的現行資料表。 | |
| 404 | 這張表上沒有該部門的授權列。PATCH 永遠不會建立新列——請改用 POST。 | |
| 404 | 授權列存在,但它的部門已無法在呼叫者的公司中解析。 | |
| 422 | Row policy 文法錯誤或未知的 `visible_columns` key,驗證方式與 POST 完全相同。 | |
/private/module/custom_tables/{scope}/tables/{table_id}/department-permissions/{target_department_id}從 department/company scope table 刪除一筆明確的 department grant row。
當另一個部門不該再透過這一列繼承存取權時使用。
- 部門
/private/module/custom_tables/department/{department_id}/tables/{table_id}/department-permissions/{target_department_id} - 公司
/private/module/custom_tables/company/tables/{table_id}/department-permissions/{target_department_id}
範圍
| 範圍 | 是否提供 | 驗證與權限 |
|---|---|---|
| 聊天室 | 未提供 | 不適用 |
| 部門 | 可使用 | CustomTableModeratorRequired |
| 公司 | 可使用 | CustomTableModeratorRequired |
參數
| 欄位 | 位置 | 型別 | 必填 | 說明 |
|---|---|---|---|---|
table_id | path | uuid | 是 | 擁有此授權的 department/company scope table UUID。 |
target_department_id | path | uuid | 是 | 要移除授權列的部門。 |
回應結構
SuccessResponse| 欄位 | 型別 | 必填 | 說明 |
|---|---|---|---|
message | string | 否 | 未指定 |
範例
移除部門或公司資料表上的一筆 department grant
{
"pathParams": {
"department_id": "33333333-3333-4333-8333-333333333333",
"table_id": "22222222-2222-4222-8222-222222222222",
"target_department_id": "99999999-9999-4999-8999-999999999999"
}
}{
"message": "Department permissions revoked successfully"
}錯誤
驗證錯誤範例列出識別欄位;部署中的驗證器也可能回傳 input 與 ctx。錯誤訊息 URL 的版本由建置決定,因此使用 pydantic-version 佔位符。
| 狀態 | 發生條件 | 錯誤本文 |
|---|---|---|
| 404 | 資料表 id 不存在或已在垃圾桶(版主權限相依,先於路由的 scope 查詢)。 | |
| 404 | 呼叫者擔任版主、但不在路徑指定的部門或公司 scope 中的現行資料表。 | |
| 404 | 這張表上沒有該部門的授權列;對同一列 DELETE 第二次就會拿到這個。 | |
/private/module/custom_tables/{scope}/tables/{table_id}/moderators回傳部門或公司資料表的版主:明確指定的版主(僅限現行使用者),加上預設版主——部門資料表為該部門中 role ≥ `CAN_MANAGE_DEPARTMENT`(2)的所有使用者;公司資料表為公司中 role ≥ `CAN_MANAGE_COMPANY`(3)的所有使用者。
用來顯示誰在管理共享範圍的資料表。它不是完整的權限稽核:其他部門的公司管理者同樣能管理部門資料表,卻不會出現在這裡。
- 部門
/private/module/custom_tables/department/{department_id}/tables/{table_id}/moderators - 公司
/private/module/custom_tables/company/tables/{table_id}/moderators
範圍
| 範圍 | 是否提供 | 驗證與權限 |
|---|---|---|
| 聊天室 | 未提供 | 不適用 |
| 部門 | 可使用 | DepartmentTableMetadataReadRequired |
| 公司 | 可使用 | CompanyTableMetadataReadRequired |
參數
| 欄位 | 位置 | 型別 | 必填 | 說明 |
|---|---|---|---|---|
table_id | path | uuid | 是 | 部門或公司資料表 UUID。 |
回應結構
List[ModeratorResponse]OpenAPI 快照中沒有此模型的頂層元件欄位。
範例
列出部門資料表的管理者
{
"pathParams": {
"department_id": "33333333-3333-4333-8333-333333333333",
"table_id": "22222222-2222-4222-8222-222222222222"
}
}[
{
"id": "55555555-5555-4555-8555-555555555555",
"created_at": "2026-07-19T02:00:00",
"expired_at": "2027-07-19T02:00:00",
"photo_url": null,
"enabled": true,
"is_verified": true,
"username": "catalog.manager",
"email": "manager@example.invalid",
"nickname": "林主管",
"department_id": "33333333-3333-4333-8333-333333333333",
"department_name": "營運部"
}
]錯誤
驗證錯誤範例列出識別欄位;部署中的驗證器也可能回傳 input 與 ctx。錯誤訊息 URL 的版本由建置決定,因此使用 pydantic-version 佔位符。
| 狀態 | 發生條件 | 錯誤本文 |
|---|---|---|
| 404 | 資料表不存在或已在垃圾桶、屬於其他 scope 或(自後端 5.21.0 起)其他公司,或你對它完全沒有權利(不是管理者、沒有讀取/編輯/新增權利,也不是版主)。以上全部回應得與 id 不存在完全相同。 | |
/private/module/custom_tables/{scope}/tables/{table_id}/moderators/{user_id}將一位同公司的真人使用者(服務帳號會被拒絕)設為部門或公司資料表的明確版主。
要將共用範圍資料表的結構、規則與權限管理委派給他人時使用。
- 部門
/private/module/custom_tables/department/{department_id}/tables/{table_id}/moderators/{user_id} - 公司
/private/module/custom_tables/company/tables/{table_id}/moderators/{user_id}
範圍
| 範圍 | 是否提供 | 驗證與權限 |
|---|---|---|
| 聊天室 | 未提供 | 不適用 |
| 部門 | 可使用 | CustomTableModeratorRequired |
| 公司 | 可使用 | CustomTableModeratorRequired |
參數
| 欄位 | 位置 | 型別 | 必填 | 說明 |
|---|---|---|---|---|
table_id | path | uuid | 是 | 部門或公司資料表 UUID。 |
user_id | path | uuid | 是 | 要設為管理者的使用者 UUID。 |
回應結構
SuccessResponse| 欄位 | 型別 | 必填 | 說明 |
|---|---|---|---|
message | string | 否 | 未指定 |
範例
新增部門資料表管理者
{
"pathParams": {
"department_id": "33333333-3333-4333-8333-333333333333",
"table_id": "22222222-2222-4222-8222-222222222222",
"user_id": "44444444-4444-4444-8444-444444444444"
}
}{
"message": "Moderator added successfully"
}錯誤
驗證錯誤範例列出識別欄位;部署中的驗證器也可能回傳 input 與 ctx。錯誤訊息 URL 的版本由建置決定,因此使用 pydantic-version 佔位符。
| 狀態 | 發生條件 | 錯誤本文 |
|---|---|---|
| 400 | 這位使用者已是管理者。 | |
| 400 | 目標是部門服務帳號(虛擬使用者)。服務帳號不能擔任資料表版主;這項檢查在查找使用者之後、「已是版主」檢查之前執行。 | |
| 404 | 該表的 effective company 內沒有這個 id 的使用者。現在所有 scope 都是同一句訊息;department scope 以前會回 `User not found or not in this department`,並拒絕來自其他部門的同公司使用者。 | |
| 404 | 這張表不在被指定的 scope 內(company 介面上是 `Table not found in this company`)。與目標使用者那個 404 是不同的一種。 | |
/private/module/custom_tables/{scope}/tables/{table_id}/moderators/{user_id}從部門或公司資料表刪除一筆明確管理者指派。
明確的代理管理應結束時使用,之後請重新檢查有效權限:該表所屬部門的部門管理者(role ≥ 2)與公司管理者(role ≥ 3)仍是隱含版主。
- 部門
/private/module/custom_tables/department/{department_id}/tables/{table_id}/moderators/{user_id} - 公司
/private/module/custom_tables/company/tables/{table_id}/moderators/{user_id}
範圍
| 範圍 | 是否提供 | 驗證與權限 |
|---|---|---|
| 聊天室 | 未提供 | 不適用 |
| 部門 | 可使用 | CustomTableModeratorRequired |
| 公司 | 可使用 | CustomTableModeratorRequired |
參數
| 欄位 | 位置 | 型別 | 必填 | 說明 |
|---|---|---|---|---|
table_id | path | uuid | 是 | 部門或公司資料表 UUID。 |
user_id | path | uuid | 是 | 要移除的管理者 UUID。 |
回應結構
SuccessResponse| 欄位 | 型別 | 必填 | 說明 |
|---|---|---|---|
message | string | 否 | 未指定 |
範例
移除部門資料表管理者
{
"pathParams": {
"department_id": "33333333-3333-4333-8333-333333333333",
"table_id": "22222222-2222-4222-8222-222222222222",
"user_id": "44444444-4444-4444-8444-444444444444"
}
}{
"message": "Moderator removed successfully"
}錯誤
驗證錯誤範例列出識別欄位;部署中的驗證器也可能回傳 input 與 ctx。錯誤訊息 URL 的版本由建置決定,因此使用 pydantic-version 佔位符。
| 狀態 | 發生條件 | 錯誤本文 |
|---|---|---|
| 404 | 這位使用者不是此資料表的明確管理者。 | |
| 404 | 這張表不在被指定的 scope 內(company 介面上是 `Table not found in this company`)。 | |
/private/module/custom_tables/{scope}/tables/{table_id}/chatroom-permissions列出可存取某張部門資料表的聊天室與受眾。
適合在變更或撤銷存取權前,稽核部門資料表已分享至哪些地方。
- 部門
/private/module/custom_tables/department/{department_id}/tables/{table_id}/chatroom-permissions
範圍
| 範圍 | 是否提供 | 驗證與權限 |
|---|---|---|
| 聊天室 | 未提供 | 不適用 |
| 部門 | 可使用 | CustomTableModeratorRequired |
| 公司 | 未提供 | 不適用 |
參數
| 欄位 | 位置 | 型別 | 必填 | 說明 |
|---|---|---|---|---|
table_id | path | uuid | 是 | 要列出聊天室授權的部門資料表 UUID。 |
回應結構
ChatroomGrantListResponse| 欄位 | 型別 | 必填 | 說明 |
|---|---|---|---|
grants | ChatroomGrantResponse[] | 是 | Grant rows |
範例
列出共用某張部門資料表的聊天室
{
"pathParams": {
"department_id": "33333333-3333-4333-8333-333333333333",
"table_id": "22222222-2222-4222-8222-222222222222"
}
}{
"grants": [
{
"chatroom_id": "11111111-1111-4111-8111-111111111111",
"chatroom_name": "訂單協作室",
"audience": "internal",
"can_read": "all",
"can_insert": true,
"can_edit": "own",
"read_filter": null,
"edit_filter": null,
"visible_columns": [
"col_88888888_8888_4888_8888_888888888888"
],
"scope_values": null,
"granted_by": {
"id": "55555555-5555-4555-8555-555555555555",
"created_at": "2026-07-19T02:00:00",
"expired_at": "2027-07-19T02:00:00",
"photo_url": null,
"enabled": true,
"is_verified": true,
"username": "catalog.manager",
"email": "manager@example.invalid",
"nickname": "林主管",
"department_id": "33333333-3333-4333-8333-333333333333",
"department_name": "營運部"
},
"granted_at": "2026-07-19T02:05:00"
}
]
}錯誤
驗證錯誤範例列出識別欄位;部署中的驗證器也可能回傳 input 與 ctx。錯誤訊息 URL 的版本由建置決定,因此使用 pydantic-version 佔位符。
| 狀態 | 發生條件 | 錯誤本文 |
|---|---|---|
| 404 | 路徑指定部門之外的現行資料表。資料表 id 不存在或已在垃圾桶時,會先由版主門檻回 `404 CustomTable Not found`。 | |
/private/module/custom_tables/{scope}/tables/{table_id}/chatroom-permissions/{chatroom_id}在部門資料表建立或取代一筆按受眾區分的聊天室授權。
要把部門資料分享給聊天室的內部成員、外部用戶端,或兩種受眾時使用。
- 部門
/private/module/custom_tables/department/{department_id}/tables/{table_id}/chatroom-permissions/{chatroom_id}
範圍
| 範圍 | 是否提供 | 驗證與權限 |
|---|---|---|
| 聊天室 | 未提供 | 不適用 |
| 部門 | 可使用 | CustomTableModeratorRequired |
| 公司 | 未提供 | 不適用 |
參數
| 欄位 | 位置 | 型別 | 必填 | 說明 |
|---|---|---|---|---|
table_id | path | uuid | 是 | 要分享的部門資料表 UUID。 |
chatroom_id | path | uuid | 是 | 要取得存取權的目標聊天室 UUID,與部門範圍 ID 不同。 |
請求結構
ChatroomGrantPayload| 欄位 | 型別 | 必填 | 說明 |
|---|---|---|---|
audience | "internal" | "external" | 是 | Which audience this grant covers: 'internal' = the chatroom's member users; 'external' = its social media clients. |
can_edit | "none" | "own" | "all" | "filtered" | 否 | Edit level. 'filtered' requires edit_filter. |
can_insert | boolean | 否 | May the audience insert records. |
can_read | "none" | "own" | "all" | "filtered" | 否 | Read level. 'filtered' requires read_filter. |
edit_filter | object | null | 否 | Row policy limiting editable rows when can_edit='filtered'. Token grammar is identical to read_filter: A row policy is a boolean tree over scalar predicates: a node is {"and": [node, ...]} (every child holds), {"or": [node, ...]} (at least one holds), {"not": node} (strict complement) or a predicate {"column": "col_<hex>", "op": "eq|neq|gt|gte|lt|lte|in|contains|is_null|is_not_null", "value": ...}. Groups are non-empty; a node carries exactly its kind's keys; the root may be any node (the legacy {"and": [preds]} is one such tree). Caps: depth <= 5 (a bare predicate is depth 1), <= 24 predicates, <= 100 entries per "in" list (caps bind AUTHORED policies; the server may compose wider resolved trees). Predicates reference internal column keys of stored scalar columns only. An empty/unset cell fails every value op and matches only is_null, so {"not": {col eq X}} INCLUDES blank cells while {col neq X} excludes them. Unresolvable tokens and malformed nodes always NARROW the row set (never-match under "and"/"not", dropped under "or"), never widen it. A node may also be a link_target leaf {"link": "col_<hex> of a LINK column of this table", "quantifier": "any"|"all", "target": node, "require_present": bool} — it follows the link ONE hop and evaluates "target" (any row-policy node over the LINKED table's stored scalar columns; no link node inside it: 1-hop) against the linked rows: "any" = at least one linked row matches, "all" = every linked row matches (vacuously true when none — require_present MUST be stated on "all" and must be absent on "any"). Only LIVE (not trashed) linked rows count and the linked table's SCP channel floor applies; the linked table's own row/column ACL does NOT. Tokens inside "target" resolve against the LINKED column's type ("$me" on its user column = rows assigned to me). Caps: <= 6 link leaves; a target costs one depth level and its predicates count toward the 24. A {"link", "op", "value"} membership leaf is NOT accepted — write link_target with a target {"column": "id", "op": "in", "value": [...]}. Example: {"or": [{"column": "col_a1", "op": "eq", "value": "TW"}, {"not": {"column": "col_b2", "op": "eq", "value": "$me"}}]}. Predicate values may use per-request tokens (stored RAW, substituted at permission-resolution time): "$me" = the acting principal's own id (user id; client id for social clients; the linked user's id for agent-platform clients) — ops eq/neq or as an element of an "in" list, on user/social_client/principal/string/text columns; on a principal column "$me" is a SET — the caller's own user:<id> plus room:<id> for every live chatroom they belong to (REST) or the acting chatroom only inside an agent / command session; social clients match smc:<id> only; a public-read token never matches — compiled as an "in" over tagged cells ("neq" excludes blank cells); "$me.department" = the acting user's department id — string/text columns; on a principal column it is a SET: user:<id> for every live user of that department plus room:<id> for every live chatroom of that department (the acting chatroom only inside an agent / command session, and only if it belongs to the department) — matches NO rows for clients; "$today"/"$today+Nd"/"$today-Nd" (N 1..730, day boundary of the company business timezone when configured, otherwise Asia/Taipei) — date columns with eq/neq/gt/gte/lt/lte, datetime columns with gt/gte/lt/lte only (an eq day is unsatisfiable — author gte "$today" AND lt "$today+1d"), never inside "in" lists; "$now" = the current wall-clock instant of the company business timezone when configured, otherwise Asia/Taipei, at minute precision — datetime columns with gt/gte/lt/lte only (never eq/neq, never a date column — use "$today" there — never inside an "in" list). Any other "$"-prefixed value is rejected (reserved). Grant endpoints echo the RAW token, never a resolved literal. Unlike "own" (fixed to the record creator), "$me" may target any legal column — e.g. an assigned-owner column. A token whose principal kind never matches the column (a user-audience "$me" on a social_client column) is legal but matches nothing. |
read_filter | object | null | 否 | Row policy limiting readable rows when can_read='filtered'. A row policy is a boolean tree over scalar predicates: a node is {"and": [node, ...]} (every child holds), {"or": [node, ...]} (at least one holds), {"not": node} (strict complement) or a predicate {"column": "col_<hex>", "op": "eq|neq|gt|gte|lt|lte|in|contains|is_null|is_not_null", "value": ...}. Groups are non-empty; a node carries exactly its kind's keys; the root may be any node (the legacy {"and": [preds]} is one such tree). Caps: depth <= 5 (a bare predicate is depth 1), <= 24 predicates, <= 100 entries per "in" list (caps bind AUTHORED policies; the server may compose wider resolved trees). Predicates reference internal column keys of stored scalar columns only. An empty/unset cell fails every value op and matches only is_null, so {"not": {col eq X}} INCLUDES blank cells while {col neq X} excludes them. Unresolvable tokens and malformed nodes always NARROW the row set (never-match under "and"/"not", dropped under "or"), never widen it. A node may also be a link_target leaf {"link": "col_<hex> of a LINK column of this table", "quantifier": "any"|"all", "target": node, "require_present": bool} — it follows the link ONE hop and evaluates "target" (any row-policy node over the LINKED table's stored scalar columns; no link node inside it: 1-hop) against the linked rows: "any" = at least one linked row matches, "all" = every linked row matches (vacuously true when none — require_present MUST be stated on "all" and must be absent on "any"). Only LIVE (not trashed) linked rows count and the linked table's SCP channel floor applies; the linked table's own row/column ACL does NOT. Tokens inside "target" resolve against the LINKED column's type ("$me" on its user column = rows assigned to me). Caps: <= 6 link leaves; a target costs one depth level and its predicates count toward the 24. A {"link", "op", "value"} membership leaf is NOT accepted — write link_target with a target {"column": "id", "op": "in", "value": [...]}. Example: {"or": [{"column": "col_a1", "op": "eq", "value": "TW"}, {"not": {"column": "col_b2", "op": "eq", "value": "$me"}}]}. Predicate values may use per-request tokens (stored RAW, substituted at permission-resolution time): "$me" = the acting principal's own id (user id; client id for social clients; the linked user's id for agent-platform clients) — ops eq/neq or as an element of an "in" list, on user/social_client/principal/string/text columns; on a principal column "$me" is a SET — the caller's own user:<id> plus room:<id> for every live chatroom they belong to (REST) or the acting chatroom only inside an agent / command session; social clients match smc:<id> only; a public-read token never matches — compiled as an "in" over tagged cells ("neq" excludes blank cells); "$me.department" = the acting user's department id — string/text columns; on a principal column it is a SET: user:<id> for every live user of that department plus room:<id> for every live chatroom of that department (the acting chatroom only inside an agent / command session, and only if it belongs to the department) — matches NO rows for clients; "$today"/"$today+Nd"/"$today-Nd" (N 1..730, day boundary of the company business timezone when configured, otherwise Asia/Taipei) — date columns with eq/neq/gt/gte/lt/lte, datetime columns with gt/gte/lt/lte only (an eq day is unsatisfiable — author gte "$today" AND lt "$today+1d"), never inside "in" lists; "$now" = the current wall-clock instant of the company business timezone when configured, otherwise Asia/Taipei, at minute precision — datetime columns with gt/gte/lt/lte only (never eq/neq, never a date column — use "$today" there — never inside an "in" list). Any other "$"-prefixed value is rejected (reserved). Grant endpoints echo the RAW token, never a resolved literal. Unlike "own" (fixed to the record creator), "$me" may target any legal column — e.g. an assigned-owner column. A token whose principal kind never matches the column (a user-audience "$me" on a social_client column) is legal but matches nothing. |
scope_values | string[] | null | 否 | SCP v1.3 channel scope: the record ids of the linked table this chatroom may read and write through, for the link column named by the table's `channel` rule (e.g. this room's warehouses). Only meaningful on a channel-governed table. Every id must be a LIVE record of the linked table (422 otherwise). null/omitted = UNDECLARED, which DENIES every row on a governed table — 'no narrowing' is expressed by not authoring the channel rule, never by an empty or absent scope. Empty list rejected (422). |
visible_columns | string[] | null | 否 | Column allowlist: INTERNAL column keys (col_<hex>) visible under this grant; all other columns are hidden (read strip + write reject + formula/export/view/history scoping). null/omitted = all columns (v1.2 column_acl still applies on top). Empty list rejected (422); unknown keys rejected (422). |
回應結構
SuccessResponse| 欄位 | 型別 | 必填 | 說明 |
|---|---|---|---|
message | string | 否 | 未指定 |
範例
將部門資料表分享給聊天室內部成員
{
"pathParams": {
"department_id": "33333333-3333-4333-8333-333333333333",
"table_id": "22222222-2222-4222-8222-222222222222",
"chatroom_id": "11111111-1111-4111-8111-111111111111"
},
"body": {
"audience": "internal",
"can_read": "all",
"can_insert": true,
"can_edit": "own",
"visible_columns": [
"col_88888888_8888_4888_8888_888888888888"
]
}
}{
"message": "Chatroom grant updated successfully"
}錯誤
驗證錯誤範例列出識別欄位;部署中的驗證器也可能回傳 input 與 ctx。錯誤訊息 URL 的版本由建置決定,因此使用 pydantic-version 佔位符。
| 狀態 | 發生條件 | 錯誤本文 |
|---|---|---|
| 400 | scope_values 的 id 不是 channel link 目標表的紀錄;拒絕授權。 | |
| 404 | 目標聊天室不存在,或不屬於呼叫者的公司。 | |
| 422 | 授權內容驗證失敗,例如未知的 visible column,或無效 scope_values 清單。 | |
/private/module/custom_tables/{scope}/tables/{table_id}/chatroom-permissions/{chatroom_id}刪除該聊天室某一個 audience 的授權列;省略 `audience` 時(預設 `both`)則兩列都刪除。
聊天室或某種受眾不應再繼承部門資料表存取權時使用。
- 部門
/private/module/custom_tables/department/{department_id}/tables/{table_id}/chatroom-permissions/{chatroom_id}
範圍
| 範圍 | 是否提供 | 驗證與權限 |
|---|---|---|
| 聊天室 | 未提供 | 不適用 |
| 部門 | 可使用 | CustomTableModeratorRequired |
| 公司 | 未提供 | 不適用 |
參數
| 欄位 | 位置 | 型別 | 必填 | 說明 |
|---|---|---|---|---|
table_id | path | uuid | 是 | 擁有授權的部門資料表 UUID。 |
chatroom_id | path | uuid | 是 | 要移除授權的目標聊天室 UUID。 |
audience | query | enum:internal|external|both | 否 | 要移除的受眾資料列;省略時預設為 both。 |
回應結構
SuccessResponse| 欄位 | 型別 | 必填 | 說明 |
|---|---|---|---|
message | string | 否 | 未指定 |
範例
移除內部受眾的聊天室授權
{
"pathParams": {
"department_id": "33333333-3333-4333-8333-333333333333",
"table_id": "22222222-2222-4222-8222-222222222222",
"chatroom_id": "11111111-1111-4111-8111-111111111111"
},
"query": {
"audience": "internal"
}
}{
"message": "Chatroom grant revoked"
}錯誤
驗證錯誤範例列出識別欄位;部署中的驗證器也可能回傳 input 與 ctx。錯誤訊息 URL 的版本由建置決定,因此使用 pydantic-version 佔位符。
| 狀態 | 發生條件 | 錯誤本文 |
|---|---|---|
| 404 | 該資料表、聊天室與受眾組合沒有授權。 | |
/private/module/custom_tables/{scope}/tables/{table_id}/client-permissions回傳聊天室、部門或公司資料表上,外部或 agent client 的明確 grants。
用來稽核每一筆明確的個別用戶端授權列:手動授權(`granted_via: "manual"`)、核准的申請(`"review"`)與通關密語加入(`"passphrase"`,`granted_by: null`)。聊天室 audience 與資料表預設權限不會列出。
- 聊天室
/private/module/custom_tables/chatroom/{chatroom_id}/tables/{table_id}/client-permissions - 部門
/private/module/custom_tables/department/{department_id}/tables/{table_id}/client-permissions - 公司
/private/module/custom_tables/company/tables/{table_id}/client-permissions
範圍
| 範圍 | 是否提供 | 驗證與權限 |
|---|---|---|
| 聊天室 | 可使用 | CustomTableModeratorRequired |
| 部門 | 可使用 | CustomTableModeratorRequired |
| 公司 | 可使用 | CustomTableModeratorRequired |
參數
| 欄位 | 位置 | 型別 | 必填 | 說明 |
|---|---|---|---|---|
table_id | path | uuid | 是 | 聊天室、部門或公司資料表 UUID。 |
回應結構
ClientPermissionListResponse| 欄位 | 型別 | 必填 | 說明 |
|---|---|---|---|
clients | ClientPermissionResponse[] | 是 | 未指定 |
範例
列出明確用戶端授權
{
"pathParams": {
"chatroom_id": "11111111-1111-4111-8111-111111111111",
"table_id": "22222222-2222-4222-8222-222222222222"
}
}{
"clients": [
{
"social_media_client_id": "66666666-6666-4666-8666-666666666666",
"client": {
"id": "66666666-6666-4666-8666-666666666666",
"nickname": "範例客戶",
"platform": "line",
"external_id": "U0123456789abcdef0123456789abcdef",
"external_channel_id": "2000000001",
"external_profile": {
"nickname": "範例客戶"
}
},
"can_read": "own",
"can_insert": true,
"can_edit": "own",
"granted_by": {
"id": "55555555-5555-4555-8555-555555555555",
"created_at": "2026-07-19T02:00:00",
"expired_at": "2027-07-19T02:00:00",
"photo_url": null,
"enabled": true,
"is_verified": true,
"username": "catalog.manager",
"email": "manager@example.invalid",
"nickname": "林主管",
"department_id": "33333333-3333-4333-8333-333333333333",
"department_name": "營運部"
},
"granted_at": "2026-07-19T02:05:00",
"granted_via": "manual"
}
]
}錯誤
驗證錯誤範例列出識別欄位;部署中的驗證器也可能回傳 input 與 ctx。錯誤訊息 URL 的版本由建置決定,因此使用 pydantic-version 佔位符。
| 狀態 | 發生條件 | 錯誤本文 |
|---|---|---|
| 404 | 任何 scope:呼叫者擔任版主、但不在路徑指定的聊天室、部門或公司內的現行資料表。資料表 id 不存在或已在垃圾桶時,會先得到版主門檻的 `404 CustomTable Not found`。 | |
/private/module/custom_tables/{scope}/tables/{table_id}/client-permissions/{client_id}建立或取代一個 same-company external client 對聊天室、部門或公司資料表的明確權限。
找到需要直接覆寫的正確 social_media_client_id 後使用。
- 聊天室
/private/module/custom_tables/chatroom/{chatroom_id}/tables/{table_id}/client-permissions/{client_id} - 部門
/private/module/custom_tables/department/{department_id}/tables/{table_id}/client-permissions/{client_id} - 公司
/private/module/custom_tables/company/tables/{table_id}/client-permissions/{client_id}
範圍
| 範圍 | 是否提供 | 驗證與權限 |
|---|---|---|
| 聊天室 | 可使用 | CustomTableModeratorRequired |
| 部門 | 可使用 | CustomTableModeratorRequired |
| 公司 | 可使用 | CustomTableModeratorRequired |
參數
| 欄位 | 位置 | 型別 | 必填 | 說明 |
|---|---|---|---|---|
table_id | path | uuid | 是 | 要加入用戶端授權的資料表 UUID。 |
client_id | path | uuid | 是 | 接受存取權的外部社群用戶端 UUID(LINE、Messenger、Instagram、Web)。agent 平台用戶端也會被接受,但它們以連結的使用者身分解析,因此這類授權列沒有作用。 |
請求結構
ClientPermissionPayload| 欄位 | 型別 | 必填 | 說明 |
|---|---|---|---|
can_edit | "none" | "own" | "all" | "filtered" | 否 | Edit level. 'filtered' requires edit_filter. |
can_insert | boolean | 否 | May the client insert records. |
can_read | "none" | "own" | "all" | "filtered" | 否 | Read level. 'filtered' requires read_filter. |
edit_filter | object | null | 否 | v1.2 §11 row policy — required when can_edit="filtered". Token grammar is identical to read_filter: A row policy is a boolean tree over scalar predicates: a node is {"and": [node, ...]} (every child holds), {"or": [node, ...]} (at least one holds), {"not": node} (strict complement) or a predicate {"column": "col_<hex>", "op": "eq|neq|gt|gte|lt|lte|in|contains|is_null|is_not_null", "value": ...}. Groups are non-empty; a node carries exactly its kind's keys; the root may be any node (the legacy {"and": [preds]} is one such tree). Caps: depth <= 5 (a bare predicate is depth 1), <= 24 predicates, <= 100 entries per "in" list (caps bind AUTHORED policies; the server may compose wider resolved trees). Predicates reference internal column keys of stored scalar columns only. An empty/unset cell fails every value op and matches only is_null, so {"not": {col eq X}} INCLUDES blank cells while {col neq X} excludes them. Unresolvable tokens and malformed nodes always NARROW the row set (never-match under "and"/"not", dropped under "or"), never widen it. A node may also be a link_target leaf {"link": "col_<hex> of a LINK column of this table", "quantifier": "any"|"all", "target": node, "require_present": bool} — it follows the link ONE hop and evaluates "target" (any row-policy node over the LINKED table's stored scalar columns; no link node inside it: 1-hop) against the linked rows: "any" = at least one linked row matches, "all" = every linked row matches (vacuously true when none — require_present MUST be stated on "all" and must be absent on "any"). Only LIVE (not trashed) linked rows count and the linked table's SCP channel floor applies; the linked table's own row/column ACL does NOT. Tokens inside "target" resolve against the LINKED column's type ("$me" on its user column = rows assigned to me). Caps: <= 6 link leaves; a target costs one depth level and its predicates count toward the 24. A {"link", "op", "value"} membership leaf is NOT accepted — write link_target with a target {"column": "id", "op": "in", "value": [...]}. Example: {"or": [{"column": "col_a1", "op": "eq", "value": "TW"}, {"not": {"column": "col_b2", "op": "eq", "value": "$me"}}]}. Predicate values may use per-request tokens (stored RAW, substituted at permission-resolution time): "$me" = the acting principal's own id (user id; client id for social clients; the linked user's id for agent-platform clients) — ops eq/neq or as an element of an "in" list, on user/social_client/principal/string/text columns; on a principal column "$me" is a SET — the caller's own user:<id> plus room:<id> for every live chatroom they belong to (REST) or the acting chatroom only inside an agent / command session; social clients match smc:<id> only; a public-read token never matches — compiled as an "in" over tagged cells ("neq" excludes blank cells); "$me.department" = the acting user's department id — string/text columns; on a principal column it is a SET: user:<id> for every live user of that department plus room:<id> for every live chatroom of that department (the acting chatroom only inside an agent / command session, and only if it belongs to the department) — matches NO rows for clients; "$today"/"$today+Nd"/"$today-Nd" (N 1..730, day boundary of the company business timezone when configured, otherwise Asia/Taipei) — date columns with eq/neq/gt/gte/lt/lte, datetime columns with gt/gte/lt/lte only (an eq day is unsatisfiable — author gte "$today" AND lt "$today+1d"), never inside "in" lists; "$now" = the current wall-clock instant of the company business timezone when configured, otherwise Asia/Taipei, at minute precision — datetime columns with gt/gte/lt/lte only (never eq/neq, never a date column — use "$today" there — never inside an "in" list). Any other "$"-prefixed value is rejected (reserved). Grant endpoints echo the RAW token, never a resolved literal. Unlike "own" (fixed to the record creator), "$me" may target any legal column — e.g. an assigned-owner column. A token whose principal kind never matches the column (a user-audience "$me" on a social_client column) is legal but matches nothing. |
read_filter | object | null | 否 | v1.2 §11 row policy — required when can_read="filtered". A row policy is a boolean tree over scalar predicates: a node is {"and": [node, ...]} (every child holds), {"or": [node, ...]} (at least one holds), {"not": node} (strict complement) or a predicate {"column": "col_<hex>", "op": "eq|neq|gt|gte|lt|lte|in|contains|is_null|is_not_null", "value": ...}. Groups are non-empty; a node carries exactly its kind's keys; the root may be any node (the legacy {"and": [preds]} is one such tree). Caps: depth <= 5 (a bare predicate is depth 1), <= 24 predicates, <= 100 entries per "in" list (caps bind AUTHORED policies; the server may compose wider resolved trees). Predicates reference internal column keys of stored scalar columns only. An empty/unset cell fails every value op and matches only is_null, so {"not": {col eq X}} INCLUDES blank cells while {col neq X} excludes them. Unresolvable tokens and malformed nodes always NARROW the row set (never-match under "and"/"not", dropped under "or"), never widen it. A node may also be a link_target leaf {"link": "col_<hex> of a LINK column of this table", "quantifier": "any"|"all", "target": node, "require_present": bool} — it follows the link ONE hop and evaluates "target" (any row-policy node over the LINKED table's stored scalar columns; no link node inside it: 1-hop) against the linked rows: "any" = at least one linked row matches, "all" = every linked row matches (vacuously true when none — require_present MUST be stated on "all" and must be absent on "any"). Only LIVE (not trashed) linked rows count and the linked table's SCP channel floor applies; the linked table's own row/column ACL does NOT. Tokens inside "target" resolve against the LINKED column's type ("$me" on its user column = rows assigned to me). Caps: <= 6 link leaves; a target costs one depth level and its predicates count toward the 24. A {"link", "op", "value"} membership leaf is NOT accepted — write link_target with a target {"column": "id", "op": "in", "value": [...]}. Example: {"or": [{"column": "col_a1", "op": "eq", "value": "TW"}, {"not": {"column": "col_b2", "op": "eq", "value": "$me"}}]}. Predicate values may use per-request tokens (stored RAW, substituted at permission-resolution time): "$me" = the acting principal's own id (user id; client id for social clients; the linked user's id for agent-platform clients) — ops eq/neq or as an element of an "in" list, on user/social_client/principal/string/text columns; on a principal column "$me" is a SET — the caller's own user:<id> plus room:<id> for every live chatroom they belong to (REST) or the acting chatroom only inside an agent / command session; social clients match smc:<id> only; a public-read token never matches — compiled as an "in" over tagged cells ("neq" excludes blank cells); "$me.department" = the acting user's department id — string/text columns; on a principal column it is a SET: user:<id> for every live user of that department plus room:<id> for every live chatroom of that department (the acting chatroom only inside an agent / command session, and only if it belongs to the department) — matches NO rows for clients; "$today"/"$today+Nd"/"$today-Nd" (N 1..730, day boundary of the company business timezone when configured, otherwise Asia/Taipei) — date columns with eq/neq/gt/gte/lt/lte, datetime columns with gt/gte/lt/lte only (an eq day is unsatisfiable — author gte "$today" AND lt "$today+1d"), never inside "in" lists; "$now" = the current wall-clock instant of the company business timezone when configured, otherwise Asia/Taipei, at minute precision — datetime columns with gt/gte/lt/lte only (never eq/neq, never a date column — use "$today" there — never inside an "in" list). Any other "$"-prefixed value is rejected (reserved). Grant endpoints echo the RAW token, never a resolved literal. Unlike "own" (fixed to the record creator), "$me" may target any legal column — e.g. an assigned-owner column. A token whose principal kind never matches the column (a user-audience "$me" on a social_client column) is legal but matches nothing. |
visible_columns | string[] | null | 否 | Column allowlist: INTERNAL column keys (col_<hex>) visible under this grant; all other columns are hidden (read strip + write reject + formula/export/view/history scoping). null/omitted = all columns (v1.2 column_acl still applies on top). Empty list rejected (422); unknown keys rejected (422). |
回應結構
SuccessResponse| 欄位 | 型別 | 必填 | 說明 |
|---|---|---|---|
message | string | 否 | 未指定 |
範例
授權外部用戶端存取自己的資料列
{
"pathParams": {
"chatroom_id": "11111111-1111-4111-8111-111111111111",
"table_id": "22222222-2222-4222-8222-222222222222",
"client_id": "66666666-6666-4666-8666-666666666666"
},
"body": {
"can_read": "own",
"can_insert": true,
"can_edit": "own",
"visible_columns": [
"col_88888888_8888_4888_8888_888888888888"
]
}
}{
"message": "Client permissions granted"
}錯誤
驗證錯誤範例列出識別欄位;部署中的驗證器也可能回傳 input 與 ctx。錯誤訊息 URL 的版本由建置決定,因此使用 pydantic-version 佔位符。
| 狀態 | 發生條件 | 錯誤本文 |
|---|---|---|
| 404 | 用戶端不存在、其聊天室已刪除或屬於其他公司,或是 agent 用戶端但其連結的使用者不是現行的同公司使用者。這是公司檢查,而不是 scope 檢查:找不到資料表時會回 `CustomTable Not found`(id 不存在,或聊天室資料表不在路徑指定的聊天室中)或 `Table not found`(department/company 路由,資料表屬於其他 scope)。 | |
| 422 | 權限、篩選條件或可見欄位無效。 | |
/private/module/custom_tables/{scope}/tables/{table_id}/client-permissions/{client_id}刪除該用戶端的明確授權列——手動、審核與通關密語授權共用這同一列。存取權會回到聊天室的 external audience 授權(部門資料表)或資料表預設權限(僅限該用戶端所屬聊天室的聊天室資料表);通關密語存取權只有在用戶端再次使用通關密語時才會恢復。
用戶端不再需要單獨覆寫時使用;撤銷後應重新檢查 fallback,不要假設有效權限必然是 none。
- 聊天室
/private/module/custom_tables/chatroom/{chatroom_id}/tables/{table_id}/client-permissions/{client_id} - 部門
/private/module/custom_tables/department/{department_id}/tables/{table_id}/client-permissions/{client_id} - 公司
/private/module/custom_tables/company/tables/{table_id}/client-permissions/{client_id}
範圍
| 範圍 | 是否提供 | 驗證與權限 |
|---|---|---|
| 聊天室 | 可使用 | CustomTableModeratorRequired |
| 部門 | 可使用 | CustomTableModeratorRequired |
| 公司 | 可使用 | CustomTableModeratorRequired |
參數
| 欄位 | 位置 | 型別 | 必填 | 說明 |
|---|---|---|---|---|
table_id | path | uuid | 是 | 擁有這筆用戶端授權的資料表 UUID。 |
client_id | path | uuid | 是 | 要移除明確授權的用戶端 UUID。 |
回應結構
SuccessResponse| 欄位 | 型別 | 必填 | 說明 |
|---|---|---|---|
message | string | 否 | 未指定 |
範例
移除明確用戶端授權
{
"pathParams": {
"chatroom_id": "11111111-1111-4111-8111-111111111111",
"table_id": "22222222-2222-4222-8222-222222222222",
"client_id": "66666666-6666-4666-8666-666666666666"
}
}{
"message": "Client permissions revoked successfully"
}錯誤
驗證錯誤範例列出識別欄位;部署中的驗證器也可能回傳 input 與 ctx。錯誤訊息 URL 的版本由建置決定,因此使用 pydantic-version 佔位符。
| 狀態 | 發生條件 | 錯誤本文 |
|---|---|---|
| 404 | 這張資料表不存在該用戶端的明確權限列。 | |
/private/module/custom_tables/{scope}/tables/{table_id}/permission-requests列出用戶端提交的待審或已審存取申請——對象可以是聊天室資料表,或分享進該聊天室的部門資料表。
適合製作用戶端存取申請的管理者審核清單。
- 聊天室
/private/module/custom_tables/chatroom/{chatroom_id}/tables/{table_id}/permission-requests
範圍
| 範圍 | 是否提供 | 驗證與權限 |
|---|---|---|
| 聊天室 | 可使用 | CustomTableModeratorRequired |
| 部門 | 未提供 | 不適用 |
| 公司 | 未提供 | 不適用 |
參數
| 欄位 | 位置 | 型別 | 必填 | 說明 |
|---|---|---|---|---|
table_id | path | uuid | 是 | 要列出申請的資料表 UUID——聊天室資料表,或分享進此聊天室的部門資料表。 |
status | query | enum:pending|approved|rejected | 否 | 選填的審核狀態:pending、approved 或 rejected。 |
回應結構
ClientPermissionRequestListResponse| 欄位 | 型別 | 必填 | 說明 |
|---|---|---|---|
requests | ClientPermissionRequestResponse[] | 是 | 未指定 |
範例
列出待審用戶端申請
{
"pathParams": {
"chatroom_id": "11111111-1111-4111-8111-111111111111",
"table_id": "22222222-2222-4222-8222-222222222222"
},
"query": {
"status": "pending"
}
}{
"requests": [
{
"id": "77777777-7777-4777-8777-777777777777",
"social_media_client_id": "66666666-6666-4666-8666-666666666666",
"client": {
"id": "66666666-6666-4666-8666-666666666666",
"nickname": "範例客戶",
"platform": "line",
"external_id": "U0123456789abcdef0123456789abcdef",
"external_channel_id": "2000000001",
"external_profile": {
"nickname": "範例客戶"
}
},
"custom_table_id": "22222222-2222-4222-8222-222222222222",
"table_name": "訂單",
"status": "pending",
"requested_permissions": "insert,read_all",
"requested_at": "2026-07-19T02:00:00",
"reviewed_by": null,
"reviewed_at": null,
"review_note": null
}
]
}錯誤
驗證錯誤範例列出識別欄位;部署中的驗證器也可能回傳 input 與 ctx。錯誤訊息 URL 的版本由建置決定,因此使用 pydantic-version 佔位符。
| 狀態 | 發生條件 | 錯誤本文 |
|---|---|---|
| 404 | 資料表 id 不存在或已在垃圾桶(版主門檻),或資料表所屬聊天室已被刪除(`Chatroom Not found`)。路徑中的 `chatroom_id` 從不與資料表比對。 | |
/private/module/custom_tables/{scope}/tables/{table_id}/permission-requests/{request_id}/approve將待審用戶端申請標記為已核准,並寫入選定的明確用戶端授權。
管理者審閱待審申請並確定最終權限後使用。
- 聊天室
/private/module/custom_tables/chatroom/{chatroom_id}/tables/{table_id}/permission-requests/{request_id}/approve
範圍
| 範圍 | 是否提供 | 驗證與權限 |
|---|---|---|
| 聊天室 | 可使用 | CustomTableModeratorRequired |
| 部門 | 未提供 | 不適用 |
| 公司 | 未提供 | 不適用 |
參數
| 欄位 | 位置 | 型別 | 必填 | 說明 |
|---|---|---|---|---|
table_id | path | uuid | 是 | 申請指向的資料表 UUID(聊天室資料表,或分享進此聊天室的部門資料表)。 |
request_id | path | uuid | 是 | 待審權限申請 UUID。 |
請求結構
ApprovePermissionRequestPayload| 欄位 | 型別 | 必填 | 說明 |
|---|---|---|---|
can_edit | "none" | "own" | "all" | "filtered" | 否 | Edit access: "none" | "own" | "all" | "filtered" (v1.2 §11) |
can_insert | boolean | 否 | Insert permission |
can_read | "none" | "own" | "all" | "filtered" | 否 | Read access: "none" | "own" | "all" | "filtered" (v1.2 §11) |
edit_filter | object | null | 否 | v1.2 §11 row policy — required when can_edit="filtered". Token grammar is identical to read_filter: A row policy is a boolean tree over scalar predicates: a node is {"and": [node, ...]} (every child holds), {"or": [node, ...]} (at least one holds), {"not": node} (strict complement) or a predicate {"column": "col_<hex>", "op": "eq|neq|gt|gte|lt|lte|in|contains|is_null|is_not_null", "value": ...}. Groups are non-empty; a node carries exactly its kind's keys; the root may be any node (the legacy {"and": [preds]} is one such tree). Caps: depth <= 5 (a bare predicate is depth 1), <= 24 predicates, <= 100 entries per "in" list (caps bind AUTHORED policies; the server may compose wider resolved trees). Predicates reference internal column keys of stored scalar columns only. An empty/unset cell fails every value op and matches only is_null, so {"not": {col eq X}} INCLUDES blank cells while {col neq X} excludes them. Unresolvable tokens and malformed nodes always NARROW the row set (never-match under "and"/"not", dropped under "or"), never widen it. A node may also be a link_target leaf {"link": "col_<hex> of a LINK column of this table", "quantifier": "any"|"all", "target": node, "require_present": bool} — it follows the link ONE hop and evaluates "target" (any row-policy node over the LINKED table's stored scalar columns; no link node inside it: 1-hop) against the linked rows: "any" = at least one linked row matches, "all" = every linked row matches (vacuously true when none — require_present MUST be stated on "all" and must be absent on "any"). Only LIVE (not trashed) linked rows count and the linked table's SCP channel floor applies; the linked table's own row/column ACL does NOT. Tokens inside "target" resolve against the LINKED column's type ("$me" on its user column = rows assigned to me). Caps: <= 6 link leaves; a target costs one depth level and its predicates count toward the 24. A {"link", "op", "value"} membership leaf is NOT accepted — write link_target with a target {"column": "id", "op": "in", "value": [...]}. Example: {"or": [{"column": "col_a1", "op": "eq", "value": "TW"}, {"not": {"column": "col_b2", "op": "eq", "value": "$me"}}]}. Predicate values may use per-request tokens (stored RAW, substituted at permission-resolution time): "$me" = the acting principal's own id (user id; client id for social clients; the linked user's id for agent-platform clients) — ops eq/neq or as an element of an "in" list, on user/social_client/principal/string/text columns; on a principal column "$me" is a SET — the caller's own user:<id> plus room:<id> for every live chatroom they belong to (REST) or the acting chatroom only inside an agent / command session; social clients match smc:<id> only; a public-read token never matches — compiled as an "in" over tagged cells ("neq" excludes blank cells); "$me.department" = the acting user's department id — string/text columns; on a principal column it is a SET: user:<id> for every live user of that department plus room:<id> for every live chatroom of that department (the acting chatroom only inside an agent / command session, and only if it belongs to the department) — matches NO rows for clients; "$today"/"$today+Nd"/"$today-Nd" (N 1..730, day boundary of the company business timezone when configured, otherwise Asia/Taipei) — date columns with eq/neq/gt/gte/lt/lte, datetime columns with gt/gte/lt/lte only (an eq day is unsatisfiable — author gte "$today" AND lt "$today+1d"), never inside "in" lists; "$now" = the current wall-clock instant of the company business timezone when configured, otherwise Asia/Taipei, at minute precision — datetime columns with gt/gte/lt/lte only (never eq/neq, never a date column — use "$today" there — never inside an "in" list). Any other "$"-prefixed value is rejected (reserved). Grant endpoints echo the RAW token, never a resolved literal. Unlike "own" (fixed to the record creator), "$me" may target any legal column — e.g. an assigned-owner column. A token whose principal kind never matches the column (a user-audience "$me" on a social_client column) is legal but matches nothing. |
read_filter | object | null | 否 | v1.2 §11 row policy — required when can_read="filtered". A row policy is a boolean tree over scalar predicates: a node is {"and": [node, ...]} (every child holds), {"or": [node, ...]} (at least one holds), {"not": node} (strict complement) or a predicate {"column": "col_<hex>", "op": "eq|neq|gt|gte|lt|lte|in|contains|is_null|is_not_null", "value": ...}. Groups are non-empty; a node carries exactly its kind's keys; the root may be any node (the legacy {"and": [preds]} is one such tree). Caps: depth <= 5 (a bare predicate is depth 1), <= 24 predicates, <= 100 entries per "in" list (caps bind AUTHORED policies; the server may compose wider resolved trees). Predicates reference internal column keys of stored scalar columns only. An empty/unset cell fails every value op and matches only is_null, so {"not": {col eq X}} INCLUDES blank cells while {col neq X} excludes them. Unresolvable tokens and malformed nodes always NARROW the row set (never-match under "and"/"not", dropped under "or"), never widen it. A node may also be a link_target leaf {"link": "col_<hex> of a LINK column of this table", "quantifier": "any"|"all", "target": node, "require_present": bool} — it follows the link ONE hop and evaluates "target" (any row-policy node over the LINKED table's stored scalar columns; no link node inside it: 1-hop) against the linked rows: "any" = at least one linked row matches, "all" = every linked row matches (vacuously true when none — require_present MUST be stated on "all" and must be absent on "any"). Only LIVE (not trashed) linked rows count and the linked table's SCP channel floor applies; the linked table's own row/column ACL does NOT. Tokens inside "target" resolve against the LINKED column's type ("$me" on its user column = rows assigned to me). Caps: <= 6 link leaves; a target costs one depth level and its predicates count toward the 24. A {"link", "op", "value"} membership leaf is NOT accepted — write link_target with a target {"column": "id", "op": "in", "value": [...]}. Example: {"or": [{"column": "col_a1", "op": "eq", "value": "TW"}, {"not": {"column": "col_b2", "op": "eq", "value": "$me"}}]}. Predicate values may use per-request tokens (stored RAW, substituted at permission-resolution time): "$me" = the acting principal's own id (user id; client id for social clients; the linked user's id for agent-platform clients) — ops eq/neq or as an element of an "in" list, on user/social_client/principal/string/text columns; on a principal column "$me" is a SET — the caller's own user:<id> plus room:<id> for every live chatroom they belong to (REST) or the acting chatroom only inside an agent / command session; social clients match smc:<id> only; a public-read token never matches — compiled as an "in" over tagged cells ("neq" excludes blank cells); "$me.department" = the acting user's department id — string/text columns; on a principal column it is a SET: user:<id> for every live user of that department plus room:<id> for every live chatroom of that department (the acting chatroom only inside an agent / command session, and only if it belongs to the department) — matches NO rows for clients; "$today"/"$today+Nd"/"$today-Nd" (N 1..730, day boundary of the company business timezone when configured, otherwise Asia/Taipei) — date columns with eq/neq/gt/gte/lt/lte, datetime columns with gt/gte/lt/lte only (an eq day is unsatisfiable — author gte "$today" AND lt "$today+1d"), never inside "in" lists; "$now" = the current wall-clock instant of the company business timezone when configured, otherwise Asia/Taipei, at minute precision — datetime columns with gt/gte/lt/lte only (never eq/neq, never a date column — use "$today" there — never inside an "in" list). Any other "$"-prefixed value is rejected (reserved). Grant endpoints echo the RAW token, never a resolved literal. Unlike "own" (fixed to the record creator), "$me" may target any legal column — e.g. an assigned-owner column. A token whose principal kind never matches the column (a user-audience "$me" on a social_client column) is legal but matches nothing. |
回應結構
SuccessResponse| 欄位 | 型別 | 必填 | 說明 |
|---|---|---|---|
message | string | 否 | 未指定 |
範例
核准讀取與新增權限
{
"pathParams": {
"chatroom_id": "11111111-1111-4111-8111-111111111111",
"table_id": "22222222-2222-4222-8222-222222222222",
"request_id": "77777777-7777-4777-8777-777777777777"
},
"body": {
"can_read": "all",
"can_insert": true,
"can_edit": "none"
}
}{
"message": "Permission request approved"
}錯誤
驗證錯誤範例列出識別欄位;部署中的驗證器也可能回傳 input 與 ctx。錯誤訊息 URL 的版本由建置決定,因此使用 pydantic-version 佔位符。
| 狀態 | 發生條件 | 錯誤本文 |
|---|---|---|
| 404 | 申請不存在、屬於其他資料表,或已不是待審狀態。 | |
| 422 | 核准內容的 row policy 不合法——`filtered` 層級缺少對應的 filter,或有其他文法錯誤。這項檢查在查找申請之前執行,所以即使申請 id 不存在,不合法的內容也會回 422。 | |
/private/module/custom_tables/{scope}/tables/{table_id}/permission-requests/{request_id}/reject將待審用戶端申請標記為已駁回;選填的審核說明會保存下來,並隨駁回通知送達申請人。
適用於不應轉成明確 grant 的 pending 申請;review_note 會保存並隨駁回通知送達申請人,申請終結為 rejected。
- 聊天室
/private/module/custom_tables/chatroom/{chatroom_id}/tables/{table_id}/permission-requests/{request_id}/reject
範圍
| 範圍 | 是否提供 | 驗證與權限 |
|---|---|---|
| 聊天室 | 可使用 | CustomTableModeratorRequired |
| 部門 | 未提供 | 不適用 |
| 公司 | 未提供 | 不適用 |
參數
| 欄位 | 位置 | 型別 | 必填 | 說明 |
|---|---|---|---|---|
table_id | path | uuid | 是 | 申請指向的資料表 UUID(聊天室資料表,或分享進此聊天室的部門資料表)。 |
request_id | path | uuid | 是 | 待審權限申請 UUID。 |
請求結構
RejectPermissionRequestPayload| 欄位 | 型別 | 必填 | 說明 |
|---|---|---|---|
review_note | string | null | 否 | Optional reason for rejection |
回應結構
SuccessResponse| 欄位 | 型別 | 必填 | 說明 |
|---|---|---|---|
message | string | 否 | 未指定 |
範例
附上審核說明駁回申請
{
"pathParams": {
"chatroom_id": "11111111-1111-4111-8111-111111111111",
"table_id": "22222222-2222-4222-8222-222222222222",
"request_id": "77777777-7777-4777-8777-777777777777"
},
"body": {
"review_note": "請先補充使用情境"
}
}{
"message": "Permission request rejected"
}錯誤
驗證錯誤範例列出識別欄位;部署中的驗證器也可能回傳 input 與 ctx。錯誤訊息 URL 的版本由建置決定,因此使用 pydantic-version 佔位符。
| 狀態 | 發生條件 | 錯誤本文 |
|---|---|---|
| 404 | 申請不存在、屬於其他資料表,或已不是待審狀態。 | |
/private/module/custom_tables/{scope}/tables/{table_id}/client-access在三種 table scope 上啟用、輪替或停用 passphrase,並取代驗證成功後授予的權限。
外部用戶端需透過密語解鎖受控資料表存取,而不是逐一手動授權時使用。
- 聊天室
/private/module/custom_tables/chatroom/{chatroom_id}/tables/{table_id}/client-access - 部門
/private/module/custom_tables/department/{department_id}/tables/{table_id}/client-access - 公司
/private/module/custom_tables/company/tables/{table_id}/client-access
範圍
| 範圍 | 是否提供 | 驗證與權限 |
|---|---|---|
| 聊天室 | 可使用 | CustomTableModeratorRequired |
| 部門 | 可使用 | CustomTableModeratorRequired |
| 公司 | 可使用 | CustomTableModeratorRequired |
參數
| 欄位 | 位置 | 型別 | 必填 | 說明 |
|---|---|---|---|---|
table_id | path | uuid | 是 | 要變更 client-access 設定的聊天室、部門或公司資料表 UUID。 |
請求結構
ClientAccessConfigPayload| 欄位 | 型別 | 必填 | 說明 |
|---|---|---|---|
passphrase | string | null | 否 | New plaintext passphrase: 1–72 UTF-8 bytes (at most 72 characters), bcrypt-hashed before storage. Omit or send null to keep the live hash; a literal '<REDACTED>' is plaintext on REST, not an export sentinel. |
passphrase_enabled | boolean | 是 | Whether passphrase access is enabled |
passphrase_permissions | DefaultPermissionsPayload | null | 否 | Permissions granted when a client uses the passphrase. Defaults to read-only if omitted. Row-policy token values ($me/$today/$now…) are legal here: the RAW token is copied onto each joining client's grant and resolves per request thereafter. |
回應結構
SuccessResponse| 欄位 | 型別 | 必填 | 說明 |
|---|---|---|---|
message | string | 否 | 未指定 |
範例
啟用密語並授予自有資料列權限
{
"pathParams": {
"chatroom_id": "11111111-1111-4111-8111-111111111111",
"table_id": "22222222-2222-4222-8222-222222222222"
},
"body": {
"passphrase_enabled": true,
"passphrase": "catalog-demo-passphrase",
"passphrase_permissions": {
"can_read": "own",
"can_insert": true,
"can_edit": "own"
}
}
}{
"message": "Client access configuration updated"
}錯誤
驗證錯誤範例列出識別欄位;部署中的驗證器也可能回傳 input 與 ctx。錯誤訊息 URL 的版本由建置決定,因此使用 pydantic-version 佔位符。
| 狀態 | 發生條件 | 錯誤本文 |
|---|---|---|
| 404 | 任何 scope:不在路徑指定的聊天室、部門或公司內的現行資料表(`Table not found`)。資料表 id 不存在或已在垃圾桶時,會先得到版主門檻的 `404 CustomTable Not found`。 | |
| 409 | settings 合併在伺服器自身處理之後仍輸掉資料表列鎖。通行碼設定維持原狀——重送 PATCH;拿到 200 才代表合併真的成立。 | |
| 422 | `passphrase_permissions` 的列規則不合法(字串 detail,如上所示)。 | |
| 422 | 新的 `passphrase` 為空(`string_too_short`)、超過 72 個字元(`string_too_long`)、不是合法的 Unicode——例如以 JSON 跳脫 `�` 送出的孤立代理字元(`string_unicode`)——或雖在 72 個字元以內但超過 72 個 UTF-8 位元組(`value_error`,如上所示——例如 25 個中日韓文字)。這些都是請求驗證錯誤,發生在任何雜湊或儲存之前;回顯的 `input` 會遮蔽為 `"***"`。 | |