前言

完成 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,提供複製與另開無痕視窗驗證。遺失時不能「找回」,只能輪替:

  1. 撤銷目前有效分享。
  2. 產生新的高熵 Token。
  3. 保存新 hash。
  4. 回傳一次性新 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-store
  • Referrer-Policy: no-referrer
  • X-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 公開任務資源權限實錄:附件與內部連結如何避免洩漏