前言
完成 AI Agent API 後,KnockersTaskBlocks 已經可以從網頁或 JSON-RPC 建立完整任務文件。接下來遇到的需求,是把其中一筆任務交付給未登入的外部對象查看。
Kanboard 原本已有專案公開 Token,但它授權的是整個公開專案。持有連結的人可以進入公開看板,再查看專案中的其他任務。這不適合「只分享這一筆設計交付」或「只讓客戶看這份規格」的情境。
登入任務網址又需要 Session,不能直接交給外部訪客。於是外掛需要一種只授權單一任務、可以過期、輪替與撤銷的公開連結。
分享連結本身就是一項權限
這類連結不是一般識別碼,而是一種 bearer capability:知道完整 URL 的人,就能取得它代表的讀取能力。
它和會員身分不同:
- 訪客不會成為 Kanboard User。
- 不會得到專案角色。
- 不能呼叫登入後 Controller 或 JSON-RPC。
- 只得到一筆任務的匿名唯讀範圍。
因此不能使用連號 ID、短亂數或可從其他欄位推導的字串。實作使用 32 bytes 密碼學安全亂數,再轉成 URL-safe Token。
資料庫不保存可直接使用的 Token
完整 Token 只在建立或輪替成功時出現一次。資料庫保存的是:
task_id- 建立分享時的
project_id快照 - Token 的 SHA-256 hash
- 只供管理介面辨識的短 prefix
- 建立人與建立時間
- 可選的到期時間
- 撤銷人與撤銷時間
匿名請求進來後,server 先對 URL 中的 Token 做 hash,再使用唯一索引查詢。資料庫外洩時,不會直接得到可放進瀏覽器使用的分享網址。
短 prefix 只能讓管理者辨識目前是哪一組連結,不能參與授權,也不能拿來重建完整 Token。
為什麼完整網址只顯示一次?
如果管理介面可以隨時再次顯示完整網址,代表 server 必須保存明文 Token 或可逆密文,增加不必要的祕密管理範圍。
因此建立成功時才回傳一次完整 URL,提供複製與另開無痕視窗驗證。遺失時不能「找回」,只能輪替:
- 撤銷目前有效分享。
- 產生新的高熵 Token。
- 保存新 hash。
- 回傳一次性新 URL。
舊網址在同一個 transaction 後立即失效。這比在介面長期展示 bearer credential 更容易控制。
一筆任務只保留一組有效分享
第一版沒有支援多組同時有效連結。一筆任務同時最多一個 active share,降低管理者搞不清楚「哪些網址還能用」的風險。
分享可以沒有期限,也可以設定未來的到期時間。有效條件包括:
- 尚未撤銷。
- 尚未過期。
- 任務仍存在。
- 任務仍屬於建立分享時的專案。
- 該專案仍啟用 Task Blocks。
任務移到其他專案後,原始 project_id 快照不再吻合,連結會自動失效。這避免任務移轉權限邊界後,舊網址繼續存取新專案中的資料。
解析結果不是一個 Task ID,而是 Public Task Scope
只把 Token 換成 task_id 還不夠。公開頁後續會載入 Task Document JSON、互動式 HTML preview、Runner、圖片與附件,每個請求都需要知道它是由哪一種公開方式授權。
因此 Resolver 產生一個不可變 Public Task Scope,至少包含:
- Scope type:公開專案或單一任務分享。
- 目前允許的 Task。
- Task 所屬 Project。
- 對應的分享紀錄或專案公開狀態。
主頁、內容區塊、HTML 與附件不能各自猜測 Token 類型,也不能只相信 URL 中的 Task ID。所有匿名資源都從相同 Scope Resolver 取得授權結果。
失敗原因不能幫助猜測資料
錯誤 Token、過期、已撤銷、任務被刪除、任務移到其他專案,以及外掛停用,對匿名訪客都呈現相同的 generic not-found。
不能分別顯示:
- 這個 Token 曾經有效。
- 任務存在但已過期。
- 任務已移到其他專案。
- 你猜對了 Task ID,但 Token 不屬於它。
對管理者而言,登入後介面可以顯示分享狀態;對匿名請求而言,這些差異都是不應外洩的內部資訊。
公開頁能看什麼?
單一任務分享第一版只提供唯讀內容:
- 任務標題與允許公開的摘要資訊。
- Task Document 中可公開的內容區塊。
- 已明確放入文件的圖片與附件。
- 使用安全 URL scheme 的外部連結。
- 經過隔離的互動式 HTML 與唯讀流程圖。
不提供:
- 專案看板或任務列表。
- 工時、分析與管理功能。
- 未放入 Task Document 的附件。
- 其他任務的可導覽網址。
- 留言、上傳、編輯與任何匿名寫入。
Capability URL 只擴大一個非常窄的讀取範圍,不應順便變成客戶帳號系統。
公開回應也需要安全政策
公開頁、區塊資料、Runner 與附件回應使用:
Cache-Control: no-storeReferrer-Policy: no-referrerX-Content-Type-Options: nosniff- 搜尋引擎
noindex
Token 不放進作者 HTML、DOM mount config 或 postMessage payload。互動式 HTML 仍然先顯示 script-disabled 預覽,訪客主動啟用後才建立允許 JavaScript 的 sandbox runner。
這些 header 不能取代 authorization,但能降低 Token 經由 cache、referrer 或搜尋索引擴散。
管理分享也必須遵守原生權限
只有真正能更新該任務的登入使用者,才能建立、輪替或撤銷分享。專案檢視者、專案外使用者,以及被自訂角色限制為不能修改該任務的人,都不能操作。
JSON-RPC 管理分享時也使用呼叫者自己的 Kanboard API Key,不建立外掛專用的超級管理密鑰。
匿名 Token 則只能進入公開 Resolver,不能拿去呼叫任何登入後 API。這兩種 credential 的命名空間和用途保持分離。
如何驗證撤銷真的生效?
測試不只開啟主頁一次,而是涵蓋:
- 有效、錯誤、過期與撤銷 Token。
- 輪替後舊 URL 立即失效。
- 任務移轉專案後失效。
- 任務刪除與專案停用。
- 不同專案角色的管理權限。
- 匿名頁不出現編輯控制。
- HTML、流程圖、圖片和附件都受同一 Scope 約束。
- 所有失敗不回傳 Token、任務標題或實作細節。
這次最大的設計提醒是:分享一筆資料不是「產生一條很難猜的網址」而已。連結只是入口,真正的能力邊界必須延伸到頁面後續載入的每一項資源。
下一篇將繼續拆開這個問題:Kanboard 公開任務資源權限實錄:附件與內部連結如何避免洩漏。
