Skip to content

Commit

Permalink
Encrypt Query Params in Page Links
Browse files Browse the repository at this point in the history
Closes: #1995
  • Loading branch information
alexanderkiel committed Sep 6, 2024
1 parent f0b86e0 commit e05de6f
Show file tree
Hide file tree
Showing 71 changed files with 1,839 additions and 512 deletions.
3 changes: 2 additions & 1 deletion .clj-kondo/config.edn
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{:lint-as
{blaze.module.test-util/with-system clojure.core/with-open}
{blaze.db.api-stub/with-system-data clojure.core/with-open
blaze.module.test-util/with-system clojure.core/with-open}

:linters
{:unsorted-required-namespaces
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,4 @@ RESULT=$(curl -sXDELETE -H "Prefer: return=OperationOutcome" "$BASE/Provenance")
test "resource type" "$(echo "$RESULT" | jq -r .resourceType)" "OperationOutcome"
test "severity" "$(echo "$RESULT" | jq -r .issue[0].severity)" "success"
test "code" "$(echo "$RESULT" | jq -r .issue[0].code)" "success"
test "diagnostics" "$(echo "$RESULT" | jq -r .issue[0].diagnostics)" "Successfully deleted 120 Provenances."
test "diagnostics" "$(echo "$RESULT" | jq -r .issue[0].diagnostics)" "Successfully deleted 119 Provenances."
8 changes: 4 additions & 4 deletions .github/scripts/link-header-encoding.sh
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,13 @@ TOTAL=$(curl -s -H 'Accept: application/fhir+json' "$URL&_summary=count" | jq .t
HEADERS=$(curl -s -H 'Accept: application/fhir+json' -o /dev/null -D - "$URL")
LINK_HEADER=$(echo "$HEADERS" | grep -i link | tr -d '\r')

test "Number of patients found" "$TOTAL" "1"
test "Encoded search param value" "$(echo "$LINK_HEADER" | awk -F'[;,<>?&=]' '{print $4}')" "Le%C3%B3n"
test "number of patients found" "$TOTAL" "1"
test "encoded search param value" "$(echo "$LINK_HEADER" | awk -F'[;,<>?&=]' '{print $9}')" "Le%C3%B3n"

URL="$BASE/Condition?code=59621000,10509002"
TOTAL=$(curl -s -H 'Accept: application/fhir+json' "$URL&_summary=count" | jq .total)
HEADERS=$(curl -s -H 'Accept: application/fhir+json' -o /dev/null -D - "$URL")
LINK_HEADER=$(echo "$HEADERS" | grep -i link | tr -d '\r')

test "Number of conditions found" "$TOTAL" "93"
test "Encoded search param value" "$(echo "$LINK_HEADER" | awk -F'[;,<>?&=]' '{print $4}')" "59621000%2C10509002"
test "number of conditions found" "$TOTAL" "93"
test "encoded search param value" "$(echo "$LINK_HEADER" | awk -F'[;,<>?&=]' '{print $9}')" "59621000%2C10509002"
10 changes: 10 additions & 0 deletions .github/scripts/patient-everything-paged.sh
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
#!/bin/bash -e

#
# This script fetches the first page of Patient $everything, delete the
# Provenance resource of the patient and expects the size of the next page
# being still the same as without the deletion
#

SCRIPT_DIR="$(dirname "$(readlink -f "$0")")"
. "$SCRIPT_DIR/util.sh"

Expand All @@ -9,6 +15,10 @@ PATIENT_ID=$(curl -s "$BASE/Patient?identifier=$PATIENT_IDENTIFIER" | jq -r '.en
FIRST_PAGE=$(curl -s "$BASE/Patient/$PATIENT_ID/\$everything?_count=2000")
FIRST_PAGE_SIZE=$(echo "$FIRST_PAGE" | jq -r '.entry | length')
NEXT_LINK="$(echo "$FIRST_PAGE" | jq -r '.link[] | select(.relation == "next") | .url')"

# delete the Provenance resource of the patient
curl -sXDELETE "$BASE/Provenance?target=$PATIENT_ID" | jq

SECOND_PAGE="$(curl -sH "Accept: application/fhir+json" "$NEXT_LINK")"
SECOND_PAGE_SIZE=$(echo "$SECOND_PAGE" | jq -r '.entry | length')

Expand Down
7 changes: 5 additions & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ jobs:
- operation-measure-evaluate-measure
- operation-patient-everything
- operation-totals
- page-id-cipher
- page-store
- page-store-cassandra
- rest-api
Expand Down Expand Up @@ -1770,8 +1771,10 @@ jobs:
- name: Wait for Blaze
run: .github/scripts/wait-for-url.sh http://localhost:8080/health

- name: Fetch Patient Expecting an Error
run: .github/scripts/fetch-resource-0-with-missing-resource-content.sh
# TODO: Reactivate if we have an idea how to handle the missing DocumentReference
# of the page ID cipher at startup.
#- name: Fetch Patient Expecting an Error
# run: .github/scripts/fetch-resource-0-with-missing-resource-content.sh

# This test ensures that older versions of Blaze will migrate successfully to
# the new database schema especially building the PatientLastChange index.
Expand Down
1 change: 1 addition & 0 deletions cljfmt.edn
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
given [[:block 1]]
given-translation [[:block 0]]
if-ok [[:block 1]]
try-one [[:block 2]]
when-ok [[:block 1]]
do-sync [[:block 1]]
do-async [[:block 1]]
Expand Down
27 changes: 27 additions & 0 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@ When searching for date/time with a search parameter value without timezone like

The special search parameter `_sort` supports the values `_id`, `_lastUpdated` and `-_lastUpdated`.

#### Paging

The search-type interaction supports paging which is described in depth in the separate [paging section](#paging-1).

### Capabilities

Get the capability statement for Blaze. Blaze supports filtering the capability statement by `_elements`. For more information, see: [FHIR - RESTful API - Capabilities][5]
Expand Down Expand Up @@ -152,6 +156,29 @@ Async requests can be cancelled before they are completed:
curl -svXDELETE "http://localhost:8080/fhir/__async-status/DD7MLX6H7OGJN7SD"
```

## Paging

Interactions and operations that return a large list of resources support paging via Bundle resources. The various Bundle resources are interlinked via the next link. The paging has the following properties:

### Paging is Stable

The initial request operates on the newest database snapshot available and all pages accessible via next links will continue to use the same database snapshot. Next links will point to custom paging endpoints. The endpoints will expire after for 4 hours in order to constrain the access to old database snapshots. That also means that clients which hold paging URLs will be able to access deleted and changed resources for up to 4 hours.

### Paging URLs are Encrypted

The variable part of paging URLs is encrypted to ensure confidentiality and integrity of the paging parameters. Confidentiality is important in case some of the original query parameters contain sensitive information. To mitigate the risk of exposing this data, FHIR searches are often executed via POST requests, which helps prevent sensitive information from being logged in URLs. Consequently, it is essential that paging URLs do not reveal any confidential data. Integrity is important, because it should not be possible to manipulate the paging URL in order to access a different database snapshot.

#### Encryption Key Management

<dl>
<dt>Key Rotation</dt>
<dd>Encryption keys are rotated every two hours. Each key is valid for a maximum of four hours, with a total of three keys stored at any time.</dd>
<dt>Storage</dt>
<dd>Currently, encryption keys are stored in plain text within the admin database. While these keys are not accessible via an API, they are also not encrypted with an external key encryption method.</dd>
<dt>Future Improvements</dt>
<dd>Implementing external key encryption is feasible but would require additional infrastructure. If you believe that key encryption is necessary, please open an issue for further discussion.</dd>
</dl>

## Absolute URLs

Blaze has to generate absolute URLs of its own in links and Location headers. By default Blaze assumes to be accessible under `http://localhost:8080`. The [environment variable](deployment/environment-variables.md) `BASE_URL` can be used to change this.
Expand Down
4 changes: 3 additions & 1 deletion modules/admin-api/test/blaze/admin_api_test.clj
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,7 @@
{:clock (ig/ref :blaze.test/fixed-clock)
:rng-fn (ig/ref :blaze.test/fixed-rng-fn)
:page-store (ig/ref :blaze/page-store)
:page-id-cipher (ig/ref :blaze.test/page-id-cipher)
:context-path "/fhir"}

:blaze.page-store/local
Expand All @@ -210,7 +211,8 @@
:blaze/scheduler {}

:blaze.test/fixed-rng {}
:blaze.test/fixed-rng-fn {}})
:blaze.test/fixed-rng-fn {}
:blaze.test/page-id-cipher {}})

(defn- new-temp-dir! []
(str (Files/createTempDirectory "blaze" (make-array FileAttribute 0))))
Expand Down
5 changes: 4 additions & 1 deletion modules/fhir-test-util/deps.edn
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,7 @@
{:local/root "../fhir-structure"}

blaze/module-test-util
{:local/root "../module-test-util"}}}
{:local/root "../module-test-util"}

com.google.crypto.tink/tink
{:mvn/version "1.15.0"}}}
18 changes: 17 additions & 1 deletion modules/fhir-test-util/src/blaze/fhir/test_util.clj
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,17 @@
[java-time.api :as time]
[juxt.iota :refer [given]])
(:import
[com.google.crypto.tink Aead DeterministicAead KeysetHandle]
[com.google.crypto.tink.daead DeterministicAeadConfig PredefinedDeterministicAeadParameters]
[java.time Clock Instant]
[java.util Random]
[java.util.concurrent Executors TimeUnit]))

(set! *warn-on-reflection* true)
(DeterministicAeadConfig/register)

(defmacro given-failed-future [future & body]
`(given (try (deref ~future) (is false) (catch Exception e# (ba/anomaly e#)))
`(given (ba/try-anomaly (deref ~future) (is false))
~@body))

(defmethod ig/init-key :blaze.test/fixed-clock
Expand Down Expand Up @@ -62,3 +65,16 @@

(defn link-url [body link-relation]
(->> body :link (filter (comp #{link-relation} :relation)) first :url type/value))

(defmethod ig/init-key :blaze.test/page-id-cipher
[_ _]
(let [^DeterministicAead aead
(-> (KeysetHandle/generateNew PredefinedDeterministicAeadParameters/AES256_SIV)
(.getPrimitive DeterministicAead))]
;; this wraps a DeterministicAead into a normal Aead
;; should be only done in tests
(reify Aead
(encrypt [_ plaintext associatedData]
(.encryptDeterministically aead plaintext associatedData))
(decrypt [_ ciphertext associatedData]
(.decryptDeterministically aead ciphertext associatedData)))))
3 changes: 3 additions & 0 deletions modules/frontend/src/params/pageId.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function match(param) {
return /[A-Za-z0-9\-_]+/.test(param);
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import { base } from '$app/paths';
import { error, type NumericRange } from '@sveltejs/kit';
import { transformBundle } from '$lib/resource/resource-card.js';

export const load: PageLoad = async ({ fetch, params, url }) => {
const res = await fetch(`${base}/${params.type}/__history-page?${url.searchParams}`, {
export const load: PageLoad = async ({ fetch, params }) => {
const res = await fetch(`${base}/${params.type}/__history-page/${params.pageId}`, {
headers: { Accept: 'application/fhir+json' }
});

Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import type { RequestHandler } from './$types';
import { base } from '$app/paths';

export const GET: RequestHandler = async ({ params, fetch, url }) => {
const res = await fetch(`${base}/${params.type}/__page?${url.searchParams}`, {
export const GET: RequestHandler = async ({ params, fetch }) => {
const res = await fetch(`${base}/${params.type}/__history-page/${params.pageId}`, {
headers: { Accept: 'application/fhir+json' }
});
return new Response(await res.blob(), res);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@
import BreadcrumbEntryType from '$lib/breadcrumb/type.svelte';
import BreadcrumbEntryPage from '$lib/breadcrumb/page.svelte';
import SearchForm from './../search-form.svelte';
import SearchForm from '../../search-form.svelte';
import TotalCard from '$lib/total-card.svelte';
import TotalBadge from '$lib/total-badge.svelte';
import DurationBadge from '$lib/duration-badge.svelte';
import EntryCard from '$lib/history/entry-card.svelte';
import NoResultsCard from './../no-results-card.svelte';
import ErrorCard from './../../error-card.svelte';
import NoResultsCard from '../../no-results-card.svelte';
import ErrorCard from '../../../error-card.svelte';
export let data: PageData;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import type { PageLoad } from './$types';
import type { CapabilityStatementRestResourceSearchParam } from 'fhir/r4';

import { fetchPageBundleWithDuration } from '../util.js';
import { fetchPageBundleWithDuration } from '../../util.js';
import { base } from '$app/paths';
import { error, type NumericRange } from '@sveltejs/kit';

export const load: PageLoad = async ({ fetch, params, url }) => {
export const load: PageLoad = async ({ fetch, params }) => {
const res = await fetch(`${base}/${params.type}/__search-params`, {
headers: { Accept: 'application/json' }
});
Expand All @@ -18,7 +18,7 @@ export const load: PageLoad = async ({ fetch, params, url }) => {
searchParams: (await res.json()).searchParams as CapabilityStatementRestResourceSearchParam[],
streamed: {
start: Date.now(),
bundle: fetchPageBundleWithDuration(fetch, params, url)
bundle: fetchPageBundleWithDuration(fetch, params, params.pageId)
}
};
};
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import type { RequestHandler } from './$types';
import { base } from '$app/paths';

export const GET: RequestHandler = async ({ params, fetch, url }) => {
const res = await fetch(`${base}/${params.type}/__history-page?${url.searchParams}`, {
export const GET: RequestHandler = async ({ params, fetch }) => {
const res = await fetch(`${base}/${params.type}/__page/${params.pageId}`, {
headers: { Accept: 'application/fhir+json' }
});
return new Response(await res.blob(), res);
Expand Down
4 changes: 2 additions & 2 deletions modules/frontend/src/routes/[type=type]/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,11 +58,11 @@ export async function fetchBundleWithDuration(
export async function fetchPageBundleWithDuration(
fetch: typeof window.fetch,
params: RouteParams,
url: URL
pageId: string
) {
const start = Date.now();

const res = await fetch(`${base}/${params.type}/__page?${processParams(url.searchParams)}`, {
const res = await fetch(`${base}/${params.type}/__page/${pageId}`, {
headers: { Accept: 'application/fhir+json' }
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import { base } from '$app/paths';
import { error, type NumericRange } from '@sveltejs/kit';
import { transformBundle } from '$lib/resource/resource-card.js';

export const load: PageLoad = async ({ fetch, url }) => {
const res = await fetch(`${base}/__history-page?${url.searchParams}`, {
export const load: PageLoad = async ({ fetch, params }) => {
const res = await fetch(`${base}/__history-page/${params.pageId}`, {
headers: { Accept: 'application/fhir+json' }
});

Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import type { RequestHandler } from './$types';
import { base } from '$app/paths';

export const GET: RequestHandler = async ({ fetch, url }) => {
const res = await fetch(`${base}/__history-page?${url.searchParams}`, {
export const GET: RequestHandler = async ({ fetch, params }) => {
const res = await fetch(`${base}/__history-page/${params.pageId}`, {
headers: { Accept: 'application/fhir+json' }
});
return new Response(await res.blob(), res);
Expand Down
3 changes: 3 additions & 0 deletions modules/interaction/deps.edn
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@
blaze/module-base
{:local/root "../module-base"}

blaze/page-id-cipher
{:local/root "../page-id-cipher"}

blaze/page-store
{:local/root "../page-store"}

Expand Down
10 changes: 4 additions & 6 deletions modules/interaction/src/blaze/interaction/history/instance.clj
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
[blaze.handler.util :as handler-util]
[blaze.interaction.history.util :as history-util]
[blaze.module :as m]
[blaze.page-id-cipher.spec]
[blaze.spec]
[clojure.spec.alpha :as s]
[cognitect.anomalies :as anom]
Expand All @@ -20,9 +21,6 @@
[ring.util.response :as ring]
[taoensso.timbre :as log]))

(defn- match [router type name id]
(reitit/match-by-name router (keyword type name) {:id id}))

(defn- next-link [context query-params resource-handle]
{:fhir/type :fhir.Bundle/link
:relation "next"
Expand Down Expand Up @@ -56,7 +54,7 @@
(update :link conj (next-link (peek paged-version-handles))))))))))

(defmethod m/pre-init-spec :blaze.interaction.history/instance [_]
(s/keys :req-un [:blaze/clock :blaze/rng-fn]))
(s/keys :req-un [:blaze/clock :blaze/rng-fn :blaze/page-id-cipher]))

(defmethod ig/init-key :blaze.interaction.history/instance [_ context]
(log/info "Init FHIR history instance interaction handler")
Expand All @@ -73,8 +71,8 @@
:blaze/base-url base-url
:blaze/db db
::reitit/router router
::reitit/match (match router type "history-instance" id)
::reitit/page-match (match router type "history-instance-page" id))]
::reitit/match (reitit/match-by-name router (keyword type "history-instance") {:id id})
:page-match #(reitit/match-by-name router (keyword type "history-instance-page") {:id id :page-id %}))]
(build-response context params total version-handles since))
(ac/completed-future
(ba/not-found
Expand Down
5 changes: 3 additions & 2 deletions modules/interaction/src/blaze/interaction/history/system.clj
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
[blaze.handler.util :as handler-util]
[blaze.interaction.history.util :as history-util]
[blaze.module :as m]
[blaze.page-id-cipher.spec]
[blaze.spec]
[blaze.util :refer [conj-vec]]
[clojure.spec.alpha :as s]
Expand Down Expand Up @@ -61,7 +62,7 @@
(update :link conj-vec (next-link (peek paged-version-handles))))))))))

(defmethod m/pre-init-spec :blaze.interaction.history/system [_]
(s/keys :req-un [:blaze/clock :blaze/rng-fn]))
(s/keys :req-un [:blaze/clock :blaze/rng-fn :blaze/page-id-cipher]))

(defmethod ig/init-key :blaze.interaction.history/system [_ context]
(log/info "Init FHIR history system interaction handler")
Expand All @@ -79,5 +80,5 @@
:blaze/db db
::reitit/router router
::reitit/match (match router :history)
::reitit/page-match (match router :history-page))]
:page-match #(reitit/match-by-name router :history-page {:page-id %}))]
(build-response context params total version-handles since))))
Loading

0 comments on commit e05de6f

Please sign in to comment.