下拉重新整理

在 Keycloak 和 HAPI FHIR 中間放一層 Rails(二):把 scope 展開成真正的資料邊界

476 字 2 分鐘閱讀 15 次閱讀

承上一篇。這層 Rails policy gateway 最核心的價值,就是把 token 裡粗略的 scope,展開成真正能約束資料範圍的 constraint。

Token 裡的 scope 通常很粗。像這樣:

{
  "scope": "patient/Observation.read",
  "patient": "123",
  "purpose_of_use": "patient-access",
  "policy": "recent-clinical-data"
}

它只說了「這個人可以讀 Patient 123 的 Observation」。但實務上我要的邊界細很多。所以 Rails 收到 token 後,根據 policy 把它展開成一組明確的執行規則:

{
  patient_id: "123",
  resource_types: ["Observation"],
  operations: ["read", "search"],
  date_window_days: 7,
  exclude_security_labels: ["restricted", "psychiatric"],
  max_results: 100
}

查詢改寫:使用者送的,不等於送進 HAPI 的

假設 App 發出的請求是:

GET /fhir/Observation?category=laboratory

Rails 不會照原樣轉給 HAPI。它會依 constraint 把查詢收斂之後,才送出去:

GET /fhir/Observation
  ?patient=Patient/123
  &category=laboratory
  &date=ge2026-07-15
  &_count=100

這種改寫很適合處理一整排需求:僅限本人、僅限最近七天、僅限某次 Encounter、僅限某個院區、僅限特定 Resource、限制 _include_revinclude、禁止 $everything、限制 batch 跟 transaction、把大量匯出擋掉、遮蔽特定欄位。這些都在 Rails 這層用資料表達,而不是散在各處的 if。

一個容易漏掉的陷阱:直接用 ID 讀取

這裡有個坑我一定要提。Rails 不能只改寫 search。如果有人知道 Resource ID,直接打:

GET /Observation/987

這條路徑繞過了你精心設計的搜尋條件。所以取得 Resource 之後,還要再確認一次:

resource.subject == "Patient/123"
resource.effective_date >= 7.days.ago

不驗這一關,等於前面的 constraint 只擋得住乖乖走 search 的人,知道 ID 的人照樣撈得到別人的資料。search 改寫跟 by-ID 驗證,兩邊都要做,缺一個邊界就是破的。

constraint 這件事看起來瑣碎,但它就是這層 gateway 真正在賺價值的地方。下一篇我會講 CQL,以及為什麼我不讓它碰授權。