diff --git a/.clj-kondo/config.edn b/.clj-kondo/config.edn
index 91038d3ec..90a2f95ce 100644
--- a/.clj-kondo/config.edn
+++ b/.clj-kondo/config.edn
@@ -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
diff --git a/.github/scripts/conditional-delete-type/delete-all-provenance-resources.sh b/.github/scripts/conditional-delete-type/delete-all-provenance-resources.sh
index 80421c639..c61f40918 100755
--- a/.github/scripts/conditional-delete-type/delete-all-provenance-resources.sh
+++ b/.github/scripts/conditional-delete-type/delete-all-provenance-resources.sh
@@ -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."
diff --git a/.github/scripts/link-header-encoding.sh b/.github/scripts/link-header-encoding.sh
index cdab32e19..ac616e166 100755
--- a/.github/scripts/link-header-encoding.sh
+++ b/.github/scripts/link-header-encoding.sh
@@ -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"
diff --git a/.github/scripts/patient-everything-paged.sh b/.github/scripts/patient-everything-paged.sh
index d9df537d6..f842963da 100755
--- a/.github/scripts/patient-everything-paged.sh
+++ b/.github/scripts/patient-everything-paged.sh
@@ -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"
@@ -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')
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 22f78f5c7..0dc26749b 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -109,6 +109,7 @@ jobs:
- operation-measure-evaluate-measure
- operation-patient-everything
- operation-totals
+ - page-id-cipher
- page-store
- page-store-cassandra
- rest-api
@@ -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.
diff --git a/cljfmt.edn b/cljfmt.edn
index 01b8feb27..f90509483 100644
--- a/cljfmt.edn
+++ b/cljfmt.edn
@@ -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]]
diff --git a/docs/api.md b/docs/api.md
index 116dc0dce..6417059c0 100644
--- a/docs/api.md
+++ b/docs/api.md
@@ -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]
@@ -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
+
+
+ - Key Rotation
+ - 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.
+ - Storage
+ - 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.
+ - Future Improvements
+ - 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.
+
+
## 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.
diff --git a/modules/admin-api/test/blaze/admin_api_test.clj b/modules/admin-api/test/blaze/admin_api_test.clj
index d3629d89e..d76ad93b0 100644
--- a/modules/admin-api/test/blaze/admin_api_test.clj
+++ b/modules/admin-api/test/blaze/admin_api_test.clj
@@ -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
@@ -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))))
diff --git a/modules/fhir-test-util/deps.edn b/modules/fhir-test-util/deps.edn
index 05a65d1a3..a70f4765c 100644
--- a/modules/fhir-test-util/deps.edn
+++ b/modules/fhir-test-util/deps.edn
@@ -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"}}}
diff --git a/modules/fhir-test-util/src/blaze/fhir/test_util.clj b/modules/fhir-test-util/src/blaze/fhir/test_util.clj
index 0e854c306..543920249 100644
--- a/modules/fhir-test-util/src/blaze/fhir/test_util.clj
+++ b/modules/fhir-test-util/src/blaze/fhir/test_util.clj
@@ -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
@@ -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)))))
diff --git a/modules/frontend/src/params/pageId.ts b/modules/frontend/src/params/pageId.ts
new file mode 100644
index 000000000..3fb166dab
--- /dev/null
+++ b/modules/frontend/src/params/pageId.ts
@@ -0,0 +1,3 @@
+export function match(param) {
+ return /[A-Za-z0-9\-_]+/.test(param);
+}
diff --git a/modules/frontend/src/routes/[type=type]/__history-page/+page.svelte b/modules/frontend/src/routes/[type=type]/__history-page/[pageId=pageId]/+page.svelte
similarity index 100%
rename from modules/frontend/src/routes/[type=type]/__history-page/+page.svelte
rename to modules/frontend/src/routes/[type=type]/__history-page/[pageId=pageId]/+page.svelte
diff --git a/modules/frontend/src/routes/[type=type]/__history-page/+page.ts b/modules/frontend/src/routes/[type=type]/__history-page/[pageId=pageId]/+page.ts
similarity index 74%
rename from modules/frontend/src/routes/[type=type]/__history-page/+page.ts
rename to modules/frontend/src/routes/[type=type]/__history-page/[pageId=pageId]/+page.ts
index f32316bd9..1dab0a120 100644
--- a/modules/frontend/src/routes/[type=type]/__history-page/+page.ts
+++ b/modules/frontend/src/routes/[type=type]/__history-page/[pageId=pageId]/+page.ts
@@ -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' }
});
diff --git a/modules/frontend/src/routes/[type=type]/__page/+server.ts b/modules/frontend/src/routes/[type=type]/__history-page/[pageId=pageId]/+server.ts
similarity index 54%
rename from modules/frontend/src/routes/[type=type]/__page/+server.ts
rename to modules/frontend/src/routes/[type=type]/__history-page/[pageId=pageId]/+server.ts
index dcc95919d..abb8185f3 100644
--- a/modules/frontend/src/routes/[type=type]/__page/+server.ts
+++ b/modules/frontend/src/routes/[type=type]/__history-page/[pageId=pageId]/+server.ts
@@ -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);
diff --git a/modules/frontend/src/routes/[type=type]/__page/+page.svelte b/modules/frontend/src/routes/[type=type]/__page/[pageId=pageId]/+page.svelte
similarity index 93%
rename from modules/frontend/src/routes/[type=type]/__page/+page.svelte
rename to modules/frontend/src/routes/[type=type]/__page/[pageId=pageId]/+page.svelte
index 4b7ed2160..d6277712c 100644
--- a/modules/frontend/src/routes/[type=type]/__page/+page.svelte
+++ b/modules/frontend/src/routes/[type=type]/__page/[pageId=pageId]/+page.svelte
@@ -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;
diff --git a/modules/frontend/src/routes/[type=type]/__page/+page.ts b/modules/frontend/src/routes/[type=type]/__page/[pageId=pageId]/+page.ts
similarity index 75%
rename from modules/frontend/src/routes/[type=type]/__page/+page.ts
rename to modules/frontend/src/routes/[type=type]/__page/[pageId=pageId]/+page.ts
index 1e4718b5a..70391c073 100644
--- a/modules/frontend/src/routes/[type=type]/__page/+page.ts
+++ b/modules/frontend/src/routes/[type=type]/__page/[pageId=pageId]/+page.ts
@@ -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' }
});
@@ -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)
}
};
};
diff --git a/modules/frontend/src/routes/[type=type]/__history-page/+server.ts b/modules/frontend/src/routes/[type=type]/__page/[pageId=pageId]/+server.ts
similarity index 53%
rename from modules/frontend/src/routes/[type=type]/__history-page/+server.ts
rename to modules/frontend/src/routes/[type=type]/__page/[pageId=pageId]/+server.ts
index 8343897b9..5de176f12 100644
--- a/modules/frontend/src/routes/[type=type]/__history-page/+server.ts
+++ b/modules/frontend/src/routes/[type=type]/__page/[pageId=pageId]/+server.ts
@@ -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);
diff --git a/modules/frontend/src/routes/[type=type]/util.ts b/modules/frontend/src/routes/[type=type]/util.ts
index 2bf6a7d94..5da273218 100644
--- a/modules/frontend/src/routes/[type=type]/util.ts
+++ b/modules/frontend/src/routes/[type=type]/util.ts
@@ -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' }
});
diff --git a/modules/frontend/src/routes/__history-page/+page.svelte b/modules/frontend/src/routes/__history-page/[pageId=pageId]/+page.svelte
similarity index 100%
rename from modules/frontend/src/routes/__history-page/+page.svelte
rename to modules/frontend/src/routes/__history-page/[pageId=pageId]/+page.svelte
diff --git a/modules/frontend/src/routes/__history-page/+page.ts b/modules/frontend/src/routes/__history-page/[pageId=pageId]/+page.ts
similarity index 78%
rename from modules/frontend/src/routes/__history-page/+page.ts
rename to modules/frontend/src/routes/__history-page/[pageId=pageId]/+page.ts
index 264c1c62e..43a541414 100644
--- a/modules/frontend/src/routes/__history-page/+page.ts
+++ b/modules/frontend/src/routes/__history-page/[pageId=pageId]/+page.ts
@@ -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' }
});
diff --git a/modules/frontend/src/routes/__history-page/+server.ts b/modules/frontend/src/routes/__history-page/[pageId=pageId]/+server.ts
similarity index 57%
rename from modules/frontend/src/routes/__history-page/+server.ts
rename to modules/frontend/src/routes/__history-page/[pageId=pageId]/+server.ts
index a13980382..5897df698 100644
--- a/modules/frontend/src/routes/__history-page/+server.ts
+++ b/modules/frontend/src/routes/__history-page/[pageId=pageId]/+server.ts
@@ -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);
diff --git a/modules/interaction/deps.edn b/modules/interaction/deps.edn
index 4ab2d8848..51e9b58a7 100644
--- a/modules/interaction/deps.edn
+++ b/modules/interaction/deps.edn
@@ -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"}
diff --git a/modules/interaction/src/blaze/interaction/history/instance.clj b/modules/interaction/src/blaze/interaction/history/instance.clj
index 364aaeabb..3f3498745 100644
--- a/modules/interaction/src/blaze/interaction/history/instance.clj
+++ b/modules/interaction/src/blaze/interaction/history/instance.clj
@@ -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]
@@ -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"
@@ -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")
@@ -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
diff --git a/modules/interaction/src/blaze/interaction/history/system.clj b/modules/interaction/src/blaze/interaction/history/system.clj
index e26e6006d..3f5464d87 100644
--- a/modules/interaction/src/blaze/interaction/history/system.clj
+++ b/modules/interaction/src/blaze/interaction/history/system.clj
@@ -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]
@@ -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")
@@ -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))))
diff --git a/modules/interaction/src/blaze/interaction/history/type.clj b/modules/interaction/src/blaze/interaction/history/type.clj
index 9f7058eb7..d2088eec6 100644
--- a/modules/interaction/src/blaze/interaction/history/type.clj
+++ b/modules/interaction/src/blaze/interaction/history/type.clj
@@ -11,6 +11,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]
@@ -20,9 +21,6 @@
[ring.util.response :as ring]
[taoensso.timbre :as log]))
-(defn- match [router type name]
- (reitit/match-by-name router (keyword type name)))
-
(defn- next-link [context query-params resource-handle]
{:fhir/type :fhir.Bundle/link
:relation "next"
@@ -58,7 +56,7 @@
(update :link conj-vec (next-link (peek paged-version-handles))))))))))
(defmethod m/pre-init-spec :blaze.interaction.history/type [_]
- (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/type [_ context]
(log/info "Init FHIR history type interaction handler")
@@ -75,6 +73,6 @@
:blaze/base-url base-url
:blaze/db db
::reitit/router router
- ::reitit/match (match router type "history")
- ::reitit/page-match (match router type "history-page"))]
+ ::reitit/match (reitit/match-by-name router (keyword type "history"))
+ :page-match #(reitit/match-by-name router (keyword type "history-page") {:page-id %}))]
(build-response context params total version-handles since))))
diff --git a/modules/interaction/src/blaze/interaction/history/util.clj b/modules/interaction/src/blaze/interaction/history/util.clj
index 722fcf1cc..79701ad7b 100644
--- a/modules/interaction/src/blaze/interaction/history/util.clj
+++ b/modules/interaction/src/blaze/interaction/history/util.clj
@@ -3,6 +3,7 @@
[blaze.db.api :as d]
[blaze.fhir.spec.type :as type]
[blaze.handler.fhir.util :as fhir-util]
+ [blaze.middleware.fhir.decrypt-page-id :as decrypt-page-id]
[blaze.util :as u]
[reitit.core :as reitit])
(:import
@@ -61,15 +62,17 @@
'([context query-params page-t]
[context query-params page-t id]
[context query-params page-t type id])}
- [{:blaze/keys [base-url db] ::reitit/keys [page-match]} query-params page-t & more]
- (let [path (reitit/match->path
- page-match
- (cond-> (assoc query-params "__t" (d/t db) "__page-t" page-t)
- (= 1 (count more))
- (assoc "__page-id" (first more))
- (= 2 (count more))
- (assoc "__page-type" (first more) "__page-id" (second more))))]
- (str base-url path)))
+ [{:blaze/keys [base-url db] :keys [page-id-cipher page-match]} query-params
+ page-t & more]
+ (->> (cond-> (assoc query-params "__t" (str (d/t db)) "__page-t" (str page-t))
+ (= 1 (count more))
+ (assoc "__page-id" (first more))
+ (= 2 (count more))
+ (assoc "__page-type" (first more) "__page-id" (second more)))
+ (decrypt-page-id/encrypt page-id-cipher)
+ (page-match)
+ (reitit/match->path)
+ (str base-url)))
(defn- method [resource]
((-> resource meta :blaze.db/op)
diff --git a/modules/interaction/src/blaze/interaction/search/nav.clj b/modules/interaction/src/blaze/interaction/search/nav.clj
index 7165f4246..7851a9aa0 100644
--- a/modules/interaction/src/blaze/interaction/search/nav.clj
+++ b/modules/interaction/src/blaze/interaction/search/nav.clj
@@ -1,11 +1,14 @@
(ns blaze.interaction.search.nav
(:require
[blaze.async.comp :as ac :refer [do-sync]]
+ [blaze.middleware.fhir.decrypt-page-id :as decrypt-page-id]
[blaze.page-store :as page-store]
[blaze.util :refer [conj-vec]]
[clojure.string :as str]
[reitit.core :as reitit]))
+(set! *warn-on-reflection* true)
+
(defmulti clause->query-param (fn [_ret [key]] key))
(defmethod clause->query-param :sort
@@ -79,7 +82,7 @@
(seq elements)
(assoc "_elements" (str/join "," (map name elements)))
page-size
- (assoc "_count" page-size)))
+ (assoc "_count" (str page-size))))
(defn- query-params [params clauses]
(merge-params (clauses->query-params clauses) params))
@@ -98,11 +101,16 @@
(defn token-url!
"Returns a CompletableFuture that will complete with a URL that will encode
`clauses` in a token."
- [page-store base-url match params clauses t offset]
+ [{:keys [page-store page-id-cipher] :blaze/keys [base-url]} match params
+ clauses t offset]
(do-sync [query-params (token-query-params! page-store params clauses)]
- (str base-url (reitit/match->path match (-> query-params
- (assoc "__t" t)
- (merge offset))))))
+ (->> (-> query-params
+ (assoc "__t" (str t))
+ (merge offset))
+ (decrypt-page-id/encrypt page-id-cipher)
+ (match)
+ (reitit/match->path)
+ (str base-url))))
(defn- clauses->token-query-params [token clauses]
(if token {"__token" token} (clauses->query-params clauses)))
@@ -111,8 +119,12 @@
(merge-params (clauses->token-query-params token clauses) params))
(defn token-url
- [base-url match params token clauses t offset]
+ [base-url page-id-cipher match params token clauses t offset]
(let [query-params (token-query-params params token clauses)]
- (str base-url (reitit/match->path match (-> query-params
- (assoc "__t" t)
- (merge offset))))))
+ (->> (-> query-params
+ (assoc "__t" (str t))
+ (merge offset))
+ (decrypt-page-id/encrypt page-id-cipher)
+ (match)
+ (reitit/match->path)
+ (str base-url))))
diff --git a/modules/interaction/src/blaze/interaction/search_compartment.clj b/modules/interaction/src/blaze/interaction/search_compartment.clj
index 88a1bc3f9..f579b679f 100644
--- a/modules/interaction/src/blaze/interaction/search_compartment.clj
+++ b/modules/interaction/src/blaze/interaction/search_compartment.clj
@@ -52,12 +52,12 @@
:url (nav/url base-url match params clauses)})
(defn- next-link-offset [{:keys [page-offset]} entries]
- {"__page-offset" (+ page-offset (dec (count entries)))})
+ {"__page-offset" (str (+ page-offset (dec (count entries))))})
(defn- next-link
- [{:keys [page-store match params] :blaze/keys [base-url db]} clauses entries]
- (do-sync [url (nav/token-url! page-store base-url match params clauses
- (d/t db) (next-link-offset params entries))]
+ [{:keys [page-match params] :blaze/keys [db] :as context} clauses entries]
+ (do-sync [url (nav/token-url! context page-match params clauses (d/t db)
+ (next-link-offset params entries))]
{:fhir/type :fhir.Bundle/link
:relation "next"
:url url}))
@@ -100,6 +100,10 @@
(ac/completed-future (search-summary context))
(search-normal context)))
+(defn page-match [router code id type]
+ #(reitit/match-by-name router (keyword code "compartment-page")
+ {:id id :type type :page-id %}))
+
(defn- search-context
[{:keys [page-store] :as context}
{{{:fhir.compartment/keys [code]} :data :as match} ::reitit/match
@@ -132,12 +136,14 @@
:code code
:id id
:type type
+ :page-match (page-match router code id type)
:params params)
handling
(assoc :blaze.preference/handling handling))))))
(defmethod m/pre-init-spec :blaze.interaction/search-compartment [_]
- (s/keys :req-un [:blaze/clock :blaze/rng-fn :blaze/page-store]))
+ (s/keys :req-un [:blaze/clock :blaze/rng-fn :blaze/page-store
+ :blaze/page-id-cipher]))
(defmethod ig/init-key :blaze.interaction/search-compartment [_ context]
(log/info "Init FHIR search-compartment interaction handler")
diff --git a/modules/interaction/src/blaze/interaction/search_system.clj b/modules/interaction/src/blaze/interaction/search_system.clj
index 2f1279eb4..914c1ab07 100644
--- a/modules/interaction/src/blaze/interaction/search_system.clj
+++ b/modules/interaction/src/blaze/interaction/search_system.clj
@@ -49,9 +49,9 @@
{"__page-type" (name type) "__page-id" id}))
(defn- next-link
- [{:keys [page-store page-match params] :blaze/keys [base-url db]} entries]
- (do-sync [url (nav/token-url! page-store base-url page-match params []
- (d/t db) (next-link-offset entries))]
+ [{:keys [page-match params] :blaze/keys [db] :as context} entries]
+ (do-sync [url (nav/token-url! context page-match params [] (d/t db)
+ (next-link-offset entries))]
{:fhir/type :fhir.Bundle/link
:relation "next"
:url url}))
@@ -106,11 +106,12 @@
:blaze/db db
::reitit/router router
::reitit/match match
- :page-match (reitit/match-by-name router :page)
+ :page-match #(reitit/match-by-name router :page {:page-id %})
:params params))))
(defmethod m/pre-init-spec :blaze.interaction/search-system [_]
- (s/keys :req-un [:blaze/clock :blaze/rng-fn :blaze/page-store]))
+ (s/keys :req-un [:blaze/clock :blaze/rng-fn :blaze/page-store
+ :blaze/page-id-cipher]))
(defmethod ig/init-key :blaze.interaction/search-system [_ context]
(log/info "Init FHIR search-system interaction handler")
diff --git a/modules/interaction/src/blaze/interaction/search_type.clj b/modules/interaction/src/blaze/interaction/search_type.clj
index 69df3b2c7..2269a96fe 100644
--- a/modules/interaction/src/blaze/interaction/search_type.clj
+++ b/modules/interaction/src/blaze/interaction/search_type.clj
@@ -15,6 +15,7 @@
[blaze.interaction.search.util :as search-util]
[blaze.job.async-interaction.request :as req]
[blaze.module :as m]
+ [blaze.page-id-cipher.spec]
[blaze.page-store :as page-store]
[blaze.page-store.spec]
[blaze.spec]
@@ -139,8 +140,8 @@
(defn- next-link
[{:keys [next-link-url-fn]} token clauses next-handle]
- (let [url (next-link-url-fn token clauses (next-link-offset next-handle))]
- (link "next" url)))
+ (->> (next-link-url-fn token clauses (next-link-offset next-handle))
+ (link "next")))
(defn- total
"Calculates the total number of resources returned.
@@ -246,6 +247,11 @@
name]
(reitit/match-by-name router (keyword type name)))
+(defn- page-match
+ [{{{:fhir.resource/keys [type]} :data} ::reitit/match
+ ::reitit/keys [router]}]
+ (fn [page-id] (reitit/match-by-name router (keyword type "page") {:page-id page-id})))
+
(defn- self-link-url-fn [{:blaze/keys [base-url] :as request} params]
(fn [clauses]
(nav/url base-url (match request "type") params clauses)))
@@ -263,21 +269,21 @@
(defn- first-link-url-fn
"Returns a function of `token` and `clauses` that returns the URL of the first
link."
- [{:blaze/keys [base-url db] :as request} params]
+ [{:blaze/keys [base-url db] :as request} page-id-cipher params]
(fn [token clauses]
- (nav/token-url base-url (match request "page") params token clauses
+ (nav/token-url base-url page-id-cipher (page-match request) params token clauses
(d/t db) nil)))
(defn- next-link-url-fn
"Returns a function of `token`, `clauses` and `offset` that returns the URL
of the next link."
- [{:blaze/keys [base-url db] :as request} params]
+ [{:blaze/keys [base-url db] :as request} page-id-cipher params]
(fn [token clauses offset]
- (nav/token-url base-url (match request "page") params token clauses
+ (nav/token-url base-url page-id-cipher (page-match request) params token clauses
(d/t db) offset)))
(defn- search-context
- [{:keys [page-store] :as context}
+ [{:keys [page-store page-id-cipher] :as context}
{{{:fhir.resource/keys [type]} :data} ::reitit/match
:keys [headers params]
:blaze/keys [base-url db]
@@ -297,15 +303,16 @@
:params params
:self-link-url-fn (self-link-url-fn request params)
:gen-token-fn (gen-token-fn context request)
- :first-link-url-fn (first-link-url-fn request params)
- :next-link-url-fn (next-link-url-fn request params))
+ :first-link-url-fn (first-link-url-fn request page-id-cipher params)
+ :next-link-url-fn (next-link-url-fn request page-id-cipher params))
handling
(assoc :blaze.preference/handling handling)
respond-async
(assoc :blaze.preference/respond-async true)))))
(defmethod m/pre-init-spec :blaze.interaction/search-type [_]
- (s/keys :req-un [:blaze/clock :blaze/rng-fn :blaze/page-store]
+ (s/keys :req-un [:blaze/clock :blaze/rng-fn :blaze/page-store
+ :blaze/page-id-cipher]
:opt-un [:blaze/context-path]))
(defmethod ig/init-key :blaze.interaction/search-type [_ context]
diff --git a/modules/interaction/test/blaze/interaction/history/instance_test.clj b/modules/interaction/test/blaze/interaction/history/instance_test.clj
index 4dfe1cada..1a825f18d 100644
--- a/modules/interaction/test/blaze/interaction/history/instance_test.clj
+++ b/modules/interaction/test/blaze/interaction/history/instance_test.clj
@@ -17,6 +17,9 @@
[blaze.interaction.test-util :refer [wrap-error]]
[blaze.middleware.fhir.db :as db]
[blaze.middleware.fhir.db-spec]
+ [blaze.middleware.fhir.decrypt-page-id :as decrypt-page-id]
+ [blaze.middleware.fhir.decrypt-page-id-spec]
+ [blaze.page-id-cipher.spec :refer [page-id-cipher?]]
[blaze.test-util :as tu :refer [given-thrown]]
[clojure.spec.alpha :as s]
[clojure.spec.test.alpha :as st]
@@ -31,7 +34,7 @@
(set! *warn-on-reflection* true)
(st/instrument)
-(log/set-level! :trace)
+(log/set-min-level! :trace)
(test/use-fixtures :each tu/fixture)
@@ -46,7 +49,7 @@
["/Patient/{id}/_history"
{:fhir.resource/type "Patient"
:name :Patient/history-instance}]
- ["/Patient/{id}/__history-page"
+ ["/Patient/{id}/__history-page/{page-id}"
{:fhir.resource/type "Patient"
:name :Patient/history-instance-page}]]
{:syntax :bracket
@@ -80,23 +83,46 @@
:key := :blaze.interaction.history/instance
:reason := ::ig/build-failed-spec
[:cause-data ::s/problems 0 :pred] := `(fn ~'[%] (contains? ~'% :clock))
- [:cause-data ::s/problems 1 :pred] := `(fn ~'[%] (contains? ~'% :rng-fn))))
+ [:cause-data ::s/problems 1 :pred] := `(fn ~'[%] (contains? ~'% :rng-fn))
+ [:cause-data ::s/problems 2 :pred] := `(fn ~'[%] (contains? ~'% :page-id-cipher))))
(testing "invalid clock"
(given-thrown (ig/init {:blaze.interaction.history/instance {:clock ::invalid}})
:key := :blaze.interaction.history/instance
:reason := ::ig/build-failed-spec
[:cause-data ::s/problems 0 :pred] := `(fn ~'[%] (contains? ~'% :rng-fn))
- [:cause-data ::s/problems 1 :pred] := `time/clock?
- [:cause-data ::s/problems 1 :val] := ::invalid)))
+ [:cause-data ::s/problems 1 :pred] := `(fn ~'[%] (contains? ~'% :page-id-cipher))
+ [:cause-data ::s/problems 2 :pred] := `time/clock?
+ [:cause-data ::s/problems 2 :val] := ::invalid))
+
+ (testing "invalid rng-fn"
+ (given-thrown (ig/init {:blaze.interaction.history/instance {:rng-fn ::invalid}})
+ :key := :blaze.interaction.history/instance
+ :reason := ::ig/build-failed-spec
+ [:cause-data ::s/problems 0 :pred] := `(fn ~'[%] (contains? ~'% :clock))
+ [:cause-data ::s/problems 1 :pred] := `(fn ~'[%] (contains? ~'% :page-id-cipher))
+ [:cause-data ::s/problems 2 :pred] := `fn?
+ [:cause-data ::s/problems 2 :val] := ::invalid))
+
+ (testing "invalid page-id-cipher"
+ (given-thrown (ig/init {:blaze.interaction.history/instance {:page-id-cipher ::invalid}})
+ :key := :blaze.interaction.history/instance
+ :reason := ::ig/build-failed-spec
+ [:cause-data ::s/problems 0 :pred] := `(fn ~'[%] (contains? ~'% :clock))
+ [:cause-data ::s/problems 1 :pred] := `(fn ~'[%] (contains? ~'% :rng-fn))
+ [:cause-data ::s/problems 2 :pred] := `page-id-cipher?
+ [:cause-data ::s/problems 2 :val] := ::invalid)))
(def config
- (assoc api-stub/mem-node-config
- :blaze.interaction.history/instance
- {:node (ig/ref :blaze.db/node)
- :clock (ig/ref :blaze.test/fixed-clock)
- :rng-fn (ig/ref :blaze.test/fixed-rng-fn)}
- :blaze.test/fixed-rng-fn {}))
+ (assoc
+ api-stub/mem-node-config
+ :blaze.interaction.history/instance
+ {:node (ig/ref :blaze.db/node)
+ :clock (ig/ref :blaze.test/fixed-clock)
+ :rng-fn (ig/ref :blaze.test/fixed-rng-fn)
+ :page-id-cipher (ig/ref :blaze.test/page-id-cipher)}
+ :blaze.test/fixed-rng-fn {}
+ :blaze.test/page-id-cipher {}))
(def system-clock-config
(-> (assoc config :blaze.test/system-clock {})
@@ -111,22 +137,34 @@
(nil? match)
(assoc ::reitit/match default-match)))))
-(defn wrap-db [handler node]
+(defn wrap-db [handler node page-id-cipher]
(fn [{::reitit/keys [match] :as request}]
(if (= page-match match)
- ((db/wrap-snapshot-db handler node 100) request)
+ ((decrypt-page-id/wrap-decrypt-page-id
+ (db/wrap-snapshot-db handler node 100)
+ page-id-cipher)
+ request)
((db/wrap-db handler node 100) request))))
-(defmacro with-handler [[handler-binding & [node-binding]] & more]
+(defmacro with-handler [[handler-binding & [node-binding page-id-cipher-binding]] & more]
(let [[txs body] (api-stub/extract-txs-body more)]
`(with-system-data [{node# :blaze.db/node
+ page-id-cipher# :blaze.test/page-id-cipher
handler# :blaze.interaction.history/instance} config]
~txs
- (let [~handler-binding (-> handler# wrap-defaults (wrap-db node#)
+ (let [~handler-binding (-> handler# wrap-defaults
+ (wrap-db node# page-id-cipher#)
wrap-error)
- ~(or node-binding '_) node#]
+ ~(or node-binding '_) node#
+ ~(or page-id-cipher-binding '_) page-id-cipher#]
~@body))))
+(defn- page-url [page-id-cipher query-params]
+ (str base-url context-path "/Patient/0/__history-page/" (decrypt-page-id/encrypt page-id-cipher query-params)))
+
+(defn- page-path-params [page-id-cipher params]
+ {:id "0" :page-id (decrypt-page-id/encrypt page-id-cipher params)})
+
(deftest handler-test
(testing "returns not found on empty node"
(with-handler [handler]
@@ -267,7 +305,7 @@
:lastModified := Instant/EPOCH)))))
(testing "with two versions of one patient"
- (with-handler [handler node]
+ (with-handler [handler node page-id-cipher]
[[[:put {:fhir/type :fhir/Patient :id "0" :gender #fhir/code"male"}]]
[[:put {:fhir/type :fhir/Patient :id "0" :gender #fhir/code"female"}]]]
@@ -281,7 +319,7 @@
(link-url body "self"))))
(testing "has a next link"
- (is (= (str base-url context-path "/Patient/0/__history-page?_count=1&__t=2&__page-t=1")
+ (is (= (page-url page-id-cipher {"_count" "1" "__t" "2" "__page-t" "1"})
(link-url body "next")))))
(testing "calling the second page"
@@ -292,8 +330,10 @@
(let [{{[first-entry] :entry :as body} :body}
@(handler
{::reitit/match page-match
- :path-params {:id "0"}
- :params {"_count" "1" "__t" "2" "__page-t" "1"}})]
+ :path-params
+ (page-path-params
+ page-id-cipher
+ {"_count" "1" "__t" "2" "__page-t" "1"})})]
(testing "the total count is 2"
(is (= #fhir/unsignedInt 2 (:total body))))
@@ -313,7 +353,8 @@
:gender := #fhir/code"male"))))))
(testing "with two versions, using since"
- (with-system-data [{:blaze.db/keys [node] :blaze.test/keys [system-clock]
+ (with-system-data [{:blaze.db/keys [node]
+ :blaze.test/keys [system-clock page-id-cipher]
handler :blaze.interaction.history/instance}
system-clock-config]
[[[:put {:fhir/type :fhir/Patient :id "0" :gender #fhir/code"male"}]]]
@@ -323,7 +364,7 @@
_ (Thread/sleep 2000)
_ @(d/transact node [[:put {:fhir/type :fhir/Patient :id "0"
:gender #fhir/code"female"}]])
- handler (-> handler wrap-defaults (wrap-db node) wrap-error)
+ handler (-> handler wrap-defaults (wrap-db node page-id-cipher) wrap-error)
{:keys [body]}
@(handler
{:path-params {:id "0"}
diff --git a/modules/interaction/test/blaze/interaction/history/system_test.clj b/modules/interaction/test/blaze/interaction/history/system_test.clj
index dcb0b1cdf..e4170f93d 100644
--- a/modules/interaction/test/blaze/interaction/history/system_test.clj
+++ b/modules/interaction/test/blaze/interaction/history/system_test.clj
@@ -16,6 +16,9 @@
[blaze.interaction.test-util :refer [wrap-error]]
[blaze.middleware.fhir.db :as db]
[blaze.middleware.fhir.db-spec]
+ [blaze.middleware.fhir.decrypt-page-id :as decrypt-page-id]
+ [blaze.middleware.fhir.decrypt-page-id-spec]
+ [blaze.page-id-cipher.spec :refer [page-id-cipher?]]
[blaze.test-util :as tu :refer [given-thrown]]
[clojure.spec.alpha :as s]
[clojure.spec.test.alpha :as st]
@@ -30,7 +33,7 @@
(set! *warn-on-reflection* true)
(st/instrument)
-(log/set-level! :trace)
+(log/set-min-level! :trace)
(test/use-fixtures :each tu/fixture)
@@ -41,7 +44,7 @@
(reitit/router
[["/Patient" {:name :Patient/type}]
["/_history" {:name :history}]
- ["/__history-page" {:name :history-page}]]
+ ["/__history-page/{page-id}" {:name :history-page}]]
{:syntax :bracket
:path context-path}))
@@ -71,23 +74,45 @@
:key := :blaze.interaction.history/system
:reason := ::ig/build-failed-spec
[:cause-data ::s/problems 0 :pred] := `(fn ~'[%] (contains? ~'% :clock))
- [:cause-data ::s/problems 1 :pred] := `(fn ~'[%] (contains? ~'% :rng-fn))))
+ [:cause-data ::s/problems 1 :pred] := `(fn ~'[%] (contains? ~'% :rng-fn))
+ [:cause-data ::s/problems 2 :pred] := `(fn ~'[%] (contains? ~'% :page-id-cipher))))
(testing "invalid clock"
(given-thrown (ig/init {:blaze.interaction.history/system {:clock ::invalid}})
:key := :blaze.interaction.history/system
:reason := ::ig/build-failed-spec
[:cause-data ::s/problems 0 :pred] := `(fn ~'[%] (contains? ~'% :rng-fn))
- [:cause-data ::s/problems 1 :pred] := `time/clock?
- [:cause-data ::s/problems 1 :val] := ::invalid)))
+ [:cause-data ::s/problems 1 :pred] := `(fn ~'[%] (contains? ~'% :page-id-cipher))
+ [:cause-data ::s/problems 2 :pred] := `time/clock?
+ [:cause-data ::s/problems 2 :val] := ::invalid))
+
+ (testing "invalid rng-fn"
+ (given-thrown (ig/init {:blaze.interaction.history/system {:rng-fn ::invalid}})
+ :key := :blaze.interaction.history/system
+ :reason := ::ig/build-failed-spec
+ [:cause-data ::s/problems 0 :pred] := `(fn ~'[%] (contains? ~'% :clock))
+ [:cause-data ::s/problems 1 :pred] := `(fn ~'[%] (contains? ~'% :page-id-cipher))
+ [:cause-data ::s/problems 2 :pred] := `fn?
+ [:cause-data ::s/problems 2 :val] := ::invalid))
+
+ (testing "invalid page-id-cipher"
+ (given-thrown (ig/init {:blaze.interaction.history/system {:page-id-cipher ::invalid}})
+ :key := :blaze.interaction.history/system
+ :reason := ::ig/build-failed-spec
+ [:cause-data ::s/problems 0 :pred] := `(fn ~'[%] (contains? ~'% :clock))
+ [:cause-data ::s/problems 1 :pred] := `(fn ~'[%] (contains? ~'% :rng-fn))
+ [:cause-data ::s/problems 2 :pred] := `page-id-cipher?
+ [:cause-data ::s/problems 2 :val] := ::invalid)))
(def config
- (assoc api-stub/mem-node-config
- :blaze.interaction.history/system
- {:node (ig/ref :blaze.db/node)
- :clock (ig/ref :blaze.test/fixed-clock)
- :rng-fn (ig/ref :blaze.test/fixed-rng-fn)}
- :blaze.test/fixed-rng-fn {}))
+ (assoc
+ api-stub/mem-node-config
+ :blaze.interaction.history/system
+ {:clock (ig/ref :blaze.test/fixed-clock)
+ :rng-fn (ig/ref :blaze.test/fixed-rng-fn)
+ :page-id-cipher (ig/ref :blaze.test/page-id-cipher)}
+ :blaze.test/fixed-rng-fn {}
+ :blaze.test/page-id-cipher {}))
(def system-clock-config
(-> (assoc config :blaze.test/system-clock {})
@@ -102,22 +127,34 @@
(nil? match)
(assoc ::reitit/match default-match)))))
-(defn wrap-db [handler node]
+(defn wrap-db [handler node page-id-cipher]
(fn [{::reitit/keys [match] :as request}]
(if (= page-match match)
- ((db/wrap-snapshot-db handler node 100) request)
+ ((decrypt-page-id/wrap-decrypt-page-id
+ (db/wrap-snapshot-db handler node 100)
+ page-id-cipher)
+ request)
((db/wrap-db handler node 100) request))))
-(defmacro with-handler [[handler-binding & [node-binding]] & more]
+(defmacro with-handler [[handler-binding & [node-binding page-id-cipher-binding]] & more]
(let [[txs body] (api-stub/extract-txs-body more)]
`(with-system-data [{node# :blaze.db/node
+ page-id-cipher# :blaze.test/page-id-cipher
handler# :blaze.interaction.history/system} config]
~txs
- (let [~handler-binding (-> handler# wrap-defaults (wrap-db node#)
+ (let [~handler-binding (-> handler# wrap-defaults
+ (wrap-db node# page-id-cipher#)
wrap-error)
- ~(or node-binding '_) node#]
+ ~(or node-binding '_) node#
+ ~(or page-id-cipher-binding '_) page-id-cipher#]
~@body))))
+(defn- page-url [page-id-cipher query-params]
+ (str base-url context-path "/__history-page/" (decrypt-page-id/encrypt page-id-cipher query-params)))
+
+(defn- page-path-params [page-id-cipher params]
+ {:page-id (decrypt-page-id/encrypt page-id-cipher params)})
+
(deftest handler-test
(testing "with empty node"
(with-handler [handler]
@@ -194,7 +231,7 @@
:lastModified := Instant/EPOCH)))))
(testing "with two patients in one transaction"
- (with-handler [handler node]
+ (with-handler [handler node page-id-cipher]
[[[:put {:fhir/type :fhir/Patient :id "0"}]
[:put {:fhir/type :fhir/Patient :id "1"}]]]
@@ -211,7 +248,7 @@
(link-url body "self"))))
(testing "has a next link"
- (is (= (str base-url context-path "/__history-page?_count=1&__t=1&__page-t=1&__page-type=Patient&__page-id=1")
+ (is (= (page-url page-id-cipher {"_count" "1" "__t" "1" "__page-t" "1" "__page-type" "Patient" "__page-id" "1"})
(link-url body "next"))))
(testing "the entry has the right fullUrl"
@@ -225,8 +262,11 @@
(let [{:keys [status] {[first-entry] :entry :as body} :body}
@(handler
{::reitit/match page-match
- :params {"_count" "1" "__t" "1" "__page-t" "1"
- "__page-type" "Patient" "__page-id" "1"}})]
+ :path-params
+ (page-path-params
+ page-id-cipher
+ {"_count" "1" "__t" "1" "__page-t" "1"
+ "__page-type" "Patient" "__page-id" "1"})})]
(is (= 200 status))
@@ -248,13 +288,16 @@
(let [{:keys [body]}
@(handler
{::reitit/match page-match
- :params {"_count" "1" "__t" "1" "__page-t" "1" "__page-id" "1"}})]
+ :path-params
+ (page-path-params
+ page-id-cipher
+ {"_count" "1" "__t" "1" "__page-t" "1" "__page-id" "1"})})]
(given (-> body :entry first)
[:resource :id] := "0")))))
(testing "two patients in two transactions"
- (with-handler [handler node]
+ (with-handler [handler node page-id-cipher]
[[[:put {:fhir/type :fhir/Patient :id "0"}]]
[[:put {:fhir/type :fhir/Patient :id "1"}]]]
@@ -271,7 +314,7 @@
(link-url body "self"))))
(testing "has a next link"
- (is (= (str base-url context-path "/__history-page?_count=1&__t=2&__page-t=1&__page-type=Patient&__page-id=0")
+ (is (= (page-url page-id-cipher {"_count" "1" "__t" "2" "__page-t" "1" "__page-type" "Patient" "__page-id" "0"})
(link-url body "next"))))
(testing "the entry has the right fullUrl"
@@ -285,8 +328,11 @@
(let [{:keys [status] {[first-entry] :entry :as body} :body}
@(handler
{::reitit/match page-match
- :params {"_count" "1" "__t" "2" "__page-t" "1"
- "__page-type" "Patient" "__page-id" "0"}})]
+ :path-params
+ (page-path-params
+ page-id-cipher
+ {"_count" "1" "__t" "2" "__page-t" "1"
+ "__page-type" "Patient" "__page-id" "0"})})]
(is (= 200 status))
@@ -302,7 +348,8 @@
(:fullUrl first-entry))))))))
(testing "with two versions, using since"
- (with-system-data [{:blaze.db/keys [node] :blaze.test/keys [system-clock]
+ (with-system-data [{:blaze.db/keys [node]
+ :blaze.test/keys [system-clock page-id-cipher]
handler :blaze.interaction.history/system}
system-clock-config]
[[[:put {:fhir/type :fhir/Patient :id "0" :gender #fhir/code"male"}]]]
@@ -312,7 +359,7 @@
_ (Thread/sleep 2000)
_ @(d/transact node [[:put {:fhir/type :fhir/Patient :id "0"
:gender #fhir/code"female"}]])
- handler (-> handler wrap-defaults (wrap-db node) wrap-error)
+ handler (-> handler wrap-defaults (wrap-db node page-id-cipher) wrap-error)
{:keys [body]}
@(handler
{:params {"_since" (str since)}})]
diff --git a/modules/interaction/test/blaze/interaction/history/type_test.clj b/modules/interaction/test/blaze/interaction/history/type_test.clj
index 1e550468c..79e127e70 100644
--- a/modules/interaction/test/blaze/interaction/history/type_test.clj
+++ b/modules/interaction/test/blaze/interaction/history/type_test.clj
@@ -16,6 +16,9 @@
[blaze.interaction.test-util :refer [wrap-error]]
[blaze.middleware.fhir.db :as db]
[blaze.middleware.fhir.db-spec]
+ [blaze.middleware.fhir.decrypt-page-id :as decrypt-page-id]
+ [blaze.middleware.fhir.decrypt-page-id-spec]
+ [blaze.page-id-cipher.spec :refer [page-id-cipher?]]
[blaze.test-util :as tu :refer [given-thrown]]
[clojure.spec.alpha :as s]
[clojure.spec.test.alpha :as st]
@@ -30,7 +33,7 @@
(set! *warn-on-reflection* true)
(st/instrument)
-(log/set-level! :trace)
+(log/set-min-level! :trace)
(test/use-fixtures :each tu/fixture)
@@ -45,7 +48,7 @@
["/Patient/_history"
{:fhir.resource/type "Patient"
:name :Patient/history}]
- ["/Patient/__history-page"
+ ["/Patient/__history-page/{page-id}"
{:fhir.resource/type "Patient"
:name :Patient/history-page}]]
{:syntax :bracket
@@ -79,23 +82,46 @@
:key := :blaze.interaction.history/type
:reason := ::ig/build-failed-spec
[:cause-data ::s/problems 0 :pred] := `(fn ~'[%] (contains? ~'% :clock))
- [:cause-data ::s/problems 1 :pred] := `(fn ~'[%] (contains? ~'% :rng-fn))))
+ [:cause-data ::s/problems 1 :pred] := `(fn ~'[%] (contains? ~'% :rng-fn))
+ [:cause-data ::s/problems 2 :pred] := `(fn ~'[%] (contains? ~'% :page-id-cipher))))
(testing "invalid clock"
(given-thrown (ig/init {:blaze.interaction.history/type {:clock ::invalid}})
:key := :blaze.interaction.history/type
:reason := ::ig/build-failed-spec
[:cause-data ::s/problems 0 :pred] := `(fn ~'[%] (contains? ~'% :rng-fn))
- [:cause-data ::s/problems 1 :pred] := `time/clock?
- [:cause-data ::s/problems 1 :val] := ::invalid)))
+ [:cause-data ::s/problems 1 :pred] := `(fn ~'[%] (contains? ~'% :page-id-cipher))
+ [:cause-data ::s/problems 2 :pred] := `time/clock?
+ [:cause-data ::s/problems 2 :val] := ::invalid))
+
+ (testing "invalid rng-fn"
+ (given-thrown (ig/init {:blaze.interaction.history/type {:rng-fn ::invalid}})
+ :key := :blaze.interaction.history/type
+ :reason := ::ig/build-failed-spec
+ [:cause-data ::s/problems 0 :pred] := `(fn ~'[%] (contains? ~'% :clock))
+ [:cause-data ::s/problems 1 :pred] := `(fn ~'[%] (contains? ~'% :page-id-cipher))
+ [:cause-data ::s/problems 2 :pred] := `fn?
+ [:cause-data ::s/problems 2 :val] := ::invalid))
+
+ (testing "invalid page-id-cipher"
+ (given-thrown (ig/init {:blaze.interaction.history/type {:page-id-cipher ::invalid}})
+ :key := :blaze.interaction.history/type
+ :reason := ::ig/build-failed-spec
+ [:cause-data ::s/problems 0 :pred] := `(fn ~'[%] (contains? ~'% :clock))
+ [:cause-data ::s/problems 1 :pred] := `(fn ~'[%] (contains? ~'% :rng-fn))
+ [:cause-data ::s/problems 2 :pred] := `page-id-cipher?
+ [:cause-data ::s/problems 2 :val] := ::invalid)))
(def config
- (assoc api-stub/mem-node-config
- :blaze.interaction.history/type
- {:node (ig/ref :blaze.db/node)
- :clock (ig/ref :blaze.test/fixed-clock)
- :rng-fn (ig/ref :blaze.test/fixed-rng-fn)}
- :blaze.test/fixed-rng-fn {}))
+ (assoc
+ api-stub/mem-node-config
+ :blaze.interaction.history/type
+ {:node (ig/ref :blaze.db/node)
+ :clock (ig/ref :blaze.test/fixed-clock)
+ :rng-fn (ig/ref :blaze.test/fixed-rng-fn)
+ :page-id-cipher (ig/ref :blaze.test/page-id-cipher)}
+ :blaze.test/fixed-rng-fn {}
+ :blaze.test/page-id-cipher {}))
(def system-clock-config
(-> (assoc config :blaze.test/system-clock {})
@@ -110,22 +136,34 @@
(nil? match)
(assoc ::reitit/match default-match)))))
-(defn wrap-db [handler node]
+(defn wrap-db [handler node page-id-cipher]
(fn [{::reitit/keys [match] :as request}]
(if (= page-match match)
- ((db/wrap-snapshot-db handler node 100) request)
+ ((decrypt-page-id/wrap-decrypt-page-id
+ (db/wrap-snapshot-db handler node 100)
+ page-id-cipher)
+ request)
((db/wrap-db handler node 100) request))))
-(defmacro with-handler [[handler-binding & [node-binding]] & more]
+(defmacro with-handler [[handler-binding & [node-binding page-id-cipher-binding]] & more]
(let [[txs body] (api-stub/extract-txs-body more)]
`(with-system-data [{node# :blaze.db/node
+ page-id-cipher# :blaze.test/page-id-cipher
handler# :blaze.interaction.history/type} config]
~txs
- (let [~handler-binding (-> handler# wrap-defaults (wrap-db node#)
+ (let [~handler-binding (-> handler# wrap-defaults
+ (wrap-db node# page-id-cipher#)
wrap-error)
- ~(or node-binding '_) node#]
+ ~(or node-binding '_) node#
+ ~(or page-id-cipher-binding '_) page-id-cipher#]
~@body))))
+(defn- page-url [page-id-cipher query-params]
+ (str base-url context-path "/Patient/__history-page/" (decrypt-page-id/encrypt page-id-cipher query-params)))
+
+(defn- page-path-params [page-id-cipher params]
+ {:page-id (decrypt-page-id/encrypt page-id-cipher params)})
+
(deftest handler-test
(testing "with empty node"
(with-handler [handler]
@@ -202,7 +240,7 @@
:lastModified := Instant/EPOCH)))))
(testing "with two patients in one transaction"
- (with-handler [handler node]
+ (with-handler [handler node page-id-cipher]
[[[:put {:fhir/type :fhir/Patient :id "0"}]
[:put {:fhir/type :fhir/Patient :id "1"}]]]
@@ -219,7 +257,7 @@
(link-url body "self"))))
(testing "has a next link"
- (is (= (str base-url context-path "/Patient/__history-page?_count=1&__t=1&__page-t=1&__page-id=1")
+ (is (= (page-url page-id-cipher {"_count" "1" "__t" "1" "__page-t" "1" "__page-id" "1"})
(link-url body "next"))))
(testing "the entry has the right fullUrl"
@@ -233,8 +271,11 @@
(let [{:keys [status] {[first-entry] :entry :as body} :body}
@(handler
{::reitit/match page-match
- :params {"_count" "1" "__t" "1" "__page-t" "1"
- "__page-type" "Patient" "__page-id" "1"}})]
+ :path-params
+ (page-path-params
+ page-id-cipher
+ {"_count" "1" "__t" "1" "__page-t" "1"
+ "__page-type" "Patient" "__page-id" "1"})})]
(is (= 200 status))
@@ -253,7 +294,8 @@
(:fullUrl first-entry))))))))
(testing "with two versions, using since"
- (with-system-data [{:blaze.db/keys [node] :blaze.test/keys [system-clock]
+ (with-system-data [{:blaze.db/keys [node]
+ :blaze.test/keys [system-clock page-id-cipher]
handler :blaze.interaction.history/type}
system-clock-config]
[[[:put {:fhir/type :fhir/Patient :id "0" :gender #fhir/code"male"}]]]
@@ -263,7 +305,8 @@
_ (Thread/sleep 2000)
_ @(d/transact node [[:put {:fhir/type :fhir/Patient :id "0"
:gender #fhir/code"female"}]])
- handler (-> handler wrap-defaults (wrap-db node) wrap-error)
+ handler (-> handler wrap-defaults (wrap-db node page-id-cipher)
+ wrap-error)
{:keys [body]}
@(handler
{:params {"_since" (str since)}})]
diff --git a/modules/interaction/test/blaze/interaction/search/nav/spec.clj b/modules/interaction/test/blaze/interaction/search/nav/spec.clj
new file mode 100644
index 000000000..d83e9c2ce
--- /dev/null
+++ b/modules/interaction/test/blaze/interaction/search/nav/spec.clj
@@ -0,0 +1,11 @@
+(ns blaze.interaction.search.nav.spec
+ (:require
+ [blaze.interaction.search.nav.token-url :as-alias token-url]
+ [blaze.page-id-cipher.spec]
+ [blaze.page-store.spec]
+ [blaze.spec]
+ [clojure.spec.alpha :as s]))
+
+(s/def ::token-url/context
+ (s/keys :req [:blaze/base-url]
+ :req-un [:blaze/page-store :blaze/page-id-cipher]))
diff --git a/modules/interaction/test/blaze/interaction/search/nav_spec.clj b/modules/interaction/test/blaze/interaction/search/nav_spec.clj
index 901706cc9..11b6f426c 100644
--- a/modules/interaction/test/blaze/interaction/search/nav_spec.clj
+++ b/modules/interaction/test/blaze/interaction/search/nav_spec.clj
@@ -2,7 +2,10 @@
(:require
[blaze.async.comp :as ac]
[blaze.db.spec]
+ [blaze.handler.fhir.util.spec]
[blaze.interaction.search.nav :as nav]
+ [blaze.interaction.search.nav.spec]
+ [blaze.interaction.search.nav.token-url :as-alias token-url]
[blaze.page-store.spec]
[blaze.spec]
[clojure.spec.alpha :as s]))
@@ -15,9 +18,8 @@
:ret string?)
(s/fdef nav/token-url!
- :args (s/cat :page-store :blaze/page-store
- :base-url string?
- :match some?
+ :args (s/cat :context ::token-url/context
+ :match fn?
:params (s/nilable map?)
:clauses (s/nilable :blaze.db.query/clauses)
:t :blaze.db/t
@@ -26,7 +28,8 @@
(s/fdef nav/token-url
:args (s/cat :base-url string?
- :match some?
+ :page-id-cipher :blaze/page-id-cipher
+ :match fn?
:params (s/nilable map?)
:token any?
:clauses (s/nilable :blaze.db.query/clauses)
diff --git a/modules/interaction/test/blaze/interaction/search/nav_test.clj b/modules/interaction/test/blaze/interaction/search/nav_test.clj
index 788e3d7d6..ce71e39e5 100644
--- a/modules/interaction/test/blaze/interaction/search/nav_test.clj
+++ b/modules/interaction/test/blaze/interaction/search/nav_test.clj
@@ -1,12 +1,9 @@
(ns blaze.interaction.search.nav-test
(:require
- [blaze.async.comp :as ac]
[blaze.interaction.search.nav :as nav]
[blaze.interaction.search.nav-spec]
- [blaze.page-store.protocols :as p]
[blaze.test-util :as tu]
[clojure.spec.test.alpha :as st]
- [clojure.string :as str]
[clojure.test :as test :refer [deftest is testing]]))
(st/instrument)
@@ -154,55 +151,3 @@
{:reverse
{:any [{:source-type "Observation" :code "subject"}]}}}}
nil))))))
-
-(def clauses-1
- [["foo" "bar"]])
-
-(def page-store
- (reify p/PageStore
- (-put [_ clauses]
- (assert (= clauses-1 clauses))
- (ac/completed-future (str/join (repeat 32 "A"))))))
-
-(deftest token-url-test
- (testing "stores clauses and puts token into the query params"
- (is (= "base-url-195241/Observation?__token=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA&__t=195312"
- @(nav/token-url! page-store "base-url-195241" match {} clauses-1 195312 nil))))
-
- (testing "reuses existing token"
- (is (= "base-url-195241/Observation?__token=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB&__t=195312"
- @(nav/token-url!
- (reify p/PageStore
- (-put [_ _]
- (assert false)))
- "base-url-195241" match
- {:token "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB"}
- clauses-1
- 195312
- nil))))
-
- (testing "_summary"
- (is (= "base-url-134538/Observation?__token=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB&_summary=true&__t=1"
- @(nav/token-url!
- (reify p/PageStore
- (-put [_ _]
- (assert false)))
- "base-url-134538" match
- {:token "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB"
- :summary "true"}
- []
- 1
- nil))))
-
- (testing "_elements"
- (is (= "base-url-134538/Observation?__token=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB&_elements=a&__t=1"
- @(nav/token-url!
- (reify p/PageStore
- (-put [_ _]
- (assert false)))
- "base-url-134538" match
- {:token "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB"
- :elements [:a]}
- []
- 1
- nil)))))
diff --git a/modules/interaction/test/blaze/interaction/search_compartment_test.clj b/modules/interaction/test/blaze/interaction/search_compartment_test.clj
index 9d3cd77de..47fa86b1b 100644
--- a/modules/interaction/test/blaze/interaction/search_compartment_test.clj
+++ b/modules/interaction/test/blaze/interaction/search_compartment_test.clj
@@ -13,10 +13,14 @@
[blaze.interaction.search.params-spec]
[blaze.interaction.search.util-spec]
[blaze.interaction.test-util :refer [wrap-error]]
- [blaze.middleware.fhir.db :refer [wrap-db]]
+ [blaze.middleware.fhir.db :as db]
[blaze.middleware.fhir.db-spec]
+ [blaze.middleware.fhir.decrypt-page-id :as decrypt-page-id]
+ [blaze.middleware.fhir.decrypt-page-id-spec]
+ [blaze.page-id-cipher.spec :refer [page-id-cipher?]]
[blaze.page-store-spec]
[blaze.page-store.local]
+ [blaze.page-store.spec :refer [page-store?]]
[blaze.test-util :as tu :refer [given-thrown]]
[clojure.spec.alpha :as s]
[clojure.spec.test.alpha :as st]
@@ -38,6 +42,7 @@
(def router
(reitit/router
[["/Patient/{id}/{type}" {:name :Patient/compartment}]
+ ["/Patient/{id}/{type}/__page/{page-id}" {:name :Patient/compartment-page}]
["/Observation" {:name :Observation/type}]]
{:syntax :bracket
:path context-path}))
@@ -45,7 +50,15 @@
(def match
(reitit/map->Match
{:data
- {:fhir.compartment/code "Patient"}
+ {:fhir.compartment/code "Patient"
+ :name :Patient/compartment}
+ :path (str context-path "/Patient/0/Observation")}))
+
+(def page-match
+ (reitit/map->Match
+ {:data
+ {:fhir.compartment/code "Patient"
+ :name :Patient/compartment-page}
:path (str context-path "/Patient/0/Observation")}))
(deftest init-test
@@ -61,7 +74,8 @@
:reason := ::ig/build-failed-spec
[:cause-data ::s/problems 0 :pred] := `(fn ~'[%] (contains? ~'% :clock))
[:cause-data ::s/problems 1 :pred] := `(fn ~'[%] (contains? ~'% :rng-fn))
- [:cause-data ::s/problems 2 :pred] := `(fn ~'[%] (contains? ~'% :page-store))))
+ [:cause-data ::s/problems 2 :pred] := `(fn ~'[%] (contains? ~'% :page-store))
+ [:cause-data ::s/problems 3 :pred] := `(fn ~'[%] (contains? ~'% :page-id-cipher))))
(testing "invalid clock"
(given-thrown (ig/init {:blaze.interaction/search-compartment {:clock ::invalid}})
@@ -69,18 +83,52 @@
:reason := ::ig/build-failed-spec
[:cause-data ::s/problems 0 :pred] := `(fn ~'[%] (contains? ~'% :rng-fn))
[:cause-data ::s/problems 1 :pred] := `(fn ~'[%] (contains? ~'% :page-store))
- [:cause-data ::s/problems 2 :pred] := `time/clock?
- [:cause-data ::s/problems 2 :val] := ::invalid)))
+ [:cause-data ::s/problems 2 :pred] := `(fn ~'[%] (contains? ~'% :page-id-cipher))
+ [:cause-data ::s/problems 3 :pred] := `time/clock?
+ [:cause-data ::s/problems 3 :val] := ::invalid))
+
+ (testing "invalid rng-fn"
+ (given-thrown (ig/init {:blaze.interaction/search-compartment {:rng-fn ::invalid}})
+ :key := :blaze.interaction/search-compartment
+ :reason := ::ig/build-failed-spec
+ [:cause-data ::s/problems 0 :pred] := `(fn ~'[%] (contains? ~'% :clock))
+ [:cause-data ::s/problems 1 :pred] := `(fn ~'[%] (contains? ~'% :page-store))
+ [:cause-data ::s/problems 2 :pred] := `(fn ~'[%] (contains? ~'% :page-id-cipher))
+ [:cause-data ::s/problems 3 :pred] := `fn?
+ [:cause-data ::s/problems 3 :val] := ::invalid))
+
+ (testing "invalid page-store"
+ (given-thrown (ig/init {:blaze.interaction/search-compartment {:page-store ::invalid}})
+ :key := :blaze.interaction/search-compartment
+ :reason := ::ig/build-failed-spec
+ [:cause-data ::s/problems 0 :pred] := `(fn ~'[%] (contains? ~'% :clock))
+ [:cause-data ::s/problems 1 :pred] := `(fn ~'[%] (contains? ~'% :rng-fn))
+ [:cause-data ::s/problems 2 :pred] := `(fn ~'[%] (contains? ~'% :page-id-cipher))
+ [:cause-data ::s/problems 3 :pred] := `page-store?
+ [:cause-data ::s/problems 3 :val] := ::invalid))
+
+ (testing "invalid page-id-cipher"
+ (given-thrown (ig/init {:blaze.interaction/search-compartment {:page-id-cipher ::invalid}})
+ :key := :blaze.interaction/search-compartment
+ :reason := ::ig/build-failed-spec
+ [:cause-data ::s/problems 0 :pred] := `(fn ~'[%] (contains? ~'% :clock))
+ [:cause-data ::s/problems 1 :pred] := `(fn ~'[%] (contains? ~'% :rng-fn))
+ [:cause-data ::s/problems 2 :pred] := `(fn ~'[%] (contains? ~'% :page-store))
+ [:cause-data ::s/problems 3 :pred] := `page-id-cipher?
+ [:cause-data ::s/problems 3 :val] := ::invalid)))
(def config
- (assoc api-stub/mem-node-config
- :blaze.interaction/search-compartment
- {:clock (ig/ref :blaze.test/fixed-clock)
- :rng-fn (ig/ref :blaze.test/fixed-rng-fn)
- :page-store (ig/ref :blaze.page-store/local)}
- :blaze.test/fixed-rng-fn {}
- :blaze.page-store/local {:secure-rng (ig/ref :blaze.test/fixed-rng)}
- :blaze.test/fixed-rng {}))
+ (assoc
+ api-stub/mem-node-config
+ :blaze.interaction/search-compartment
+ {:clock (ig/ref :blaze.test/fixed-clock)
+ :rng-fn (ig/ref :blaze.test/fixed-rng-fn)
+ :page-store (ig/ref :blaze.page-store/local)
+ :page-id-cipher (ig/ref :blaze.test/page-id-cipher)}
+ :blaze.test/fixed-rng-fn {}
+ :blaze.page-store/local {:secure-rng (ig/ref :blaze.test/fixed-rng)}
+ :blaze.test/fixed-rng {}
+ :blaze.test/page-id-cipher {}))
(defn wrap-defaults [handler]
(fn [request]
@@ -90,15 +138,33 @@
::reitit/router router
::reitit/match match))))
-(defmacro with-handler [[handler-binding] & more]
+(defn wrap-db [handler node page-id-cipher]
+ (fn [{::reitit/keys [match] :as request}]
+ (if (= page-match match)
+ ((decrypt-page-id/wrap-decrypt-page-id
+ (db/wrap-snapshot-db handler node 100)
+ page-id-cipher)
+ request)
+ ((db/wrap-db handler node 100) request))))
+
+(defmacro with-handler [[handler-binding & [node-binding page-id-cipher-binding]] & more]
(let [[txs body] (api-stub/extract-txs-body more)]
`(with-system-data [{node# :blaze.db/node
+ page-id-cipher# :blaze.test/page-id-cipher
handler# :blaze.interaction/search-compartment} config]
~txs
- (let [~handler-binding (-> handler# wrap-defaults (wrap-db node# 100)
- wrap-error)]
+ (let [~handler-binding (-> handler# wrap-defaults (wrap-db node# page-id-cipher#)
+ wrap-error)
+ ~(or node-binding '_) node#
+ ~(or page-id-cipher-binding '_) page-id-cipher#]
~@body))))
+(defn- page-url [page-id-cipher type query-params]
+ (str base-url context-path "/Patient/0/" type "/__page/" (decrypt-page-id/encrypt page-id-cipher query-params)))
+
+(defn- page-path-params [page-id-cipher params]
+ {:id "0" :type "Observation" :page-id (decrypt-page-id/encrypt page-id-cipher params)})
+
(deftest handler-test
(testing "Returns an Error on Invalid Id"
(with-handler [handler]
@@ -449,7 +515,7 @@
:total := #fhir/unsignedInt 0))))
(testing "with two Observations"
- (with-handler [handler]
+ (with-handler [handler _ page-id-cipher]
[[[:put {:fhir/type :fhir/Patient :id "0"}]
[:put {:fhir/type :fhir/Observation :id "0"
:status #fhir/code"final"
@@ -530,6 +596,61 @@
[:resource :fhir/type] := :fhir/Observation
[:resource :id] := "1"))))
+ (testing "with _count=1"
+ (let [{:keys [status body]}
+ @(handler (assoc-in request [:params "_count"] "1"))]
+
+ (is (= 200 status))
+
+ (testing "the body contains a bundle"
+ (is (= :fhir/Bundle (:fhir/type body))))
+
+ (testing "the bundle type is searchset"
+ (is (= #fhir/code"searchset" (:type body))))
+
+ (testing "the total count is 2"
+ (is (= #fhir/unsignedInt 2 (:total body))))
+
+ (testing "has a self link"
+ (is (= (str base-url context-path "/Patient/0/Observation?_count=1")
+ (link-url body "self"))))
+
+ (testing "has a next link"
+ (is (= (page-url page-id-cipher "Observation" {"_count" "1" "__t" "1" "__page-offset" "1"})
+ (link-url body "next"))))
+
+ (testing "the bundle contains one entry"
+ (is (= 1 (count (:entry body)))))
+
+ (testing "the entry"
+ (given (-> body :entry first)
+ :fullUrl := (str base-url context-path "/Observation/0")
+ [:resource :fhir/type] := :fhir/Observation
+ [:resource :id] := "0")))
+
+ (testing "following the next link"
+ (let [{:keys [status body]}
+ @(handler
+ {::reitit/match page-match
+ :path-params (page-path-params page-id-cipher {"_count" "1" "__t" "1" "__page-offset" "1"})})]
+
+ (is (= 200 status))
+
+ (testing "the total count is 2"
+ (is (= #fhir/unsignedInt 2 (:total body))))
+
+ (testing "has no next link"
+ (is (nil? (link-url body "next"))))
+
+ (testing "the bundle contains one entry"
+ (is (= 1 (count (:entry body)))))
+
+ (testing "the entry"
+ (given (-> body :entry first)
+ :fullUrl := (str base-url context-path "/Observation/1")
+ [:resource :fhir/type] := :fhir/Observation
+ [:resource :id] := "1")))))
+
(testing "missing resource contents"
(with-redefs [rs/multi-get (fn [_ _] (ac/completed-future {}))]
(let [{:keys [status body]} @(handler request)]
diff --git a/modules/interaction/test/blaze/interaction/search_system_test.clj b/modules/interaction/test/blaze/interaction/search_system_test.clj
index 3458d5bad..6ba15fbe0 100644
--- a/modules/interaction/test/blaze/interaction/search_system_test.clj
+++ b/modules/interaction/test/blaze/interaction/search_system_test.clj
@@ -15,8 +15,11 @@
[blaze.interaction.test-util :refer [wrap-error]]
[blaze.middleware.fhir.db :as db]
[blaze.middleware.fhir.db-spec]
+ [blaze.middleware.fhir.decrypt-page-id :as decrypt-page-id]
+ [blaze.page-id-cipher.spec :refer [page-id-cipher?]]
[blaze.page-store-spec]
[blaze.page-store.local]
+ [blaze.page-store.spec :refer [page-store?]]
[blaze.test-util :as tu :refer [given-thrown]]
[clojure.spec.alpha :as s]
[clojure.spec.test.alpha :as st]
@@ -38,7 +41,7 @@
(def router
(reitit/router
- [["/__page" {:name :page}]
+ [["/__page/{page-id}" {:name :page}]
["/Patient" {:name :Patient/type}]]
{:syntax :bracket}))
@@ -68,7 +71,8 @@
:reason := ::ig/build-failed-spec
[:cause-data ::s/problems 0 :pred] := `(fn ~'[%] (contains? ~'% :clock))
[:cause-data ::s/problems 1 :pred] := `(fn ~'[%] (contains? ~'% :rng-fn))
- [:cause-data ::s/problems 2 :pred] := `(fn ~'[%] (contains? ~'% :page-store))))
+ [:cause-data ::s/problems 2 :pred] := `(fn ~'[%] (contains? ~'% :page-store))
+ [:cause-data ::s/problems 3 :pred] := `(fn ~'[%] (contains? ~'% :page-id-cipher))))
(testing "invalid clock"
(given-thrown (ig/init {:blaze.interaction/search-system {:clock ::invalid}})
@@ -76,19 +80,53 @@
:reason := ::ig/build-failed-spec
[:cause-data ::s/problems 0 :pred] := `(fn ~'[%] (contains? ~'% :rng-fn))
[:cause-data ::s/problems 1 :pred] := `(fn ~'[%] (contains? ~'% :page-store))
- [:cause-data ::s/problems 2 :pred] := `time/clock?
- [:cause-data ::s/problems 2 :val] := ::invalid)))
+ [:cause-data ::s/problems 2 :pred] := `(fn ~'[%] (contains? ~'% :page-id-cipher))
+ [:cause-data ::s/problems 3 :pred] := `time/clock?
+ [:cause-data ::s/problems 3 :val] := ::invalid))
+
+ (testing "invalid rng-fn"
+ (given-thrown (ig/init {:blaze.interaction/search-system {:rng-fn ::invalid}})
+ :key := :blaze.interaction/search-system
+ :reason := ::ig/build-failed-spec
+ [:cause-data ::s/problems 0 :pred] := `(fn ~'[%] (contains? ~'% :clock))
+ [:cause-data ::s/problems 1 :pred] := `(fn ~'[%] (contains? ~'% :page-store))
+ [:cause-data ::s/problems 2 :pred] := `(fn ~'[%] (contains? ~'% :page-id-cipher))
+ [:cause-data ::s/problems 3 :pred] := `fn?
+ [:cause-data ::s/problems 3 :val] := ::invalid))
+
+ (testing "invalid page-store"
+ (given-thrown (ig/init {:blaze.interaction/search-system {:page-store ::invalid}})
+ :key := :blaze.interaction/search-system
+ :reason := ::ig/build-failed-spec
+ [:cause-data ::s/problems 0 :pred] := `(fn ~'[%] (contains? ~'% :clock))
+ [:cause-data ::s/problems 1 :pred] := `(fn ~'[%] (contains? ~'% :rng-fn))
+ [:cause-data ::s/problems 2 :pred] := `(fn ~'[%] (contains? ~'% :page-id-cipher))
+ [:cause-data ::s/problems 3 :pred] := `page-store?
+ [:cause-data ::s/problems 3 :val] := ::invalid))
+
+ (testing "invalid page-id-cipher"
+ (given-thrown (ig/init {:blaze.interaction/search-system {:page-id-cipher ::invalid}})
+ :key := :blaze.interaction/search-system
+ :reason := ::ig/build-failed-spec
+ [:cause-data ::s/problems 0 :pred] := `(fn ~'[%] (contains? ~'% :clock))
+ [:cause-data ::s/problems 1 :pred] := `(fn ~'[%] (contains? ~'% :rng-fn))
+ [:cause-data ::s/problems 2 :pred] := `(fn ~'[%] (contains? ~'% :page-store))
+ [:cause-data ::s/problems 3 :pred] := `page-id-cipher?
+ [:cause-data ::s/problems 3 :val] := ::invalid)))
(def config
- (assoc api-stub/mem-node-config
- :blaze.interaction/search-system
- {:node (ig/ref :blaze.db/node)
- :clock (ig/ref :blaze.test/fixed-clock)
- :rng-fn (ig/ref :blaze.test/fixed-rng-fn)
- :page-store (ig/ref :blaze.page-store/local)}
- :blaze.test/fixed-rng-fn {}
- :blaze.page-store/local {:secure-rng (ig/ref :blaze.test/fixed-rng)}
- :blaze.test/fixed-rng {}))
+ (assoc
+ api-stub/mem-node-config
+ :blaze.interaction/search-system
+ {:node (ig/ref :blaze.db/node)
+ :clock (ig/ref :blaze.test/fixed-clock)
+ :rng-fn (ig/ref :blaze.test/fixed-rng-fn)
+ :page-store (ig/ref :blaze.page-store/local)
+ :page-id-cipher (ig/ref :blaze.test/page-id-cipher)}
+ :blaze.test/fixed-rng-fn {}
+ :blaze.page-store/local {:secure-rng (ig/ref :blaze.test/fixed-rng)}
+ :blaze.test/fixed-rng {}
+ :blaze.test/page-id-cipher {}))
(defn wrap-defaults [handler]
(fn [{::reitit/keys [match] :as request}]
@@ -99,22 +137,34 @@
(nil? match)
(assoc ::reitit/match default-match)))))
-(defn wrap-db [handler node]
+(defn wrap-db [handler node page-id-cipher]
(fn [{::reitit/keys [match] :as request}]
(if (= page-match match)
- ((db/wrap-snapshot-db handler node 100) request)
+ ((decrypt-page-id/wrap-decrypt-page-id
+ (db/wrap-snapshot-db handler node 100)
+ page-id-cipher)
+ request)
((db/wrap-db handler node 100) request))))
-(defmacro with-handler [[handler-binding & [node-binding]] & more]
+(defmacro with-handler [[handler-binding & [node-binding page-id-cipher-binding]] & more]
(let [[txs body] (api-stub/extract-txs-body more)]
`(with-system-data [{node# :blaze.db/node
+ page-id-cipher# :blaze.test/page-id-cipher
handler# :blaze.interaction/search-system} config]
~txs
- (let [~handler-binding (-> handler# wrap-defaults (wrap-db node#)
+ (let [~handler-binding (-> handler# wrap-defaults
+ (wrap-db node# page-id-cipher#)
wrap-error)
- ~(or node-binding '_) node#]
+ ~(or node-binding '_) node#
+ ~(or page-id-cipher-binding '_) page-id-cipher#]
~@body))))
+(defn- page-url [page-id-cipher query-params]
+ (str base-url "/__page/" (decrypt-page-id/encrypt page-id-cipher query-params)))
+
+(defn- page-path-params [page-id-cipher params]
+ {:page-id (decrypt-page-id/encrypt page-id-cipher params)})
+
(deftest handler-test
(testing "on empty database"
(with-handler [handler]
@@ -243,14 +293,16 @@
(is (empty? (:entry body))))))))
(testing "with two patients"
- (with-handler [handler node]
+ (with-handler [handler node page-id-cipher]
[[[:put {:fhir/type :fhir/Patient :id "0"}]
[:put {:fhir/type :fhir/Patient :id "1"}]]]
(testing "search for all patients with _count=1"
- (let [{:keys [body]}
+ (let [{:keys [status body]}
@(handler {:params {"_count" "1"}})]
+ (is (= 200 status))
+
(testing "the total count is 2"
(is (= #fhir/unsignedInt 2 (:total body))))
@@ -259,7 +311,7 @@
(link-url body "self"))))
(testing "has a next link"
- (is (= (str base-url "/__page?_count=1&__t=1&__page-type=Patient&__page-id=1")
+ (is (= (page-url page-id-cipher {"_count" "1" "__t" "1" "__page-type" "Patient" "__page-id" "1"})
(link-url body "next"))))
(testing "the bundle contains one entry"
@@ -269,8 +321,11 @@
(let [{:keys [body]}
@(handler
{::reitit/match page-match
- :params {"_count" "1" "__t" "1" "__page-type" "Patient"
- "__page-id" "1"}})]
+ :path-params
+ (page-path-params
+ page-id-cipher
+ {"_count" "1" "__t" "1" "__page-type" "Patient"
+ "__page-id" "1"})})]
(testing "the total count is 2"
(is (= #fhir/unsignedInt 2 (:total body))))
@@ -291,8 +346,11 @@
(let [{:keys [body]}
@(handler
{::reitit/match page-match
- :params {"_count" "1" "__t" "1" "__page-type" "Patient"
- "__page-id" "1"}})]
+ :path-params
+ (page-path-params
+ page-id-cipher
+ {"_count" "1" "__t" "1" "__page-type" "Patient"
+ "__page-id" "1"})})]
(testing "the total count is 2"
(is (= #fhir/unsignedInt 2 (:total body))))
diff --git a/modules/interaction/test/blaze/interaction/search_type_test.clj b/modules/interaction/test/blaze/interaction/search_type_test.clj
index be1873034..48e5b2b77 100644
--- a/modules/interaction/test/blaze/interaction/search_type_test.clj
+++ b/modules/interaction/test/blaze/interaction/search_type_test.clj
@@ -18,6 +18,9 @@
[blaze.job-scheduler-spec]
[blaze.middleware.fhir.db :as db]
[blaze.middleware.fhir.db-spec]
+ [blaze.middleware.fhir.decrypt-page-id :as decrypt-page-id]
+ [blaze.middleware.fhir.decrypt-page-id-spec]
+ [blaze.page-id-cipher.spec :refer [page-id-cipher?]]
[blaze.page-store-spec]
[blaze.page-store.local]
[blaze.page-store.spec :refer [page-store?]]
@@ -34,6 +37,7 @@
(:import
[java.time Instant]))
+(set! *warn-on-reflection* true)
(st/instrument)
(log/set-min-level! :trace)
@@ -45,23 +49,23 @@
(def router
(reitit/router
[["/Patient" {:name :Patient/type}]
- ["/Patient/__page" {:name :Patient/page}]
+ ["/Patient/__page/{page-id}" {:name :Patient/page}]
["/MeasureReport" {:name :MeasureReport/type}]
- ["/MeasureReport/__page" {:name :MeasureReport/page}]
+ ["/MeasureReport/__page/{page-id}" {:name :MeasureReport/page}]
["/Library" {:name :Library/type}]
- ["/Library/__page" {:name :Library/page}]
+ ["/Library/__page/{page-id}" {:name :Library/page}]
["/List" {:name :List/type}]
- ["/List/__page" {:name :List/page}]
+ ["/List/__page/{page-id}" {:name :List/page}]
["/Condition" {:name :Condition/type}]
- ["/Condition/__page" {:name :Condition/page}]
+ ["/Condition/__page/{page-id}" {:name :Condition/page}]
["/Observation" {:name :Observation/type}]
- ["/Observation/__page" {:name :Observation/page}]
+ ["/Observation/__page/{page-id}" {:name :Observation/page}]
["/MedicationStatement" {:name :MedicationStatement/type}]
- ["/MedicationStatement/__page" {:name :MedicationStatement/page}]
+ ["/MedicationStatement/__page/{page-id}" {:name :MedicationStatement/page}]
["/Medication" {:name :Medication/type}]
["/Organization" {:name :Organization/type}]
["/Encounter" {:name :Encounter/type}]
- ["/Encounter/__page" {:name :Encounter/page}]]
+ ["/Encounter/__page/{page-id}" {:name :Encounter/page}]]
{:syntax :bracket
:path context-path}))
@@ -72,6 +76,13 @@
:fhir.resource/type type}
:path (str context-path "/" type)}))
+(defn page-match-of [type]
+ (reitit/map->Match
+ {:data
+ {:name (keyword type "page")
+ :fhir.resource/type type}
+ :path (str context-path "/" type)}))
+
(def patient-search-match
(reitit/map->Match
{:data
@@ -80,11 +91,7 @@
:path (str context-path "/Patient")}))
(def patient-page-match
- (reitit/map->Match
- {:data
- {:name :Patient/page
- :fhir.resource/type "Patient"}
- :path (str context-path "/Patient")}))
+ (page-match-of "Patient"))
(deftest init-test
(testing "nil config"
@@ -99,7 +106,8 @@
:reason := ::ig/build-failed-spec
[:cause-data ::s/problems 0 :pred] := `(fn ~'[%] (contains? ~'% :clock))
[:cause-data ::s/problems 1 :pred] := `(fn ~'[%] (contains? ~'% :rng-fn))
- [:cause-data ::s/problems 2 :pred] := `(fn ~'[%] (contains? ~'% :page-store))))
+ [:cause-data ::s/problems 2 :pred] := `(fn ~'[%] (contains? ~'% :page-store))
+ [:cause-data ::s/problems 3 :pred] := `(fn ~'[%] (contains? ~'% :page-id-cipher))))
(testing "invalid clock"
(given-thrown (ig/init {:blaze.interaction/search-type {:clock ::invalid}})
@@ -107,8 +115,9 @@
:reason := ::ig/build-failed-spec
[:cause-data ::s/problems 0 :pred] := `(fn ~'[%] (contains? ~'% :rng-fn))
[:cause-data ::s/problems 1 :pred] := `(fn ~'[%] (contains? ~'% :page-store))
- [:cause-data ::s/problems 2 :pred] := `time/clock?
- [:cause-data ::s/problems 2 :val] := ::invalid))
+ [:cause-data ::s/problems 2 :pred] := `(fn ~'[%] (contains? ~'% :page-id-cipher))
+ [:cause-data ::s/problems 3 :pred] := `time/clock?
+ [:cause-data ::s/problems 3 :val] := ::invalid))
(testing "invalid rng-fn"
(given-thrown (ig/init {:blaze.interaction/search-type {:rng-fn ::invalid}})
@@ -116,8 +125,9 @@
:reason := ::ig/build-failed-spec
[:cause-data ::s/problems 0 :pred] := `(fn ~'[%] (contains? ~'% :clock))
[:cause-data ::s/problems 1 :pred] := `(fn ~'[%] (contains? ~'% :page-store))
- [:cause-data ::s/problems 2 :pred] := `fn?
- [:cause-data ::s/problems 2 :val] := ::invalid))
+ [:cause-data ::s/problems 2 :pred] := `(fn ~'[%] (contains? ~'% :page-id-cipher))
+ [:cause-data ::s/problems 3 :pred] := `fn?
+ [:cause-data ::s/problems 3 :val] := ::invalid))
(testing "invalid page-store"
(given-thrown (ig/init {:blaze.interaction/search-type {:page-store ::invalid}})
@@ -125,8 +135,19 @@
:reason := ::ig/build-failed-spec
[:cause-data ::s/problems 0 :pred] := `(fn ~'[%] (contains? ~'% :clock))
[:cause-data ::s/problems 1 :pred] := `(fn ~'[%] (contains? ~'% :rng-fn))
- [:cause-data ::s/problems 2 :pred] := `page-store?
- [:cause-data ::s/problems 2 :val] := ::invalid))
+ [:cause-data ::s/problems 2 :pred] := `(fn ~'[%] (contains? ~'% :page-id-cipher))
+ [:cause-data ::s/problems 3 :pred] := `page-store?
+ [:cause-data ::s/problems 3 :val] := ::invalid))
+
+ (testing "invalid page-id-cipher"
+ (given-thrown (ig/init {:blaze.interaction/search-type {:page-id-cipher ::invalid}})
+ :key := :blaze.interaction/search-type
+ :reason := ::ig/build-failed-spec
+ [:cause-data ::s/problems 0 :pred] := `(fn ~'[%] (contains? ~'% :clock))
+ [:cause-data ::s/problems 1 :pred] := `(fn ~'[%] (contains? ~'% :rng-fn))
+ [:cause-data ::s/problems 2 :pred] := `(fn ~'[%] (contains? ~'% :page-store))
+ [:cause-data ::s/problems 3 :pred] := `page-id-cipher?
+ [:cause-data ::s/problems 3 :val] := ::invalid))
(testing "invalid context-path"
(given-thrown (ig/init {:blaze.interaction/search-type {:context-path ::invalid}})
@@ -135,8 +156,9 @@
[:cause-data ::s/problems 0 :pred] := `(fn ~'[%] (contains? ~'% :clock))
[:cause-data ::s/problems 1 :pred] := `(fn ~'[%] (contains? ~'% :rng-fn))
[:cause-data ::s/problems 2 :pred] := `(fn ~'[%] (contains? ~'% :page-store))
- [:cause-data ::s/problems 3 :pred] := `string?
- [:cause-data ::s/problems 3 :val] := ::invalid)))
+ [:cause-data ::s/problems 3 :pred] := `(fn ~'[%] (contains? ~'% :page-id-cipher))
+ [:cause-data ::s/problems 4 :pred] := `string?
+ [:cause-data ::s/problems 4 :val] := ::invalid)))
(def config
(assoc
@@ -145,6 +167,7 @@
{:clock (ig/ref :blaze.test/fixed-clock)
:rng-fn (ig/ref :blaze.test/fixed-rng-fn)
:page-store (ig/ref :blaze.page-store/local)
+ :page-id-cipher (ig/ref :blaze.test/page-id-cipher)
:context-path context-path}
:blaze/job-scheduler
{:node (ig/ref :blaze.db/node)
@@ -152,7 +175,8 @@
:rng-fn (ig/ref :blaze.test/fixed-rng-fn)}
:blaze.page-store/local {:secure-rng (ig/ref :blaze.test/fixed-rng)}
:blaze.test/fixed-rng-fn {}
- :blaze.test/fixed-rng {}))
+ :blaze.test/fixed-rng {}
+ :blaze.test/page-id-cipher {}))
(defn- wrap-defaults [handler]
(fn [{::reitit/keys [match] :as request}]
@@ -163,28 +187,39 @@
(nil? match)
(assoc ::reitit/match (match-of "Patient"))))))
-(defn- wrap-db [handler node]
+(defn- wrap-db [handler node page-id-cipher]
(fn [{::reitit/keys [match] :as request}]
- (if (= patient-page-match match)
- ((db/wrap-snapshot-db handler node 100) request)
+ (if (= "page" (some-> match :data :name name))
+ ((decrypt-page-id/wrap-decrypt-page-id
+ (db/wrap-snapshot-db handler node 100)
+ page-id-cipher)
+ request)
((db/wrap-db handler node 100) request))))
(defn- wrap-job-scheduler [handler job-scheduler]
(fn [request]
(handler (assoc request :blaze/job-scheduler job-scheduler))))
-(defmacro with-handler [[handler-binding & [node-binding]] & more]
+(defmacro with-handler [[handler-binding & [node-binding page-id-cipher-binding]] & more]
(let [[txs body] (api-stub/extract-txs-body more)]
`(with-system-data [{node# :blaze.db/node
+ page-id-cipher# :blaze.test/page-id-cipher
job-scheduler# :blaze/job-scheduler
handler# :blaze.interaction/search-type} config]
~txs
- (let [~handler-binding (-> handler# wrap-defaults (wrap-db node#)
+ (let [~handler-binding (-> handler# wrap-defaults (wrap-db node# page-id-cipher#)
(wrap-job-scheduler job-scheduler#)
wrap-error)
- ~(or node-binding '_) node#]
+ ~(or node-binding '_) node#
+ ~(or page-id-cipher-binding '_) page-id-cipher#]
~@body))))
+(defn- page-url [page-id-cipher type query-params]
+ (str base-url context-path "/" type "/__page/" (decrypt-page-id/encrypt page-id-cipher query-params)))
+
+(defn- page-path-params [page-id-cipher params]
+ {:page-id (decrypt-page-id/encrypt page-id-cipher params)})
+
(deftest handler-test
(testing "on unknown search parameter"
(testing "with strict handling"
@@ -551,11 +586,11 @@
(testing "on invalid token"
(testing "returns error"
- (with-handler [handler]
+ (with-handler [handler _ page-id-cipher]
(let [{:keys [status body]}
@(handler
{::reitit/match patient-page-match
- :params {"__t" "0" "__token" "invalid-token-175424"}})]
+ :path-params (page-path-params page-id-cipher {"__t" "0" "__token" "invalid-token-175424"})})]
(is (= 422 status))
@@ -567,11 +602,11 @@
(testing "on missing token"
(testing "returns error"
- (with-handler [handler]
+ (with-handler [handler _ page-id-cipher]
(let [{:keys [status body]}
@(handler
{::reitit/match patient-page-match
- :params {"__t" "0" "__token" (str/join (repeat 32 "A"))}})]
+ :path-params (page-path-params page-id-cipher {"__t" "0" "__token" (str/join (repeat 32 "A"))})})]
(is (= 404 status))
@@ -671,7 +706,7 @@
(is (empty? (:entry body))))))))
(testing "with two patients"
- (with-handler [handler node]
+ (with-handler [handler node page-id-cipher]
[[[:put {:fhir/type :fhir/Patient :id "0"}]
[:put {:fhir/type :fhir/Patient :id "1"}]]]
@@ -688,34 +723,11 @@
(link-url body "self"))))
(testing "has a first link"
- (is (= (str base-url context-path "/Patient/__page?_count=1&__t=1")
+ (is (= (page-url page-id-cipher "Patient" {"_count" "1" "__t" "1"})
(link-url body "first"))))
(testing "has a next link"
- (is (= (str base-url context-path "/Patient/__page?_count=1&__t=1&__page-id=1")
- (link-url body "next"))))
-
- (testing "the bundle contains one entry"
- (is (= 1 (count (:entry body)))))))
-
- (testing "following the self link"
- (let [{:keys [body]}
- @(handler
- {:params {"_count" "1" "__t" "1" "__page-id" "0"}})]
-
- (testing "the total count is 2"
- (is (= #fhir/unsignedInt 2 (:total body))))
-
- (testing "has a self link"
- (is (= (str base-url context-path "/Patient?_count=1")
- (link-url body "self"))))
-
- (testing "has a first link"
- (is (= (str base-url context-path "/Patient/__page?_count=1&__t=1")
- (link-url body "first"))))
-
- (testing "has a next link"
- (is (= (str base-url context-path "/Patient/__page?_count=1&__t=1&__page-id=1")
+ (is (= (page-url page-id-cipher "Patient" {"_count" "1" "__t" "1" "__page-id" "1"})
(link-url body "next"))))
(testing "the bundle contains one entry"
@@ -724,17 +736,17 @@
(testing "following the next link"
(let [{:keys [body]}
@(handler
- {:params {"_count" "1" "__t" "1" "__page-id" "1"}})]
+ {::reitit/match patient-page-match
+ :path-params (page-path-params page-id-cipher {"_count" "1" "__t" "1" "__page-id" "1"})})]
(testing "the total count is 2"
(is (= #fhir/unsignedInt 2 (:total body))))
- (testing "has a self link"
- (is (= (str base-url context-path "/Patient?_count=1")
- (link-url body "self"))))
+ (testing "has no self link"
+ (is (nil? (link-url body "self"))))
(testing "has a first link"
- (is (= (str base-url context-path "/Patient/__page?_count=1&__t=1")
+ (is (= (page-url page-id-cipher "Patient" {"_count" "1" "__t" "1"})
(link-url body "first"))))
(testing "has no next link"
@@ -758,11 +770,11 @@
(link-url body "self"))))
(testing "has a first link"
- (is (= (str base-url context-path "/Patient/__page?_count=1&__t=1")
+ (is (= (page-url page-id-cipher "Patient" {"_count" "1" "__t" "1"})
(link-url body "first"))))
(testing "has a next link"
- (is (= (str base-url context-path "/Patient/__page?_count=1&__t=1&__page-id=1")
+ (is (= (page-url page-id-cipher "Patient" {"_count" "1" "__t" "1" "__page-id" "1"})
(link-url body "next"))))
(testing "the bundle contains one entry"
@@ -775,7 +787,7 @@
(let [{:keys [body]}
@(handler
{::reitit/match patient-page-match
- :params {"_count" "1" "__t" "1" "__page-id" "1"}})]
+ :path-params (page-path-params page-id-cipher {"_count" "1" "__t" "1" "__page-id" "1"})})]
(testing "the total count is 2"
(is (= #fhir/unsignedInt 2 (:total body))))
@@ -784,7 +796,7 @@
(is (nil? (link-url body "self"))))
(testing "has a first link"
- (is (= (str base-url context-path "/Patient/__page?_count=1&__t=1")
+ (is (= (page-url page-id-cipher "Patient" {"_count" "1" "__t" "1"})
(link-url body "first"))))
(testing "has no next link"
@@ -794,7 +806,7 @@
(is (= 1 (count (:entry body))))))))))
(testing "with three patients"
- (with-handler [handler]
+ (with-handler [handler _ page-id-cipher]
[[[:put {:fhir/type :fhir/Patient :id "0"}]
[:put {:fhir/type :fhir/Patient :id "1" :active true}]
[:put {:fhir/type :fhir/Patient :id "2" :active true}]]]
@@ -832,11 +844,11 @@
(link-url body "self"))))
(testing "has a first link"
- (is (= (str base-url context-path "/Patient/__page?active=true&_count=1&__t=1")
+ (is (= (page-url page-id-cipher "Patient" {"active" ["true"] "_count" "1" "__t" "1"})
(link-url body "first"))))
(testing "has a next link with search params"
- (is (= (str base-url context-path "/Patient/__page?active=true&_count=1&__t=1&__page-id=2")
+ (is (= (page-url page-id-cipher "Patient" {"active" ["true"] "_count" "1" "__t" "1" "__page-id" "2"})
(link-url body "next"))))
(testing "the bundle contains one entry"
@@ -879,11 +891,11 @@
(link-url body "self"))))
(testing "has a first link with token"
- (is (= (str base-url context-path "/Patient/__page?__token=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB&_count=1&__t=1")
+ (is (= (page-url page-id-cipher "Patient" {"__token" "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB" "_count" "1" "__t" "1"})
(link-url body "first"))))
(testing "has a next link with token"
- (is (= (str base-url context-path "/Patient/__page?__token=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB&_count=1&__t=1&__page-id=2")
+ (is (= (page-url page-id-cipher "Patient" {"__token" "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB" "_count" "1" "__t" "1" "__page-id" "2"})
(link-url body "next"))))
(testing "the bundle contains one entry"
@@ -911,35 +923,11 @@
(testing "the bundle contains no entry"
(is (zero? (count (:entry body))))))))
- (testing "following the self link"
- (let [{:keys [body]}
- @(handler
- {:params {"active" "true" "_count" "1" "__t" "1" "__page-id" "1"}})]
-
- (testing "their is no total count because we have clauses and we have
- more hits than page-size"
- (is (nil? (:total body))))
-
- (testing "has a self link"
- (is (= (str base-url context-path "/Patient?active=true&_count=1")
- (link-url body "self"))))
-
- (testing "has a first link with search params"
- (is (= (str base-url context-path "/Patient/__page?active=true&_count=1&__t=1")
- (link-url body "first"))))
-
- (testing "has a next link with search params"
- (is (= (str base-url context-path "/Patient/__page?active=true&_count=1&__t=1&__page-id=2")
- (link-url body "next"))))
-
- (testing "the bundle contains one entry"
- (is (= 1 (count (:entry body)))))))
-
(testing "following the next link"
(let [{:keys [body]}
@(handler
{::reitit/match patient-page-match
- :params {"__token" "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB" "_count" "1" "__t" "1" "__page-id" "2"}})]
+ :path-params (page-path-params page-id-cipher {"__token" "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB" "_count" "1" "__t" "1" "__page-id" "2"})})]
(testing "their is no total count because we have clauses and we have
more hits than page-size"
@@ -949,7 +937,7 @@
(is (nil? (link-url body "self"))))
(testing "has a first link with token"
- (is (= (str base-url context-path "/Patient/__page?__token=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB&_count=1&__t=1")
+ (is (= (page-url page-id-cipher "Patient" {"__token" "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB" "_count" "1" "__t" "1"})
(link-url body "first"))))
(testing "has no next link"
@@ -959,7 +947,7 @@
(is (= 1 (count (:entry body)))))))))
(testing "with four patients"
- (with-handler [handler]
+ (with-handler [handler _ page-id-cipher]
[[[:put {:fhir/type :fhir/Patient :id "0"}]
[:put {:fhir/type :fhir/Patient :id "1" :active true}]
[:put {:fhir/type :fhir/Patient :id "2" :active true}]
@@ -972,7 +960,7 @@
{:params {"active" "true" "_count" "1"}})]
(testing "has a next link with search params"
- (is (= (str base-url context-path "/Patient/__page?active=true&_count=1&__t=1&__page-id=2")
+ (is (= (page-url page-id-cipher "Patient" {"active" ["true"] "_count" "1" "__t" "1" "__page-id" "2"})
(link-url body "next"))))
(testing "the bundle contains one entry"
@@ -982,13 +970,13 @@
(let [{:keys [body]}
@(handler
{::reitit/match patient-page-match
- :params {"active" "true" "_count" "1" "__t" "1" "__page-id" "2"}})]
+ :path-params (page-path-params page-id-cipher {"active" "true" "_count" "1" "__t" "1" "__page-id" "2"})})]
(testing "has no self link"
(is (nil? (link-url body "self"))))
(testing "has a next link with search params"
- (is (= (str base-url context-path "/Patient/__page?active=true&_count=1&__t=1&__page-id=3")
+ (is (= (page-url page-id-cipher "Patient" {"active" ["true"] "_count" "1" "__t" "1" "__page-id" "3"})
(link-url body "next"))))
(testing "the bundle contains one entry"
@@ -1002,24 +990,24 @@
:params {"active" "true" "_count" "1"}})]
(testing "has a first link with token"
- (is (= (str base-url context-path "/Patient/__page?__token=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB&_count=1&__t=1")
+ (is (= (page-url page-id-cipher "Patient" {"__token" "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB" "_count" "1" "__t" "1"})
(link-url body "first"))))
(testing "has a first link with token"
- (is (= (str base-url context-path "/Patient/__page?__token=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB&_count=1&__t=1&__page-id=2")
+ (is (= (page-url page-id-cipher "Patient" {"__token" "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB" "_count" "1" "__t" "1" "__page-id" "2"})
(link-url body "next"))))))
(testing "following the next link"
(let [{:keys [body]}
@(handler
{::reitit/match patient-page-match
- :params {"__token" "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB" "_count" "1" "__t" "1" "__page-id" "2"}})]
+ :path-params (page-path-params page-id-cipher {"__token" "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB" "_count" "1" "__t" "1" "__page-id" "2"})})]
(testing "has no self link"
(is (nil? (link-url body "self"))))
(testing "has a next link with token"
- (is (= (str base-url context-path "/Patient/__page?__token=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB&_count=1&__t=1&__page-id=3")
+ (is (= (page-url page-id-cipher "Patient" {"__token" "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB" "_count" "1" "__t" "1" "__page-id" "3"})
(link-url body "next")))))))))
(testing "_id search"
@@ -1277,7 +1265,7 @@
(is (zero? (count (:entry body))))))))))
(testing "_id sort"
- (with-handler [handler]
+ (with-handler [handler _ page-id-cipher]
[[[:put {:fhir/type :fhir/Patient :id "0"}]]
[[:put {:fhir/type :fhir/Patient :id "2"}]]
[[:put {:fhir/type :fhir/Patient :id "1"}]]]
@@ -1314,7 +1302,7 @@
(link-url body "self"))))
(testing "has a first link"
- (is (= (str base-url context-path "/Patient/__page?_sort=_id&_count=50&__t=3")
+ (is (= (page-url page-id-cipher "Patient" {"_sort" "_id" "_count" "50" "__t" "3"})
(link-url body "first")))))))
(testing "descending"
@@ -1333,7 +1321,7 @@
[:issue 0 :diagnostics] := "Unsupported sort direction `desc` for search param `_id`."))))))
(testing "_lastUpdated sort"
- (with-handler [handler]
+ (with-handler [handler _ page-id-cipher]
[[[:put {:fhir/type :fhir/Patient :id "0"}]]
[[:put {:fhir/type :fhir/Patient :id "1"}]]
[[:put {:fhir/type :fhir/Patient :id "2"}]]]
@@ -1370,7 +1358,7 @@
(link-url body "self"))))
(testing "has a first link"
- (is (= (str base-url context-path "/Patient/__page?_sort=_lastUpdated&_count=50&__t=3")
+ (is (= (page-url page-id-cipher "Patient" {"_sort" "_lastUpdated" "_count" "50" "__t" "3"})
(link-url body "first")))))))
(testing "descending"
@@ -1405,7 +1393,7 @@
(link-url body "self"))))
(testing "has a first link"
- (is (= (str base-url context-path "/Patient/__page?_sort=-_lastUpdated&_count=50&__t=3")
+ (is (= (page-url page-id-cipher "Patient" {"_sort" "-_lastUpdated" "_count" "50" "__t" "3"})
(link-url body "first")))))))))
(testing "_profile search"
@@ -1931,7 +1919,7 @@
:id := "id-143814"))))))
(testing "Observation combo-code-value-quantity search"
- (with-handler [handler]
+ (with-handler [handler _ page-id-cipher]
[[[:put {:fhir/type :fhir/Observation :id "id-121049"
:component
[{:fhir/type :fhir.Observation/component
@@ -2008,7 +1996,12 @@
(is (= 1 (count (:entry body)))))
(testing "has a next link with search params"
- (is (= (str base-url context-path "/Observation/__page?combo-code-value-quantity=http%3A%2F%2Floinc.org%7C8480-6%24ge140%7Cmm%5BHg%5D&combo-code-value-quantity=http%3A%2F%2Floinc.org%7C8462-4%24ge90%7Cmm%5BHg%5D&_count=1&__t=2&__page-id=id-123130")
+ (is (= (page-url page-id-cipher "Observation"
+ {"combo-code-value-quantity"
+ ["http://loinc.org|8480-6$ge140|mm[Hg]"
+ "http://loinc.org|8462-4$ge90|mm[Hg]"]
+ "_count" "1" "__t" "2"
+ "__page-id" "id-123130"})
(link-url body "next"))))))))
(testing "Duplicate OR Search Parameters have no Effect (#293)"
@@ -2051,7 +2044,7 @@
[:code :coding 0 :code] := #fhir/code"C71.4"))))))
(testing "Paging works with OR Search Parameters"
- (with-handler [handler]
+ (with-handler [handler _ page-id-cipher]
[[[:put {:fhir/type :fhir/Condition :id "0"
:code
#fhir/CodeableConcept
@@ -2074,10 +2067,10 @@
(doseq [handling ["strict" "lenient"]]
(let [{:keys [status] {[first-entry] :entry :as body} :body}
@(handler
- {::reitit/match (match-of "Condition")
+ {::reitit/match (page-match-of "Condition")
:headers {"prefer" (str "handling=" handling)}
- :params {"code" "0,1" "_count" "2"
- "__t" "1" "__page-id" "1"}})]
+ :path-params (page-path-params page-id-cipher {"code" "0,1" "_count" "2"
+ "__t" "1" "__page-id" "1"})})]
(is (= 200 status))
@@ -2168,7 +2161,7 @@
(testing "Include Resources"
(testing "direct include"
- (with-handler [handler]
+ (with-handler [handler _ page-id-cipher]
[[[:put {:fhir/type :fhir/Patient :id "0"}]
[:put {:fhir/type :fhir/Observation :id "0"
:subject #fhir/Reference{:reference "Patient/0"}}]]]
@@ -2194,7 +2187,9 @@
(link-url body "self"))))
(testing "has a first link"
- (is (= (str base-url context-path "/Observation/__page?_include=Observation%3Asubject&_count=50&__t=1")
+ (is (= (page-url page-id-cipher "Observation"
+ {"_include" ["Observation:subject"]
+ "_count" "50" "__t" "1"})
(link-url body "first"))))
(testing "the bundle contains two entries"
@@ -2336,7 +2331,7 @@
[:search :mode] := #fhir/code"include")))))
(testing "with paging"
- (with-handler [handler]
+ (with-handler [handler _ page-id-cipher]
[[[:put {:fhir/type :fhir/Patient :id "0"}]
[:put {:fhir/type :fhir/Observation :id "1"
:subject #fhir/Reference{:reference "Patient/0"}}]
@@ -2361,7 +2356,10 @@
(is (= #fhir/unsignedInt 2 (:total body))))
(testing "has a next link"
- (is (= (str base-url context-path "/Observation/__page?_include=Observation%3Asubject&_count=1&__t=1&__page-id=3")
+ (is (= (page-url page-id-cipher "Observation"
+ {"_include" ["Observation:subject"]
+ "_count" "1" "__t" "1"
+ "__page-id" "3"})
(link-url body "next"))))
(testing "the bundle contains two entries"
@@ -2402,7 +2400,9 @@
(link-url body "self"))))
(testing "has a first link"
- (is (= (str base-url context-path "/Observation/__page?_include=Observation%3Asubject&_count=2&__t=1")
+ (is (= (page-url page-id-cipher "Observation"
+ {"_include" ["Observation:subject"]
+ "_count" "2" "__t" "1"})
(link-url body "first"))))
(testing "the bundle contains two entries"
@@ -2517,7 +2517,7 @@
[:search :mode] := #fhir/code"include")))))
(testing "revinclude"
- (with-handler [handler]
+ (with-handler [handler _ page-id-cipher]
[[[:put {:fhir/type :fhir/Patient :id "0"}]
[:put {:fhir/type :fhir/Observation :id "1"
:subject #fhir/Reference{:reference "Patient/0"}}]]]
@@ -2542,7 +2542,8 @@
(link-url body "self"))))
(testing "has a first link"
- (is (= (str base-url context-path "/Patient/__page?_revinclude=Observation%3Asubject&_count=50&__t=1")
+ (is (= (page-url page-id-cipher "Patient" {"_revinclude" ["Observation:subject"]
+ "_count" "50" "__t" "1"})
(link-url body "first"))))
(testing "the bundle contains two entries"
@@ -2561,7 +2562,7 @@
[:search :mode] := #fhir/code"include"))))
(testing "two revincludes"
- (with-handler [handler]
+ (with-handler [handler _ page-id-cipher]
[[[:put {:fhir/type :fhir/Patient :id "0"}]
[:put {:fhir/type :fhir/Observation :id "1"
:subject #fhir/Reference{:reference "Patient/0"}}]
@@ -2589,7 +2590,8 @@
(link-url body "self"))))
(testing "has a first link"
- (is (= (str base-url context-path "/Patient/__page?_revinclude=Observation%3Asubject&_revinclude=Condition%3Asubject&_count=50&__t=1")
+ (is (= (page-url page-id-cipher "Patient" {"_revinclude" ["Observation:subject" "Condition:subject"]
+ "_count" "50" "__t" "1"})
(link-url body "first"))))
(testing "the bundle contains two entries"
@@ -2629,7 +2631,7 @@
[:issue 0 :diagnostics] := "Missing search parameter code in _include search parameter with source type `Observation`.")))))
(testing "_elements"
- (with-handler [handler]
+ (with-handler [handler _ page-id-cipher]
[[[:put {:fhir/type :fhir/Patient :id "0"}]
[:put {:fhir/type :fhir/Observation :id "0"
:subject #fhir/Reference{:reference "Patient/0"}
@@ -2655,8 +2657,12 @@
(testing "the total count is 2"
(is (= #fhir/unsignedInt 2 (:total body))))
- (testing "the next link includes the _elements query param"
- (is (str/includes? (link-url body "next") "_elements=subject")))
+ (testing "has a next link"
+ (is (= (page-url page-id-cipher "Observation"
+ {"_elements" "subject"
+ "_count" "1" "__t" "1"
+ "__page-id" "1"})
+ (link-url body "next"))))
(testing "the bundle contains one entry"
(is (= 1 (count (:entry body)))))
diff --git a/modules/interaction/test/blaze/interaction/transaction_test.clj b/modules/interaction/test/blaze/interaction/transaction_test.clj
index 3128b0eae..b26e5d9ba 100644
--- a/modules/interaction/test/blaze/interaction/transaction_test.clj
+++ b/modules/interaction/test/blaze/interaction/transaction_test.clj
@@ -158,7 +158,8 @@
{:job-scheduler (ig/ref :blaze/job-scheduler)
:clock (ig/ref :blaze.test/fixed-clock)
:rng-fn (ig/ref :blaze.test/fixed-rng-fn)
- :page-store (ig/ref :blaze.page-store/local)}
+ :page-store (ig/ref :blaze.page-store/local)
+ :page-id-cipher (ig/ref :blaze.test/page-id-cipher)}
:blaze.interaction/read
{:node (ig/ref :blaze.db/node)}
@@ -196,7 +197,8 @@
:blaze.test/fixed-rng-fn {}
:blaze.page-store/local {:secure-rng (ig/ref :blaze.test/fixed-rng)}
- :blaze.test/fixed-rng {}))
+ :blaze.test/fixed-rng {}
+ :blaze.test/page-id-cipher {}))
(defn wrap-defaults [handler router]
(fn [request]
diff --git a/modules/interaction/test/blaze/interaction/update_test.clj b/modules/interaction/test/blaze/interaction/update_test.clj
index da4c5c535..90f3af658 100644
--- a/modules/interaction/test/blaze/interaction/update_test.clj
+++ b/modules/interaction/test/blaze/interaction/update_test.clj
@@ -32,7 +32,7 @@
(set! *warn-on-reflection* true)
(st/instrument)
-(log/set-level! :trace)
+(log/set-min-level! :trace)
(test/use-fixtures :each tu/fixture)
diff --git a/modules/job-async-interaction/src/blaze/job/async_interaction/util.clj b/modules/job-async-interaction/src/blaze/job/async_interaction/util.clj
index 3364960e2..f1e392422 100644
--- a/modules/job-async-interaction/src/blaze/job/async_interaction/util.clj
+++ b/modules/job-async-interaction/src/blaze/job/async_interaction/util.clj
@@ -3,7 +3,6 @@
[blaze.anomaly :as ba :refer [if-ok]]
[blaze.async.comp :as ac]
[blaze.db.api :as d]
- [blaze.db.impl.index.resource-handle :as rh]
[blaze.fhir.spec.references :as fsr]
[blaze.fhir.spec.type :as type]
[blaze.job.util :as job-util]))
@@ -43,8 +42,8 @@
(defn pull-request-bundle [node job]
(if-ok [[type id] (request-bundle-ref job)]
- (if-let [handle (d/resource-handle (d/db node) type id)]
- (if-not (rh/deleted? handle)
+ (if-let [{:keys [op] :as handle} (d/resource-handle (d/db node) type id)]
+ (if-not (identical? :delete op)
(d/pull node handle)
(ac/completed-future (deleted-anom job id)))
(ac/completed-future (not-found-anom job id)))
diff --git a/modules/operation-patient-everything/src/blaze/operation/patient/everything.clj b/modules/operation-patient-everything/src/blaze/operation/patient/everything.clj
index 09f434449..5089ae1f1 100644
--- a/modules/operation-patient-everything/src/blaze/operation/patient/everything.clj
+++ b/modules/operation-patient-everything/src/blaze/operation/patient/everything.clj
@@ -8,6 +8,7 @@
[blaze.handler.fhir.util :as fhir-util]
[blaze.interaction.search.util :as search-util]
[blaze.luid :as luid]
+ [blaze.middleware.fhir.decrypt-page-id :as decrypt-page-id]
[blaze.module :as m]
[blaze.spec]
[clojure.spec.alpha :as s]
@@ -49,13 +50,22 @@
(defn- luid [{:keys [clock rng-fn]}]
(luid/luid clock (rng-fn)))
+(defn- page-match [{::reitit/keys [router] {:keys [id]} :path-params} page-id]
+ (reitit/match-by-name router :Patient.operation/everything-page
+ {:id id :page-id page-id}))
+
(defn- next-link
- [{:blaze/keys [base-url db] ::reitit/keys [match]} page-size offset]
+ [{:keys [page-id-cipher]} {:blaze/keys [base-url db] :as request} page-size
+ offset]
{:fhir/type :fhir.Bundle/link
:relation "next"
- :url (str base-url (reitit/match->path match {"_count" page-size
- "__t" (d/t db)
- "__page-offset" offset}))})
+ :url (->> {"_count" (str page-size)
+ "__t" (str (d/t db))
+ "__page-offset" (str offset)}
+ (decrypt-page-id/encrypt page-id-cipher)
+ (page-match request)
+ (reitit/match->path)
+ (str base-url))})
(defn- bundle [context request resources page-size next-offset]
(let [entries (mapv (partial search-util/entry request) resources)]
@@ -66,7 +76,7 @@
:entry entries}
(some? next-offset)
- (assoc :link [(next-link request page-size next-offset)])
+ (assoc :link [(next-link context request page-size next-offset)])
(nil? page-size)
(assoc :total (type/->UnsignedInt (count entries))))))
@@ -81,7 +91,7 @@
(ring/response (bundle context request resources page-size next-offset)))))))
(defmethod m/pre-init-spec :blaze.operation.patient/everything [_]
- (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.operation.patient/everything [_ context]
(log/info "Init FHIR Patient $everything operation handler")
diff --git a/modules/operation-patient-everything/test/blaze/operation/patient/everything_test.clj b/modules/operation-patient-everything/test/blaze/operation/patient/everything_test.clj
index f98f445f9..70602c142 100644
--- a/modules/operation-patient-everything/test/blaze/operation/patient/everything_test.clj
+++ b/modules/operation-patient-everything/test/blaze/operation/patient/everything_test.clj
@@ -7,7 +7,10 @@
[blaze.handler.fhir.util-spec]
[blaze.handler.util :as handler-util]
[blaze.middleware.fhir.db :as db]
+ [blaze.middleware.fhir.decrypt-page-id :as decrypt-page-id]
+ [blaze.middleware.fhir.decrypt-page-id-spec]
[blaze.operation.patient.everything]
+ [blaze.page-id-cipher.spec :refer [page-id-cipher?]]
[blaze.test-util :as tu :refer [given-thrown]]
[clojure.spec.alpha :as s]
[clojure.spec.test.alpha :as st]
@@ -22,7 +25,7 @@
(st/instrument)
(log/set-min-level! :trace)
-(tu/set-default-locale-english!) ; important for the thousands separator in 10,000
+(tu/set-default-locale-english!) ; important for the thousands separator in 10,000
(test/use-fixtures :each tu/fixture)
@@ -38,23 +41,45 @@
:key := :blaze.operation.patient/everything
:reason := ::ig/build-failed-spec
[:cause-data ::s/problems 0 :pred] := `(fn ~'[%] (contains? ~'% :clock))
- [:cause-data ::s/problems 1 :pred] := `(fn ~'[%] (contains? ~'% :rng-fn))))
+ [:cause-data ::s/problems 1 :pred] := `(fn ~'[%] (contains? ~'% :rng-fn))
+ [:cause-data ::s/problems 2 :pred] := `(fn ~'[%] (contains? ~'% :page-id-cipher))))
(testing "invalid clock"
(given-thrown (ig/init {:blaze.operation.patient/everything {:clock ::invalid}})
:key := :blaze.operation.patient/everything
:reason := ::ig/build-failed-spec
[:cause-data ::s/problems 0 :pred] := `(fn ~'[%] (contains? ~'% :rng-fn))
- [:cause-data ::s/problems 1 :pred] := `time/clock?
- [:cause-data ::s/problems 1 :val] := ::invalid)))
+ [:cause-data ::s/problems 1 :pred] := `(fn ~'[%] (contains? ~'% :page-id-cipher))
+ [:cause-data ::s/problems 2 :pred] := `time/clock?
+ [:cause-data ::s/problems 2 :val] := ::invalid))
+
+ (testing "invalid rng-fn"
+ (given-thrown (ig/init {:blaze.operation.patient/everything {:rng-fn ::invalid}})
+ :key := :blaze.operation.patient/everything
+ :reason := ::ig/build-failed-spec
+ [:cause-data ::s/problems 0 :pred] := `(fn ~'[%] (contains? ~'% :clock))
+ [:cause-data ::s/problems 1 :pred] := `(fn ~'[%] (contains? ~'% :page-id-cipher))
+ [:cause-data ::s/problems 2 :pred] := `fn?
+ [:cause-data ::s/problems 2 :val] := ::invalid))
+
+ (testing "invalid page-id-cipher"
+ (given-thrown (ig/init {:blaze.operation.patient/everything {:page-id-cipher ::invalid}})
+ :key := :blaze.operation.patient/everything
+ :reason := ::ig/build-failed-spec
+ [:cause-data ::s/problems 0 :pred] := `(fn ~'[%] (contains? ~'% :clock))
+ [:cause-data ::s/problems 1 :pred] := `(fn ~'[%] (contains? ~'% :rng-fn))
+ [:cause-data ::s/problems 2 :pred] := `page-id-cipher?
+ [:cause-data ::s/problems 2 :val] := ::invalid)))
(def base-url "base-url-113047")
(def context-path "/context-path-173858")
(def router
(reitit/router
- (mapv (fn [type] [(str "/" type) {:name (keyword type "type")}])
- ["Patient" "Condition" "Observation" "Specimen" "MedicationAdministration"])
+ (into
+ [["/Patient/{id}/__everything-page/{page-id}" {:name :Patient.operation/everything-page}]]
+ (map (fn [type] [(str "/" type) {:name (keyword type "type")}]))
+ ["Patient" "Condition" "Observation" "Specimen" "MedicationAdministration"])
{:syntax :bracket
:path context-path}))
@@ -62,12 +87,19 @@
(reitit/map->Match
{:path (str context-path "/Patient/0/$everything")}))
+(def page-match
+ (reitit/map->Match
+ {:path (str context-path "/Patient/0/__everything-page")}))
+
(def config
- (assoc api-stub/mem-node-config
- :blaze.operation.patient/everything
- {:clock (ig/ref :blaze.test/fixed-clock)
- :rng-fn (ig/ref :blaze.test/fixed-rng-fn)}
- :blaze.test/fixed-rng-fn {}))
+ (assoc
+ api-stub/mem-node-config
+ :blaze.operation.patient/everything
+ {:clock (ig/ref :blaze.test/fixed-clock)
+ :rng-fn (ig/ref :blaze.test/fixed-rng-fn)
+ :page-id-cipher (ig/ref :blaze.test/page-id-cipher)}
+ :blaze.test/fixed-rng-fn {}
+ :blaze.test/page-id-cipher {}))
(defn wrap-defaults [handler]
(fn [request]
@@ -76,21 +108,39 @@
:blaze/base-url base-url
::reitit/router router))))
+(defn- wrap-db [handler node page-id-cipher]
+ (fn [{::reitit/keys [match] :as request}]
+ (if (= page-match match)
+ ((decrypt-page-id/wrap-decrypt-page-id
+ (db/wrap-snapshot-db handler node 100)
+ page-id-cipher)
+ request)
+ ((db/wrap-db handler node 100) request))))
+
(defn wrap-error [handler]
(fn [request]
(-> (handler request)
(ac/exceptionally handler-util/error-response))))
-(defmacro with-handler [[handler-binding & [node-binding]] & more]
+(defmacro with-handler [[handler-binding & [node-binding page-id-cipher-binding]] & more]
(let [[txs body] (api-stub/extract-txs-body more)]
`(with-system-data [{node# :blaze.db/node
+ page-id-cipher# :blaze.test/page-id-cipher
handler# :blaze.operation.patient/everything} config]
~txs
- (let [~handler-binding (-> handler# wrap-defaults (db/wrap-db node# 100)
+ (let [~handler-binding (-> handler# wrap-defaults
+ (wrap-db node# page-id-cipher#)
wrap-error)
- ~(or node-binding '_) node#]
+ ~(or node-binding '_) node#
+ ~(or page-id-cipher-binding '_) page-id-cipher#]
~@body))))
+(defn- page-url [page-id-cipher query-params]
+ (str base-url context-path "/Patient/0/__everything-page/" (decrypt-page-id/encrypt page-id-cipher query-params)))
+
+(defn- page-path-params [page-id-cipher params]
+ {:id "0" :page-id (decrypt-page-id/encrypt page-id-cipher params)})
+
(deftest handler-test
(testing "Patient not found"
(with-handler [handler]
@@ -562,7 +612,7 @@
[:issue 0 :diagnostics] := "The compartment of the Patient with the id `0` has more than 10,000 resources which is too costly to output. Please use paging by specifying the _count query param."))))
(testing "paging"
- (with-handler [handler]
+ (with-handler [handler _ page-id-cipher]
[(into
[[:put {:fhir/type :fhir/Patient :id "0"}]]
(map (fn [idx]
@@ -590,7 +640,7 @@
(is (nil? (:total body))))
(testing "has a next link"
- (is (= (str base-url context-path "/Patient/0/$everything?_count=2&__t=1&__page-offset=2")
+ (is (= (page-url page-id-cipher {"_count" "2" "__t" "1" "__page-offset" "2"})
(link-url body "next"))))
(testing "the bundle contains 2 entries"
@@ -606,9 +656,12 @@
(testing "following the first next link"
(let [{:keys [status] {[first-entry second-entry] :entry :as body} :body}
- @(handler {::reitit/match match
- :path-params {:id "0"}
- :query-params {"_count" "2" "__t" "1" "__page-offset" "2"}})]
+ @(handler
+ {::reitit/match page-match
+ :path-params
+ (page-path-params
+ page-id-cipher
+ {"_count" "2" "__t" "1" "__page-offset" "2"})})]
(is (= 200 status))
@@ -625,7 +678,7 @@
(is (nil? (:total body))))
(testing "has a next link"
- (is (= (str base-url context-path "/Patient/0/$everything?_count=2&__t=1&__page-offset=4")
+ (is (= (page-url page-id-cipher {"_count" "2" "__t" "1" "__page-offset" "4"})
(link-url body "next"))))
(testing "the bundle contains 2 entries"
@@ -641,9 +694,12 @@
(testing "following the second next link"
(let [{:keys [status] {[entry] :entry :as body} :body}
- @(handler {::reitit/match match
- :path-params {:id "0"}
- :query-params {"_count" "2" "__t" "1" "__page-offset" "4"}})]
+ @(handler
+ {::reitit/match page-match
+ :path-params
+ (page-path-params
+ page-id-cipher
+ {"_count" "2" "__t" "1" "__page-offset" "4"})})]
(is (= 200 status))
@@ -670,7 +726,7 @@
(:fullUrl entry))))))))
(testing "page size of 10,000"
- (with-handler [handler]
+ (with-handler [handler _ page-id-cipher]
[(into
[[:put {:fhir/type :fhir/Patient :id "0"}]]
(map (fn [i]
@@ -698,7 +754,7 @@
(is (nil? (:total body))))
(testing "has a next link"
- (is (= (str base-url context-path "/Patient/0/$everything?_count=10000&__t=1&__page-offset=10000")
+ (is (= (page-url page-id-cipher {"_count" "10000" "__t" "1" "__page-offset" "10000"})
(link-url body "next"))))
(testing "the bundle contains 10,000 entries"
diff --git a/modules/operation-totals/test/blaze/fhir/operation/totals_test.clj b/modules/operation-totals/test/blaze/fhir/operation/totals_test.clj
index 43af56ea6..5f9c52b7e 100644
--- a/modules/operation-totals/test/blaze/fhir/operation/totals_test.clj
+++ b/modules/operation-totals/test/blaze/fhir/operation/totals_test.clj
@@ -16,7 +16,7 @@
[taoensso.timbre :as log]))
(st/instrument)
-(log/set-level! :trace)
+(log/set-min-level! :trace)
(test/use-fixtures :each tu/fixture)
diff --git a/modules/page-id-cipher/.clj-kondo/config.edn b/modules/page-id-cipher/.clj-kondo/config.edn
new file mode 100644
index 000000000..595018a64
--- /dev/null
+++ b/modules/page-id-cipher/.clj-kondo/config.edn
@@ -0,0 +1,4 @@
+{:config-paths
+ ["../../../.clj-kondo/root"
+ "../../module-test-util/resources/clj-kondo.exports/blaze/module-test-util"
+ "../../fhir-test-util/resources/clj-kondo.exports/blaze/fhir-test-util"]}
diff --git a/modules/page-id-cipher/Makefile b/modules/page-id-cipher/Makefile
new file mode 100644
index 000000000..ba7cb4aba
--- /dev/null
+++ b/modules/page-id-cipher/Makefile
@@ -0,0 +1,28 @@
+fmt:
+ cljfmt check src test deps.edn tests.edn
+
+lint:
+ clj-kondo --lint src test deps.edn
+
+test:
+ clojure -M:test:kaocha --profile :ci
+
+test-coverage:
+ clojure -M:test:coverage
+
+deps-tree:
+ clojure -X:deps tree
+
+deps-list:
+ clojure -X:deps list
+
+cloc-prod:
+ cloc src
+
+cloc-test:
+ cloc test
+
+clean:
+ rm -rf .clj-kondo/.cache .cpcache target
+
+.PHONY: fmt lint test test-coverage deps-tree deps-list cloc-prod cloc-test clean
diff --git a/modules/page-id-cipher/deps.edn b/modules/page-id-cipher/deps.edn
new file mode 100644
index 000000000..c2db5c3af
--- /dev/null
+++ b/modules/page-id-cipher/deps.edn
@@ -0,0 +1,37 @@
+{:deps
+ {blaze/db
+ {:local/root "../db"}
+
+ blaze/luid
+ {:local/root "../luid"}
+
+ blaze/module-base
+ {:local/root "../module-base"}
+
+ blaze/scheduler
+ {:local/root "../scheduler"}
+
+ com.google.crypto.tink/tink
+ {:mvn/version "1.15.0"}}
+
+ :aliases
+ {:test
+ {:extra-paths ["test"]
+
+ :extra-deps
+ {blaze/fhir-test-util
+ {:local/root "../fhir-test-util"}}}
+
+ :kaocha
+ {:extra-deps
+ {lambdaisland/kaocha
+ {:mvn/version "1.91.1392"}}
+
+ :main-opts ["-m" "kaocha.runner"]}
+
+ :coverage
+ {:extra-deps
+ {lambdaisland/kaocha-cloverage
+ {:mvn/version "1.1.89"}}
+
+ :main-opts ["-m" "kaocha.runner" "--profile" "coverage"]}}}
diff --git a/modules/page-id-cipher/src/blaze/page_id_cipher.clj b/modules/page-id-cipher/src/blaze/page_id_cipher.clj
new file mode 100644
index 000000000..07f449af5
--- /dev/null
+++ b/modules/page-id-cipher/src/blaze/page_id_cipher.clj
@@ -0,0 +1,163 @@
+(ns blaze.page-id-cipher
+ (:require
+ [blaze.async.comp :as ac]
+ [blaze.async.flow :as flow]
+ [blaze.coll.core :as coll]
+ [blaze.db.api :as d]
+ [blaze.db.spec]
+ [blaze.fhir.spec.type :as type]
+ [blaze.log]
+ [blaze.luid :as luid]
+ [blaze.module :as m]
+ [blaze.page-id-cipher.impl :as impl]
+ [blaze.page-id-cipher.spec]
+ [blaze.scheduler :as sched]
+ [blaze.scheduler.spec]
+ [clojure.spec.alpha :as s]
+ [integrant.core :as ig]
+ [java-time.api :as time]
+ [taoensso.timbre :as log])
+ (:import
+ [com.google.crypto.tink Aead InsecureSecretKeyAccess TinkProtoKeysetFormat]
+ [java.util Base64]
+ [java.util.concurrent Flow$Subscriber]))
+
+(set! *warn-on-reflection* true)
+
+(defn- luid [{:keys [clock rng-fn]}]
+ (luid/luid clock (rng-fn)))
+
+(def ^:private ^:const identifier
+ "page-id-cipher")
+
+(defn- find-key-set-resource [db]
+ (log/trace "try to find the key set resource")
+ (when-let [{:keys [op] :as handle} (coll/first (d/type-query db "DocumentReference" [["identifier" identifier]]))]
+ (when-not (identical? :delete op)
+ handle)))
+
+(defn- b64-encode [bytes]
+ (.encodeToString (Base64/getEncoder) bytes))
+
+(defn- encode-key-set-handle [key-set-handle]
+ (-> key-set-handle
+ (TinkProtoKeysetFormat/serializeKeyset (InsecureSecretKeyAccess/get))
+ (b64-encode)))
+
+(defn- key-set-attachment [key-set-handle]
+ (type/map->Attachment {:data (type/base64Binary (encode-key-set-handle key-set-handle))}))
+
+(defn- key-set-content [key-set-handle]
+ {:fhir/type :fhir.DocumentReference/content
+ :attachment (key-set-attachment key-set-handle)})
+
+(defn- key-set-resource [context]
+ {:fhir/type :fhir/DocumentReference
+ :id (luid context)
+ :identifier [(type/map->Identifier {:value identifier})]
+ :status #fhir/code"current"
+ :content
+ [(key-set-content (impl/gen-new-key-set-handle))]})
+
+(defn- key-set-resource-create-op [context]
+ [:create (key-set-resource context) [["identifier" identifier]]])
+
+(defn- find-or-create-key-set-resource
+ "Returns a CompletableFuture that will complete with the DocumentReference
+ resource with the key set as first attachment."
+ {:arglists '([context])}
+ ([{:keys [node] :as context}]
+ (find-or-create-key-set-resource context (d/db node)))
+ ([{:keys [node] :as context} db]
+ (if-let [handle (find-key-set-resource db)]
+ (d/pull node handle)
+ (-> (d/transact node [(key-set-resource-create-op context)])
+ (ac/then-compose (partial find-or-create-key-set-resource context))))))
+
+(defn- b64-decode [s]
+ (.decode (Base64/getDecoder) ^String s))
+
+(defn- parse-key-set [data]
+ (-> (b64-decode data)
+ (TinkProtoKeysetFormat/parseKeyset (InsecureSecretKeyAccess/get))))
+
+(defn- decode-key-set-handle
+ {:argLists '([key-set-resource])}
+ [{[{{:keys [data]} :attachment}] :content}]
+ (parse-key-set (type/value data)))
+
+(defn- decode-state [key-set-resource]
+ (let [key-set-handle (decode-key-set-handle key-set-resource)]
+ {:key-set-handle key-set-handle
+ :aead (impl/get-aead key-set-handle)}))
+
+(deftype TaskSubscriber [node state ^:volatile-mutable subscription]
+ Flow$Subscriber
+ (onSubscribe [_ s]
+ (set! subscription s)
+ (flow/request! subscription 1))
+ (onNext [_ document-reference-handles]
+ (log/trace "Got" (count document-reference-handles)
+ "changed document-reference(s)")
+ (run!
+ (fn [{[{:keys [value]}] :identifier :as document-reference}]
+ (when (= identifier value)
+ (log/debug "Refresh key set")
+ (reset! state (decode-state document-reference))))
+ @(d/pull-many node document-reference-handles))
+ (flow/request! subscription 1))
+ (onError [_ e]
+ (log/fatal "Page ID cipher failed. Please restart Blaze. Cause:" (ex-message e))
+ (flow/cancel! subscription))
+ (onComplete [_]))
+
+(defrecord Cipher [state future]
+ Aead
+ (encrypt [_ plaintext associatedData]
+ (.encrypt ^Aead (:aead @state) plaintext associatedData))
+ (decrypt [_ ciphertext associatedData]
+ (.decrypt ^Aead (:aead @state) ciphertext associatedData)))
+
+(defn- update-key-set-handle [key-set-resource f]
+ (let [handle (decode-key-set-handle key-set-resource)]
+ (assoc key-set-resource :content [(key-set-content (f handle))])))
+
+(defn- update-tx-op [{{version-id :versionId} :meta :as resource}]
+ [:put resource [:if-match (parse-long (type/value version-id))]])
+
+(defn- update-resource [node resource f]
+ (d/transact node [(update-tx-op (f resource))]))
+
+(defn- rotate-keys
+ "Rotates the keys in the key set available at DocumentReference with
+ identifier `page-id-cipher` in `node`."
+ ([node]
+ (when-let [handle (find-key-set-resource (d/db node))]
+ (rotate-keys node @(d/pull node handle))))
+ ([node key-set-resource]
+ (log/debug "Rotate page ID cipher keys")
+ (update-resource node key-set-resource #(update-key-set-handle % impl/rotate-keys))))
+
+(defn- schedule-key-rotation
+ [{:keys [node scheduler key-rotation-period]
+ :or {key-rotation-period (time/hours 1)}}]
+ (sched/schedule-at-fixed-rate scheduler (partial rotate-keys node)
+ key-rotation-period key-rotation-period))
+
+(defmethod m/pre-init-spec :blaze/page-id-cipher [_]
+ (s/keys :req-un [:blaze.db/node :blaze/scheduler :blaze/clock :blaze/rng-fn]
+ :opt-un [:blaze.page-id-cipher/key-rotation-period]))
+
+(defmethod ig/init-key :blaze/page-id-cipher
+ [_ {:keys [node] :as context}]
+ (log/info "Init page ID cipher")
+ (let [state (atom (decode-state @(find-or-create-key-set-resource context)))
+ publisher (d/changed-resources-publisher node "DocumentReference")
+ subscriber (->TaskSubscriber node state nil)]
+ (flow/subscribe! publisher subscriber)
+ (->Cipher state (schedule-key-rotation context))))
+
+(defmethod ig/halt-key! :blaze/page-id-cipher
+ [_ {:keys [future]}]
+ (log/info "Stop page ID cipher")
+ (sched/cancel future false))
diff --git a/modules/page-id-cipher/src/blaze/page_id_cipher/impl.clj b/modules/page-id-cipher/src/blaze/page_id_cipher/impl.clj
new file mode 100644
index 000000000..f092c7070
--- /dev/null
+++ b/modules/page-id-cipher/src/blaze/page_id_cipher/impl.clj
@@ -0,0 +1,80 @@
+(ns blaze.page-id-cipher.impl
+ (:require
+ [clojure.core.protocols :as p]
+ [clojure.datafy :as datafy]
+ [clojure.string :as str])
+ (:import
+ [com.google.crypto.tink Aead KeyStatus KeysetHandle KeysetHandle$Entry Parameters]
+ [com.google.crypto.tink.aead AeadConfig PredefinedAeadParameters]))
+
+(set! *warn-on-reflection* true)
+(AeadConfig/register)
+
+(def ^:private ^Parameters parameters
+ PredefinedAeadParameters/AES128_GCM)
+
+(defn gen-new-key-set-handle []
+ (-> (KeysetHandle/newBuilder)
+ (.addEntry (-> (KeysetHandle/generateEntryFromParameters parameters)
+ (.withFixedId 0)
+ (.makePrimary)))
+ (.build)))
+
+(defn size [handle]
+ (.size ^KeysetHandle handle))
+
+(defn- last-entry [handle]
+ (.getAt ^KeysetHandle handle (dec (size handle))))
+
+(defn- add-new-entry [handle]
+ (-> (KeysetHandle/newBuilder ^KeysetHandle handle)
+ (.addEntry (-> (KeysetHandle/generateEntryFromParameters parameters)
+ (.withFixedId (inc (.getId ^KeysetHandle$Entry (last-entry handle))))))
+ (.build)))
+
+(defn- remove-first-entry [handle]
+ (let [builder (KeysetHandle/newBuilder ^KeysetHandle handle)]
+ (.deleteAt builder 0)
+ (.build builder)))
+
+(defn- set-last-entry-primary [handle]
+ (let [builder (KeysetHandle/newBuilder ^KeysetHandle handle)]
+ (.makePrimary (.getAt builder (dec (size handle))))
+ (.build builder)))
+
+(defn- last-entry-primary? [handle]
+ (.isPrimary ^KeysetHandle$Entry (last-entry handle)))
+
+(defn rotate-keys
+ "Rotates keys in the key set `handle` according the following rules:
+
+ [primary-key] -> [primary-key new-key]
+ [primary-key new-key] -> [old-key primary-key]
+ [old-key primary-key] -> [old-key primary-key new-key]
+ [old-key primary-key new-key] -> [old-key old-key primary-key]
+ [old-key old-key primary-key] -> [old-key primary-key new-key]
+ [old-key primary-key new-key] -> [old-key old-key primary-key]"
+ [handle]
+ (if (last-entry-primary? handle)
+ (cond-> (add-new-entry handle)
+ (= 3 (size handle))
+ (remove-first-entry))
+ (set-last-entry-primary handle)))
+
+(defn get-aead [key-set-handle]
+ (.getPrimitive ^KeysetHandle key-set-handle Aead))
+
+(extend-protocol p/Datafiable
+ KeysetHandle
+ (datafy [handle]
+ (mapv #(datafy/datafy (.getAt handle %)) (range (.size handle))))
+
+ KeysetHandle$Entry
+ (datafy [entry]
+ {:id (.getId entry)
+ :primary (.isPrimary entry)
+ :status (datafy/datafy (.getStatus entry))})
+
+ KeyStatus
+ (datafy [status]
+ (keyword "key.status" (str/lower-case (str status)))))
diff --git a/modules/page-id-cipher/src/blaze/page_id_cipher/spec.clj b/modules/page-id-cipher/src/blaze/page_id_cipher/spec.clj
new file mode 100644
index 000000000..000d83690
--- /dev/null
+++ b/modules/page-id-cipher/src/blaze/page_id_cipher/spec.clj
@@ -0,0 +1,15 @@
+(ns blaze.page-id-cipher.spec
+ (:require
+ [clojure.spec.alpha :as s]
+ [java-time.api :as time])
+ (:import
+ [com.google.crypto.tink Aead]))
+
+(defn page-id-cipher? [x]
+ (instance? Aead x))
+
+(s/def :blaze/page-id-cipher
+ page-id-cipher?)
+
+(s/def :blaze.page-id-cipher/key-rotation-period
+ time/duration?)
diff --git a/modules/page-id-cipher/test/blaze/page_id_cipher/impl_test.clj b/modules/page-id-cipher/test/blaze/page_id_cipher/impl_test.clj
new file mode 100644
index 000000000..f3f829992
--- /dev/null
+++ b/modules/page-id-cipher/test/blaze/page_id_cipher/impl_test.clj
@@ -0,0 +1,88 @@
+(ns blaze.page-id-cipher.impl-test
+ (:require
+ [blaze.db.kv.mem]
+ [blaze.db.search-param-registry]
+ [blaze.db.tx-cache]
+ [blaze.db.tx-log.local]
+ [blaze.page-id-cipher.impl :as impl]
+ [blaze.spec]
+ [blaze.test-util :as tu :refer [satisfies-prop]]
+ [clojure.datafy :as datafy]
+ [clojure.spec.test.alpha :as st]
+ [clojure.test :as test :refer [deftest testing]]
+ [clojure.test.check.generators :as gen]
+ [clojure.test.check.properties :as prop]
+ [juxt.iota :refer [given]]
+ [taoensso.timbre :as log]))
+
+(st/instrument)
+(log/set-min-level! :trace)
+
+(test/use-fixtures :each tu/fixture)
+
+(deftest gen-new-key-set-handle-test
+ (given (datafy/datafy (impl/gen-new-key-set-handle))
+ count := 1
+ [0 :primary] := true
+ [0 :status] := :key.status/enabled))
+
+(defn- rotate-keys-n [n]
+ (nth (iterate impl/rotate-keys (impl/gen-new-key-set-handle)) n))
+
+(defn- primary-key? [id]
+ (fn [entry]
+ (and (true? (:primary entry)) (= id (:id entry)))))
+
+(defn- new-key? [id]
+ (fn [entry]
+ (and (false? (:primary entry)) (= id (:id entry)))))
+
+(defn- old-key? [id]
+ (fn [entry]
+ (and (false? (:primary entry)) (= id (:id entry)))))
+
+(deftest rotate-keys-test
+ (testing "[primary-key] -> [primary-key new-key]"
+ (given (datafy/datafy (rotate-keys-n 1))
+ count := 2
+ 0 :? (primary-key? 0)
+ 1 :? (new-key? 1)))
+
+ (testing "[primary-key new-key] -> [old-key primary-key]"
+ (given (datafy/datafy (rotate-keys-n 2))
+ count := 2
+ 0 :? (old-key? 0)
+ 1 :? (primary-key? 1)))
+
+ (testing "[old-key primary-key] -> [old-key primary-key new-key]"
+ (given (datafy/datafy (rotate-keys-n 3))
+ count := 3
+ 0 :? (old-key? 0)
+ 1 :? (primary-key? 1)
+ 2 :? (new-key? 2)))
+
+ (testing "[old-key primary-key new-key] -> [old-key old-key primary-key]"
+ (given (datafy/datafy (rotate-keys-n 4))
+ count := 3
+ 0 :? (old-key? 0)
+ 1 :? (old-key? 1)
+ 2 :? (primary-key? 2)))
+
+ (testing "[old-key old-key primary-key] -> [old-key primary-key new-key]"
+ (given (datafy/datafy (rotate-keys-n 5))
+ count := 3
+ 0 :? (old-key? 1)
+ 1 :? (primary-key? 2)
+ 2 :? (new-key? 3)))
+
+ (testing "[old-key primary-key new-key] -> [old-key old-key primary-key]"
+ (given (datafy/datafy (rotate-keys-n 6))
+ count := 3
+ 0 :? (old-key? 1)
+ 1 :? (old-key? 2)
+ 2 :? (primary-key? 3)))
+
+ (testing "size after 3 rotations is always 3"
+ (satisfies-prop 10
+ (prop/for-all [n (gen/choose 3 10000)]
+ (= 3 (impl/size (rotate-keys-n n)))))))
diff --git a/modules/page-id-cipher/test/blaze/page_id_cipher_test.clj b/modules/page-id-cipher/test/blaze/page_id_cipher_test.clj
new file mode 100644
index 000000000..809a13c32
--- /dev/null
+++ b/modules/page-id-cipher/test/blaze/page_id_cipher_test.clj
@@ -0,0 +1,179 @@
+(ns blaze.page-id-cipher-test
+ (:require
+ [blaze.db.kv :as kv]
+ [blaze.db.kv.mem]
+ [blaze.db.node :as node :refer [node?]]
+ [blaze.db.resource-store :as rs]
+ [blaze.db.resource-store.kv :as rs-kv]
+ [blaze.db.search-param-registry]
+ [blaze.db.tx-cache]
+ [blaze.db.tx-log :as tx-log]
+ [blaze.db.tx-log.local]
+ [blaze.fhir.test-util :refer [structure-definition-repo]]
+ [blaze.log]
+ [blaze.module.test-util :refer [with-system]]
+ [blaze.page-id-cipher]
+ [blaze.page-id-cipher.spec]
+ [blaze.scheduler.spec :refer [scheduler?]]
+ [blaze.test-util :as tu :refer [given-thrown]]
+ [clojure.datafy :as datafy]
+ [clojure.spec.alpha :as s]
+ [clojure.spec.test.alpha :as st]
+ [clojure.test :as test :refer [deftest is testing]]
+ [integrant.core :as ig]
+ [java-time.api :as time]
+ [juxt.iota :refer [given]]
+ [taoensso.timbre :as log]))
+
+(set! *warn-on-reflection* true)
+(st/instrument)
+(log/set-min-level! :trace)
+
+(test/use-fixtures :each tu/fixture)
+
+(derive :blaze.db.admin/node :blaze.db/node)
+
+(def config
+ {:blaze/page-id-cipher
+ {:node (ig/ref :blaze.db.admin/node)
+ :scheduler (ig/ref :blaze/scheduler)
+ :clock (ig/ref :blaze.test/fixed-clock)
+ :rng-fn (ig/ref :blaze.test/fixed-rng-fn)
+ :key-rotation-period (time/seconds 1)}
+
+ :blaze.db.admin/node
+ {:tx-log (ig/ref :blaze.db.admin/tx-log)
+ :tx-cache (ig/ref :blaze.db.admin/tx-cache)
+ :indexer-executor (ig/ref :blaze.db.node.admin/indexer-executor)
+ :resource-store (ig/ref :blaze.db/resource-store)
+ :kv-store (ig/ref :blaze.db.admin/index-kv-store)
+ :resource-indexer (ig/ref :blaze.db.node.admin/resource-indexer)
+ :search-param-registry (ig/ref :blaze.db/search-param-registry)
+ :scheduler (ig/ref :blaze/scheduler)
+ :poll-timeout (time/millis 10)}
+
+ [::tx-log/local :blaze.db.admin/tx-log]
+ {:kv-store (ig/ref :blaze.db.admin/transaction-kv-store)
+ :clock (ig/ref :blaze.test/fixed-clock)}
+
+ [::kv/mem :blaze.db.admin/transaction-kv-store]
+ {:column-families {}}
+
+ [:blaze.db/tx-cache :blaze.db.admin/tx-cache]
+ {:kv-store (ig/ref :blaze.db.admin/index-kv-store)}
+
+ [::node/indexer-executor :blaze.db.node.admin/indexer-executor]
+ {}
+
+ [::kv/mem :blaze.db.admin/index-kv-store]
+ {:column-families
+ {:search-param-value-index nil
+ :resource-value-index nil
+ :compartment-search-param-value-index nil
+ :compartment-resource-type-index nil
+ :active-search-params nil
+ :tx-success-index {:reverse-comparator? true}
+ :tx-error-index nil
+ :t-by-instant-index {:reverse-comparator? true}
+ :resource-as-of-index nil
+ :type-as-of-index nil
+ :system-as-of-index nil
+ :type-stats-index nil
+ :system-stats-index nil}}
+
+ [::node/resource-indexer :blaze.db.node.admin/resource-indexer]
+ {:kv-store (ig/ref :blaze.db.admin/index-kv-store)
+ :resource-store (ig/ref :blaze.db/resource-store)
+ :search-param-registry (ig/ref :blaze.db/search-param-registry)
+ :executor (ig/ref :blaze.db.node.resource-indexer.admin/executor)}
+
+ [:blaze.db.node.resource-indexer/executor :blaze.db.node.resource-indexer.admin/executor]
+ {}
+
+ ::rs/kv
+ {:kv-store (ig/ref :blaze.db/resource-kv-store)
+ :executor (ig/ref ::rs-kv/executor)}
+
+ [::kv/mem :blaze.db/resource-kv-store]
+ {:column-families {}}
+
+ ::rs-kv/executor {}
+
+ :blaze.db/search-param-registry
+ {:structure-definition-repo structure-definition-repo}
+
+ :blaze/scheduler {}
+
+ :blaze.test/fixed-clock {}
+
+ :blaze.test/fixed-rng-fn {}})
+
+(deftest init-test
+ (testing "nil config"
+ (given-thrown (ig/init {:blaze/page-id-cipher nil})
+ :key := :blaze/page-id-cipher
+ :reason := ::ig/build-failed-spec
+ [:cause-data ::s/problems 0 :pred] := `map?))
+
+ (testing "missing config"
+ (given-thrown (ig/init {:blaze/page-id-cipher {}})
+ :key := :blaze/page-id-cipher
+ :reason := ::ig/build-failed-spec
+ [:cause-data ::s/problems 0 :pred] := `(fn ~'[%] (contains? ~'% :node))
+ [:cause-data ::s/problems 1 :pred] := `(fn ~'[%] (contains? ~'% :scheduler))
+ [:cause-data ::s/problems 2 :pred] := `(fn ~'[%] (contains? ~'% :clock))
+ [:cause-data ::s/problems 3 :pred] := `(fn ~'[%] (contains? ~'% :rng-fn))))
+
+ (testing "invalid node"
+ (given-thrown (ig/init {:blaze/page-id-cipher {:node ::invalid}})
+ :key := :blaze/page-id-cipher
+ :reason := ::ig/build-failed-spec
+ [:cause-data ::s/problems 0 :pred] := `(fn ~'[%] (contains? ~'% :scheduler))
+ [:cause-data ::s/problems 1 :pred] := `(fn ~'[%] (contains? ~'% :clock))
+ [:cause-data ::s/problems 2 :pred] := `(fn ~'[%] (contains? ~'% :rng-fn))
+ [:cause-data ::s/problems 3 :pred] := `node?
+ [:cause-data ::s/problems 3 :val] := ::invalid))
+
+ (testing "invalid scheduler"
+ (given-thrown (ig/init {:blaze/page-id-cipher {:scheduler ::invalid}})
+ :key := :blaze/page-id-cipher
+ :reason := ::ig/build-failed-spec
+ [:cause-data ::s/problems 0 :pred] := `(fn ~'[%] (contains? ~'% :node))
+ [:cause-data ::s/problems 1 :pred] := `(fn ~'[%] (contains? ~'% :clock))
+ [:cause-data ::s/problems 2 :pred] := `(fn ~'[%] (contains? ~'% :rng-fn))
+ [:cause-data ::s/problems 3 :pred] := `scheduler?
+ [:cause-data ::s/problems 3 :val] := ::invalid))
+
+ (testing "invalid clock"
+ (given-thrown (ig/init {:blaze/page-id-cipher {:clock ::invalid}})
+ :key := :blaze/page-id-cipher
+ :reason := ::ig/build-failed-spec
+ [:cause-data ::s/problems 0 :pred] := `(fn ~'[%] (contains? ~'% :node))
+ [:cause-data ::s/problems 1 :pred] := `(fn ~'[%] (contains? ~'% :scheduler))
+ [:cause-data ::s/problems 2 :pred] := `(fn ~'[%] (contains? ~'% :rng-fn))
+ [:cause-data ::s/problems 3 :pred] := `time/clock?
+ [:cause-data ::s/problems 3 :val] := ::invalid))
+
+ (testing "invalid rng-fn"
+ (given-thrown (ig/init {:blaze/page-id-cipher {:rng-fn ::invalid}})
+ :key := :blaze/page-id-cipher
+ :reason := ::ig/build-failed-spec
+ [:cause-data ::s/problems 0 :pred] := `(fn ~'[%] (contains? ~'% :node))
+ [:cause-data ::s/problems 1 :pred] := `(fn ~'[%] (contains? ~'% :scheduler))
+ [:cause-data ::s/problems 2 :pred] := `(fn ~'[%] (contains? ~'% :clock))
+ [:cause-data ::s/problems 3 :pred] := `fn?
+ [:cause-data ::s/problems 3 :val] := ::invalid))
+
+ (testing "success"
+ (with-system [{:blaze/keys [page-id-cipher]} config]
+ (is (s/valid? :blaze/page-id-cipher page-id-cipher)))))
+
+(deftest key-rotation-test
+ (with-system [{:blaze/keys [page-id-cipher]} config]
+ (Thread/sleep 1500)
+ (given (datafy/datafy (:key-set-handle @(:state page-id-cipher)))
+ count := 2
+ [0 :primary] := true
+ [0 :status] := :key.status/enabled
+ [1 :primary] := false
+ [1 :status] := :key.status/enabled)))
diff --git a/modules/page-id-cipher/tests.edn b/modules/page-id-cipher/tests.edn
new file mode 100644
index 000000000..12727a10d
--- /dev/null
+++ b/modules/page-id-cipher/tests.edn
@@ -0,0 +1,10 @@
+#kaocha/v1
+ #merge
+ [{}
+ #profile {:ci {:reporter kaocha.report/documentation
+ :color? false}
+ :coverage {:plugins [:kaocha.plugin/cloverage]
+ :cloverage/opts
+ {:codecov? true}
+ :reporter kaocha.report/documentation
+ :color? false}}]
diff --git a/modules/rest-api/src/blaze/rest_api.clj b/modules/rest-api/src/blaze/rest_api.clj
index 83fde229d..b3a7b6ea6 100644
--- a/modules/rest-api/src/blaze/rest_api.clj
+++ b/modules/rest-api/src/blaze/rest_api.clj
@@ -82,7 +82,8 @@
::async-status-handler
::async-status-cancel-handler
::capabilities-handler
- ::db-sync-timeout]
+ ::db-sync-timeout
+ :blaze/page-id-cipher]
:opt-un
[:blaze/context-path
::auth-backends
diff --git a/modules/rest-api/src/blaze/rest_api/routes.clj b/modules/rest-api/src/blaze/rest_api/routes.clj
index 39ef0f671..f8f2bb9c8 100644
--- a/modules/rest-api/src/blaze/rest_api/routes.clj
+++ b/modules/rest-api/src/blaze/rest_api/routes.clj
@@ -2,6 +2,7 @@
(:require
[blaze.fhir.structure-definition-repo :as sdr]
[blaze.middleware.fhir.db :as db]
+ [blaze.middleware.fhir.decrypt-page-id :as decrypt-page-id]
[blaze.middleware.fhir.error :as error]
[blaze.middleware.fhir.output :as fhir-output]
[blaze.middleware.fhir.resource :as resource]
@@ -12,6 +13,7 @@
[blaze.rest-api.middleware.link-headers :as link-headers]
[blaze.rest-api.middleware.metrics :as metrics]
[blaze.rest-api.middleware.sync :as sync]
+ [blaze.rest-api.operation :as-alias operation]
[blaze.rest-api.spec]
[blaze.rest-api.util :as u]
[blaze.spec]
@@ -83,6 +85,10 @@
(metrics/wrap-observe-request-duration-fn interaction)
identity))})
+(def ^:private wrap-decrypt-page-id
+ {:name :decrypt-page-id
+ :wrap decrypt-page-id/wrap-decrypt-page-id})
+
(defn resource-route
"Builds routes for one resource according to `structure-definition`.
@@ -90,7 +96,7 @@
Route data contains the resource type under :fhir.resource/type."
{:arglists '([config resource-patterns structure-definition])}
- [{:keys [node db-sync-timeout batch?]} resource-patterns
+ [{:keys [node db-sync-timeout batch? page-id-cipher]} resource-patterns
{:keys [name] :as structure-definition}]
(when-let
[{:blaze.rest-api.resource-pattern/keys [interactions]}
@@ -134,25 +140,28 @@
:blaze.rest-api.interaction/handler)}))]]
(not batch?)
(conj
- ["/__page"
+ ["/__page/{page-id}"
(cond-> {:name (keyword name "page") :conflicting true}
(contains? interactions :search-type)
(assoc
:get {:interaction "search-type"
- :middleware [[wrap-snapshot-db node db-sync-timeout]
+ :middleware [[wrap-decrypt-page-id page-id-cipher]
+ [wrap-snapshot-db node db-sync-timeout]
wrap-link-headers]
:handler (-> interactions :search-type
:blaze.rest-api.interaction/handler)}
:post {:interaction "search-type"
- :middleware [[wrap-snapshot-db node db-sync-timeout]
+ :middleware [[wrap-decrypt-page-id page-id-cipher]
+ [wrap-snapshot-db node db-sync-timeout]
wrap-link-headers]
:handler (-> interactions :search-type
:blaze.rest-api.interaction/handler)}))]
- ["/__history-page"
+ ["/__history-page/{page-id}"
(cond-> {:name (keyword name "history-page") :conflicting true}
(contains? interactions :history-type)
(assoc :get {:interaction "history-type"
- :middleware [[wrap-snapshot-db node db-sync-timeout]
+ :middleware [[wrap-decrypt-page-id page-id-cipher]
+ [wrap-snapshot-db node db-sync-timeout]
wrap-link-headers]
:handler (-> interactions :history-type
:blaze.rest-api.interaction/handler)}))])
@@ -198,33 +207,47 @@
:blaze.rest-api.interaction/handler)}))]]]
(not batch?)
(conj
- ["/__history-page"
+ ["/__history-page/{page-id}"
(cond->
{:name (keyword name "history-instance-page")
:conflicting true}
(contains? interactions :history-instance)
(assoc :get {:interaction "history-instance"
- :middleware [[wrap-snapshot-db node db-sync-timeout]
+ :middleware [[wrap-decrypt-page-id page-id-cipher]
+ [wrap-snapshot-db node db-sync-timeout]
wrap-link-headers]
:handler (-> interactions :history-instance
:blaze.rest-api.interaction/handler)}))]))))))
(defn compartment-route
{:arglists '([context compartment])}
- [{:keys [node db-sync-timeout]} {:blaze.rest-api.compartment/keys [code search-handler]}]
- [(format "/%s/{id}/{type}" code)
- {:name (keyword code "compartment")
- :fhir.compartment/code code
- :conflicting true
- :get {:interaction "search-compartment"
- :middleware [[wrap-db node db-sync-timeout]
- wrap-link-headers]
- :handler search-handler}}])
+ [{:keys [node db-sync-timeout batch? page-id-cipher]}
+ {:blaze.rest-api.compartment/keys [code search-handler]}]
+ (cond->
+ [(format "/%s/{id}/{type}" code)
+ {:fhir.compartment/code code}
+ [""
+ {:name (keyword code "compartment")
+ :fhir.compartment/code code
+ :conflicting true
+ :get {:interaction "search-compartment"
+ :middleware [[wrap-db node db-sync-timeout]
+ wrap-link-headers]
+ :handler search-handler}}]]
+ (not batch?)
+ (conj
+ ["/__page/{page-id}"
+ {:name (keyword code "compartment-page")
+ :conflicting true
+ :get {:interaction "search-compartment"
+ :middleware [[wrap-decrypt-page-id page-id-cipher]
+ [wrap-snapshot-db node db-sync-timeout]
+ wrap-link-headers]
+ :handler search-handler}}])))
(defn- operation-system-handler-route
[{:keys [node db-sync-timeout]}
- {:blaze.rest-api.operation/keys
- [code response-type post-middleware system-handler]}]
+ {::operation/keys [code response-type post-middleware system-handler]}]
(when system-handler
[[(str "/$" code)
(cond-> {:interaction (str "operation-system-" code)
@@ -239,7 +262,7 @@
(defn operation-type-handler-route
[{:keys [node db-sync-timeout]}
- {:blaze.rest-api.operation/keys [code resource-types type-handler]}]
+ {::operation/keys [code resource-types type-handler]}]
(when type-handler
(map
(fn [resource-type]
@@ -253,18 +276,31 @@
resource-types)))
(defn operation-instance-handler-route
- [{:keys [node db-sync-timeout]}
- {:blaze.rest-api.operation/keys [code resource-types instance-handler]}]
+ [{:keys [node db-sync-timeout page-id-cipher]}
+ {::operation/keys [code resource-types instance-handler
+ instance-page-handler]}]
(when instance-handler
(map
(fn [resource-type]
- [(str "/" resource-type "/{id}/$" code)
- {:interaction (str "operation-instance-" code)
- :conflicting true
- :middleware [[wrap-db node db-sync-timeout]]
- :get instance-handler
- :post {:middleware [wrap-resource]
- :handler instance-handler}}])
+ (cond->
+ [(str "/" resource-type "/{id}")
+ {:interaction (str "operation-instance-" code)}
+ [(str "/$" code)
+ {:name (keyword (str resource-type ".operation") code)
+ :conflicting true
+ :middleware [[wrap-db node db-sync-timeout]]
+ :get instance-handler
+ :post {:middleware [wrap-resource]
+ :handler instance-handler}}]]
+
+ instance-page-handler
+ (conj
+ [(str "/__" code "-page/{page-id}")
+ {:name (keyword (str resource-type ".operation") (str code "-page"))
+ :conflicting true
+ :middleware [[wrap-decrypt-page-id page-id-cipher]
+ [wrap-snapshot-db node db-sync-timeout]]
+ :get instance-page-handler}])))
resource-types)))
(defn routes
@@ -288,7 +324,8 @@
async-status-cancel-handler
capabilities-handler
metadata-handler
- admin-handler]
+ admin-handler
+ page-id-cipher]
:or {context-path ""}
:as config}]
(cond->
@@ -322,23 +359,26 @@
:handler history-system-handler}))]]
(not batch?)
(conj
- ["/__page"
+ ["/__page/{page-id}"
(cond-> {:name :page}
(some? search-system-handler)
(assoc
:get {:interaction "search-system"
- :middleware [[wrap-snapshot-db node db-sync-timeout]
+ :middleware [[wrap-decrypt-page-id page-id-cipher]
+ [wrap-snapshot-db node db-sync-timeout]
wrap-link-headers]
:handler search-system-handler}
:post {:interaction "search-system"
- :middleware [[wrap-snapshot-db node db-sync-timeout]
+ :middleware [[wrap-decrypt-page-id page-id-cipher]
+ [wrap-snapshot-db node db-sync-timeout]
wrap-link-headers]
:handler search-system-handler}))]
- ["/__history-page"
+ ["/__history-page/{page-id}"
(cond-> {:name :history-page}
(some? history-system-handler)
(assoc :get {:interaction "history-system"
- :middleware [[wrap-snapshot-db node db-sync-timeout]
+ :middleware [[wrap-decrypt-page-id page-id-cipher]
+ [wrap-snapshot-db node db-sync-timeout]
wrap-link-headers]
:handler history-system-handler}))])
true
diff --git a/modules/rest-api/test/blaze/rest_api/routes_test.clj b/modules/rest-api/test/blaze/rest_api/routes_test.clj
index b250b5d37..7c18c68b9 100644
--- a/modules/rest-api/test/blaze/rest_api/routes_test.clj
+++ b/modules/rest-api/test/blaze/rest_api/routes_test.clj
@@ -37,8 +37,8 @@
[2] := ["" {:name :Patient/type}]
[3] := ["/_history" {:name :Patient/history :conflicting true}]
[4] := ["/_search" {:name :Patient/search :conflicting true}]
- [5] := ["/__page" {:name :Patient/page :conflicting true}]
- [6] := ["/__history-page" {:name :Patient/history-page :conflicting true}]
+ [5] := ["/__page/{page-id}" {:name :Patient/page :conflicting true}]
+ [6] := ["/__history-page/{page-id}" {:name :Patient/history-page :conflicting true}]
[7 1 1 :name] := :Patient/instance
[7 1 1 :conflicting] := true
[7 1 1 :get :middleware count] := 1
@@ -135,7 +135,12 @@
{:code "evaluate-measure"
:resource-types ["Measure"]
:type-handler (handler ::evaluate-measure-type)
- :instance-handler (handler ::evaluate-measure-instance)}]
+ :instance-handler (handler ::evaluate-measure-instance)}
+ #:blaze.rest-api.operation
+ {:code "everything"
+ :resource-types ["Patient"]
+ :instance-handler (handler ::everything)
+ :instance-page-handler (handler ::everything-page)}]
:capabilities-handler (handler ::capabilities)
:metadata-handler (handler ::metadata)
:admin-handler (handler ::admin)
@@ -166,20 +171,24 @@
"" :get "search-system"
"" :post "transaction"
"/_history" :get "history-system"
- "/__history-page" :get "history-system"
- "/__page" :get "search-system"
- "/__page" :post "search-system"
+ "/__page/0" :get "search-system"
+ "/__page/0" :post "search-system"
+ "/__history-page/0" :get "history-system"
"/Patient" :get "search-type"
"/Patient" :post "create"
"/Patient/_search" :post "search-type"
"/Patient/_history" :get "history-type"
- "/Patient/__history-page" :get "history-type"
+ "/Patient/__page/0" :get "search-type"
+ "/Patient/__page/0" :post "search-type"
+ "/Patient/__history-page/0" :get "history-type"
"/Patient/0" :get "read"
"/Patient/0" :put "update"
"/Patient/0" :delete "delete"
"/Patient/0/_history" :get "history-instance"
- "/Patient/0/__history-page" :get "history-instance"
+ "/Patient/0/__history-page/0" :get "history-instance"
"/Patient/0/_history/42" :get "vread"
+ "/Patient/0/$everything" :get "operation-instance-everything"
+ "/Patient/0/__everything-page/0" :get "operation-instance-everything"
"/Patient/0/Condition" :get "search-compartment"
"/Patient/0/Observation" :get "search-compartment"
"/$compact-db" :get "operation-system-compact-db"
@@ -203,23 +212,30 @@
"" :get ::search-system
"" :post ::transaction
"/_history" :get ::history-system
- "/__history-page" :get ::history-system
- "/__page" :get ::search-system
- "/__page" :post ::search-system
+ "/__page/0" :get ::search-system
+ "/__page/0" :post ::search-system
+ "/__history-page/0" :get ::history-system
"/Patient" :get ::search-type
"/Patient" :post ::create
"/Patient" :delete ::conditional-delete-type
- "/Patient/_history" :get ::history-type
- "/Patient/__history-page" :get ::history-type
"/Patient/_search" :post ::search-type
+ "/Patient/_history" :get ::history-type
+ "/Patient/__page/0" :get ::search-type
+ "/Patient/__page/0" :post ::search-type
+ "/Patient/__history-page/0" :get ::history-type
"/Patient/0" :get ::read
"/Patient/0" :put ::update
"/Patient/0" :delete ::delete
"/Patient/0/_history" :get ::history-instance
- "/Patient/0/__history-page" :get ::history-instance
+ "/Patient/0/__history-page/0" :get ::history-instance
"/Patient/0/_history/42" :get ::vread
+ "/Patient/0/$everything" :get ::everything
+ "/Patient/0/$everything" :post ::everything
+ "/Patient/0/__everything-page/0" :get ::everything-page
"/Patient/0/Condition" :get ::search-patient-compartment
"/Patient/0/Observation" :get ::search-patient-compartment
+ "/Patient/0/Condition/__page/0" :get ::search-patient-compartment
+ "/Patient/0/Observation/__page/0" :get ::search-patient-compartment
"/$compact-db" :get ::compact-db
"/$compact-db" :post ::compact-db
"/Measure/$evaluate-measure" :get ::evaluate-measure-type
@@ -243,23 +259,25 @@
"" :get [:observe-request-duration :params :output :error :forwarded :sync :db :link-headers]
"" :post [:observe-request-duration :params :output :error :forwarded :sync :resource]
"/_history" :get [:observe-request-duration :params :output :error :forwarded :sync :db :link-headers]
- "/__history-page" :get [:observe-request-duration :params :output :error :forwarded :sync :snapshot-db :link-headers]
- "/__page" :get [:observe-request-duration :params :output :error :forwarded :sync :snapshot-db :link-headers]
- "/__page" :post [:observe-request-duration :params :output :error :forwarded :sync :snapshot-db :link-headers]
+ "/__page/0" :get [:observe-request-duration :params :output :error :forwarded :sync :decrypt-page-id :snapshot-db :link-headers]
+ "/__page/0" :post [:observe-request-duration :params :output :error :forwarded :sync :decrypt-page-id :snapshot-db :link-headers]
+ "/__history-page/0" :get [:observe-request-duration :params :output :error :forwarded :sync :decrypt-page-id :snapshot-db :link-headers]
"/Patient" :get [:observe-request-duration :params :output :error :forwarded :sync :db :link-headers]
"/Patient" :post [:observe-request-duration :params :output :error :forwarded :sync :resource]
"/Patient" :delete [:observe-request-duration :params :output :error :forwarded :sync]
"/Patient/_history" :get [:observe-request-duration :params :output :error :forwarded :sync :db :link-headers]
- "/Patient/__history-page" :get [:observe-request-duration :params :output :error :forwarded :sync :snapshot-db :link-headers]
+ "/Patient/__history-page/0" :get [:observe-request-duration :params :output :error :forwarded :sync :decrypt-page-id :snapshot-db :link-headers]
"/Patient/_search" :post [:observe-request-duration :params :output :error :forwarded :sync :ensure-form-body :db :link-headers]
- "/Patient/__page" :get [:observe-request-duration :params :output :error :forwarded :sync :snapshot-db :link-headers]
- "/Patient/__page" :post [:observe-request-duration :params :output :error :forwarded :sync :snapshot-db :link-headers]
+ "/Patient/__page/0" :get [:observe-request-duration :params :output :error :forwarded :sync :decrypt-page-id :snapshot-db :link-headers]
+ "/Patient/__page/0" :post [:observe-request-duration :params :output :error :forwarded :sync :decrypt-page-id :snapshot-db :link-headers]
"/Patient/0" :get [:observe-request-duration :params :output :error :forwarded :sync :db]
"/Patient/0" :put [:observe-request-duration :params :output :error :forwarded :sync :resource]
"/Patient/0" :delete [:observe-request-duration :params :output :error :forwarded :sync]
"/Patient/0/_history" :get [:observe-request-duration :params :output :error :forwarded :sync :db :link-headers]
- "/Patient/0/__history-page" :get [:observe-request-duration :params :output :error :forwarded :sync :snapshot-db :link-headers]
+ "/Patient/0/__history-page/0" :get [:observe-request-duration :params :output :error :forwarded :sync :decrypt-page-id :snapshot-db :link-headers]
"/Patient/0/_history/42" :get [:observe-request-duration :params :output :error :forwarded :sync :versioned-instance-db]
+ "/Patient/0/$everything" :get [:observe-request-duration :params :output :error :forwarded :sync :db]
+ "/Patient/0/__everything-page/0" :get [:observe-request-duration :params :output :error :forwarded :sync :decrypt-page-id :snapshot-db]
"/Patient/0/Condition" :get [:observe-request-duration :params :output :error :forwarded :sync :db :link-headers]
"/Patient/0/Observation" :get [:observe-request-duration :params :output :error :forwarded :sync :db :link-headers]
"/$compact-db" :get [:observe-request-duration :params :output :error :forwarded :sync :db]
diff --git a/modules/rest-api/test/blaze/rest_api_test.clj b/modules/rest-api/test/blaze/rest_api_test.clj
index b6603b236..bea4a7c6d 100644
--- a/modules/rest-api/test/blaze/rest_api_test.clj
+++ b/modules/rest-api/test/blaze/rest_api_test.clj
@@ -113,6 +113,7 @@
:async-status-cancel-handler success-handler
:capabilities-handler (ig/ref ::rest-api/capabilities-handler)
:db-sync-timeout 10000
+ :page-id-cipher (ig/ref :blaze.test/page-id-cipher)
:search-system-handler success-handler
:transaction-handler success-handler
:resource-patterns
@@ -142,7 +143,8 @@
:search-param-registry (ig/ref :blaze.db/search-param-registry)}
:blaze.db/search-param-registry
{:structure-definition-repo structure-definition-repo}
- :blaze.test/fixed-rng-fn {}))
+ :blaze.test/fixed-rng-fn {}
+ :blaze.test/page-id-cipher {}))
(defmethod ig/init-key ::empty-structure-definition-repo
[_ _]
diff --git a/modules/rest-util/deps.edn b/modules/rest-util/deps.edn
index cdce63a75..97e4ff34c 100644
--- a/modules/rest-util/deps.edn
+++ b/modules/rest-util/deps.edn
@@ -11,6 +11,12 @@
blaze/job-scheduler
{:local/root "../job-scheduler"}
+ blaze/page-id-cipher
+ {:local/root "../page-id-cipher"}
+
+ buddy/buddy-core
+ {:mvn/version "1.12.0-430"}
+
org.apache.httpcomponents.core5/httpcore5
{:mvn/version "5.2.5"}
diff --git a/modules/rest-util/src/blaze/handler/fhir/util.clj b/modules/rest-util/src/blaze/handler/fhir/util.clj
index c502654a2..721aea17e 100644
--- a/modules/rest-util/src/blaze/handler/fhir/util.clj
+++ b/modules/rest-util/src/blaze/handler/fhir/util.clj
@@ -141,8 +141,8 @@
(str "W/\"" t "\""))
(defn- resource-handle [db type id]
- (if-let [handle (d/resource-handle db type id)]
- (if (rh/deleted? handle)
+ (if-let [{:keys [op] :as handle} (d/resource-handle db type id)]
+ (if (identical? :delete op)
(let [tx (d/tx db (rh/t handle))]
(ba/not-found
(format "Resource `%s/%s` was deleted." type id)
diff --git a/modules/rest-util/src/blaze/handler/fhir/util/spec.clj b/modules/rest-util/src/blaze/handler/fhir/util/spec.clj
index b882add6e..2087db0d4 100644
--- a/modules/rest-util/src/blaze/handler/fhir/util/spec.clj
+++ b/modules/rest-util/src/blaze/handler/fhir/util/spec.clj
@@ -7,7 +7,7 @@
string?)
(s/def :ring.request.query-params/value
- (s/or :string string? :strings (s/coll-of string? :min-count 2)))
+ (s/or :string string? :strings (s/coll-of string? :min-count 1)))
(s/def :ring.request/query-params
(s/map-of :ring.request.query-params/key :ring.request.query-params/value))
diff --git a/modules/rest-util/src/blaze/middleware/fhir/db.clj b/modules/rest-util/src/blaze/middleware/fhir/db.clj
index 94e01a6c2..13b690ee9 100644
--- a/modules/rest-util/src/blaze/middleware/fhir/db.clj
+++ b/modules/rest-util/src/blaze/middleware/fhir/db.clj
@@ -32,7 +32,7 @@
(-> (fhir-util/sync node t timeout)
(ac/then-compose #(handler (assoc request :blaze/db %))))
(ac/completed-future
- (ba/incorrect (format "Missing or invalid `__t` query param `%s`." (params "__t"))))))))
+ (ba/incorrect (format "Missing or invalid `__t` query param `%s`." (get params "__t"))))))))
(defn wrap-versioned-instance-db
"Database wrapping for versioned read requests.
diff --git a/modules/rest-util/src/blaze/middleware/fhir/decrypt_page_id.clj b/modules/rest-util/src/blaze/middleware/fhir/decrypt_page_id.clj
new file mode 100644
index 000000000..975333f35
--- /dev/null
+++ b/modules/rest-util/src/blaze/middleware/fhir/decrypt_page_id.clj
@@ -0,0 +1,55 @@
+(ns blaze.middleware.fhir.decrypt-page-id
+ "Contains functionality around encrypting/decrypting query parameters into
+ page id's that are used in paging URL's.
+
+ The middleware `wrap-decrypt-page-id` will decrypt the page id's into query
+ parameters, while the `encrypt` function will encrypt query params into page
+ id's."
+ (:require
+ [blaze.anomaly :as ba :refer [if-ok try-one]]
+ [blaze.async.comp :as ac]
+ [cheshire.core :as json]
+ [cognitect.anomalies :as anom])
+ (:import
+ [com.google.crypto.tink Aead]
+ [java.security GeneralSecurityException]
+ [java.util Base64 Base64$Encoder]))
+
+(set! *warn-on-reflection* true)
+
+(defn- decrypt* [page-id-cipher input]
+ (try-one GeneralSecurityException ::anom/incorrect
+ (.decrypt ^Aead page-id-cipher input nil)))
+
+(defn- b64-decode [page-id]
+ (try-one IllegalArgumentException ::anom/incorrect
+ (.decode (Base64/getUrlDecoder) ^String page-id)))
+
+(defn- decrypt [page-id-cipher page-id]
+ (if-ok [cipher-text (b64-decode page-id)
+ clear-text (decrypt* page-id-cipher cipher-text)]
+ (json/parse-cbor clear-text)
+ (fn [_] (ba/not-found (format "Page with id `%s` not found." page-id)))))
+
+(defn wrap-decrypt-page-id
+ "Wraps a middleware round `handler` that decrypts the :page-id path param from
+ the request using `page-id-cipher` and overrides :params of the request with
+ the result."
+ {:arglists '([handler page-id-cipher])}
+ [handler page-id-cipher]
+ (fn [{{:keys [page-id]} :path-params :as request}]
+ (if-ok [params (decrypt page-id-cipher page-id)]
+ (handler (assoc request :params params :query-params params))
+ ac/completed-future)))
+
+(def ^:private ^Base64$Encoder b64-encoder
+ (.withoutPadding (Base64/getUrlEncoder)))
+
+(defn encrypt
+ "Encrypts `query-params` using `page-id-cipher` into an base 64 encoded string
+ that can be used as page id."
+ {:arglists '([page-id-cipher query-params])}
+ [page-id-cipher query-params]
+ (let [clear-text (json/generate-cbor query-params)
+ cipher-text (.encrypt ^Aead page-id-cipher clear-text nil)]
+ (.encodeToString b64-encoder cipher-text)))
diff --git a/modules/rest-util/src/blaze/middleware/fhir/decrypt_page_id_spec.clj b/modules/rest-util/src/blaze/middleware/fhir/decrypt_page_id_spec.clj
new file mode 100644
index 000000000..ff0e0edf4
--- /dev/null
+++ b/modules/rest-util/src/blaze/middleware/fhir/decrypt_page_id_spec.clj
@@ -0,0 +1,16 @@
+(ns blaze.middleware.fhir.decrypt-page-id-spec
+ (:require
+ [blaze.handler.fhir.util.spec]
+ [blaze.middleware.fhir.decrypt-page-id :as decrypt-page-id]
+ [blaze.page-id-cipher.spec]
+ [blaze.spec]
+ [clojure.spec.alpha :as s]))
+
+(s/fdef decrypt-page-id/wrap-decrypt-page-id
+ :args (s/cat :handler fn? :page-id-cipher :blaze/page-id-cipher)
+ :ret fn?)
+
+(s/fdef decrypt-page-id/encrypt
+ :args (s/cat :page-id-cipher :blaze/page-id-cipher
+ :query-params (s/nilable :ring.request/query-params))
+ :ret :blaze/page-id)
diff --git a/modules/rest-util/test/blaze/middleware/fhir/db_test.clj b/modules/rest-util/test/blaze/middleware/fhir/db_test.clj
index 93a0dd4bf..f0e3c7bb8 100644
--- a/modules/rest-util/test/blaze/middleware/fhir/db_test.clj
+++ b/modules/rest-util/test/blaze/middleware/fhir/db_test.clj
@@ -75,6 +75,11 @@
(testing "uses existing database value"
(is (= ::db @((db/wrap-snapshot-db handler ::node timeout) {:blaze/db ::db}))))
+ (testing "with missing params"
+ (given-failed-future ((db/wrap-snapshot-db handler ::node timeout) nil)
+ ::anom/category := ::anom/incorrect
+ ::anom/message := "Missing or invalid `__t` query param `null`."))
+
(testing "with missing or invalid __t"
(doseq [t ["a" "-1"]]
(given-failed-future ((db/wrap-snapshot-db handler ::node timeout) {:params {"__t" t}})
diff --git a/modules/rest-util/test/blaze/middleware/fhir/decrypt_page_id_test.clj b/modules/rest-util/test/blaze/middleware/fhir/decrypt_page_id_test.clj
new file mode 100644
index 000000000..fbf86e387
--- /dev/null
+++ b/modules/rest-util/test/blaze/middleware/fhir/decrypt_page_id_test.clj
@@ -0,0 +1,43 @@
+(ns blaze.middleware.fhir.decrypt-page-id-test
+ (:require
+ [blaze.anomaly :as ba]
+ [blaze.async.comp :as ac]
+ [blaze.middleware.fhir.decrypt-page-id :refer [encrypt wrap-decrypt-page-id]]
+ [blaze.middleware.fhir.decrypt-page-id-spec]
+ [blaze.test-util :as tu :refer [satisfies-prop]]
+ [clojure.spec.alpha :as s]
+ [clojure.spec.test.alpha :as st]
+ [clojure.test :as test :refer [deftest testing]]
+ [clojure.test.check.generators :as gen]
+ [clojure.test.check.properties :as prop]
+ [cognitect.anomalies :as anom])
+ (:import
+ [com.google.crypto.tink Aead KeysetHandle]
+ [com.google.crypto.tink.aead AeadConfig PredefinedAeadParameters]))
+
+(set! *warn-on-reflection* true)
+(st/instrument)
+(AeadConfig/register)
+
+(test/use-fixtures :each tu/fixture)
+
+(def page-id-cipher
+ (-> (KeysetHandle/generateNew PredefinedAeadParameters/AES128_GCM)
+ (.getPrimitive Aead)))
+
+(def handler
+ (wrap-decrypt-page-id ac/completed-future page-id-cipher))
+
+(deftest wrap-decrypt-page-id-test
+ (testing "random string results in an anomaly with category incorrect"
+ (satisfies-prop 1000
+ (prop/for-all [page-id gen/string]
+ (let [{::anom/keys [category message]} (ba/try-anomaly @(handler {:path-params {:page-id page-id}}))]
+ (and (= category ::anom/not-found)
+ (= message (format "Page with id `%s` not found." page-id)))))))
+
+ (testing "random query params can be encrypted"
+ (satisfies-prop 100
+ (prop/for-all [query-params (s/gen :ring.request/query-params)]
+ (let [page-id (encrypt page-id-cipher query-params)]
+ (= query-params (:params @(handler {:path-params {:page-id page-id}}))))))))
diff --git a/modules/spec/src/blaze/spec.clj b/modules/spec/src/blaze/spec.clj
index a8c10ab9a..004ea1838 100644
--- a/modules/spec/src/blaze/spec.clj
+++ b/modules/spec/src/blaze/spec.clj
@@ -39,6 +39,9 @@
(s/def :blaze/cancelled?
(s/fspec :args (s/cat) :ret (s/nilable ::anom/anomaly)))
+(s/def :blaze/page-id
+ (s/and string? #(re-matches #"[A-Za-z0-9-_]+" %)))
+
;; ---- DB ------------------------------------------------------------------
(s/def :blaze.db.query/search-clause
diff --git a/modules/test-util/deps.edn b/modules/test-util/deps.edn
index f1ede5ac2..d3af51e56 100644
--- a/modules/test-util/deps.edn
+++ b/modules/test-util/deps.edn
@@ -1,5 +1,13 @@
{:deps
- {com.taoensso/timbre
+ {com.google.guava/guava
+ {:mvn/version "33.3.0-jre"
+ :exclusions
+ [com.google.code.findbugs/jsr305
+ org.checkerframework/checker-qual
+ com.google.errorprone/error_prone_annotations
+ com.google.j2objc/j2objc-annotations]}
+
+ com.taoensso/timbre
{:mvn/version "6.5.0"}
org.clojars.akiel/iota
diff --git a/resources/blaze.edn b/resources/blaze.edn
index a3bc70750..720f6d144 100644
--- a/resources/blaze.edn
+++ b/resources/blaze.edn
@@ -41,7 +41,8 @@
:enforce-referential-integrity #blaze/cfg ["ENFORCE_REFERENTIAL_INTEGRITY" boolean? true]
:job-scheduler #blaze/ref :blaze/job-scheduler
:clock #blaze/ref :blaze/clock
- :rng-fn #blaze/ref :blaze/rng-fn}
+ :rng-fn #blaze/ref :blaze/rng-fn
+ :page-id-cipher #blaze/ref :blaze/page-id-cipher}
;;
;; Resource patterns
@@ -101,6 +102,7 @@
:def-uri "http://hl7.org/fhir/OperationDefinition/Patient-everything"
:resource-types ["Patient"]
:instance-handler #blaze/ref :blaze.operation.patient/everything
+ :instance-page-handler #blaze/ref :blaze.operation.patient/everything
:documentation "Returns all resources from the patient compartment of one concrete patient including the patient. Has a fix limit of 10,000 resources if paging isn't used. Paging is supported when the _count parameter is used. No other params are supported."}
#:blaze.rest-api.operation
{:code "totals"
@@ -119,15 +121,18 @@
;;
:blaze.interaction.history/system
{:clock #blaze/ref :blaze/clock
- :rng-fn #blaze/ref :blaze/rng-fn}
+ :rng-fn #blaze/ref :blaze/rng-fn
+ :page-id-cipher #blaze/ref :blaze/page-id-cipher}
:blaze.interaction.history/type
{:clock #blaze/ref :blaze/clock
- :rng-fn #blaze/ref :blaze/rng-fn}
+ :rng-fn #blaze/ref :blaze/rng-fn
+ :page-id-cipher #blaze/ref :blaze/page-id-cipher}
:blaze.interaction.history/instance
{:clock #blaze/ref :blaze/clock
- :rng-fn #blaze/ref :blaze/rng-fn}
+ :rng-fn #blaze/ref :blaze/rng-fn
+ :page-id-cipher #blaze/ref :blaze/page-id-cipher}
[:blaze.interaction/create :blaze.interaction.main/create]
{:node #blaze/ref :blaze.db.main/node
@@ -145,18 +150,21 @@
:blaze.interaction/search-system
{:clock #blaze/ref :blaze/clock
:rng-fn #blaze/ref :blaze/rng-fn
- :page-store #blaze/ref :blaze/page-store}
+ :page-store #blaze/ref :blaze/page-store
+ :page-id-cipher #blaze/ref :blaze/page-id-cipher}
:blaze.interaction/search-type
{:clock #blaze/ref :blaze/clock
:rng-fn #blaze/ref :blaze/rng-fn
:page-store #blaze/ref :blaze/page-store
+ :page-id-cipher #blaze/ref :blaze/page-id-cipher
:context-path #blaze/cfg ["CONTEXT_PATH" string? "/fhir"]}
:blaze.interaction/search-compartment
{:clock #blaze/ref :blaze/clock
:rng-fn #blaze/ref :blaze/rng-fn
- :page-store #blaze/ref :blaze/page-store}
+ :page-store #blaze/ref :blaze/page-store
+ :page-id-cipher #blaze/ref :blaze/page-id-cipher}
:blaze.interaction/transaction
{:node #blaze/ref :blaze.db.main/node
@@ -241,7 +249,8 @@
;;
:blaze.operation.patient/everything
{:clock #blaze/ref :blaze/clock
- :rng-fn #blaze/ref :blaze/rng-fn}
+ :rng-fn #blaze/ref :blaze/rng-fn
+ :page-id-cipher #blaze/ref :blaze/page-id-cipher}
;;
;; FHIR Operation Totals
@@ -422,7 +431,21 @@
:clock #blaze/ref :blaze/clock
:extra-bundle-file #blaze/cfg ["DB_SEARCH_PARAM_BUNDLE" string?]}
- :blaze/scheduler {}}
+ :blaze/scheduler {}
+
+ :blaze/clock {}
+
+ :blaze/rng-fn {}
+
+ :blaze/secure-rng {}
+
+ :blaze/page-id-cipher
+ {:node #blaze/ref :blaze.db.admin/node
+ :scheduler #blaze/ref :blaze/scheduler
+ :clock #blaze/ref :blaze/clock
+ :rng-fn #blaze/ref :blaze/rng-fn}
+
+ :blaze.fhir/structure-definition-repo {}}
:storage
{:in-memory
diff --git a/src/blaze/system.clj b/src/blaze/system.clj
index 5e82a04a3..490d90a11 100644
--- a/src/blaze/system.clj
+++ b/src/blaze/system.clj
@@ -111,14 +111,6 @@
:blaze/release-date (str (LocalDate/now))
- :blaze/clock {}
-
- :blaze/rng-fn {}
-
- :blaze/secure-rng {}
-
- :blaze.fhir/structure-definition-repo {}
-
:blaze.handler/health {}
:blaze/rest-api
diff --git a/test/blaze/system_test.clj b/test/blaze/system_test.clj
index 2a2568fe6..b7cd5ebe3 100644
--- a/test/blaze/system_test.clj
+++ b/test/blaze/system_test.clj
@@ -1,7 +1,7 @@
(ns blaze.system-test
(:require
[blaze.async.comp :as ac]
- [blaze.db.api-stub :refer [mem-node-config]]
+ [blaze.db.api-stub :refer [mem-node-config with-system-data]]
[blaze.fhir.spec :as fhir-spec]
[blaze.fhir.test-util :refer [structure-definition-repo]]
[blaze.interaction.conditional-delete-type]
@@ -11,6 +11,8 @@
[blaze.interaction.search-system]
[blaze.interaction.search-type]
[blaze.interaction.transaction]
+ [blaze.middleware.fhir.decrypt-page-id :as decrypt-page-id]
+ [blaze.middleware.fhir.decrypt-page-id-spec]
[blaze.module.test-util :refer [with-system]]
[blaze.module.test-util.ring :refer [call]]
[blaze.page-store.protocols :as pp]
@@ -25,6 +27,7 @@
[buddy.auth.protocols :as ap]
[clojure.spec.alpha :as s]
[clojure.spec.test.alpha :as st]
+ [clojure.string :as str]
[clojure.test :as test :refer [are deftest testing]]
[integrant.core :as ig]
[juxt.iota :refer [given]]
@@ -103,6 +106,7 @@
:node (ig/ref :blaze.db/node)
:admin-node (ig/ref :blaze.db/node)
:db-sync-timeout 10000
+ :page-id-cipher (ig/ref :blaze.test/page-id-cipher)
:auth-backends [(ig/ref ::auth-backend)]
:search-system-handler (ig/ref :blaze.interaction/search-system)
:transaction-handler (ig/ref :blaze.interaction/transaction)
@@ -130,14 +134,17 @@
:blaze.interaction/search-system
{:clock (ig/ref :blaze.test/fixed-clock)
:rng-fn (ig/ref :blaze.test/fixed-rng-fn)
- :page-store (ig/ref ::page-store)}
+ :page-store (ig/ref ::page-store)
+ :page-id-cipher (ig/ref :blaze.test/page-id-cipher)}
:blaze.interaction/search-type
{:clock (ig/ref :blaze.test/fixed-clock)
:rng-fn (ig/ref :blaze.test/fixed-rng-fn)
- :page-store (ig/ref ::page-store)}
+ :page-store (ig/ref ::page-store)
+ :page-id-cipher (ig/ref :blaze.test/page-id-cipher)}
:blaze.interaction.history/type
{:clock (ig/ref :blaze.test/fixed-clock)
- :rng-fn (ig/ref :blaze.test/fixed-rng-fn)}
+ :rng-fn (ig/ref :blaze.test/fixed-rng-fn)
+ :page-id-cipher (ig/ref :blaze.test/page-id-cipher)}
::rest-api/async-status-handler
{}
::rest-api/async-status-cancel-handler
@@ -180,7 +187,8 @@
:blaze.test/executor {}
:blaze.test/fixed-clock {}
:blaze.test/fixed-rng-fn {}
- ::page-store {}))
+ ::page-store {}
+ :blaze.test/page-id-cipher {}))
(defmethod ig/init-key ::auth-backend
[_ _]
@@ -303,7 +311,19 @@
(with-system [{:blaze/keys [rest-api]} config]
(given (call rest-api {:request-method :get :uri ""})
:status := 200
- [:body fhir-spec/parse-json :resourceType] := "Bundle")))
+ [:headers "Link"] := ";rel=\"self\""
+ [:body fhir-spec/parse-json :resourceType] := "Bundle"))
+
+ (testing "with two patients"
+ (with-system-data [{:blaze/keys [rest-api] :blaze.test/keys [page-id-cipher]} config]
+ [[[:put {:fhir/type :fhir/Patient :id "0"}]
+ [:put {:fhir/type :fhir/Patient :id "1"}]]]
+
+ (given (call rest-api {:request-method :get :uri "" :query-string "_count=1"})
+ :status := 200
+ [:headers "Link" #(str/split % #",") 0] := ";rel=\"self\""
+ [:headers "Link" #(str/split % #",") 1] := (format ";rel=\"next\"" (decrypt-page-id/encrypt page-id-cipher {"_count" "1" "__t" "1" "__page-type" "Patient" "__page-id" "1"}))
+ [:body fhir-spec/parse-json :resourceType] := "Bundle"))))
(deftest search-type-test
(testing "using GET"
@@ -311,23 +331,77 @@
(given (call rest-api {:request-method :get :uri "/Patient"})
:status := 200
[:headers "Link"] := ";rel=\"self\""
- [:body fhir-spec/parse-json :resourceType] := "Bundle")))
+ [:body fhir-spec/parse-json :resourceType] := "Bundle"))
+
+ (testing "with two patients"
+ (with-system-data [{:blaze/keys [rest-api] :blaze.test/keys [page-id-cipher]} config]
+ [[[:put {:fhir/type :fhir/Patient :id "0"}]
+ [:put {:fhir/type :fhir/Patient :id "1"}]]]
+
+ (given (call rest-api {:request-method :get :uri "/Patient" :query-string "_count=1"})
+ :status := 200
+ [:headers "Link" #(str/split % #",") 0] := (format ";rel=\"first\"" (decrypt-page-id/encrypt page-id-cipher {"_count" "1" "__t" "1"}))
+ [:headers "Link" #(str/split % #",") 1] := ";rel=\"self\""
+ [:headers "Link" #(str/split % #",") 2] := (format ";rel=\"next\"" (decrypt-page-id/encrypt page-id-cipher {"_count" "1" "__t" "1" "__page-id" "1"}))
+ [:body fhir-spec/parse-json :resourceType] := "Bundle")
+
+ (testing "fetch the second page"
+ (given (call rest-api {:request-method :get :uri (str "/Patient/__page/" (decrypt-page-id/encrypt page-id-cipher {"__page-id" "1" "__t" "1" "_count" "1"}))})
+ :status := 200
+
+ [:headers "Link" #(str/split % #",") 0] := (format ";rel=\"first\"" (decrypt-page-id/encrypt page-id-cipher {"_count" "1" "__t" "1"}))
+ [:body fhir-spec/parse-json :resourceType] := "Bundle"))
+
+ (testing "fetch an unknown page"
+ (given (call rest-api {:request-method :get :uri "/Patient/__page/unknown"})
+ :status := 404
+
+ [:body fhir-spec/parse-json :resourceType] := "OperationOutcome")))))
(testing "using POST"
+ (testing "with unsupported media-type"
+ (with-system [{:blaze/keys [rest-api]} config]
+ (given (call rest-api {:request-method :post :uri "/Patient/_search"
+ :headers {"content-type" "application/fhir+json"}
+ :body (input-stream (byte-array 0))})
+ :status := 415
+ [:body fhir-spec/parse-json :resourceType] := "OperationOutcome")))
+
(with-system [{:blaze/keys [rest-api]} config]
(given (call rest-api {:request-method :post :uri "/Patient/_search"
:headers {"content-type" "application/x-www-form-urlencoded"}
:body (input-stream (byte-array 0))})
:status := 200
+ [:headers "Link"] := ";rel=\"self\""
[:body fhir-spec/parse-json :resourceType] := "Bundle"))
- (testing "with unsupported media-type"
- (with-system [{:blaze/keys [rest-api]} config]
+ (testing "with two patients"
+ (with-system-data [{:blaze/keys [rest-api] :blaze.test/keys [page-id-cipher]} config]
+ [[[:put {:fhir/type :fhir/Patient :id "0"}]
+ [:put {:fhir/type :fhir/Patient :id "1"}]]]
+
(given (call rest-api {:request-method :post :uri "/Patient/_search"
- :headers {"content-type" "application/fhir+json"}
+ :query-string "_count=1"
+ :headers {"content-type" "application/x-www-form-urlencoded"}
:body (input-stream (byte-array 0))})
- :status := 415
- [:body fhir-spec/parse-json :resourceType] := "OperationOutcome")))))
+ :status := 200
+ [:headers "Link" #(str/split % #",") 0] := (format ";rel=\"first\"" (decrypt-page-id/encrypt page-id-cipher {"_count" "1" "__t" "1"}))
+ [:headers "Link" #(str/split % #",") 1] := ";rel=\"self\""
+ [:headers "Link" #(str/split % #",") 2] := (format ";rel=\"next\"" (decrypt-page-id/encrypt page-id-cipher {"_count" "1" "__t" "1" "__page-id" "1"}))
+ [:body fhir-spec/parse-json :resourceType] := "Bundle")
+
+ (testing "fetch the second page"
+ (given (call rest-api {:request-method :post :uri (str "/Patient/__page/" (decrypt-page-id/encrypt page-id-cipher {"__page-id" "1" "__t" "1" "_count" "1"}))})
+ :status := 200
+
+ [:headers "Link" #(str/split % #",") 0] := (format ";rel=\"first\"" (decrypt-page-id/encrypt page-id-cipher {"_count" "1" "__t" "1"}))
+ [:body fhir-spec/parse-json :resourceType] := "Bundle"))
+
+ (testing "fetch an unknown page"
+ (given (call rest-api {:request-method :post :uri "/Patient/__page/unknown"})
+ :status := 404
+
+ [:body fhir-spec/parse-json :resourceType] := "OperationOutcome"))))))
(deftest history-type-test
(with-system [{:blaze/keys [rest-api]} config]