diff --git a/.github/integration-test/basic-auth/docker-compose.yml b/.github/integration-test/basic-auth/docker-compose.yml
new file mode 100644
index 0000000..bd93c4e
--- /dev/null
+++ b/.github/integration-test/basic-auth/docker-compose.yml
@@ -0,0 +1,29 @@
+services:
+ fhir-server:
+ image: "samply/blaze:0.30"
+ environment:
+ BASE_URL: "http://fhir-server:8080"
+ JAVA_TOOL_OPTIONS: "-Xmx1g"
+ LOG_LEVEL: "debug"
+ ENFORCE_REFERENTIAL_INTEGRITY: false
+ ports:
+ - "8082:8080"
+ volumes:
+ - "data-store-data:/app/data"
+ proxy:
+ image: "nginx:1.27.0"
+ volumes:
+ - "./nginx.conf:/etc/nginx/nginx.conf"
+ - "./proxy.htpasswd:/etc/auth/.htpasswd"
+ fhir-data-evaluator:
+ image: fhir-data-evaluator
+ environment:
+ CONVERT_TO_CSV: ${FDE_CONVERT_TO_CSV:-true}
+ FHIR_SERVER: "http://proxy:8080/fhir"
+ FHIR_USER: "test"
+ FHIR_PASSWORD: "bar"
+ volumes:
+ - "${FDE_INPUT_MEASURE:-../Documentation/example-measures/example-measure-kds.json}:/app/measure.json"
+ - "${FDE_OUTPUT_DIR:-../output}:/app/output"
+volumes:
+ data-store-data:
diff --git a/.github/integration-test/basic-auth/load-data.sh b/.github/integration-test/basic-auth/load-data.sh
new file mode 100755
index 0000000..b60ebc0
--- /dev/null
+++ b/.github/integration-test/basic-auth/load-data.sh
@@ -0,0 +1,5 @@
+#!/bin/bash -e
+
+DIR="$1"
+
+blazectl --no-progress --server http://localhost:8082/fhir upload "$DIR"
diff --git a/.github/integration-test/basic-auth/nginx.conf b/.github/integration-test/basic-auth/nginx.conf
new file mode 100644
index 0000000..0cc643d
--- /dev/null
+++ b/.github/integration-test/basic-auth/nginx.conf
@@ -0,0 +1,37 @@
+user nginx;
+worker_processes 1;
+
+error_log /var/log/nginx/error.log debug;
+pid /var/run/nginx.pid;
+
+events {
+ worker_connections 1024;
+}
+
+http {
+ include /etc/nginx/mime.types;
+ log_format main '$remote_addr - $remote_user [$time_local] "$request" '
+ '$status $body_bytes_sent "$http_referer" '
+ '"$http_user_agent" "$http_x_forwarded_for"';
+ access_log /var/log/nginx/access.log main;
+ sendfile on;
+ keepalive_timeout 65;
+
+ server {
+ listen 8080;
+ listen [::]:8080;
+
+ location / {
+ root /usr/share/nginx/html;
+ index index.html;
+ }
+
+ location /fhir {
+ auth_basic "Test Area";
+ auth_basic_user_file /etc/auth/.htpasswd;
+
+ proxy_pass http://fhir-server:8080;
+ proxy_read_timeout 43200s;
+ }
+ }
+}
diff --git a/.github/integration-test/basic-auth/proxy.htpasswd b/.github/integration-test/basic-auth/proxy.htpasswd
new file mode 100644
index 0000000..91c9509
--- /dev/null
+++ b/.github/integration-test/basic-auth/proxy.htpasswd
@@ -0,0 +1 @@
+test:$apr1$ZHbwVw0h$6uvjvv1NqGY47sVmYEvwE0
diff --git a/.github/integration-test/docker-compose.yml b/.github/integration-test/docker-compose.yml
deleted file mode 100644
index 453e1f7..0000000
--- a/.github/integration-test/docker-compose.yml
+++ /dev/null
@@ -1,20 +0,0 @@
-services:
- fhir-server:
- image: "samply/blaze:0.22"
- environment:
- BASE_URL: "http://fhir-server:8080"
- JAVA_TOOL_OPTIONS: "-Xmx1g"
- LOG_LEVEL: "debug"
- ENFORCE_REFERENTIAL_INTEGRITY: false
- ports:
- - "8082:8080"
- volumes:
- - "data-store-data:/app/data"
- networks:
- - testing-network
-
-volumes:
- data-store-data:
-networks:
- testing-network:
- driver: bridge
diff --git a/.github/integration-test/evaluate-and-post-different-doc-ref.sh b/.github/integration-test/evaluate-and-post-different-doc-ref.sh
new file mode 100755
index 0000000..4ce9876
--- /dev/null
+++ b/.github/integration-test/evaluate-and-post-different-doc-ref.sh
@@ -0,0 +1,28 @@
+#!/bin/bash -e
+
+PROJECT_IDENTIFIER_VALUE="$1"
+
+DOCKER_COMPOSE_FILE=.github/integration-test/no-auth/docker-compose.yml
+export FDE_INPUT_MEASURE=/${PWD}/.github/integration-test/measures/icd10-measure.json
+export FDE_CONVERT_TO_CSV=false
+export FDE_FHIR_REPORT_DESTINATION_SERVER=http://fhir-server:8080/fhir
+export FDE_AUTHOR_IDENTIFIER_SYSTEM=http://dsf.dev/sid/organization-identifier
+export FDE_AUTHOR_IDENTIFIER_VALUE=Test_DIC1
+export FDE_PROJECT_IDENTIFIER_SYSTEM=http://medizininformatik-initiative.de/sid/project-identifier
+export FDE_PROJECT_IDENTIFIER_VALUE="$PROJECT_IDENTIFIER_VALUE"
+export FDE_SEND_REPORT_TO_SERVER=true
+
+docker compose -f "$DOCKER_COMPOSE_FILE" run -e TZ="$(cat /etc/timezone)" fhir-data-evaluator
+
+reference_response=$(curl -s "http://localhost:8082/fhir/DocumentReference" \
+ -H "Content-Type: application/fhir+json")
+
+reference_count=$(echo "$reference_response" | jq -r '.entry | length')
+
+EXPECTED_REFERENCE_COUNT=2
+if [ "$reference_count" = "$EXPECTED_REFERENCE_COUNT" ]; then
+ echo "OK 👍: reference count ($reference_count) equals the expected count"
+else
+ echo "Fail 😞: reference count ($reference_count) != ($EXPECTED_REFERENCE_COUNT)"
+ exit 1
+fi
diff --git a/.github/integration-test/evaluate-and-post-report.sh b/.github/integration-test/evaluate-and-post-report.sh
new file mode 100755
index 0000000..900aaa9
--- /dev/null
+++ b/.github/integration-test/evaluate-and-post-report.sh
@@ -0,0 +1,50 @@
+#!/bin/bash -e
+
+PROJECT_IDENTIFIER_VALUE="$1"
+DOCKER_COMPOSE_FILE=.github/integration-test/no-auth/docker-compose.yml
+export FDE_INPUT_MEASURE=/${PWD}/.github/integration-test/measures/icd10-measure.json
+export FDE_CONVERT_TO_CSV=false
+export FDE_FHIR_REPORT_DESTINATION_SERVER=http://fhir-server:8080/fhir
+export FDE_AUTHOR_IDENTIFIER_SYSTEM=http://dsf.dev/sid/organization-identifier
+export FDE_AUTHOR_IDENTIFIER_VALUE=Test_DIC1
+export FDE_PROJECT_IDENTIFIER_SYSTEM=http://medizininformatik-initiative.de/sid/project-identifier
+export FDE_PROJECT_IDENTIFIER_VALUE="$PROJECT_IDENTIFIER_VALUE"
+export FDE_SEND_REPORT_TO_SERVER=true
+
+docker compose -f "$DOCKER_COMPOSE_FILE" run -e TZ="$(cat /etc/timezone)" fhir-data-evaluator
+
+report_response=$(curl -s "http://localhost:8082/fhir/MeasureReport" \
+ -H "Content-Type: application/fhir+json")
+
+reference_response=$(curl -s "http://localhost:8082/fhir/DocumentReference" \
+ -H "Content-Type: application/fhir+json")
+
+report_url=MeasureReport/$(echo "$report_response" | jq -r '.entry[0].resource.id')
+reference_url=$(echo "$reference_response" | jq -r '.entry[0].resource.content[0].attachment.url')
+
+if [ "$report_url" = "$reference_url" ]; then
+ echo "OK 👍: Id of MeasureReport is the same as the referenced attachment in the DocumentReference"
+else
+ echo "Fail 😞: Id of MeasureReport ($report_url) is not the same as the referenced attachment in the DocumentReference ($reference_url)"
+ exit 1
+fi
+
+REPORT=$(echo "$report_response" | jq '.entry[0].resource')
+EXPECTED_POPULATION_COUNT=2
+EXPECTED_STRATIFIER_COUNT=2
+
+POPULATION_COUNT=$(echo "$REPORT" | jq '.group[0].population[0].count')
+if [ "$POPULATION_COUNT" = "$EXPECTED_POPULATION_COUNT" ]; then
+ echo "OK 👍: population count ($POPULATION_COUNT) equals the expected count"
+else
+ echo "Fail 😞: population count ($POPULATION_COUNT) != ($EXPECTED_POPULATION_COUNT)"
+ exit 1
+fi
+
+STRATIFIER_COUNT=$(echo "$REPORT" | jq -r '.group[0].stratifier[0].stratum[0].population[0].count')
+if [ "$STRATIFIER_COUNT" = "$EXPECTED_STRATIFIER_COUNT" ]; then
+ echo "OK 👍: stratifier count ($STRATIFIER_COUNT) equals the expected count"
+else
+ echo "Fail 😞: stratifier ($STRATIFIER_COUNT) != ($EXPECTED_STRATIFIER_COUNT)"
+ exit 1
+fi
diff --git a/.github/integration-test/evaluate-and-post-update.sh b/.github/integration-test/evaluate-and-post-update.sh
new file mode 100755
index 0000000..1445850
--- /dev/null
+++ b/.github/integration-test/evaluate-and-post-update.sh
@@ -0,0 +1,51 @@
+#!/bin/bash -e
+
+PROJECT_IDENTIFIER_VALUE="$1"
+
+DOCKER_COMPOSE_FILE=.github/integration-test/no-auth/docker-compose.yml
+export FDE_INPUT_MEASURE=/${PWD}/.github/integration-test/measures/icd10-measure.json
+export FDE_CONVERT_TO_CSV=false
+export FDE_FHIR_REPORT_DESTINATION_SERVER=http://fhir-server:8080/fhir
+export FDE_AUTHOR_IDENTIFIER_SYSTEM=http://dsf.dev/sid/organization-identifier
+export FDE_AUTHOR_IDENTIFIER_VALUE=Test_DIC1
+export FDE_PROJECT_IDENTIFIER_SYSTEM=http://medizininformatik-initiative.de/sid/project-identifier
+export FDE_PROJECT_IDENTIFIER_VALUE="$PROJECT_IDENTIFIER_VALUE"
+export FDE_SEND_REPORT_TO_SERVER=true
+
+reference_response=$(curl -s "http://localhost:8082/fhir/DocumentReference" \
+ -H "Content-Type: application/fhir+json")
+reference_url_before=$(echo "$reference_response" | jq -r '.entry[0].resource.content[0].attachment.url')
+
+docker compose -f "$DOCKER_COMPOSE_FILE" run -e TZ="$(cat /etc/timezone)" fhir-data-evaluator
+
+report_response=$(curl -s "http://localhost:8082/fhir/MeasureReport" \
+ -H "Content-Type: application/fhir+json")
+reference_response=$(curl -s "http://localhost:8082/fhir/DocumentReference" \
+ -H "Content-Type: application/fhir+json")
+
+report_count=$(echo "$report_response" | jq -r '.entry | length')
+reference_count=$(echo "$reference_response" | jq -r '.entry | length')
+reference_url_after=$(echo "$reference_response" | jq -r '.entry[0].resource.content[0].attachment.url')
+
+EXPECTED_REPORT_COUNT=2
+if [ "$report_count" = "$EXPECTED_REPORT_COUNT" ]; then
+ echo "OK 👍: report count ($report_count) equals the expected count"
+else
+ echo "Fail 😞: report count ($report_count) != ($EXPECTED_REPORT_COUNT)"
+ exit 1
+fi
+
+EXPECTED_REFERENCE_COUNT=1
+if [ "$reference_count" = "$EXPECTED_REFERENCE_COUNT" ]; then
+ echo "OK 👍: reference count ($reference_count) equals the expected count"
+else
+ echo "Fail 😞: reference count ($reference_count) != ($EXPECTED_REFERENCE_COUNT)"
+ exit 1
+fi
+
+if [ "$reference_url_before" != "$reference_url_after" ]; then
+ echo "OK 👍: referenced measure report url changed"
+else
+ echo "Fail 😞: referenced measure report url did not change"
+ exit 1
+fi
diff --git a/.github/integration-test/evaluate-code.sh b/.github/integration-test/evaluate-code.sh
index cf74533..4852e55 100755
--- a/.github/integration-test/evaluate-code.sh
+++ b/.github/integration-test/evaluate-code.sh
@@ -1,14 +1,15 @@
#!/bin/bash -e
-INPUT_MEASURE=$1
-BASE_OUTPUT_DIR=$PWD/.github/integration-test/evaluate-code-test
-mkdir "$BASE_OUTPUT_DIR"
+DOCKER_COMPOSE_FILE=.github/integration-test/$1/docker-compose.yml
+export FDE_INPUT_MEASURE=/${PWD}/.github/integration-test/measures/code-measure.json
+export FDE_OUTPUT_DIR=$PWD/.github/integration-test/evaluate-code-test
+export FDE_CONVERT_TO_CSV=false
-docker run -v "$INPUT_MEASURE":/app/measure.json -v "$BASE_OUTPUT_DIR":/app/output/ -e FHIR_SERVER=http://fhir-server:8080/fhir \
- -e TZ="$(cat /etc/timezone)" --network integration-test_testing-network fhir-data-evaluator
+mkdir "$FDE_OUTPUT_DIR"
+docker compose -f "$DOCKER_COMPOSE_FILE" run -e TZ="$(cat /etc/timezone)" fhir-data-evaluator
today=$(date +"%Y-%m-%d")
-OUTPUT_DIR=$(find "$BASE_OUTPUT_DIR" -type d -name "*$today*" | head -n 1)
+OUTPUT_DIR=$(find "$FDE_OUTPUT_DIR" -type d -name "*$today*" | head -n 1)
REPORT="$OUTPUT_DIR/measure-report.json"
EXPECTED_POPULATION_COUNT=2
diff --git a/.github/integration-test/evaluate-exists.sh b/.github/integration-test/evaluate-exists.sh
index 5c4e75c..91b19fc 100755
--- a/.github/integration-test/evaluate-exists.sh
+++ b/.github/integration-test/evaluate-exists.sh
@@ -1,14 +1,15 @@
#!/bin/bash -e
-INPUT_MEASURE=$1
-BASE_OUTPUT_DIR=$PWD/.github/integration-test/evaluate-exists-test
-mkdir "$BASE_OUTPUT_DIR"
+DOCKER_COMPOSE_FILE=.github/integration-test/$1/docker-compose.yml
+export FDE_INPUT_MEASURE=/${PWD}/.github/integration-test/measures/exists-measure.json
+export FDE_OUTPUT_DIR=$PWD/.github/integration-test/evaluate-exists-test
+export FDE_CONVERT_TO_CSV=false
-docker run -v "$INPUT_MEASURE":/app/measure.json -v "$BASE_OUTPUT_DIR":/app/output/ -e FHIR_SERVER=http://fhir-server:8080/fhir \
- -e TZ="$(cat /etc/timezone)" --network integration-test_testing-network fhir-data-evaluator
+mkdir "$FDE_OUTPUT_DIR"
+docker compose -f "$DOCKER_COMPOSE_FILE" run -e TZ="$(cat /etc/timezone)" fhir-data-evaluator
today=$(date +"%Y-%m-%d")
-OUTPUT_DIR=$(find "$BASE_OUTPUT_DIR" -type d -name "*$today*" | head -n 1)
+OUTPUT_DIR=$(find "$FDE_OUTPUT_DIR" -type d -name "*$today*" | head -n 1)
REPORT="$OUTPUT_DIR/measure-report.json"
EXPECTED_POPULATION_COUNT=2
diff --git a/.github/integration-test/evaluate-icd10-to-csv.sh b/.github/integration-test/evaluate-icd10-to-csv.sh
index 197a906..b54506b 100755
--- a/.github/integration-test/evaluate-icd10-to-csv.sh
+++ b/.github/integration-test/evaluate-icd10-to-csv.sh
@@ -1,14 +1,15 @@
#!/bin/bash -e
-INPUT_MEASURE=$1
-BASE_OUTPUT_DIR=$PWD/.github/integration-test/evaluate-icd10-test-to-csv-test
-mkdir "$BASE_OUTPUT_DIR"
+DOCKER_COMPOSE_FILE=.github/integration-test/$1/docker-compose.yml
+export FDE_INPUT_MEASURE=/${PWD}/.github/integration-test/measures/icd10-measure.json
+export FDE_OUTPUT_DIR=$PWD/.github/integration-test/evaluate-icd10-to-csv-test
+export FDE_CONVERT_TO_CSV=true
-docker run -v "$INPUT_MEASURE":/app/measure.json -v "$BASE_OUTPUT_DIR":/app/output/ -e FHIR_SERVER=http://fhir-server:8080/fhir \
- -e CONVERT_TO_CSV=true -e TZ="$(cat /etc/timezone)" --network integration-test_testing-network fhir-data-evaluator
+mkdir "$FDE_OUTPUT_DIR"
+docker compose -f "$DOCKER_COMPOSE_FILE" run -e TZ="$(cat /etc/timezone)" fhir-data-evaluator
today=$(date +"%Y-%m-%d")
-OUTPUT_DIR=$(find "$BASE_OUTPUT_DIR" -type d -name "*$today*" | head -n 1)
+OUTPUT_DIR=$(find "$FDE_OUTPUT_DIR" -type d -name "*$today*" | head -n 1)
#wait for csv file creation
sleep 1
diff --git a/.github/integration-test/evaluate-icd10.sh b/.github/integration-test/evaluate-icd10.sh
index cdbfe65..2500c76 100755
--- a/.github/integration-test/evaluate-icd10.sh
+++ b/.github/integration-test/evaluate-icd10.sh
@@ -1,14 +1,15 @@
#!/bin/bash -e
-INPUT_MEASURE=$1
-BASE_OUTPUT_DIR=$PWD/.github/integration-test/evaluate-icd10-test
-mkdir "$BASE_OUTPUT_DIR"
+DOCKER_COMPOSE_FILE=.github/integration-test/$1/docker-compose.yml
+export FDE_INPUT_MEASURE=/${PWD}/.github/integration-test/measures/icd10-measure.json
+export FDE_OUTPUT_DIR=$PWD/.github/integration-test/evaluate-icd10-test
+export FDE_CONVERT_TO_CSV=false
-docker run -v "$INPUT_MEASURE":/app/measure.json -v "$BASE_OUTPUT_DIR":/app/output/ -e FHIR_SERVER=http://fhir-server:8080/fhir \
- -e TZ="$(cat /etc/timezone)" --network integration-test_testing-network fhir-data-evaluator
+mkdir "$FDE_OUTPUT_DIR"
+docker compose -f "$DOCKER_COMPOSE_FILE" run -e TZ="$(cat /etc/timezone)" fhir-data-evaluator
today=$(date +"%Y-%m-%d")
-OUTPUT_DIR=$(find "$BASE_OUTPUT_DIR" -type d -name "*$today*" | head -n 1)
+OUTPUT_DIR=$(find "$FDE_OUTPUT_DIR" -type d -name "*$today*" | head -n 1)
REPORT=$(cat "$OUTPUT_DIR"/measure-report.json)
EXPECTED_POPULATION_COUNT=2
diff --git a/.github/integration-test/evaluate-icd10WithStatus-to-csv.sh b/.github/integration-test/evaluate-icd10WithStatus-to-csv.sh
index e324beb..69a0833 100755
--- a/.github/integration-test/evaluate-icd10WithStatus-to-csv.sh
+++ b/.github/integration-test/evaluate-icd10WithStatus-to-csv.sh
@@ -1,14 +1,15 @@
#!/bin/bash -e
-INPUT_MEASURE=$1
-BASE_OUTPUT_DIR=$PWD/.github/integration-test/evaluate-icd10WithStatus-to-csv-test
-mkdir "$BASE_OUTPUT_DIR"
+DOCKER_COMPOSE_FILE=.github/integration-test/$1/docker-compose.yml
+export FDE_INPUT_MEASURE=/${PWD}/.github/integration-test/measures/icd10withStatus-measure.json
+export FDE_OUTPUT_DIR=$PWD/.github/integration-test/evaluate-icd10withStatus-to-csv-test
+export FDE_CONVERT_TO_CSV=true
-docker run -v "$INPUT_MEASURE":/app/measure.json -v "$BASE_OUTPUT_DIR":/app/output/ -e FHIR_SERVER=http://fhir-server:8080/fhir \
- -e CONVERT_TO_CSV=true -e TZ="$(cat /etc/timezone)" --network integration-test_testing-network fhir-data-evaluator
+mkdir "$FDE_OUTPUT_DIR"
+docker compose -f "$DOCKER_COMPOSE_FILE" run -e TZ="$(cat /etc/timezone)" fhir-data-evaluator
today=$(date +"%Y-%m-%d")
-OUTPUT_DIR=$(find "$BASE_OUTPUT_DIR" -type d -name "*$today*" | head -n 1)
+OUTPUT_DIR=$(find "$FDE_OUTPUT_DIR" -type d -name "*$today*" | head -n 1)
#wait for csv file creation
sleep 1
diff --git a/.github/integration-test/evaluate-multiple-stratifiers.sh b/.github/integration-test/evaluate-multiple-stratifiers.sh
index 574f237..6b07060 100755
--- a/.github/integration-test/evaluate-multiple-stratifiers.sh
+++ b/.github/integration-test/evaluate-multiple-stratifiers.sh
@@ -1,14 +1,15 @@
#!/bin/bash -e
-INPUT_MEASURE=$1
-BASE_OUTPUT_DIR=$PWD/.github/integration-test/evaluate-multiple-stratifiers
-mkdir "$BASE_OUTPUT_DIR"
+DOCKER_COMPOSE_FILE=.github/integration-test/$1/docker-compose.yml
+export FDE_INPUT_MEASURE=/${PWD}/.github/integration-test/measures/multiple-stratifiers-measure.json
+export FDE_OUTPUT_DIR=$PWD/.github/integration-test/evaluate-multiple-stratifiers-test
+export FDE_CONVERT_TO_CSV=false
-docker run -v "$INPUT_MEASURE":/app/measure.json -v "$BASE_OUTPUT_DIR":/app/output/ -e FHIR_SERVER=http://fhir-server:8080/fhir \
- -e TZ="$(cat /etc/timezone)" --network integration-test_testing-network fhir-data-evaluator
+mkdir "$FDE_OUTPUT_DIR"
+docker compose -f "$DOCKER_COMPOSE_FILE" run -e TZ="$(cat /etc/timezone)" fhir-data-evaluator
today=$(date +"%Y-%m-%d")
-OUTPUT_DIR=$(find "$BASE_OUTPUT_DIR" -type d -name "*$today*" | head -n 1)
+OUTPUT_DIR=$(find "$FDE_OUTPUT_DIR" -type d -name "*$today*" | head -n 1)
REPORT="$OUTPUT_DIR/measure-report.json"
find_stratum() {
diff --git a/.github/integration-test/evaluate-unique-count-to-csv.sh b/.github/integration-test/evaluate-unique-count-to-csv.sh
index 243a060..b443112 100755
--- a/.github/integration-test/evaluate-unique-count-to-csv.sh
+++ b/.github/integration-test/evaluate-unique-count-to-csv.sh
@@ -1,14 +1,15 @@
#!/bin/bash -e
-INPUT_MEASURE=$1
-BASE_OUTPUT_DIR=$PWD/.github/integration-test/evaluate-unique-count-with-components-test
-mkdir "$BASE_OUTPUT_DIR"
+DOCKER_COMPOSE_FILE=.github/integration-test/$1/docker-compose.yml
+export FDE_INPUT_MEASURE=/${PWD}/.github/integration-test/measures/unique-count-measure.json
+export FDE_OUTPUT_DIR=$PWD/.github/integration-test/evaluate-unique-count-to-csv-test
+export FDE_CONVERT_TO_CSV=true
-docker run -v "$INPUT_MEASURE":/app/measure.json -v "$BASE_OUTPUT_DIR":/app/output/ -e FHIR_SERVER=http://fhir-server:8080/fhir \
- -e TZ="$(cat /etc/timezone)" --network integration-test_testing-network -e CONVERT_TO_CSV=true fhir-data-evaluator
+mkdir "$FDE_OUTPUT_DIR"
+docker compose -f "$DOCKER_COMPOSE_FILE" run -e TZ="$(cat /etc/timezone)" fhir-data-evaluator
today=$(date +"%Y-%m-%d")
-OUTPUT_DIR=$(find "$BASE_OUTPUT_DIR" -type d -name "*$today*" | head -n 1)
+OUTPUT_DIR=$(find "$FDE_OUTPUT_DIR" -type d -name "*$today*" | head -n 1)
#wait for csv file creation
sleep 1
diff --git a/.github/integration-test/evaluate-unique-count-with-components-to-csv.sh b/.github/integration-test/evaluate-unique-count-with-components-to-csv.sh
index 17cd514..e6a997f 100755
--- a/.github/integration-test/evaluate-unique-count-with-components-to-csv.sh
+++ b/.github/integration-test/evaluate-unique-count-with-components-to-csv.sh
@@ -1,14 +1,15 @@
#!/bin/bash -e
-INPUT_MEASURE=$1
-BASE_OUTPUT_DIR=$PWD/.github/integration-test/evaluate-unique-count-with-components-to-csv-test
-mkdir "$BASE_OUTPUT_DIR"
+DOCKER_COMPOSE_FILE=.github/integration-test/$1/docker-compose.yml
+export FDE_INPUT_MEASURE=/${PWD}/.github/integration-test/measures/unique-count-with-components-measure.json
+export FDE_OUTPUT_DIR=$PWD/.github/integration-test/evaluate-unique-count-with-components-to-csv-test
+export FDE_CONVERT_TO_CSV=true
-docker run -v "$INPUT_MEASURE":/app/measure.json -v "$BASE_OUTPUT_DIR":/app/output/ -e FHIR_SERVER=http://fhir-server:8080/fhir \
- -e TZ="$(cat /etc/timezone)" --network integration-test_testing-network -e CONVERT_TO_CSV=true fhir-data-evaluator
+mkdir "$FDE_OUTPUT_DIR"
+docker compose -f "$DOCKER_COMPOSE_FILE" run -e TZ="$(cat /etc/timezone)" fhir-data-evaluator
today=$(date +"%Y-%m-%d")
-OUTPUT_DIR=$(find "$BASE_OUTPUT_DIR" -type d -name "*$today*" | head -n 1)
+OUTPUT_DIR=$(find "$FDE_OUTPUT_DIR" -type d -name "*$today*" | head -n 1)
#wait for csv file creation
sleep 1
diff --git a/.github/integration-test/evaluate-unique-count.sh b/.github/integration-test/evaluate-unique-count.sh
index 003bcd4..573c401 100755
--- a/.github/integration-test/evaluate-unique-count.sh
+++ b/.github/integration-test/evaluate-unique-count.sh
@@ -1,14 +1,15 @@
#!/bin/bash -e
-INPUT_MEASURE=$1
-BASE_OUTPUT_DIR=$PWD/.github/integration-test/evaluate-unique-count-test
-mkdir "$BASE_OUTPUT_DIR"
+DOCKER_COMPOSE_FILE=.github/integration-test/$1/docker-compose.yml
+export FDE_INPUT_MEASURE=/${PWD}/.github/integration-test/measures/unique-count-measure.json
+export FDE_OUTPUT_DIR=$PWD/.github/integration-test/evaluate-unique-count-test
+export FDE_CONVERT_TO_CSV=false
-docker run -v "$INPUT_MEASURE":/app/measure.json -v "$BASE_OUTPUT_DIR":/app/output/ -e FHIR_SERVER=http://fhir-server:8080/fhir \
- -e TZ="$(cat /etc/timezone)" --network integration-test_testing-network fhir-data-evaluator
+mkdir "$FDE_OUTPUT_DIR"
+docker compose -f "$DOCKER_COMPOSE_FILE" run -e TZ="$(cat /etc/timezone)" fhir-data-evaluator
today=$(date +"%Y-%m-%d")
-OUTPUT_DIR=$(find "$BASE_OUTPUT_DIR" -type d -name "*$today*" | head -n 1)
+OUTPUT_DIR=$(find "$FDE_OUTPUT_DIR" -type d -name "*$today*" | head -n 1)
REPORT="$OUTPUT_DIR/measure-report.json"
EXPECTED_INITIAL_POPULATION_COUNT=2
diff --git a/.github/integration-test/measures/multiple-stratifiers.json b/.github/integration-test/measures/multiple-stratifiers-measure.json
similarity index 100%
rename from .github/integration-test/measures/multiple-stratifiers.json
rename to .github/integration-test/measures/multiple-stratifiers-measure.json
diff --git a/.github/integration-test/missing-permissions-test.sh b/.github/integration-test/missing-permissions-test.sh
index 4832335..50d80f9 100755
--- a/.github/integration-test/missing-permissions-test.sh
+++ b/.github/integration-test/missing-permissions-test.sh
@@ -6,8 +6,7 @@ mkdir "$BASE_OUTPUT_DIR"
# Allow docker to exit with an error
set +e
OUTPUT=$(docker run -v "$PWD"/.github/integration-test/measures/code-measure.json:/app/measure.json \
- -v "$BASE_OUTPUT_DIR":/app/output:ro --network integration-test_testing-network -e FHIR_SERVER=http://fhir-server:8080/fhir \
- fhir-data-evaluator 2>&1)
+ -v "$BASE_OUTPUT_DIR":/app/output:ro -e FHIR_SERVER=http://fhir-server:8080/fhir fhir-data-evaluator 2>&1)
EXIT_CODE=$?
set -e
diff --git a/.github/integration-test/no-auth/docker-compose.yml b/.github/integration-test/no-auth/docker-compose.yml
new file mode 100644
index 0000000..f82851e
--- /dev/null
+++ b/.github/integration-test/no-auth/docker-compose.yml
@@ -0,0 +1,29 @@
+services:
+ fhir-server:
+ image: "samply/blaze:0.30"
+ environment:
+ BASE_URL: "http://fhir-server:8080"
+ JAVA_TOOL_OPTIONS: "-Xmx1g"
+ LOG_LEVEL: "debug"
+ ENFORCE_REFERENTIAL_INTEGRITY: false
+ ports:
+ - "8082:8080"
+ volumes:
+ - "data-store-data:/app/data"
+ fhir-data-evaluator:
+ image: fhir-data-evaluator
+ environment:
+ CONVERT_TO_CSV: ${FDE_CONVERT_TO_CSV:-true}
+ FHIR_SERVER: "http://fhir-server:8080/fhir"
+ FHIR_REPORT_DESTINATION_SERVER: ${FDE_FHIR_REPORT_DESTINATION_SERVER:-http://localhost:8080/fhir}
+ SEND_REPORT_TO_SERVER: ${FDE_SEND_REPORT_TO_SERVER:-false}
+ AUTHOR_IDENTIFIER_SYSTEM: ${FDE_AUTHOR_IDENTIFIER_SYSTEM:-}
+ AUTHOR_IDENTIFIER_VALUE: ${FDE_AUTHOR_IDENTIFIER_VALUE:-}
+ PROJECT_IDENTIFIER_SYSTEM: ${FDE_PROJECT_IDENTIFIER_SYSTEM:-}
+ PROJECT_IDENTIFIER_VALUE: ${FDE_PROJECT_IDENTIFIER_VALUE:-}
+ volumes:
+ - "${FDE_INPUT_MEASURE:-../Documentation/example-measures/example-measure-kds.json}:/app/measure.json"
+ - "${FDE_OUTPUT_DIR:-../output}:/app/output"
+
+volumes:
+ data-store-data:
diff --git a/.github/integration-test/no-auth/load-data.sh b/.github/integration-test/no-auth/load-data.sh
new file mode 100755
index 0000000..b60ebc0
--- /dev/null
+++ b/.github/integration-test/no-auth/load-data.sh
@@ -0,0 +1,5 @@
+#!/bin/bash -e
+
+DIR="$1"
+
+blazectl --no-progress --server http://localhost:8082/fhir upload "$DIR"
diff --git a/.github/integration-test/oauth/docker-compose.yml b/.github/integration-test/oauth/docker-compose.yml
new file mode 100644
index 0000000..7fd8605
--- /dev/null
+++ b/.github/integration-test/oauth/docker-compose.yml
@@ -0,0 +1,124 @@
+services:
+ generate-cert:
+ image: alpine/openssl
+ networks:
+ test-oauth:
+ entrypoint: ["sh", "-c"]
+ command:
+ - openssl req -nodes -subj "/CN=proxy"
+ -addext "basicConstraints=CA:false"
+ -addext "subjectAltName = DNS:secure-fhir-server, DNS:secure-keycloak"
+ -x509 -newkey rsa:4096 -days 99999
+ -keyout /keys/key.pem -out /certs/cert.pem
+ volumes:
+ - "certs:/certs"
+ - "keys:/keys"
+ generate-trust-store:
+ image: eclipse-temurin:21
+ networks:
+ test-oauth:
+ entrypoint: ["bash", "-c"]
+ command:
+ - rm -rf "/trusts/trust-store.p12";
+ keytool -importcert -storetype PKCS12 -keystore "/trusts/trust-store.p12"
+ -storepass "insecure" -alias ca -file "/certs/cert.pem" -noprompt
+ volumes:
+ - "certs:/certs"
+ - "trusts:/trusts"
+ depends_on:
+ generate-cert:
+ condition: service_completed_successfully
+ keycloak:
+ image: "keycloak/keycloak:24.0.5"
+ command: ["start", "--import-realm"]
+ healthcheck:
+ test: ["CMD-SHELL", "exec 3<>/dev/tcp/127.0.0.1/8080;echo -e \"GET /health/ready HTTP/1.1\r\nhost: localhost\r\nConnection: close\r\n\r\n\" >&3;grep \"HTTP/1.1 200 OK\" <&3"]
+ interval: "5s"
+ timeout: "5s"
+ retries: "3"
+ start_period: "30s"
+ networks:
+ test-oauth:
+ environment:
+ KC_HOSTNAME_URL: "https://secure-keycloak:8443"
+ KC_HOSTNAME_ADMIN_URL: "https://secure-keycloak:8443"
+ KC_HTTP_RELATIVE_PATH: "/"
+ KC_PROXY_HEADERS: "xforwarded"
+ KC_HTTP_ENABLED: "true"
+ KC_HEALTH_ENABLED: "true"
+ KC_LOG_LEVEL: "info"
+ volumes:
+ - "./realm-test.json:/opt/keycloak/data/import/realm-test.json"
+ proxy:
+ image: "nginx:1.27.0"
+ healthcheck:
+ test: ["CMD-SHELL", "curl --fail -s http://localhost:8080"]
+ interval: "5s"
+ timeout: "5s"
+ retries: "3"
+ start_period: "5s"
+ networks:
+ test-oauth:
+ aliases:
+ - secure-fhir-server
+ - secure-keycloak
+ volumes:
+ - "./nginx.conf:/etc/nginx/nginx.conf"
+ - "certs:/etc/nginx/certs"
+ - "keys:/etc/nginx/keys"
+ depends_on:
+ generate-cert:
+ condition: service_completed_successfully
+ keycloak:
+ condition: service_healthy
+ fhir-server:
+ image: "samply/blaze:0.30"
+ healthcheck:
+ test: ["CMD-SHELL", "curl --fail -s http://localhost:8080/health"]
+ interval: "5s"
+ timeout: "5s"
+ retries: "3"
+ start_period: "60s"
+ networks:
+ test-oauth:
+ environment:
+ BASE_URL: "https://fhir-server:8080"
+ JAVA_TOOL_OPTIONS: "-Xmx1g"
+ OPENID_PROVIDER_URL: "https://secure-keycloak:8443/realms/test"
+ OPENID_CLIENT_TRUST_STORE: "/trusts/trust-store.p12"
+ OPENID_CLIENT_TRUST_STORE_PASS: "insecure"
+ LOG_LEVEL: "debug"
+ ENFORCE_REFERENTIAL_INTEGRITY: false
+ ports:
+ - "8082:8080"
+ volumes:
+ - "data-store-data:/app/data"
+ - "trusts:/trusts"
+ depends_on:
+ generate-trust-store:
+ condition: service_completed_successfully
+ keycloak:
+ condition: service_healthy
+ proxy:
+ condition: service_healthy
+ fhir-data-evaluator:
+ image: fhir-data-evaluator
+ networks:
+ test-oauth:
+ environment:
+ CONVERT_TO_CSV: ${FDE_CONVERT_TO_CSV:-true}
+ FHIR_SERVER: "https://secure-fhir-server:8443/fhir"
+ FHIR_OAUTH_ISSUER_URI: "https://secure-keycloak:8443/realms/test"
+ FHIR_OAUTH_CLIENT_ID: "account"
+ FHIR_OAUTH_CLIENT_SECRET: "test"
+ volumes:
+ - "${FDE_INPUT_MEASURE:-../Documentation/example-measures/example-measure-kds.json}:/app/measure.json"
+ - "${FDE_OUTPUT_DIR:-../output}:/app/output"
+ - "certs:/app/certs"
+volumes:
+ data-store-data:
+ certs:
+ keys:
+ trusts:
+networks:
+ test-oauth:
diff --git a/.github/integration-test/oauth/load-data.sh b/.github/integration-test/oauth/load-data.sh
new file mode 100755
index 0000000..4d35e31
--- /dev/null
+++ b/.github/integration-test/oauth/load-data.sh
@@ -0,0 +1,7 @@
+#!/bin/bash -e
+
+DIR="$1"
+
+TOKEN="$(docker compose -f "$DIR/../oauth/docker-compose.yml" exec -it proxy curl -s --cacert /etc/nginx/certs/cert.pem -d 'client_id=account' -d 'client_secret=test' -d 'grant_type=client_credentials' 'https://secure-keycloak:8443/realms/test/protocol/openid-connect/token' | jq -r '.access_token')"
+
+blazectl --no-progress --token "$TOKEN" --server http://localhost:8082/fhir upload "$DIR"
diff --git a/.github/integration-test/oauth/nginx.conf b/.github/integration-test/oauth/nginx.conf
new file mode 100644
index 0000000..37eac34
--- /dev/null
+++ b/.github/integration-test/oauth/nginx.conf
@@ -0,0 +1,82 @@
+user nginx;
+worker_processes 1;
+
+error_log /dev/stdout debug;
+pid /var/run/nginx.pid;
+
+events {
+ worker_connections 1024;
+}
+
+http {
+ include /etc/nginx/mime.types;
+ log_format main '$remote_addr - $remote_user [$time_local] "$request" '
+ '$status $body_bytes_sent "$http_referer" '
+ '"$http_user_agent" "$http_x_forwarded_for"';
+ access_log /dev/stdout main;
+ sendfile on;
+ keepalive_timeout 65;
+
+ # SSL-Certificate and private key
+ ssl_certificate /etc/nginx/certs/cert.pem;
+ ssl_certificate_key /etc/nginx/keys/key.pem;
+
+ # The supported SSL Protocols
+ ssl_protocols TLSv1.2 TLSv1.3;
+
+ # DNS resolver needed for Docker
+ resolver 127.0.0.11 valid=10s;
+
+ # NGINX can impose its TLS cipher suite choices over those of a connecting browser, provided the browser supports them.
+ ssl_prefer_server_ciphers on;
+
+ # The supported SSL Ciphers
+ ssl_ciphers 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-ECDSA-RC4-SHA:ECDHE-RSA-RC4-SHA:ECDH-ECDSA-RC4-SHA:ECDH-RSA-RC4-SHA:RC4-SHA';
+
+ ssl_session_cache builtin:1000 shared:SSL:10m;
+
+ server {
+ listen 8080;
+ listen [::]:8080;
+ server_name localhost;
+
+ location / {
+ root /usr/share/nginx/html;
+ index index.html;
+ }
+ }
+
+ server {
+ listen 8443 ssl;
+ listen [::]:8443 ssl;
+ http2 on;
+ server_name secure-fhir-server;
+
+ location / {
+ set $upstream fhir-server:8080;
+ proxy_pass http://$upstream;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ proxy_set_header X-Forwarded-Host $http_host;
+ proxy_set_header X-Forwarded-Port $server_port;
+ proxy_read_timeout 43200s;
+ client_max_body_size 100M;
+ }
+ }
+
+ server {
+ listen 8443 ssl;
+ listen [::]:8443 ssl;
+ server_name secure-keycloak;
+
+ location / {
+ set $upstream keycloak:8080;
+ proxy_pass http://$upstream;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ proxy_set_header X-Forwarded-Host $http_host;
+ proxy_set_header X-Forwarded-Port $server_port;
+ proxy_read_timeout 43200s;
+ }
+ }
+}
diff --git a/.github/integration-test/oauth/realm-test.json b/.github/integration-test/oauth/realm-test.json
new file mode 100644
index 0000000..7331910
--- /dev/null
+++ b/.github/integration-test/oauth/realm-test.json
@@ -0,0 +1,2056 @@
+{
+ "id" : "test",
+ "realm" : "test",
+ "displayName" : "Keycloak",
+ "displayNameHtml" : "
Keycloak
",
+ "notBefore" : 0,
+ "defaultSignatureAlgorithm" : "RS256",
+ "revokeRefreshToken" : false,
+ "refreshTokenMaxReuse" : 0,
+ "accessTokenLifespan" : 3600,
+ "accessTokenLifespanForImplicitFlow" : 900,
+ "ssoSessionIdleTimeout" : 1800,
+ "ssoSessionMaxLifespan" : 36000,
+ "ssoSessionIdleTimeoutRememberMe" : 0,
+ "ssoSessionMaxLifespanRememberMe" : 0,
+ "offlineSessionIdleTimeout" : 2592000,
+ "offlineSessionMaxLifespanEnabled" : false,
+ "offlineSessionMaxLifespan" : 5184000,
+ "clientSessionIdleTimeout" : 0,
+ "clientSessionMaxLifespan" : 0,
+ "clientOfflineSessionIdleTimeout" : 0,
+ "clientOfflineSessionMaxLifespan" : 0,
+ "accessCodeLifespan" : 60,
+ "accessCodeLifespanUserAction" : 300,
+ "accessCodeLifespanLogin" : 1800,
+ "actionTokenGeneratedByAdminLifespan" : 43200,
+ "actionTokenGeneratedByUserLifespan" : 300,
+ "oauth2DeviceCodeLifespan" : 600,
+ "oauth2DevicePollingInterval" : 5,
+ "enabled" : true,
+ "sslRequired" : "external",
+ "registrationAllowed" : false,
+ "registrationEmailAsUsername" : false,
+ "rememberMe" : false,
+ "verifyEmail" : false,
+ "loginWithEmailAllowed" : true,
+ "duplicateEmailsAllowed" : false,
+ "resetPasswordAllowed" : false,
+ "editUsernameAllowed" : false,
+ "bruteForceProtected" : false,
+ "permanentLockout" : false,
+ "maxTemporaryLockouts" : 0,
+ "maxFailureWaitSeconds" : 900,
+ "minimumQuickLoginWaitSeconds" : 60,
+ "waitIncrementSeconds" : 60,
+ "quickLoginCheckMilliSeconds" : 1000,
+ "maxDeltaTimeSeconds" : 43200,
+ "failureFactor" : 30,
+ "roles" : {
+ "realm" : [ {
+ "id" : "cfab484f-be62-43ac-ac58-4a3ca0b76895",
+ "name" : "offline_access",
+ "description" : "${role_offline-access}",
+ "composite" : false,
+ "clientRole" : false,
+ "containerId" : "test",
+ "attributes" : { }
+ }, {
+ "id" : "a405bd09-e663-4e3c-9d77-a9d965d1250a",
+ "name" : "uma_authorization",
+ "description" : "${role_uma_authorization}",
+ "composite" : false,
+ "clientRole" : false,
+ "containerId" : "test",
+ "attributes" : { }
+ }, {
+ "id" : "76043e61-4a56-4b14-b70a-54f411d73f70",
+ "name" : "default-roles-test",
+ "description" : "${role_default-roles}",
+ "composite" : true,
+ "composites" : {
+ "realm" : [ "offline_access", "uma_authorization" ],
+ "client" : {
+ "account" : [ "view-profile", "manage-account" ]
+ }
+ },
+ "clientRole" : false,
+ "containerId" : "test",
+ "attributes" : { }
+ }, {
+ "id" : "ddf3601e-7689-4e9a-8dce-69a964a14d8c",
+ "name" : "admin",
+ "description" : "${role_admin}",
+ "composite" : true,
+ "composites" : {
+ "realm" : [ "create-realm" ],
+ "client" : {
+ "test-realm" : [ "view-users", "manage-events", "view-realm", "view-clients", "view-events", "query-realms", "query-users", "impersonation", "view-authorization", "manage-clients", "manage-identity-providers", "query-groups", "query-clients", "create-client", "manage-authorization", "view-identity-providers", "manage-users", "manage-realm" ]
+ }
+ },
+ "clientRole" : false,
+ "containerId" : "test",
+ "attributes" : { }
+ }, {
+ "id" : "319558ae-0ae5-4110-b688-5f30f94f652e",
+ "name" : "create-realm",
+ "description" : "${role_create-realm}",
+ "composite" : false,
+ "clientRole" : false,
+ "containerId" : "test",
+ "attributes" : { }
+ } ],
+ "client" : {
+ "realm-management" : [ {
+ "id" : "62e4743e-a905-42a8-96d6-a8d5fdd844ea",
+ "name" : "manage-events",
+ "description" : "${role_manage-events}",
+ "composite" : false,
+ "clientRole" : true,
+ "containerId" : "b9e2f2c2-46ba-4d57-9f93-f336ad52a3bf",
+ "attributes" : { }
+ }, {
+ "id" : "6c73cf21-93e7-453d-8ba2-b3c7154c4367",
+ "name" : "view-identity-providers",
+ "description" : "${role_view-identity-providers}",
+ "composite" : false,
+ "clientRole" : true,
+ "containerId" : "b9e2f2c2-46ba-4d57-9f93-f336ad52a3bf",
+ "attributes" : { }
+ }, {
+ "id" : "d232c025-2d26-40ee-8196-cf52f171097b",
+ "name" : "manage-identity-providers",
+ "description" : "${role_manage-identity-providers}",
+ "composite" : false,
+ "clientRole" : true,
+ "containerId" : "b9e2f2c2-46ba-4d57-9f93-f336ad52a3bf",
+ "attributes" : { }
+ }, {
+ "id" : "6d7f5457-8f9a-4ffa-825a-fde1a755e09e",
+ "name" : "query-realms",
+ "description" : "${role_query-realms}",
+ "composite" : false,
+ "clientRole" : true,
+ "containerId" : "b9e2f2c2-46ba-4d57-9f93-f336ad52a3bf",
+ "attributes" : { }
+ }, {
+ "id" : "e6841db6-8b7e-4884-b2b4-65d950eaf8ac",
+ "name" : "realm-admin",
+ "description" : "${role_realm-admin}",
+ "composite" : true,
+ "composites" : {
+ "client" : {
+ "realm-management" : [ "manage-events", "view-identity-providers", "manage-identity-providers", "query-realms", "view-users", "manage-authorization", "view-clients", "view-realm", "create-client", "query-groups", "query-clients", "view-events", "manage-realm", "query-users", "view-authorization", "manage-clients", "manage-users", "impersonation" ]
+ }
+ },
+ "clientRole" : true,
+ "containerId" : "b9e2f2c2-46ba-4d57-9f93-f336ad52a3bf",
+ "attributes" : { }
+ }, {
+ "id" : "b4a8f2f3-eed6-4957-b2c0-3f0eda65ccac",
+ "name" : "view-users",
+ "description" : "${role_view-users}",
+ "composite" : true,
+ "composites" : {
+ "client" : {
+ "realm-management" : [ "query-groups", "query-users" ]
+ }
+ },
+ "clientRole" : true,
+ "containerId" : "b9e2f2c2-46ba-4d57-9f93-f336ad52a3bf",
+ "attributes" : { }
+ }, {
+ "id" : "cf664a6f-60d6-4603-8242-f7c8aa7aabb4",
+ "name" : "manage-authorization",
+ "description" : "${role_manage-authorization}",
+ "composite" : false,
+ "clientRole" : true,
+ "containerId" : "b9e2f2c2-46ba-4d57-9f93-f336ad52a3bf",
+ "attributes" : { }
+ }, {
+ "id" : "4912268a-99a5-4d06-bac1-49153e9e6330",
+ "name" : "view-clients",
+ "description" : "${role_view-clients}",
+ "composite" : true,
+ "composites" : {
+ "client" : {
+ "realm-management" : [ "query-clients" ]
+ }
+ },
+ "clientRole" : true,
+ "containerId" : "b9e2f2c2-46ba-4d57-9f93-f336ad52a3bf",
+ "attributes" : { }
+ }, {
+ "id" : "0660daed-6bb8-432c-8d65-4b7ccf3938e7",
+ "name" : "view-realm",
+ "description" : "${role_view-realm}",
+ "composite" : false,
+ "clientRole" : true,
+ "containerId" : "b9e2f2c2-46ba-4d57-9f93-f336ad52a3bf",
+ "attributes" : { }
+ }, {
+ "id" : "22510f81-7b5d-4ab4-b31f-a168a6d04f1d",
+ "name" : "create-client",
+ "description" : "${role_create-client}",
+ "composite" : false,
+ "clientRole" : true,
+ "containerId" : "b9e2f2c2-46ba-4d57-9f93-f336ad52a3bf",
+ "attributes" : { }
+ }, {
+ "id" : "9d4ba117-e4ac-4dda-80cd-5753f1443247",
+ "name" : "query-groups",
+ "description" : "${role_query-groups}",
+ "composite" : false,
+ "clientRole" : true,
+ "containerId" : "b9e2f2c2-46ba-4d57-9f93-f336ad52a3bf",
+ "attributes" : { }
+ }, {
+ "id" : "0ac7902c-0cfe-4ece-b694-a2e56b1a436a",
+ "name" : "query-clients",
+ "description" : "${role_query-clients}",
+ "composite" : false,
+ "clientRole" : true,
+ "containerId" : "b9e2f2c2-46ba-4d57-9f93-f336ad52a3bf",
+ "attributes" : { }
+ }, {
+ "id" : "2f91a5d6-4d8b-4e86-bc2b-98dafa5897a4",
+ "name" : "view-events",
+ "description" : "${role_view-events}",
+ "composite" : false,
+ "clientRole" : true,
+ "containerId" : "b9e2f2c2-46ba-4d57-9f93-f336ad52a3bf",
+ "attributes" : { }
+ }, {
+ "id" : "518da31b-1771-4fa0-ac0c-5bd9420776e9",
+ "name" : "manage-realm",
+ "description" : "${role_manage-realm}",
+ "composite" : false,
+ "clientRole" : true,
+ "containerId" : "b9e2f2c2-46ba-4d57-9f93-f336ad52a3bf",
+ "attributes" : { }
+ }, {
+ "id" : "bb608a9a-af7e-49d5-9306-c4925525129d",
+ "name" : "query-users",
+ "description" : "${role_query-users}",
+ "composite" : false,
+ "clientRole" : true,
+ "containerId" : "b9e2f2c2-46ba-4d57-9f93-f336ad52a3bf",
+ "attributes" : { }
+ }, {
+ "id" : "89e04b7d-dda2-47a5-8610-951601c52048",
+ "name" : "manage-clients",
+ "description" : "${role_manage-clients}",
+ "composite" : false,
+ "clientRole" : true,
+ "containerId" : "b9e2f2c2-46ba-4d57-9f93-f336ad52a3bf",
+ "attributes" : { }
+ }, {
+ "id" : "1e319aff-9893-482d-945d-d428ee73c014",
+ "name" : "view-authorization",
+ "description" : "${role_view-authorization}",
+ "composite" : false,
+ "clientRole" : true,
+ "containerId" : "b9e2f2c2-46ba-4d57-9f93-f336ad52a3bf",
+ "attributes" : { }
+ }, {
+ "id" : "9f68e090-fc30-43e9-8a6a-2e0f08c8b6a2",
+ "name" : "impersonation",
+ "description" : "${role_impersonation}",
+ "composite" : false,
+ "clientRole" : true,
+ "containerId" : "b9e2f2c2-46ba-4d57-9f93-f336ad52a3bf",
+ "attributes" : { }
+ }, {
+ "id" : "6b633c77-15e4-4788-8920-9b3ea1f803a0",
+ "name" : "manage-users",
+ "description" : "${role_manage-users}",
+ "composite" : false,
+ "clientRole" : true,
+ "containerId" : "b9e2f2c2-46ba-4d57-9f93-f336ad52a3bf",
+ "attributes" : { }
+ } ],
+ "security-admin-console" : [ ],
+ "admin-cli" : [ ],
+ "account-console" : [ ],
+ "broker" : [ {
+ "id" : "77f7ce3c-d305-45ff-810a-06fffe9285dd",
+ "name" : "read-token",
+ "description" : "${role_read-token}",
+ "composite" : false,
+ "clientRole" : true,
+ "containerId" : "738bfb28-835b-4707-b3fa-e8d620c4a2ad",
+ "attributes" : { }
+ } ],
+ "account" : [ {
+ "id" : "4cb842f2-27bd-4368-b99c-f505aa8b3247",
+ "name" : "manage-account-links",
+ "description" : "${role_manage-account-links}",
+ "composite" : false,
+ "clientRole" : true,
+ "containerId" : "a0f23909-cbba-4950-95c5-1a166b4b3c54",
+ "attributes" : { }
+ }, {
+ "id" : "6f3de551-2a00-4a4f-a304-0b8b5ddb7bcb",
+ "name" : "view-profile",
+ "description" : "${role_view-profile}",
+ "composite" : false,
+ "clientRole" : true,
+ "containerId" : "a0f23909-cbba-4950-95c5-1a166b4b3c54",
+ "attributes" : { }
+ }, {
+ "id" : "47a5291d-d89e-4c93-9a56-3b33b5944ace",
+ "name" : "view-consent",
+ "description" : "${role_view-consent}",
+ "composite" : false,
+ "clientRole" : true,
+ "containerId" : "a0f23909-cbba-4950-95c5-1a166b4b3c54",
+ "attributes" : { }
+ }, {
+ "id" : "92c23878-9e1d-4bd2-a73b-01d35c3a4a57",
+ "name" : "view-groups",
+ "description" : "${role_view-groups}",
+ "composite" : false,
+ "clientRole" : true,
+ "containerId" : "a0f23909-cbba-4950-95c5-1a166b4b3c54",
+ "attributes" : { }
+ }, {
+ "id" : "7ae6416a-3d1c-4fa5-8f6c-199672eb696b",
+ "name" : "manage-consent",
+ "description" : "${role_manage-consent}",
+ "composite" : true,
+ "composites" : {
+ "client" : {
+ "account" : [ "view-consent" ]
+ }
+ },
+ "clientRole" : true,
+ "containerId" : "a0f23909-cbba-4950-95c5-1a166b4b3c54",
+ "attributes" : { }
+ }, {
+ "id" : "4ac84e04-1a77-4121-bb6e-18ed6948ad93",
+ "name" : "delete-account",
+ "description" : "${role_delete-account}",
+ "composite" : false,
+ "clientRole" : true,
+ "containerId" : "a0f23909-cbba-4950-95c5-1a166b4b3c54",
+ "attributes" : { }
+ }, {
+ "id" : "54be8eec-70da-44f4-95a0-b4eb62800c8a",
+ "name" : "view-applications",
+ "description" : "${role_view-applications}",
+ "composite" : false,
+ "clientRole" : true,
+ "containerId" : "a0f23909-cbba-4950-95c5-1a166b4b3c54",
+ "attributes" : { }
+ }, {
+ "id" : "8de14797-ac52-4001-8bad-ac66f326485e",
+ "name" : "manage-account",
+ "description" : "${role_manage-account}",
+ "composite" : true,
+ "composites" : {
+ "client" : {
+ "account" : [ "manage-account-links" ]
+ }
+ },
+ "clientRole" : true,
+ "containerId" : "a0f23909-cbba-4950-95c5-1a166b4b3c54",
+ "attributes" : { }
+ } ],
+ "test-realm" : [ {
+ "id" : "154e52c7-8957-475e-9e32-193daf180c5a",
+ "name" : "view-users",
+ "description" : "${role_view-users}",
+ "composite" : true,
+ "composites" : {
+ "client" : {
+ "test-realm" : [ "query-groups", "query-users" ]
+ }
+ },
+ "clientRole" : true,
+ "containerId" : "ed0e9e16-d955-44d4-ab4c-0c6e8480bf12",
+ "attributes" : { }
+ }, {
+ "id" : "51de37c7-d9e9-44dc-8264-a5c34731101b",
+ "name" : "manage-events",
+ "description" : "${role_manage-events}",
+ "composite" : false,
+ "clientRole" : true,
+ "containerId" : "ed0e9e16-d955-44d4-ab4c-0c6e8480bf12",
+ "attributes" : { }
+ }, {
+ "id" : "c6952b26-9c30-4ae4-96ad-bd8fc8803cde",
+ "name" : "view-clients",
+ "description" : "${role_view-clients}",
+ "composite" : true,
+ "composites" : {
+ "client" : {
+ "test-realm" : [ "query-clients" ]
+ }
+ },
+ "clientRole" : true,
+ "containerId" : "ed0e9e16-d955-44d4-ab4c-0c6e8480bf12",
+ "attributes" : { }
+ }, {
+ "id" : "11eef5a3-1b86-4f4b-81d2-73c21dee786a",
+ "name" : "view-realm",
+ "description" : "${role_view-realm}",
+ "composite" : false,
+ "clientRole" : true,
+ "containerId" : "ed0e9e16-d955-44d4-ab4c-0c6e8480bf12",
+ "attributes" : { }
+ }, {
+ "id" : "c3a9ac36-2296-4c3c-9cad-4b2d3b2a8c92",
+ "name" : "view-events",
+ "description" : "${role_view-events}",
+ "composite" : false,
+ "clientRole" : true,
+ "containerId" : "ed0e9e16-d955-44d4-ab4c-0c6e8480bf12",
+ "attributes" : { }
+ }, {
+ "id" : "e8d45b32-ee65-4d35-bb29-f49022dde86c",
+ "name" : "query-realms",
+ "description" : "${role_query-realms}",
+ "composite" : false,
+ "clientRole" : true,
+ "containerId" : "ed0e9e16-d955-44d4-ab4c-0c6e8480bf12",
+ "attributes" : { }
+ }, {
+ "id" : "13a73a25-f362-4ff5-a8d2-97f1f451d8c7",
+ "name" : "query-users",
+ "description" : "${role_query-users}",
+ "composite" : false,
+ "clientRole" : true,
+ "containerId" : "ed0e9e16-d955-44d4-ab4c-0c6e8480bf12",
+ "attributes" : { }
+ }, {
+ "id" : "25a98a91-87b0-42dc-9e25-18a0fb9a4f63",
+ "name" : "impersonation",
+ "description" : "${role_impersonation}",
+ "composite" : false,
+ "clientRole" : true,
+ "containerId" : "ed0e9e16-d955-44d4-ab4c-0c6e8480bf12",
+ "attributes" : { }
+ }, {
+ "id" : "7c7d7e5b-a9a9-4b5e-a2b9-0efbfc205da0",
+ "name" : "view-authorization",
+ "description" : "${role_view-authorization}",
+ "composite" : false,
+ "clientRole" : true,
+ "containerId" : "ed0e9e16-d955-44d4-ab4c-0c6e8480bf12",
+ "attributes" : { }
+ }, {
+ "id" : "d10d7200-2fa0-4ff9-81d2-0e6f1d1bcc19",
+ "name" : "manage-clients",
+ "description" : "${role_manage-clients}",
+ "composite" : false,
+ "clientRole" : true,
+ "containerId" : "ed0e9e16-d955-44d4-ab4c-0c6e8480bf12",
+ "attributes" : { }
+ }, {
+ "id" : "ca756c34-6506-4461-a5ec-6ffabf008074",
+ "name" : "manage-identity-providers",
+ "description" : "${role_manage-identity-providers}",
+ "composite" : false,
+ "clientRole" : true,
+ "containerId" : "ed0e9e16-d955-44d4-ab4c-0c6e8480bf12",
+ "attributes" : { }
+ }, {
+ "id" : "701964cd-ea25-4df7-87fe-090de21d2495",
+ "name" : "query-clients",
+ "description" : "${role_query-clients}",
+ "composite" : false,
+ "clientRole" : true,
+ "containerId" : "ed0e9e16-d955-44d4-ab4c-0c6e8480bf12",
+ "attributes" : { }
+ }, {
+ "id" : "4526b5eb-206a-4843-a7c1-8cb59745c042",
+ "name" : "query-groups",
+ "description" : "${role_query-groups}",
+ "composite" : false,
+ "clientRole" : true,
+ "containerId" : "ed0e9e16-d955-44d4-ab4c-0c6e8480bf12",
+ "attributes" : { }
+ }, {
+ "id" : "b70afed2-5c61-400e-867b-036bcdec58e3",
+ "name" : "create-client",
+ "description" : "${role_create-client}",
+ "composite" : false,
+ "clientRole" : true,
+ "containerId" : "ed0e9e16-d955-44d4-ab4c-0c6e8480bf12",
+ "attributes" : { }
+ }, {
+ "id" : "06557d0f-4267-429d-96d8-92ccdaea9c22",
+ "name" : "manage-authorization",
+ "description" : "${role_manage-authorization}",
+ "composite" : false,
+ "clientRole" : true,
+ "containerId" : "ed0e9e16-d955-44d4-ab4c-0c6e8480bf12",
+ "attributes" : { }
+ }, {
+ "id" : "3822acc8-a18d-41f7-ba08-f6fe287cd1d7",
+ "name" : "view-identity-providers",
+ "description" : "${role_view-identity-providers}",
+ "composite" : false,
+ "clientRole" : true,
+ "containerId" : "ed0e9e16-d955-44d4-ab4c-0c6e8480bf12",
+ "attributes" : { }
+ }, {
+ "id" : "c6b461b5-a5c6-41f5-ab8f-742bfcf11cdd",
+ "name" : "manage-users",
+ "description" : "${role_manage-users}",
+ "composite" : false,
+ "clientRole" : true,
+ "containerId" : "ed0e9e16-d955-44d4-ab4c-0c6e8480bf12",
+ "attributes" : { }
+ }, {
+ "id" : "8b022167-a28f-4642-8e9b-b11fdf8e9b9c",
+ "name" : "manage-realm",
+ "description" : "${role_manage-realm}",
+ "composite" : false,
+ "clientRole" : true,
+ "containerId" : "ed0e9e16-d955-44d4-ab4c-0c6e8480bf12",
+ "attributes" : { }
+ } ]
+ }
+ },
+ "groups" : [ ],
+ "defaultRole" : {
+ "id" : "76043e61-4a56-4b14-b70a-54f411d73f70",
+ "name" : "default-roles-test",
+ "description" : "${role_default-roles}",
+ "composite" : true,
+ "clientRole" : false,
+ "containerId" : "test"
+ },
+ "requiredCredentials" : [ "password" ],
+ "otpPolicyType" : "totp",
+ "otpPolicyAlgorithm" : "HmacSHA1",
+ "otpPolicyInitialCounter" : 0,
+ "otpPolicyDigits" : 6,
+ "otpPolicyLookAheadWindow" : 1,
+ "otpPolicyPeriod" : 30,
+ "otpPolicyCodeReusable" : false,
+ "otpSupportedApplications" : [ "totpAppFreeOTPName", "totpAppGoogleName", "totpAppMicrosoftAuthenticatorName" ],
+ "localizationTexts" : { },
+ "webAuthnPolicyRpEntityName" : "keycloak",
+ "webAuthnPolicySignatureAlgorithms" : [ "ES256" ],
+ "webAuthnPolicyRpId" : "",
+ "webAuthnPolicyAttestationConveyancePreference" : "not specified",
+ "webAuthnPolicyAuthenticatorAttachment" : "not specified",
+ "webAuthnPolicyRequireResidentKey" : "not specified",
+ "webAuthnPolicyUserVerificationRequirement" : "not specified",
+ "webAuthnPolicyCreateTimeout" : 0,
+ "webAuthnPolicyAvoidSameAuthenticatorRegister" : false,
+ "webAuthnPolicyAcceptableAaguids" : [ ],
+ "webAuthnPolicyExtraOrigins" : [ ],
+ "webAuthnPolicyPasswordlessRpEntityName" : "keycloak",
+ "webAuthnPolicyPasswordlessSignatureAlgorithms" : [ "ES256" ],
+ "webAuthnPolicyPasswordlessRpId" : "",
+ "webAuthnPolicyPasswordlessAttestationConveyancePreference" : "not specified",
+ "webAuthnPolicyPasswordlessAuthenticatorAttachment" : "not specified",
+ "webAuthnPolicyPasswordlessRequireResidentKey" : "not specified",
+ "webAuthnPolicyPasswordlessUserVerificationRequirement" : "not specified",
+ "webAuthnPolicyPasswordlessCreateTimeout" : 0,
+ "webAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister" : false,
+ "webAuthnPolicyPasswordlessAcceptableAaguids" : [ ],
+ "webAuthnPolicyPasswordlessExtraOrigins" : [ ],
+ "users" : [ {
+ "id" : "b0ef6edd-d503-4c74-a5ac-587fc56ea8ec",
+ "username" : "admin",
+ "emailVerified" : false,
+ "createdTimestamp" : 1619179992044,
+ "enabled" : true,
+ "totp" : false,
+ "credentials" : [ {
+ "id" : "a364bc10-e50d-46e7-9a4b-a2e81cfb97ef",
+ "type" : "password",
+ "createdDate" : 1619179992264,
+ "secretData" : "{\"value\":\"HFaSOho+7v2/pNE05AzCJs+MGKga2UuZFpCJwrEwyRWXq8xhYI+QZlsrsvkXbg8yye0ajxvKMhoQ8StOIw92hQ==\",\"salt\":\"0FxKxt+bGWwoWSZptMOXlw==\",\"additionalParameters\":{}}",
+ "credentialData" : "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}"
+ } ],
+ "disableableCredentialTypes" : [ ],
+ "requiredActions" : [ ],
+ "realmRoles" : [ "offline_access", "uma_authorization", "admin" ],
+ "clientRoles" : {
+ "account" : [ "view-profile", "manage-account" ]
+ },
+ "notBefore" : 0,
+ "groups" : [ ]
+ }, {
+ "id" : "18fb5db1-ebae-4824-ba72-a412330fe026",
+ "username" : "john",
+ "firstName" : "John",
+ "lastName" : "Doe",
+ "emailVerified" : false,
+ "createdTimestamp" : 1710947689953,
+ "enabled" : true,
+ "totp" : false,
+ "credentials" : [ {
+ "id" : "beee2f20-4a46-41e3-ace8-c56dca1e351f",
+ "type" : "password",
+ "createdDate" : 1710947723777,
+ "secretData" : "{\"value\":\"XmSnkmkJIk2SiZmdURejFJeEV+Jrvwqfi4NIKBwvcHXpRKtyaUFRSZb+cLuy4YyhhGXK/jn7sIbY3lNg/OwJNA==\",\"salt\":\"Fe3FD77W0p8xSfIckS7BpQ==\",\"additionalParameters\":{}}",
+ "credentialData" : "{\"hashIterations\":210000,\"algorithm\":\"pbkdf2-sha512\",\"additionalParameters\":{}}"
+ } ],
+ "disableableCredentialTypes" : [ ],
+ "requiredActions" : [ ],
+ "realmRoles" : [ "default-roles-test" ],
+ "notBefore" : 0,
+ "groups" : [ ]
+ }, {
+ "id" : "72a7f37e-b33f-4908-9ad9-33be0d4c1620",
+ "username" : "service-account-account",
+ "emailVerified" : false,
+ "createdTimestamp" : 1619180273352,
+ "enabled" : true,
+ "totp" : false,
+ "serviceAccountClientId" : "account",
+ "credentials" : [ ],
+ "disableableCredentialTypes" : [ ],
+ "requiredActions" : [ ],
+ "realmRoles" : [ "offline_access", "uma_authorization" ],
+ "clientRoles" : {
+ "account" : [ "view-profile", "manage-account" ]
+ },
+ "notBefore" : 0,
+ "groups" : [ ]
+ } ],
+ "scopeMappings" : [ {
+ "clientScope" : "offline_access",
+ "roles" : [ "offline_access" ]
+ } ],
+ "clientScopeMappings" : {
+ "account" : [ {
+ "client" : "account-console",
+ "roles" : [ "manage-account", "view-groups" ]
+ } ]
+ },
+ "clients" : [ {
+ "id" : "a0f23909-cbba-4950-95c5-1a166b4b3c54",
+ "clientId" : "account",
+ "name" : "${client_account}",
+ "description" : "",
+ "rootUrl" : "${authBaseUrl}",
+ "adminUrl" : "",
+ "baseUrl" : "/realms/test/account/",
+ "surrogateAuthRequired" : false,
+ "enabled" : true,
+ "alwaysDisplayInConsole" : false,
+ "clientAuthenticatorType" : "client-secret",
+ "secret" : "test",
+ "redirectUris" : [ ],
+ "webOrigins" : [ ],
+ "notBefore" : 0,
+ "bearerOnly" : false,
+ "consentRequired" : false,
+ "standardFlowEnabled" : true,
+ "implicitFlowEnabled" : false,
+ "directAccessGrantsEnabled" : false,
+ "serviceAccountsEnabled" : true,
+ "publicClient" : false,
+ "frontchannelLogout" : false,
+ "protocol" : "openid-connect",
+ "attributes" : {
+ "saml.assertion.signature" : "false",
+ "saml.force.post.binding" : "false",
+ "saml.multivalued.roles" : "false",
+ "saml.encrypt" : "false",
+ "post.logout.redirect.uris" : "+",
+ "oauth2.device.authorization.grant.enabled" : "false",
+ "backchannel.logout.revoke.offline.tokens" : "false",
+ "saml.server.signature" : "false",
+ "saml.server.signature.keyinfo.ext" : "false",
+ "exclude.session.state.from.auth.response" : "false",
+ "oidc.ciba.grant.enabled" : "false",
+ "backchannel.logout.session.required" : "false",
+ "client_credentials.use_refresh_token" : "false",
+ "saml_force_name_id_format" : "false",
+ "saml.client.signature" : "false",
+ "tls.client.certificate.bound.access.tokens" : "false",
+ "saml.authnstatement" : "false",
+ "display.on.consent.screen" : "false",
+ "saml.onetimeuse.condition" : "false"
+ },
+ "authenticationFlowBindingOverrides" : { },
+ "fullScopeAllowed" : false,
+ "nodeReRegistrationTimeout" : 0,
+ "protocolMappers" : [ {
+ "id" : "df9007ce-cdcc-4cd3-be23-74cc3a81e518",
+ "name" : "Client IP Address",
+ "protocol" : "openid-connect",
+ "protocolMapper" : "oidc-usersessionmodel-note-mapper",
+ "consentRequired" : false,
+ "config" : {
+ "user.session.note" : "clientAddress",
+ "userinfo.token.claim" : "true",
+ "id.token.claim" : "true",
+ "access.token.claim" : "true",
+ "claim.name" : "clientAddress",
+ "jsonType.label" : "String"
+ }
+ }, {
+ "id" : "49dfb8da-2cf1-4348-a587-e11c8a2dd5e3",
+ "name" : "Client ID",
+ "protocol" : "openid-connect",
+ "protocolMapper" : "oidc-usersessionmodel-note-mapper",
+ "consentRequired" : false,
+ "config" : {
+ "user.session.note" : "clientId",
+ "userinfo.token.claim" : "true",
+ "id.token.claim" : "true",
+ "access.token.claim" : "true",
+ "claim.name" : "clientId",
+ "jsonType.label" : "String"
+ }
+ }, {
+ "id" : "facf237e-6601-4712-a854-e52134dd5122",
+ "name" : "Client Host",
+ "protocol" : "openid-connect",
+ "protocolMapper" : "oidc-usersessionmodel-note-mapper",
+ "consentRequired" : false,
+ "config" : {
+ "user.session.note" : "clientHost",
+ "userinfo.token.claim" : "true",
+ "id.token.claim" : "true",
+ "access.token.claim" : "true",
+ "claim.name" : "clientHost",
+ "jsonType.label" : "String"
+ }
+ } ],
+ "defaultClientScopes" : [ "web-origins", "profile", "roles", "email" ],
+ "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ]
+ }, {
+ "id" : "a13c25c1-0378-466f-98eb-48006045968f",
+ "clientId" : "account-console",
+ "name" : "${client_account-console}",
+ "rootUrl" : "${authBaseUrl}",
+ "baseUrl" : "/realms/test/account/",
+ "surrogateAuthRequired" : false,
+ "enabled" : true,
+ "alwaysDisplayInConsole" : false,
+ "clientAuthenticatorType" : "client-secret",
+ "secret" : "102c2a26-30b5-4dfe-a540-6bd925ceaa67",
+ "redirectUris" : [ "/realms/test/account/*" ],
+ "webOrigins" : [ ],
+ "notBefore" : 0,
+ "bearerOnly" : false,
+ "consentRequired" : false,
+ "standardFlowEnabled" : true,
+ "implicitFlowEnabled" : false,
+ "directAccessGrantsEnabled" : false,
+ "serviceAccountsEnabled" : false,
+ "publicClient" : true,
+ "frontchannelLogout" : false,
+ "protocol" : "openid-connect",
+ "attributes" : {
+ "post.logout.redirect.uris" : "+",
+ "pkce.code.challenge.method" : "S256"
+ },
+ "authenticationFlowBindingOverrides" : { },
+ "fullScopeAllowed" : false,
+ "nodeReRegistrationTimeout" : 0,
+ "protocolMappers" : [ {
+ "id" : "ccf3fe06-0eb7-4e2b-8323-7f53649d40d4",
+ "name" : "audience resolve",
+ "protocol" : "openid-connect",
+ "protocolMapper" : "oidc-audience-resolve-mapper",
+ "consentRequired" : false,
+ "config" : { }
+ } ],
+ "defaultClientScopes" : [ "web-origins", "profile", "roles", "email" ],
+ "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ]
+ }, {
+ "id" : "502c9394-9181-4ddc-b573-f0b545b2ca9c",
+ "clientId" : "admin-cli",
+ "name" : "${client_admin-cli}",
+ "surrogateAuthRequired" : false,
+ "enabled" : true,
+ "alwaysDisplayInConsole" : false,
+ "clientAuthenticatorType" : "client-secret",
+ "secret" : "2fe5724f-0328-4fe7-a4b7-37a0badf610f",
+ "redirectUris" : [ ],
+ "webOrigins" : [ ],
+ "notBefore" : 0,
+ "bearerOnly" : false,
+ "consentRequired" : false,
+ "standardFlowEnabled" : false,
+ "implicitFlowEnabled" : false,
+ "directAccessGrantsEnabled" : true,
+ "serviceAccountsEnabled" : false,
+ "publicClient" : true,
+ "frontchannelLogout" : false,
+ "protocol" : "openid-connect",
+ "attributes" : {
+ "post.logout.redirect.uris" : "+"
+ },
+ "authenticationFlowBindingOverrides" : { },
+ "fullScopeAllowed" : false,
+ "nodeReRegistrationTimeout" : 0,
+ "defaultClientScopes" : [ "web-origins", "profile", "roles", "email" ],
+ "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ]
+ }, {
+ "id" : "ed0e9e16-d955-44d4-ab4c-0c6e8480bf12",
+ "clientId" : "test-realm",
+ "name" : "test Realm",
+ "surrogateAuthRequired" : false,
+ "enabled" : true,
+ "alwaysDisplayInConsole" : false,
+ "clientAuthenticatorType" : "client-secret",
+ "secret" : "test",
+ "redirectUris" : [ ],
+ "webOrigins" : [ ],
+ "notBefore" : 0,
+ "bearerOnly" : true,
+ "consentRequired" : false,
+ "standardFlowEnabled" : true,
+ "implicitFlowEnabled" : false,
+ "directAccessGrantsEnabled" : false,
+ "serviceAccountsEnabled" : false,
+ "publicClient" : false,
+ "frontchannelLogout" : false,
+ "protocol" : "openid-connect",
+ "attributes" : {
+ "post.logout.redirect.uris" : "+"
+ },
+ "authenticationFlowBindingOverrides" : { },
+ "fullScopeAllowed" : true,
+ "nodeReRegistrationTimeout" : 0,
+ "defaultClientScopes" : [ "web-origins", "profile", "roles", "email" ],
+ "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ]
+ }, {
+ "id" : "738bfb28-835b-4707-b3fa-e8d620c4a2ad",
+ "clientId" : "broker",
+ "name" : "${client_broker}",
+ "surrogateAuthRequired" : false,
+ "enabled" : true,
+ "alwaysDisplayInConsole" : false,
+ "clientAuthenticatorType" : "client-secret",
+ "secret" : "b2569a8e-0483-44aa-aa82-c2b3ee9462fc",
+ "redirectUris" : [ ],
+ "webOrigins" : [ ],
+ "notBefore" : 0,
+ "bearerOnly" : false,
+ "consentRequired" : false,
+ "standardFlowEnabled" : true,
+ "implicitFlowEnabled" : false,
+ "directAccessGrantsEnabled" : false,
+ "serviceAccountsEnabled" : false,
+ "publicClient" : false,
+ "frontchannelLogout" : false,
+ "protocol" : "openid-connect",
+ "attributes" : {
+ "post.logout.redirect.uris" : "+"
+ },
+ "authenticationFlowBindingOverrides" : { },
+ "fullScopeAllowed" : false,
+ "nodeReRegistrationTimeout" : 0,
+ "defaultClientScopes" : [ "web-origins", "profile", "roles", "email" ],
+ "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ]
+ }, {
+ "id" : "b9e2f2c2-46ba-4d57-9f93-f336ad52a3bf",
+ "clientId" : "realm-management",
+ "name" : "${client_realm-management}",
+ "surrogateAuthRequired" : false,
+ "enabled" : true,
+ "alwaysDisplayInConsole" : false,
+ "clientAuthenticatorType" : "client-secret",
+ "redirectUris" : [ ],
+ "webOrigins" : [ ],
+ "notBefore" : 0,
+ "bearerOnly" : true,
+ "consentRequired" : false,
+ "standardFlowEnabled" : true,
+ "implicitFlowEnabled" : false,
+ "directAccessGrantsEnabled" : false,
+ "serviceAccountsEnabled" : false,
+ "publicClient" : false,
+ "frontchannelLogout" : false,
+ "protocol" : "openid-connect",
+ "attributes" : { },
+ "authenticationFlowBindingOverrides" : { },
+ "fullScopeAllowed" : false,
+ "nodeReRegistrationTimeout" : 0,
+ "defaultClientScopes" : [ ],
+ "optionalClientScopes" : [ ]
+ }, {
+ "id" : "9f6340e7-176f-44f0-ae0f-a04cc5c54921",
+ "clientId" : "security-admin-console",
+ "name" : "${client_security-admin-console}",
+ "rootUrl" : "${authAdminUrl}",
+ "baseUrl" : "/admin/test/console/",
+ "surrogateAuthRequired" : false,
+ "enabled" : true,
+ "alwaysDisplayInConsole" : false,
+ "clientAuthenticatorType" : "client-secret",
+ "secret" : "50f03c48-3691-4c39-a3c3-3d02219525dc",
+ "redirectUris" : [ "/admin/test/console/*" ],
+ "webOrigins" : [ "+" ],
+ "notBefore" : 0,
+ "bearerOnly" : false,
+ "consentRequired" : false,
+ "standardFlowEnabled" : true,
+ "implicitFlowEnabled" : false,
+ "directAccessGrantsEnabled" : false,
+ "serviceAccountsEnabled" : false,
+ "publicClient" : true,
+ "frontchannelLogout" : false,
+ "protocol" : "openid-connect",
+ "attributes" : {
+ "post.logout.redirect.uris" : "+",
+ "pkce.code.challenge.method" : "S256"
+ },
+ "authenticationFlowBindingOverrides" : { },
+ "fullScopeAllowed" : false,
+ "nodeReRegistrationTimeout" : 0,
+ "protocolMappers" : [ {
+ "id" : "69d66f56-d567-451f-b979-7be216edd68a",
+ "name" : "locale",
+ "protocol" : "openid-connect",
+ "protocolMapper" : "oidc-usermodel-attribute-mapper",
+ "consentRequired" : false,
+ "config" : {
+ "userinfo.token.claim" : "true",
+ "user.attribute" : "locale",
+ "id.token.claim" : "true",
+ "access.token.claim" : "true",
+ "claim.name" : "locale",
+ "jsonType.label" : "String"
+ }
+ } ],
+ "defaultClientScopes" : [ "web-origins", "profile", "roles", "email" ],
+ "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ]
+ } ],
+ "clientScopes" : [ {
+ "id" : "9809d86e-9b9d-4c77-96d9-483f79bbadf7",
+ "name" : "email",
+ "description" : "OpenID Connect built-in scope: email",
+ "protocol" : "openid-connect",
+ "attributes" : {
+ "include.in.token.scope" : "true",
+ "display.on.consent.screen" : "true",
+ "consent.screen.text" : "${emailScopeConsentText}"
+ },
+ "protocolMappers" : [ {
+ "id" : "5cb5d135-4ec0-48ee-b8f3-1d2eea8972a5",
+ "name" : "email verified",
+ "protocol" : "openid-connect",
+ "protocolMapper" : "oidc-usermodel-property-mapper",
+ "consentRequired" : false,
+ "config" : {
+ "userinfo.token.claim" : "true",
+ "user.attribute" : "emailVerified",
+ "id.token.claim" : "true",
+ "access.token.claim" : "true",
+ "claim.name" : "email_verified",
+ "jsonType.label" : "boolean"
+ }
+ }, {
+ "id" : "cd8a5107-e52c-4642-942e-d05bff239e3c",
+ "name" : "email",
+ "protocol" : "openid-connect",
+ "protocolMapper" : "oidc-usermodel-property-mapper",
+ "consentRequired" : false,
+ "config" : {
+ "userinfo.token.claim" : "true",
+ "user.attribute" : "email",
+ "id.token.claim" : "true",
+ "access.token.claim" : "true",
+ "claim.name" : "email",
+ "jsonType.label" : "String"
+ }
+ } ]
+ }, {
+ "id" : "f3bb04c6-97c8-47e0-b383-8e7e586d2ab8",
+ "name" : "offline_access",
+ "description" : "OpenID Connect built-in scope: offline_access",
+ "protocol" : "openid-connect",
+ "attributes" : {
+ "consent.screen.text" : "${offlineAccessScopeConsentText}",
+ "display.on.consent.screen" : "true"
+ }
+ }, {
+ "id" : "f3f3fa4a-0e7e-4ffa-a994-6297c23f908d",
+ "name" : "web-origins",
+ "description" : "OpenID Connect scope for add allowed web origins to the access token",
+ "protocol" : "openid-connect",
+ "attributes" : {
+ "include.in.token.scope" : "false",
+ "display.on.consent.screen" : "false",
+ "consent.screen.text" : ""
+ },
+ "protocolMappers" : [ {
+ "id" : "91fa2894-4e7e-404b-864e-c917f90ac77b",
+ "name" : "allowed web origins",
+ "protocol" : "openid-connect",
+ "protocolMapper" : "oidc-allowed-origins-mapper",
+ "consentRequired" : false,
+ "config" : { }
+ } ]
+ }, {
+ "id" : "3b1fce21-ed51-4f05-942c-93cecb81025c",
+ "name" : "profile",
+ "description" : "OpenID Connect built-in scope: profile",
+ "protocol" : "openid-connect",
+ "attributes" : {
+ "include.in.token.scope" : "true",
+ "display.on.consent.screen" : "true",
+ "consent.screen.text" : "${profileScopeConsentText}"
+ },
+ "protocolMappers" : [ {
+ "id" : "351876fb-061f-4c8d-838c-082928bd80f7",
+ "name" : "zoneinfo",
+ "protocol" : "openid-connect",
+ "protocolMapper" : "oidc-usermodel-attribute-mapper",
+ "consentRequired" : false,
+ "config" : {
+ "userinfo.token.claim" : "true",
+ "user.attribute" : "zoneinfo",
+ "id.token.claim" : "true",
+ "access.token.claim" : "true",
+ "claim.name" : "zoneinfo",
+ "jsonType.label" : "String"
+ }
+ }, {
+ "id" : "a4d2a8c3-36d2-4f5a-91eb-a570e7cc0d3c",
+ "name" : "updated at",
+ "protocol" : "openid-connect",
+ "protocolMapper" : "oidc-usermodel-attribute-mapper",
+ "consentRequired" : false,
+ "config" : {
+ "userinfo.token.claim" : "true",
+ "user.attribute" : "updatedAt",
+ "id.token.claim" : "true",
+ "access.token.claim" : "true",
+ "claim.name" : "updated_at",
+ "jsonType.label" : "String"
+ }
+ }, {
+ "id" : "7492477a-d4c6-4a9e-89b2-6335a5f89ada",
+ "name" : "username",
+ "protocol" : "openid-connect",
+ "protocolMapper" : "oidc-usermodel-property-mapper",
+ "consentRequired" : false,
+ "config" : {
+ "userinfo.token.claim" : "true",
+ "user.attribute" : "username",
+ "id.token.claim" : "true",
+ "access.token.claim" : "true",
+ "claim.name" : "preferred_username",
+ "jsonType.label" : "String"
+ }
+ }, {
+ "id" : "fe14acb2-1948-49d6-9b2c-ba20b64cf017",
+ "name" : "gender",
+ "protocol" : "openid-connect",
+ "protocolMapper" : "oidc-usermodel-attribute-mapper",
+ "consentRequired" : false,
+ "config" : {
+ "userinfo.token.claim" : "true",
+ "user.attribute" : "gender",
+ "id.token.claim" : "true",
+ "access.token.claim" : "true",
+ "claim.name" : "gender",
+ "jsonType.label" : "String"
+ }
+ }, {
+ "id" : "28db7700-4226-4307-a22a-0deb6f857513",
+ "name" : "website",
+ "protocol" : "openid-connect",
+ "protocolMapper" : "oidc-usermodel-attribute-mapper",
+ "consentRequired" : false,
+ "config" : {
+ "userinfo.token.claim" : "true",
+ "user.attribute" : "website",
+ "id.token.claim" : "true",
+ "access.token.claim" : "true",
+ "claim.name" : "website",
+ "jsonType.label" : "String"
+ }
+ }, {
+ "id" : "ec4bdc29-7979-45f0-9071-4b680fda049a",
+ "name" : "given name",
+ "protocol" : "openid-connect",
+ "protocolMapper" : "oidc-usermodel-property-mapper",
+ "consentRequired" : false,
+ "config" : {
+ "userinfo.token.claim" : "true",
+ "user.attribute" : "firstName",
+ "id.token.claim" : "true",
+ "access.token.claim" : "true",
+ "claim.name" : "given_name",
+ "jsonType.label" : "String"
+ }
+ }, {
+ "id" : "84d327bb-bd09-4428-b6e3-e5ba4d896074",
+ "name" : "middle name",
+ "protocol" : "openid-connect",
+ "protocolMapper" : "oidc-usermodel-attribute-mapper",
+ "consentRequired" : false,
+ "config" : {
+ "userinfo.token.claim" : "true",
+ "user.attribute" : "middleName",
+ "id.token.claim" : "true",
+ "access.token.claim" : "true",
+ "claim.name" : "middle_name",
+ "jsonType.label" : "String"
+ }
+ }, {
+ "id" : "6258fd50-c687-4a42-8b7c-964b75581042",
+ "name" : "picture",
+ "protocol" : "openid-connect",
+ "protocolMapper" : "oidc-usermodel-attribute-mapper",
+ "consentRequired" : false,
+ "config" : {
+ "userinfo.token.claim" : "true",
+ "user.attribute" : "picture",
+ "id.token.claim" : "true",
+ "access.token.claim" : "true",
+ "claim.name" : "picture",
+ "jsonType.label" : "String"
+ }
+ }, {
+ "id" : "9db28236-3a1a-4e8d-b5cd-13f689f180a0",
+ "name" : "birthdate",
+ "protocol" : "openid-connect",
+ "protocolMapper" : "oidc-usermodel-attribute-mapper",
+ "consentRequired" : false,
+ "config" : {
+ "userinfo.token.claim" : "true",
+ "user.attribute" : "birthdate",
+ "id.token.claim" : "true",
+ "access.token.claim" : "true",
+ "claim.name" : "birthdate",
+ "jsonType.label" : "String"
+ }
+ }, {
+ "id" : "ae46ed2c-2154-45ef-90a7-fa50e80dc935",
+ "name" : "full name",
+ "protocol" : "openid-connect",
+ "protocolMapper" : "oidc-full-name-mapper",
+ "consentRequired" : false,
+ "config" : {
+ "id.token.claim" : "true",
+ "access.token.claim" : "true",
+ "userinfo.token.claim" : "true"
+ }
+ }, {
+ "id" : "9a4a3bd9-b8ee-4ebc-94b4-b4da3881ae18",
+ "name" : "locale",
+ "protocol" : "openid-connect",
+ "protocolMapper" : "oidc-usermodel-attribute-mapper",
+ "consentRequired" : false,
+ "config" : {
+ "userinfo.token.claim" : "true",
+ "user.attribute" : "locale",
+ "id.token.claim" : "true",
+ "access.token.claim" : "true",
+ "claim.name" : "locale",
+ "jsonType.label" : "String"
+ }
+ }, {
+ "id" : "ebc6c1fe-d2cb-441f-8803-c4ec8506168c",
+ "name" : "nickname",
+ "protocol" : "openid-connect",
+ "protocolMapper" : "oidc-usermodel-attribute-mapper",
+ "consentRequired" : false,
+ "config" : {
+ "userinfo.token.claim" : "true",
+ "user.attribute" : "nickname",
+ "id.token.claim" : "true",
+ "access.token.claim" : "true",
+ "claim.name" : "nickname",
+ "jsonType.label" : "String"
+ }
+ }, {
+ "id" : "cb2a2a4d-a3d2-4660-9438-714f64c4f831",
+ "name" : "profile",
+ "protocol" : "openid-connect",
+ "protocolMapper" : "oidc-usermodel-attribute-mapper",
+ "consentRequired" : false,
+ "config" : {
+ "userinfo.token.claim" : "true",
+ "user.attribute" : "profile",
+ "id.token.claim" : "true",
+ "access.token.claim" : "true",
+ "claim.name" : "profile",
+ "jsonType.label" : "String"
+ }
+ }, {
+ "id" : "7e28b8ae-83b8-4f06-9184-932a06b5e619",
+ "name" : "family name",
+ "protocol" : "openid-connect",
+ "protocolMapper" : "oidc-usermodel-property-mapper",
+ "consentRequired" : false,
+ "config" : {
+ "userinfo.token.claim" : "true",
+ "user.attribute" : "lastName",
+ "id.token.claim" : "true",
+ "access.token.claim" : "true",
+ "claim.name" : "family_name",
+ "jsonType.label" : "String"
+ }
+ } ]
+ }, {
+ "id" : "8dc47e1e-202a-4aa4-945d-1e4a80763482",
+ "name" : "acr",
+ "description" : "OpenID Connect scope for add acr (authentication context class reference) to the token",
+ "protocol" : "openid-connect",
+ "attributes" : {
+ "include.in.token.scope" : "false",
+ "display.on.consent.screen" : "false"
+ },
+ "protocolMappers" : [ {
+ "id" : "7ac4dd59-25b7-42cc-a8c9-68301983dce9",
+ "name" : "acr loa level",
+ "protocol" : "openid-connect",
+ "protocolMapper" : "oidc-acr-mapper",
+ "consentRequired" : false,
+ "config" : {
+ "id.token.claim" : "true",
+ "introspection.token.claim" : "true",
+ "access.token.claim" : "true"
+ }
+ } ]
+ }, {
+ "id" : "bd5a5fc6-85c8-4e4b-b147-0dbbfd5add27",
+ "name" : "phone",
+ "description" : "OpenID Connect built-in scope: phone",
+ "protocol" : "openid-connect",
+ "attributes" : {
+ "include.in.token.scope" : "true",
+ "display.on.consent.screen" : "true",
+ "consent.screen.text" : "${phoneScopeConsentText}"
+ },
+ "protocolMappers" : [ {
+ "id" : "94fa890b-976e-48ce-8640-6b6781e7bf6c",
+ "name" : "phone number",
+ "protocol" : "openid-connect",
+ "protocolMapper" : "oidc-usermodel-attribute-mapper",
+ "consentRequired" : false,
+ "config" : {
+ "userinfo.token.claim" : "true",
+ "user.attribute" : "phoneNumber",
+ "id.token.claim" : "true",
+ "access.token.claim" : "true",
+ "claim.name" : "phone_number",
+ "jsonType.label" : "String"
+ }
+ }, {
+ "id" : "deb58ea8-1660-43ac-9097-34d38b3c9126",
+ "name" : "phone number verified",
+ "protocol" : "openid-connect",
+ "protocolMapper" : "oidc-usermodel-attribute-mapper",
+ "consentRequired" : false,
+ "config" : {
+ "userinfo.token.claim" : "true",
+ "user.attribute" : "phoneNumberVerified",
+ "id.token.claim" : "true",
+ "access.token.claim" : "true",
+ "claim.name" : "phone_number_verified",
+ "jsonType.label" : "boolean"
+ }
+ } ]
+ }, {
+ "id" : "06ee6c91-072f-461f-be27-791a6556324f",
+ "name" : "microprofile-jwt",
+ "description" : "Microprofile - JWT built-in scope",
+ "protocol" : "openid-connect",
+ "attributes" : {
+ "include.in.token.scope" : "true",
+ "display.on.consent.screen" : "false"
+ },
+ "protocolMappers" : [ {
+ "id" : "efccbda2-dd10-426b-809a-f46cb921c7a9",
+ "name" : "upn",
+ "protocol" : "openid-connect",
+ "protocolMapper" : "oidc-usermodel-property-mapper",
+ "consentRequired" : false,
+ "config" : {
+ "userinfo.token.claim" : "true",
+ "user.attribute" : "username",
+ "id.token.claim" : "true",
+ "access.token.claim" : "true",
+ "claim.name" : "upn",
+ "jsonType.label" : "String"
+ }
+ }, {
+ "id" : "73bccc6d-2d3f-4c85-8d25-c6868f2b70b8",
+ "name" : "groups",
+ "protocol" : "openid-connect",
+ "protocolMapper" : "oidc-usermodel-realm-role-mapper",
+ "consentRequired" : false,
+ "config" : {
+ "multivalued" : "true",
+ "userinfo.token.claim" : "true",
+ "user.attribute" : "foo",
+ "id.token.claim" : "true",
+ "access.token.claim" : "true",
+ "claim.name" : "groups",
+ "jsonType.label" : "String"
+ }
+ } ]
+ }, {
+ "id" : "f4cb8558-578a-41bb-815a-91f2514b71cb",
+ "name" : "roles",
+ "description" : "OpenID Connect scope for add user roles to the access token",
+ "protocol" : "openid-connect",
+ "attributes" : {
+ "include.in.token.scope" : "false",
+ "display.on.consent.screen" : "true",
+ "consent.screen.text" : "${rolesScopeConsentText}"
+ },
+ "protocolMappers" : [ {
+ "id" : "63a2ee31-2194-49cc-9724-ccb9c57d8fa2",
+ "name" : "client roles",
+ "protocol" : "openid-connect",
+ "protocolMapper" : "oidc-usermodel-client-role-mapper",
+ "consentRequired" : false,
+ "config" : {
+ "user.attribute" : "foo",
+ "access.token.claim" : "true",
+ "claim.name" : "resource_access.${client_id}.roles",
+ "jsonType.label" : "String",
+ "multivalued" : "true"
+ }
+ }, {
+ "id" : "da083c4e-081c-4f8f-8526-5fa49d71a111",
+ "name" : "audience resolve",
+ "protocol" : "openid-connect",
+ "protocolMapper" : "oidc-audience-resolve-mapper",
+ "consentRequired" : false,
+ "config" : { }
+ }, {
+ "id" : "8d7c0a1a-42cc-4efe-a322-3c56ded3424e",
+ "name" : "realm roles",
+ "protocol" : "openid-connect",
+ "protocolMapper" : "oidc-usermodel-realm-role-mapper",
+ "consentRequired" : false,
+ "config" : {
+ "user.attribute" : "foo",
+ "access.token.claim" : "true",
+ "claim.name" : "realm_access.roles",
+ "jsonType.label" : "String",
+ "multivalued" : "true"
+ }
+ } ]
+ }, {
+ "id" : "992bf614-54e3-414a-8d56-e47d7e37fc11",
+ "name" : "address",
+ "description" : "OpenID Connect built-in scope: address",
+ "protocol" : "openid-connect",
+ "attributes" : {
+ "include.in.token.scope" : "true",
+ "display.on.consent.screen" : "true",
+ "consent.screen.text" : "${addressScopeConsentText}"
+ },
+ "protocolMappers" : [ {
+ "id" : "a1999995-852d-4c55-b2bc-e096aba293f2",
+ "name" : "address",
+ "protocol" : "openid-connect",
+ "protocolMapper" : "oidc-address-mapper",
+ "consentRequired" : false,
+ "config" : {
+ "user.attribute.formatted" : "formatted",
+ "user.attribute.country" : "country",
+ "user.attribute.postal_code" : "postal_code",
+ "userinfo.token.claim" : "true",
+ "user.attribute.street" : "street",
+ "id.token.claim" : "true",
+ "user.attribute.region" : "region",
+ "access.token.claim" : "true",
+ "user.attribute.locality" : "locality"
+ }
+ } ]
+ }, {
+ "id" : "f0cb84ff-70eb-42ee-8439-da9cfd3c62ca",
+ "name" : "role_list",
+ "description" : "SAML role list",
+ "protocol" : "saml",
+ "attributes" : {
+ "consent.screen.text" : "${samlRoleListScopeConsentText}",
+ "display.on.consent.screen" : "true"
+ },
+ "protocolMappers" : [ {
+ "id" : "dba527ed-7819-4a95-8102-0c9032f25067",
+ "name" : "role list",
+ "protocol" : "saml",
+ "protocolMapper" : "saml-role-list-mapper",
+ "consentRequired" : false,
+ "config" : {
+ "single" : "false",
+ "attribute.nameformat" : "Basic",
+ "attribute.name" : "Role"
+ }
+ } ]
+ } ],
+ "defaultDefaultClientScopes" : [ "profile", "email", "role_list", "web-origins", "roles", "acr" ],
+ "defaultOptionalClientScopes" : [ "microprofile-jwt", "address", "phone", "offline_access" ],
+ "browserSecurityHeaders" : {
+ "contentSecurityPolicyReportOnly" : "",
+ "xContentTypeOptions" : "nosniff",
+ "referrerPolicy" : "no-referrer",
+ "xRobotsTag" : "none",
+ "xFrameOptions" : "SAMEORIGIN",
+ "contentSecurityPolicy" : "frame-src 'self'; frame-ancestors 'self'; object-src 'none';",
+ "xXSSProtection" : "1; mode=block",
+ "strictTransportSecurity" : "max-age=31536000; includeSubDomains"
+ },
+ "smtpServer" : { },
+ "eventsEnabled" : false,
+ "eventsListeners" : [ "jboss-logging" ],
+ "enabledEventTypes" : [ ],
+ "adminEventsEnabled" : false,
+ "adminEventsDetailsEnabled" : false,
+ "identityProviders" : [ ],
+ "identityProviderMappers" : [ ],
+ "components" : {
+ "org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy" : [ {
+ "id" : "da2969b9-5e9b-448c-86ea-36cc860a3927",
+ "name" : "Max Clients Limit",
+ "providerId" : "max-clients",
+ "subType" : "anonymous",
+ "subComponents" : { },
+ "config" : {
+ "max-clients" : [ "200" ]
+ }
+ }, {
+ "id" : "c2436a16-6e52-4161-949c-5747d4819497",
+ "name" : "Consent Required",
+ "providerId" : "consent-required",
+ "subType" : "anonymous",
+ "subComponents" : { },
+ "config" : { }
+ }, {
+ "id" : "3a935ca4-e98e-4ec1-ad6e-91023fc1eb4e",
+ "name" : "Allowed Client Scopes",
+ "providerId" : "allowed-client-templates",
+ "subType" : "anonymous",
+ "subComponents" : { },
+ "config" : {
+ "allow-default-scopes" : [ "true" ]
+ }
+ }, {
+ "id" : "86ea8008-be05-4317-9ca9-b711ea4a8c13",
+ "name" : "Trusted Hosts",
+ "providerId" : "trusted-hosts",
+ "subType" : "anonymous",
+ "subComponents" : { },
+ "config" : {
+ "host-sending-registration-request-must-match" : [ "true" ],
+ "client-uris-must-match" : [ "true" ]
+ }
+ }, {
+ "id" : "8b7ad61a-9124-4a0d-aa25-57d99eaaba1b",
+ "name" : "Allowed Client Scopes",
+ "providerId" : "allowed-client-templates",
+ "subType" : "authenticated",
+ "subComponents" : { },
+ "config" : {
+ "allow-default-scopes" : [ "true" ]
+ }
+ }, {
+ "id" : "4c31d0d7-6787-4c0f-8b41-799ff1e4b1e3",
+ "name" : "Allowed Protocol Mapper Types",
+ "providerId" : "allowed-protocol-mappers",
+ "subType" : "authenticated",
+ "subComponents" : { },
+ "config" : {
+ "allowed-protocol-mapper-types" : [ "oidc-usermodel-attribute-mapper", "saml-role-list-mapper", "saml-user-property-mapper", "oidc-address-mapper", "oidc-usermodel-property-mapper", "saml-user-attribute-mapper", "oidc-sha256-pairwise-sub-mapper", "oidc-full-name-mapper" ]
+ }
+ }, {
+ "id" : "fe477953-0991-4166-9239-8d020e9bb8f6",
+ "name" : "Full Scope Disabled",
+ "providerId" : "scope",
+ "subType" : "anonymous",
+ "subComponents" : { },
+ "config" : { }
+ }, {
+ "id" : "c3d4ebb9-7e9d-4cc8-97e2-3c3ce73da642",
+ "name" : "Allowed Protocol Mapper Types",
+ "providerId" : "allowed-protocol-mappers",
+ "subType" : "anonymous",
+ "subComponents" : { },
+ "config" : {
+ "allowed-protocol-mapper-types" : [ "saml-role-list-mapper", "oidc-usermodel-attribute-mapper", "saml-user-property-mapper", "oidc-full-name-mapper", "oidc-sha256-pairwise-sub-mapper", "oidc-address-mapper", "saml-user-attribute-mapper", "oidc-usermodel-property-mapper" ]
+ }
+ } ],
+ "org.keycloak.userprofile.UserProfileProvider" : [ {
+ "id" : "27ee9f9c-a76b-4ed5-9dea-52a5108b85d8",
+ "providerId" : "declarative-user-profile",
+ "subComponents" : { },
+ "config" : {
+ "kc.user.profile.config" : [ "{\"attributes\":[{\"name\":\"username\",\"displayName\":\"${username}\",\"validations\":{\"length\":{\"min\":3,\"max\":255},\"username-prohibited-characters\":{},\"up-username-not-idn-homograph\":{}},\"permissions\":{\"view\":[\"admin\",\"user\"],\"edit\":[\"admin\",\"user\"]},\"multivalued\":false},{\"name\":\"email\",\"displayName\":\"${email}\",\"validations\":{\"email\":{},\"length\":{\"max\":255}},\"required\":{\"roles\":[\"user\"]},\"permissions\":{\"view\":[\"admin\",\"user\"],\"edit\":[\"admin\",\"user\"]},\"multivalued\":false},{\"name\":\"firstName\",\"displayName\":\"${firstName}\",\"validations\":{\"length\":{\"max\":255},\"person-name-prohibited-characters\":{}},\"required\":{\"roles\":[\"user\"]},\"permissions\":{\"view\":[\"admin\",\"user\"],\"edit\":[\"admin\",\"user\"]},\"multivalued\":false},{\"name\":\"lastName\",\"displayName\":\"${lastName}\",\"validations\":{\"length\":{\"max\":255},\"person-name-prohibited-characters\":{}},\"required\":{\"roles\":[\"user\"]},\"permissions\":{\"view\":[\"admin\",\"user\"],\"edit\":[\"admin\",\"user\"]},\"multivalued\":false}],\"groups\":[{\"name\":\"user-metadata\",\"displayHeader\":\"User metadata\",\"displayDescription\":\"Attributes, which refer to user metadata\"}],\"unmanagedAttributePolicy\":\"ENABLED\"}" ]
+ }
+ } ],
+ "org.keycloak.keys.KeyProvider" : [ {
+ "id" : "16e1431e-e111-4466-a89e-3eaba25f9419",
+ "name" : "hmac-generated-hs512",
+ "providerId" : "hmac-generated",
+ "subComponents" : { },
+ "config" : {
+ "kid" : [ "d352b3e0-4ad7-4698-8831-63fa4cc5e6b7" ],
+ "secret" : [ "njkg8Vd2htEmKQdm9nntcgvQEuY8Tegl0d0LaB7hSpkQz0tpfbkAip1Myzs8ULQ8Y4ZMb7ddb5dgLQXJFFILh9ji1RbM3W3ZkD4m9CU14-O7tjwL0mNk_ER99393X9f6jUDMmll2lqEmFkxBUJr5G0Sqi1MyhSaXjaQfFlWpfrY" ],
+ "priority" : [ "100" ],
+ "algorithm" : [ "HS512" ]
+ }
+ }, {
+ "id" : "185b5cbc-c208-4b30-8ea4-e26d46827d8a",
+ "name" : "fallback-RS256",
+ "providerId" : "rsa-generated",
+ "subComponents" : { },
+ "config" : {
+ "privateKey" : [ "MIIEpAIBAAKCAQEAsRMbt+R+Fkym/pDoprcMQG+QuaHI0IheBXUoSl160A2TIeF6vJzbpRex1sNbTBUcfpeYesrWoevpzmk43uDWcMtLSo2cDr6pt8Gfa97V5LH1tVF+RcdwebirKdzH0kh6t8RTxN81cnl564LWz5VJvHCCPJcSvegUM6gprHVIwtlpEMzsoC2lFQNjCjfEl2aY5LPuvD9bWjeeDK/J4s5wKgr2Y9A12zjTvGKJkKDEam/4cKVapi+sGNNafrAX0DcrEM0G2S1S+FefjIOqF50mOKhRJbNkUcbK/g/VKDBCdowzmvvR6MZx0rBi7RrHhaq8KToXoYcOIvMOKAvpDc1gjwIDAQABAoIBAB9AvyCqzHJFHyhJDTb3kcsBpeqNmnLrzqRp9C2D6Dw2WSSetln52W5/Cx1bp457H2dcfEYX7N/xUnfi7G2yA0cvKl/DNKsJjczn+KpCT0ApBLP26TGJrNle9Z7S39XGgxpSJXLW7okA1brygdVrhPMkbGgjReSMxJwFby2IGcqB53oAeCgZEKkW4ZqOh/cwKjJkLCgLRT9pgh875wniKDFeKnEx5sbNZ9dWPoqmy55TfjqjTGA3cdnWThnGNgGxiN3fG2ZAPFxNAUpaBnlHPiaPGZJwv2lDisAKzP+MnA5+wz1Wx0ncEs08Aw5ngtFpljC5zktNhkmCRGlJOFLGmQkCgYEA5rpd4rg5uwtN7OKCksATRRjkLWEVgqoggW4ejnJ6LSQ+VY6OdV77nC1ftOEeddYouYtfcRAfhxhKtHPJI14IRzhGevvQcN65jnUhaoYk5N6bKHK349l0jhK5UmZAyCdQpY7N+iNAQLOrIqSL87I0B+jd8QGFu8IssR+gnEw0f40CgYEAxHhL+mxc+tUn2JxAZJiIyh+M8vLvTRCGRMW9gxtQ6w8K47rYiY93veIMU/2gk9PLYhUcU4Uz14MQfuldiuamydTw1wn3e7pgLD1EQ29Ck2vcr+nLz5G6z55wfiV4rqvb1xSnu0u5Y/k5Kopo9G2U20kDfWyNbQvXpnRSCKgJW4sCgYEAt0ji3gCUs7Y2L/B741G7vQ8Z68aMjODSs56jnWrpDUUWU2bMWgaa/6S3u3t9dAQtE7/YkHtLYEj2x0SXSoYfM1xL+NRi79auNrFrWzC2zCzdupLu64xJ37aWCxP5cEZy9SFtFMC+AOf5Ear/FhbA6GufKx2Xe+CzGf1S2/ZZWd0CgYEAlaiVJ8NX6HJqkeQkYPyYZm82LPLFOsz1mnmObMpoD0Y8I1D3FYJF0kzY2zn+Ed1pteMi2rRC002xSRt2+BHOxzv/4a5j6MoF7G0XDM85xZaKWy4a5Ji71t94DX95uISNR/8h7dg29mKoGzGn1VmL5KZvlCEWchRtRwygWJu31RUCgYBbKIVikkdS1ZxexPmXAISKZ+cO+RUffLjs6RLgE/Bt1LZLCK4gA3y3HaBfkcF4LSoXjwF35mDAQ32ZP24afasHjTwcREv1vBzhvKEppWpsaZC7pr9IJfYHhPxhHbkHD2BdxKRMg/jQ5N7cLjuqenR0DY1C4mTcYSA1W1DqezrmrQ==" ],
+ "certificate" : [ "MIICmzCCAYMCBgF4/qYe7zANBgkqhkiG9w0BAQsFADARMQ8wDQYDVQQDDAZtYXN0ZXIwHhcNMjEwNDIzMTIxMTQzWhcNMzEwNDIzMTIxMzIzWjARMQ8wDQYDVQQDDAZtYXN0ZXIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCxExu35H4WTKb+kOimtwxAb5C5ocjQiF4FdShKXXrQDZMh4Xq8nNulF7HWw1tMFRx+l5h6ytah6+nOaTje4NZwy0tKjZwOvqm3wZ9r3tXksfW1UX5Fx3B5uKsp3MfSSHq3xFPE3zVyeXnrgtbPlUm8cII8lxK96BQzqCmsdUjC2WkQzOygLaUVA2MKN8SXZpjks+68P1taN54Mr8niznAqCvZj0DXbONO8YomQoMRqb/hwpVqmL6wY01p+sBfQNysQzQbZLVL4V5+Mg6oXnSY4qFEls2RRxsr+D9UoMEJ2jDOa+9HoxnHSsGLtGseFqrwpOhehhw4i8w4oC+kNzWCPAgMBAAEwDQYJKoZIhvcNAQELBQADggEBACl0r90/fEbh6Ka8wKlV/8vtqv2TScgC+u1YSdXbA6AgoNhCOLo6+8IA6thV6pV918lzylIB1BpB4eaxN9R0EawErAYOJflw3zeUM2lbu5CIv6xe5MOcRGTqvQGFlPfiY07ug3aS15M0V6oNNdPYHgM6712Wyslsl1Bx/Weim/KeRqrjU1aedeC5GTi1znQqF+w9cc9WNEH4QVvDmzM6rmSrSTE4NkCxl3qNhKbISsZrb3mWQq9Xlli7RWSWLYnaN7/6X+PViWgMI1zugjqt4tOKKoRM7yeqHFqk98TpcG8DfAxFlnUBuQCDnKE5ntH6draRVft4K+N+fhL3B2PyG0M=" ],
+ "priority" : [ "-100" ],
+ "algorithm" : [ "RS256" ]
+ }
+ }, {
+ "id" : "5769f531-07cd-4e4e-a565-3d8731daafdd",
+ "name" : "fallback-HS256",
+ "providerId" : "hmac-generated",
+ "subComponents" : { },
+ "config" : {
+ "kid" : [ "73170ee0-e952-4573-8d9c-d6a9fb2c193e" ],
+ "secret" : [ "jlNrWr4_mB4AOXdLF7izVHaOT7rmfssy0_5hXWWVBN1G3vosStn_mO27HwdRBiALb-Ri24X83sBj_JjwJ_s3QpyJQqejTDm61_H6zCFcmD1c89-iNZc_45hSbDj38wX4rfmB7F67r254cHh5q2TcdJvqDJfuViGPS1TiRGoxWb4" ],
+ "priority" : [ "-100" ],
+ "algorithm" : [ "HS256" ]
+ }
+ } ]
+ },
+ "internationalizationEnabled" : false,
+ "supportedLocales" : [ ],
+ "authenticationFlows" : [ {
+ "id" : "f9e9054d-fedc-43b1-b0ff-8fbf84d665f9",
+ "alias" : "Account verification options",
+ "description" : "Method with which to verity the existing account",
+ "providerId" : "basic-flow",
+ "topLevel" : false,
+ "builtIn" : true,
+ "authenticationExecutions" : [ {
+ "authenticator" : "idp-email-verification",
+ "authenticatorFlow" : false,
+ "requirement" : "ALTERNATIVE",
+ "priority" : 10,
+ "autheticatorFlow" : false,
+ "userSetupAllowed" : false
+ }, {
+ "authenticatorFlow" : true,
+ "requirement" : "ALTERNATIVE",
+ "priority" : 20,
+ "autheticatorFlow" : true,
+ "flowAlias" : "Verify Existing Account by Re-authentication",
+ "userSetupAllowed" : false
+ } ]
+ }, {
+ "id" : "67760bda-4d3f-462d-a81d-5b99fdbd9057",
+ "alias" : "Browser - Conditional OTP",
+ "description" : "Flow to determine if the OTP is required for the authentication",
+ "providerId" : "basic-flow",
+ "topLevel" : false,
+ "builtIn" : true,
+ "authenticationExecutions" : [ {
+ "authenticator" : "conditional-user-configured",
+ "authenticatorFlow" : false,
+ "requirement" : "REQUIRED",
+ "priority" : 10,
+ "autheticatorFlow" : false,
+ "userSetupAllowed" : false
+ }, {
+ "authenticator" : "auth-otp-form",
+ "authenticatorFlow" : false,
+ "requirement" : "REQUIRED",
+ "priority" : 20,
+ "autheticatorFlow" : false,
+ "userSetupAllowed" : false
+ } ]
+ }, {
+ "id" : "ad30bc5a-daeb-4e39-b11c-4d209227378e",
+ "alias" : "Direct Grant - Conditional OTP",
+ "description" : "Flow to determine if the OTP is required for the authentication",
+ "providerId" : "basic-flow",
+ "topLevel" : false,
+ "builtIn" : true,
+ "authenticationExecutions" : [ {
+ "authenticator" : "conditional-user-configured",
+ "authenticatorFlow" : false,
+ "requirement" : "REQUIRED",
+ "priority" : 10,
+ "autheticatorFlow" : false,
+ "userSetupAllowed" : false
+ }, {
+ "authenticator" : "direct-grant-validate-otp",
+ "authenticatorFlow" : false,
+ "requirement" : "REQUIRED",
+ "priority" : 20,
+ "autheticatorFlow" : false,
+ "userSetupAllowed" : false
+ } ]
+ }, {
+ "id" : "d66d7cc2-f395-4e6f-b1f2-f3c650cc1223",
+ "alias" : "First broker login - Conditional OTP",
+ "description" : "Flow to determine if the OTP is required for the authentication",
+ "providerId" : "basic-flow",
+ "topLevel" : false,
+ "builtIn" : true,
+ "authenticationExecutions" : [ {
+ "authenticator" : "conditional-user-configured",
+ "authenticatorFlow" : false,
+ "requirement" : "REQUIRED",
+ "priority" : 10,
+ "autheticatorFlow" : false,
+ "userSetupAllowed" : false
+ }, {
+ "authenticator" : "auth-otp-form",
+ "authenticatorFlow" : false,
+ "requirement" : "REQUIRED",
+ "priority" : 20,
+ "autheticatorFlow" : false,
+ "userSetupAllowed" : false
+ } ]
+ }, {
+ "id" : "985ffbbb-267f-4cb6-a09b-454ebb9e5b60",
+ "alias" : "Handle Existing Account",
+ "description" : "Handle what to do if there is existing account with same email/username like authenticated identity provider",
+ "providerId" : "basic-flow",
+ "topLevel" : false,
+ "builtIn" : true,
+ "authenticationExecutions" : [ {
+ "authenticator" : "idp-confirm-link",
+ "authenticatorFlow" : false,
+ "requirement" : "REQUIRED",
+ "priority" : 10,
+ "autheticatorFlow" : false,
+ "userSetupAllowed" : false
+ }, {
+ "authenticatorFlow" : true,
+ "requirement" : "REQUIRED",
+ "priority" : 20,
+ "autheticatorFlow" : true,
+ "flowAlias" : "Account verification options",
+ "userSetupAllowed" : false
+ } ]
+ }, {
+ "id" : "5bdd0a60-8aeb-4e11-9455-ae01eed15bda",
+ "alias" : "Reset - Conditional OTP",
+ "description" : "Flow to determine if the OTP should be reset or not. Set to REQUIRED to force.",
+ "providerId" : "basic-flow",
+ "topLevel" : false,
+ "builtIn" : true,
+ "authenticationExecutions" : [ {
+ "authenticator" : "conditional-user-configured",
+ "authenticatorFlow" : false,
+ "requirement" : "REQUIRED",
+ "priority" : 10,
+ "autheticatorFlow" : false,
+ "userSetupAllowed" : false
+ }, {
+ "authenticator" : "reset-otp",
+ "authenticatorFlow" : false,
+ "requirement" : "REQUIRED",
+ "priority" : 20,
+ "autheticatorFlow" : false,
+ "userSetupAllowed" : false
+ } ]
+ }, {
+ "id" : "7793fa6d-5be1-498b-9337-170426960cb6",
+ "alias" : "User creation or linking",
+ "description" : "Flow for the existing/non-existing user alternatives",
+ "providerId" : "basic-flow",
+ "topLevel" : false,
+ "builtIn" : true,
+ "authenticationExecutions" : [ {
+ "authenticatorConfig" : "create unique user config",
+ "authenticator" : "idp-create-user-if-unique",
+ "authenticatorFlow" : false,
+ "requirement" : "ALTERNATIVE",
+ "priority" : 10,
+ "autheticatorFlow" : false,
+ "userSetupAllowed" : false
+ }, {
+ "authenticatorFlow" : true,
+ "requirement" : "ALTERNATIVE",
+ "priority" : 20,
+ "autheticatorFlow" : true,
+ "flowAlias" : "Handle Existing Account",
+ "userSetupAllowed" : false
+ } ]
+ }, {
+ "id" : "38468f69-d630-4b00-ab5b-169e7a413b44",
+ "alias" : "Verify Existing Account by Re-authentication",
+ "description" : "Reauthentication of existing account",
+ "providerId" : "basic-flow",
+ "topLevel" : false,
+ "builtIn" : true,
+ "authenticationExecutions" : [ {
+ "authenticator" : "idp-username-password-form",
+ "authenticatorFlow" : false,
+ "requirement" : "REQUIRED",
+ "priority" : 10,
+ "autheticatorFlow" : false,
+ "userSetupAllowed" : false
+ }, {
+ "authenticatorFlow" : true,
+ "requirement" : "CONDITIONAL",
+ "priority" : 20,
+ "autheticatorFlow" : true,
+ "flowAlias" : "First broker login - Conditional OTP",
+ "userSetupAllowed" : false
+ } ]
+ }, {
+ "id" : "c7324105-a924-4689-8f04-c7ea0f8effd2",
+ "alias" : "browser",
+ "description" : "browser based authentication",
+ "providerId" : "basic-flow",
+ "topLevel" : true,
+ "builtIn" : true,
+ "authenticationExecutions" : [ {
+ "authenticator" : "auth-cookie",
+ "authenticatorFlow" : false,
+ "requirement" : "DISABLED",
+ "priority" : 10,
+ "autheticatorFlow" : false,
+ "userSetupAllowed" : false
+ }, {
+ "authenticator" : "auth-spnego",
+ "authenticatorFlow" : false,
+ "requirement" : "DISABLED",
+ "priority" : 20,
+ "autheticatorFlow" : false,
+ "userSetupAllowed" : false
+ }, {
+ "authenticator" : "identity-provider-redirector",
+ "authenticatorFlow" : false,
+ "requirement" : "ALTERNATIVE",
+ "priority" : 25,
+ "autheticatorFlow" : false,
+ "userSetupAllowed" : false
+ }, {
+ "authenticatorFlow" : true,
+ "requirement" : "ALTERNATIVE",
+ "priority" : 30,
+ "autheticatorFlow" : true,
+ "flowAlias" : "forms",
+ "userSetupAllowed" : false
+ } ]
+ }, {
+ "id" : "88e04d13-4d71-4ff6-b521-7bbebb3329f5",
+ "alias" : "clients",
+ "description" : "Base authentication for clients",
+ "providerId" : "client-flow",
+ "topLevel" : true,
+ "builtIn" : true,
+ "authenticationExecutions" : [ {
+ "authenticator" : "client-secret",
+ "authenticatorFlow" : false,
+ "requirement" : "ALTERNATIVE",
+ "priority" : 10,
+ "autheticatorFlow" : false,
+ "userSetupAllowed" : false
+ }, {
+ "authenticator" : "client-jwt",
+ "authenticatorFlow" : false,
+ "requirement" : "ALTERNATIVE",
+ "priority" : 20,
+ "autheticatorFlow" : false,
+ "userSetupAllowed" : false
+ }, {
+ "authenticator" : "client-secret-jwt",
+ "authenticatorFlow" : false,
+ "requirement" : "ALTERNATIVE",
+ "priority" : 30,
+ "autheticatorFlow" : false,
+ "userSetupAllowed" : false
+ }, {
+ "authenticator" : "client-x509",
+ "authenticatorFlow" : false,
+ "requirement" : "ALTERNATIVE",
+ "priority" : 40,
+ "autheticatorFlow" : false,
+ "userSetupAllowed" : false
+ } ]
+ }, {
+ "id" : "c5c23ac7-c748-48d2-8090-7c21197238af",
+ "alias" : "direct grant",
+ "description" : "OpenID Connect Resource Owner Grant",
+ "providerId" : "basic-flow",
+ "topLevel" : true,
+ "builtIn" : true,
+ "authenticationExecutions" : [ {
+ "authenticator" : "direct-grant-validate-username",
+ "authenticatorFlow" : false,
+ "requirement" : "REQUIRED",
+ "priority" : 10,
+ "autheticatorFlow" : false,
+ "userSetupAllowed" : false
+ }, {
+ "authenticator" : "direct-grant-validate-password",
+ "authenticatorFlow" : false,
+ "requirement" : "REQUIRED",
+ "priority" : 20,
+ "autheticatorFlow" : false,
+ "userSetupAllowed" : false
+ }, {
+ "authenticatorFlow" : true,
+ "requirement" : "CONDITIONAL",
+ "priority" : 30,
+ "autheticatorFlow" : true,
+ "flowAlias" : "Direct Grant - Conditional OTP",
+ "userSetupAllowed" : false
+ } ]
+ }, {
+ "id" : "a3201f31-10e7-4b6c-85a7-169851b5e3b4",
+ "alias" : "docker auth",
+ "description" : "Used by Docker clients to authenticate against the IDP",
+ "providerId" : "basic-flow",
+ "topLevel" : true,
+ "builtIn" : true,
+ "authenticationExecutions" : [ {
+ "authenticator" : "docker-http-basic-authenticator",
+ "authenticatorFlow" : false,
+ "requirement" : "REQUIRED",
+ "priority" : 10,
+ "autheticatorFlow" : false,
+ "userSetupAllowed" : false
+ } ]
+ }, {
+ "id" : "fa162a5a-0fb5-404f-8aa3-893fec90e1c9",
+ "alias" : "first broker login",
+ "description" : "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account",
+ "providerId" : "basic-flow",
+ "topLevel" : true,
+ "builtIn" : true,
+ "authenticationExecutions" : [ {
+ "authenticatorConfig" : "review profile config",
+ "authenticator" : "idp-review-profile",
+ "authenticatorFlow" : false,
+ "requirement" : "REQUIRED",
+ "priority" : 10,
+ "autheticatorFlow" : false,
+ "userSetupAllowed" : false
+ }, {
+ "authenticatorFlow" : true,
+ "requirement" : "REQUIRED",
+ "priority" : 20,
+ "autheticatorFlow" : true,
+ "flowAlias" : "User creation or linking",
+ "userSetupAllowed" : false
+ } ]
+ }, {
+ "id" : "784b94b0-b050-406a-83fb-e83eea2e282b",
+ "alias" : "forms",
+ "description" : "Username, password, otp and other auth forms.",
+ "providerId" : "basic-flow",
+ "topLevel" : false,
+ "builtIn" : true,
+ "authenticationExecutions" : [ {
+ "authenticator" : "auth-username-password-form",
+ "authenticatorFlow" : false,
+ "requirement" : "REQUIRED",
+ "priority" : 10,
+ "autheticatorFlow" : false,
+ "userSetupAllowed" : false
+ }, {
+ "authenticatorFlow" : true,
+ "requirement" : "CONDITIONAL",
+ "priority" : 20,
+ "autheticatorFlow" : true,
+ "flowAlias" : "Browser - Conditional OTP",
+ "userSetupAllowed" : false
+ } ]
+ }, {
+ "id" : "9b62475b-9803-48ef-8c37-4786889773a4",
+ "alias" : "registration",
+ "description" : "registration flow",
+ "providerId" : "basic-flow",
+ "topLevel" : true,
+ "builtIn" : true,
+ "authenticationExecutions" : [ {
+ "authenticator" : "registration-page-form",
+ "authenticatorFlow" : true,
+ "requirement" : "REQUIRED",
+ "priority" : 10,
+ "autheticatorFlow" : true,
+ "flowAlias" : "registration form",
+ "userSetupAllowed" : false
+ } ]
+ }, {
+ "id" : "8c4579e2-0870-48c3-9b7b-faf6e1e7cb58",
+ "alias" : "registration form",
+ "description" : "registration form",
+ "providerId" : "form-flow",
+ "topLevel" : false,
+ "builtIn" : true,
+ "authenticationExecutions" : [ {
+ "authenticator" : "registration-user-creation",
+ "authenticatorFlow" : false,
+ "requirement" : "REQUIRED",
+ "priority" : 20,
+ "autheticatorFlow" : false,
+ "userSetupAllowed" : false
+ }, {
+ "authenticator" : "registration-password-action",
+ "authenticatorFlow" : false,
+ "requirement" : "REQUIRED",
+ "priority" : 50,
+ "autheticatorFlow" : false,
+ "userSetupAllowed" : false
+ }, {
+ "authenticator" : "registration-recaptcha-action",
+ "authenticatorFlow" : false,
+ "requirement" : "DISABLED",
+ "priority" : 60,
+ "autheticatorFlow" : false,
+ "userSetupAllowed" : false
+ } ]
+ }, {
+ "id" : "d8526a3d-2a46-429d-95b1-e0eec86a0130",
+ "alias" : "reset credentials",
+ "description" : "Reset credentials for a user if they forgot their password or something",
+ "providerId" : "basic-flow",
+ "topLevel" : true,
+ "builtIn" : true,
+ "authenticationExecutions" : [ {
+ "authenticator" : "reset-credentials-choose-user",
+ "authenticatorFlow" : false,
+ "requirement" : "REQUIRED",
+ "priority" : 10,
+ "autheticatorFlow" : false,
+ "userSetupAllowed" : false
+ }, {
+ "authenticator" : "reset-credential-email",
+ "authenticatorFlow" : false,
+ "requirement" : "REQUIRED",
+ "priority" : 20,
+ "autheticatorFlow" : false,
+ "userSetupAllowed" : false
+ }, {
+ "authenticator" : "reset-password",
+ "authenticatorFlow" : false,
+ "requirement" : "REQUIRED",
+ "priority" : 30,
+ "autheticatorFlow" : false,
+ "userSetupAllowed" : false
+ }, {
+ "authenticatorFlow" : true,
+ "requirement" : "CONDITIONAL",
+ "priority" : 40,
+ "autheticatorFlow" : true,
+ "flowAlias" : "Reset - Conditional OTP",
+ "userSetupAllowed" : false
+ } ]
+ }, {
+ "id" : "1cd0d9f7-bef8-4cfc-b1fe-bedb4aad0a7a",
+ "alias" : "saml ecp",
+ "description" : "SAML ECP Profile Authentication Flow",
+ "providerId" : "basic-flow",
+ "topLevel" : true,
+ "builtIn" : true,
+ "authenticationExecutions" : [ {
+ "authenticator" : "http-basic-authenticator",
+ "authenticatorFlow" : false,
+ "requirement" : "REQUIRED",
+ "priority" : 10,
+ "autheticatorFlow" : false,
+ "userSetupAllowed" : false
+ } ]
+ } ],
+ "authenticatorConfig" : [ {
+ "id" : "fdc6ae68-fe17-43fe-b7b8-0fcc04d822ce",
+ "alias" : "create unique user config",
+ "config" : {
+ "require.password.update.after.registration" : "false"
+ }
+ }, {
+ "id" : "c84572ec-5ca1-4730-817a-b7a4ca89bf79",
+ "alias" : "review profile config",
+ "config" : {
+ "update.profile.on.first.login" : "missing"
+ }
+ } ],
+ "requiredActions" : [ {
+ "alias" : "CONFIGURE_TOTP",
+ "name" : "Configure OTP",
+ "providerId" : "CONFIGURE_TOTP",
+ "enabled" : true,
+ "defaultAction" : false,
+ "priority" : 10,
+ "config" : { }
+ }, {
+ "alias" : "TERMS_AND_CONDITIONS",
+ "name" : "Terms and Conditions",
+ "providerId" : "TERMS_AND_CONDITIONS",
+ "enabled" : false,
+ "defaultAction" : false,
+ "priority" : 20,
+ "config" : { }
+ }, {
+ "alias" : "UPDATE_PASSWORD",
+ "name" : "Update Password",
+ "providerId" : "UPDATE_PASSWORD",
+ "enabled" : true,
+ "defaultAction" : false,
+ "priority" : 30,
+ "config" : { }
+ }, {
+ "alias" : "UPDATE_PROFILE",
+ "name" : "Update Profile",
+ "providerId" : "UPDATE_PROFILE",
+ "enabled" : true,
+ "defaultAction" : false,
+ "priority" : 40,
+ "config" : { }
+ }, {
+ "alias" : "VERIFY_EMAIL",
+ "name" : "Verify Email",
+ "providerId" : "VERIFY_EMAIL",
+ "enabled" : true,
+ "defaultAction" : false,
+ "priority" : 50,
+ "config" : { }
+ }, {
+ "alias" : "delete_account",
+ "name" : "Delete Account",
+ "providerId" : "delete_account",
+ "enabled" : false,
+ "defaultAction" : false,
+ "priority" : 60,
+ "config" : { }
+ }, {
+ "alias" : "update_user_locale",
+ "name" : "Update User Locale",
+ "providerId" : "update_user_locale",
+ "enabled" : true,
+ "defaultAction" : false,
+ "priority" : 1000,
+ "config" : { }
+ } ],
+ "browserFlow" : "browser",
+ "registrationFlow" : "registration",
+ "directGrantFlow" : "direct grant",
+ "resetCredentialsFlow" : "reset credentials",
+ "clientAuthenticationFlow" : "clients",
+ "dockerAuthenticationFlow" : "docker auth",
+ "firstBrokerLoginFlow" : "first broker login",
+ "attributes" : {
+ "cibaBackchannelTokenDeliveryMode" : "poll",
+ "cibaExpiresIn" : "120",
+ "cibaAuthRequestedUserHint" : "login_hint",
+ "oauth2DeviceCodeLifespan" : "600",
+ "clientOfflineSessionMaxLifespan" : "0",
+ "oauth2DevicePollingInterval" : "5",
+ "clientSessionIdleTimeout" : "0",
+ "parRequestUriLifespan" : "60",
+ "clientSessionMaxLifespan" : "0",
+ "clientOfflineSessionIdleTimeout" : "0",
+ "cibaInterval" : "5",
+ "realmReusableOtpCode" : "false"
+ },
+ "keycloakVersion" : "24.0.1",
+ "userManagedAccessAllowed" : false,
+ "clientProfiles" : {
+ "profiles" : [ ]
+ },
+ "clientPolicies" : {
+ "policies" : [ ]
+ }
+}
diff --git a/.github/scripts/install-blazectl.sh b/.github/scripts/install-blazectl.sh
index 5f561bd..f0d60a5 100755
--- a/.github/scripts/install-blazectl.sh
+++ b/.github/scripts/install-blazectl.sh
@@ -1,6 +1,6 @@
#!/bin/bash -e
-VERSION="0.13.0"
+VERSION="0.16.1"
curl -sLO "https://github.com/samply/blazectl/releases/download/v$VERSION/blazectl-$VERSION-linux-amd64.tar.gz"
tar xzf "blazectl-$VERSION-linux-amd64.tar.gz"
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index c0efdbb..27a1590 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -47,7 +47,8 @@ jobs:
build:
needs: fhir-data-evaluator-ig
- runs-on: ubuntu-latest
+ runs-on: ubuntu-22.04
+
steps:
- uses: actions/checkout@v4
@@ -91,6 +92,12 @@ jobs:
integration-test:
needs: build
+ strategy:
+ matrix:
+ test:
+ - no-auth
+ - basic-auth
+ - oauth
runs-on: ubuntu-22.04
steps:
@@ -109,47 +116,59 @@ jobs:
- name: Load FhirDataEvaluator Image
run: docker load --input /tmp/fhir-data-evaluator.tar
- - name: Run Blaze
- run: docker compose -f .github/integration-test/docker-compose.yml up -d
+ - name: Run Blaze and Proxy
+ run: docker compose -f .github/integration-test/${{ matrix.test }}/docker-compose.yml up -d --wait --wait-timeout 300 --scale fhir-data-evaluator=0
- name: Wait for Blaze
run: .github/scripts/wait-for-url.sh http://localhost:8082/health
- name: Load Data
- run: blazectl --no-progress --server http://localhost:8082/fhir upload .github/integration-test/test-data
+ run: .github/integration-test/${{ matrix.test }}/load-data.sh .github/integration-test/test-data
- name: Run Integration Test for ICD10
- run: .github/integration-test/evaluate-icd10.sh /${PWD}/.github/integration-test/measures/icd10-measure.json
+ run: .github/integration-test/evaluate-icd10.sh ${{ matrix.test }}
- name: Run Integration Test for ICD10 to CSV
- run: .github/integration-test/evaluate-icd10-to-csv.sh /${PWD}/.github/integration-test/measures/icd10-measure.json
+ run: .github/integration-test/evaluate-icd10-to-csv.sh ${{ matrix.test }}
- name: Run Integration Test for ICD10 with Status to CSV
- run: .github/integration-test/evaluate-icd10WithStatus-to-csv.sh /${PWD}/.github/integration-test/measures/icd10withStatus-measure.json
+ run: .github/integration-test/evaluate-icd10WithStatus-to-csv.sh ${{ matrix.test }}
- name: Run Integration Test for type code
- run: .github/integration-test/evaluate-code.sh /${PWD}/.github/integration-test/measures/code-measure.json
+ run: .github/integration-test/evaluate-code.sh ${{ matrix.test }}
- name: Run Integration Test for type boolean
- run: .github/integration-test/evaluate-exists.sh /${PWD}/.github/integration-test/measures/exists-measure.json
+ run: .github/integration-test/evaluate-exists.sh ${{ matrix.test }}
- name: Run Integration Test for Unique Count
- run: .github/integration-test/evaluate-unique-count.sh /${PWD}/.github/integration-test/measures/unique-count-measure.json
+ run: .github/integration-test/evaluate-unique-count.sh ${{ matrix.test }}
- name: Run Integration Test for Unique Count with CSV
- run: .github/integration-test/evaluate-unique-count-to-csv.sh /${PWD}/.github/integration-test/measures/unique-count-measure.json
+ run: .github/integration-test/evaluate-unique-count-to-csv.sh ${{ matrix.test }}
- name: Run Integration Test for Unique Count with Components and with CSV
- run: .github/integration-test/evaluate-unique-count-with-components-to-csv.sh /${PWD}/.github/integration-test/measures/unique-count-with-components-measure.json
+ run: .github/integration-test/evaluate-unique-count-with-components-to-csv.sh ${{ matrix.test }}
- name: Run Integration Test to check if it correctly exits when there are insufficient writing permissions
run: .github/integration-test/missing-permissions-test.sh
+ - name: Run Integration Test for Posting the MeasureReport to the FHIR server
+ run: .github/integration-test/evaluate-and-post-report.sh Test_PROJECT_Evaluation_1
+ if: matrix.test == 'no-auth'
+
+ - name: Run Integration Test for Posting the MeasureReport to the FHIR server with the Same Project Identifier
+ run: .github/integration-test/evaluate-and-post-update.sh Test_PROJECT_Evaluation_1
+ if: matrix.test == 'no-auth'
+
+ - name: Run Integration Test for Posting the MeasureReport to the FHIR server with a Different Project Identifier
+ run: .github/integration-test/evaluate-and-post-different-doc-ref.sh Test_PROJECT_Evaluation_2
+ if: matrix.test == 'no-auth'
+
- name: Remove Blaze volumes
- run: docker compose -f .github/integration-test/docker-compose.yml down -v
+ run: docker compose -f .github/integration-test/${{ matrix.test }}/docker-compose.yml down -v
- name: Run Blaze with fresh volumes
- run: docker compose -f .github/integration-test/docker-compose.yml up -d
+ run: docker compose -f .github/integration-test/${{ matrix.test }}/docker-compose.yml up -d
- name: Wait for Blaze
run: .github/scripts/wait-for-url.sh http://localhost:8082/health
@@ -158,11 +177,10 @@ jobs:
run: .github/integration-test/test-data/get-mii-testdata.sh
- name: Upload New Data
- run: blazectl --no-progress --server http://localhost:8082/fhir upload .github/integration-test/Vorhofflimmern
+ run: .github/integration-test/${{ matrix.test }}/load-data.sh .github/integration-test/Vorhofflimmern
- name: Run Integration Test multiple stratifiers
- run: .github/integration-test/evaluate-multiple-stratifiers.sh /${PWD}/.github/integration-test/measures/multiple-stratifiers.json
-
+ run: .github/integration-test/evaluate-multiple-stratifiers.sh ${{ matrix.test }}
push-image:
needs:
diff --git a/.gitignore b/.gitignore
index 48b477c..7a65322 100644
--- a/.gitignore
+++ b/.gitignore
@@ -35,3 +35,4 @@ build/
output
docker/.env
+certs
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8a3af40..08c95d1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -13,6 +13,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
### Fixed
### Security
+## [1.1.0] - 2024-10-30
+
+### Added
+- Add Support for OAuth Authentication
+- Add Sending Measure Report to FHIR Server
+
## [1.0.0] - 2024-09-25
### Added
diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md
index 3d89fdc..e70ec28 100644
--- a/DEVELOPMENT.md
+++ b/DEVELOPMENT.md
@@ -6,6 +6,7 @@
* rename every occurrence of the old version, say `2.2.0-SNAPSHOT` into the new version, say `2.2.0`
* rename every occurrence of old Docker images like `ghcr.io/medizininformatik-initiative/fhir-data-evaluator:0.1.0`
into the new image, say `ghcr.io/medizininformatik-initiative/fhir-data-evaluator:0.1.1`
+* rename the Docker image in the [docker-compose](docker/docker-compose.yml) from develop into the new version
* update the CHANGELOG based on the milestone
* create a commit with the title `Release v`
* create a PR from the release branch into main
@@ -13,5 +14,6 @@ into the new image, say `ghcr.io/medizininformatik-initiative/fhir-data-evaluato
* create and push a tag called `v` like `v0.1.1` on main at the merge commit
* create a new branch called `next-dev` on top of the release branch
* change the version in the POM to the next SNAPSHOT version which usually increments the minor version
+* change the Docker image in the [docker-compose](docker/docker-compose.yml) to develop
* merge the `next-dev` branch back into develop
* create release notes on GitHub
diff --git a/README.md b/README.md
index ba62a20..f0e3941 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,5 @@
-
# Fhir Data Evaluator
-
## Overview
The aim of the project is to provide a tool, which can be used to extract metadata information from multiple FHIR servers and combine the data to:
@@ -41,12 +39,12 @@ An example of a Measure can be found [here](Documentation/example-measures/examp
### MeasureReport as Output only:
```sh
-docker run -v :/app/measure.json -v :/app/output -e FHIR_SERVER= -it ghcr.io/medizininformatik-initiative/fhir-data-evaluator:1.0.0
+docker run -v :/app/measure.json -v :/app/output -e FHIR_SERVER= -e TZ=Europe/Berlin -it ghcr.io/medizininformatik-initiative/fhir-data-evaluator:1.1.0
```
### MeasureReport and CSV Output:
```sh
-docker run -v :/app/measure.json -v :/app/output -e CONVERT_TO_CSV=true -e FHIR_SERVER= -it ghcr.io/medizininformatik-initiative/fhir-data-evaluator:1.0.0
+docker run -v :/app/measure.json -v :/app/output -e CONVERT_TO_CSV=true -e FHIR_SERVER= -e TZ=Europe/Berlin -it ghcr.io/medizininformatik-initiative/fhir-data-evaluator:1.1.0
```
* this generates a CSV file for each Stratifier and stores the files in a directory named after the current date combined
with the Measure's name
@@ -55,16 +53,14 @@ with the Measure's name
### Usage with Docker Networks
* to any of the listed docker run commands add ```--network ``` to run the container inside a Docker network
```sh
-docker run -v :/app/measure.json -v :/app/output -e CONVERT_TO_CSV=true -e FHIR_SERVER= --network -it ghcr.io/medizininformatik-initiative/fhir-data-evaluator:1.0.0
+docker run -v :/app/measure.json -v :/app/output -e CONVERT_TO_CSV=true -e FHIR_SERVER= -e TZ=Europe/Berlin --network -it ghcr.io/medizininformatik-initiative/fhir-data-evaluator:1.1.0
```
### Time Zones
-When generating the CSV files from the MeasureReport, the files will be saved in a directory named after the current date
+When generating the MeasureReport, the output files will be saved in a directory named after the current date
combined with the Measure's name. Since it is run inside a Docker container, the time zone might differ from the one on
-the host machine. If you want to match the time zones, add for example ```-e TZ=Europe/Berlin```:
-```sh
-docker run -v :/app/measure.json -v :/app/output -e CONVERT_TO_CSV=true -e FHIR_SERVER= -e TZ=Europe/Berlin -it ghcr.io/medizininformatik-initiative/fhir-data-evaluator:1.0.0
-```
+the host machine. This time zone is also used to set the date for the DocumentReference if the MeasureReport is sent to
+a FHIR server. If you want to match the time zones, set the time zone for example to ```-e TZ=Europe/Berlin```:
### Passing Additional Environment Variables:
@@ -72,22 +68,41 @@ The environment variables are used inside the docker container, so if they are s
be visible in the container. Each additional environment variable can be passed using the `-e` flag.
* Example of passing a page count of 50:
```sh
-docker run -v :/app/measure.json -v :/app/output -e FHIR_SERVER= -e FHIR_PAGE_COUNT=50 -it ghcr.io/medizininformatik-initiative/fhir-data-evaluator:1.0.0
+docker run -v :/app/measure.json -v :/app/output -e FHIR_SERVER= -e FHIR_PAGE_COUNT=50 -e TZ=Europe/Berlin -it ghcr.io/medizininformatik-initiative/fhir-data-evaluator:1.1.0
```
+### Sending the MeasureReport to a FHIR Server
+
+If `SEND_REPORT_TO_SERVER` is set to true, the MeasureReport is sent to the `FHIR_REPORT_DESTINATION_SERVER` along with a
+DocumentReference that is configured with the following environment variables:
+* `AUTHOR_IDENTIFIER_SYSTEM` (example: `http://dsf.dev/sid/organization-identifier`)
+* `AUTHOR_IDENTIFIER_VALUE` (example: `Test_DIC1`)
+* `PROJECT_IDENTIFIER_SYSTEM` (example: `http://medizininformatik-initiative.de/sid/project-identifier`)
+* `PROJECT_IDENTIFIER_VALUE` (example: `Test_PROJECT_Evaluation`)
+
## Environment Variables
-| Name | Default | Description |
-|:-----------------------|:---------------------------|:---------------------------------------------------------------------------------------|
-| FHIR_SERVER | http://localhost:8080/fhir | The base URL of the FHIR server to use. |
-| FHIR_USER | | The username to use for HTTP Basic Authentication. |
-| FHIR_PASSWORD | | The password to use for HTTP Basic Authentication. |
-| FHIR_MAX_CONNECTIONS | 4 | The maximum number of connections to open towards the FHIR server. |
-| FHIR_MAX_QUEUE_SIZE | 500 | The maximum number FHIR server requests to queue before returning an error. |
-| FHIR_PAGE_COUNT | 1000 | The number of resources per page to request from the FHIR server. |
-| FHIR_BEARER_TOKEN | | Bearer token for authentication. |
-| MAX_IN_MEMORY_SIZE_MIB | 10 | The maximum in-memory buffer size for the webclient in MiB. |
-| CONVERT_TO_CSV | false | Whether for the MeasureReport should be generated CSV files. |
+| Name | Default | Description |
+|:-------------------------------|:--------------------------------------------------------------|:---------------------------------------------------------------------------------------------|
+| FHIR_SERVER | http://localhost:8080/fhir | The base URL of the FHIR server to use. |
+| FHIR_USER | | The username to use for HTTP Basic Authentication. |
+| FHIR_PASSWORD | | The password to use for HTTP Basic Authentication. |
+| FHIR_MAX_CONNECTIONS | 4 | The maximum number of connections to open towards the FHIR server. |
+| FHIR_MAX_QUEUE_SIZE | 500 | The maximum number FHIR server requests to queue before returning an error. |
+| FHIR_PAGE_COUNT | 1000 | The number of resources per page to request from the FHIR server. |
+| FHIR_BEARER_TOKEN | | Bearer token for authentication. |
+| FHIR_OAUTH_ISSUER_URI | | The issuer URI of the OpenID Connect provider. |
+| FHIR_OAUTH_CLIENT_ID | | The client ID to use for authentication with OpenID Connect provider. |
+| FHIR_OAUTH_CLIENT_SECRET | | The client secret to use for authentication with OpenID Connect provider. |
+| MAX_IN_MEMORY_SIZE_MIB | 10 | The maximum in-memory buffer size for the webclient in MiB. |
+| TZ | Europe/Berlin | The time zone used to create the output directory and set the date in the DocumentReference. |
+| CONVERT_TO_CSV | false | Whether for the MeasureReport should be generated CSV files. |
+| SEND_REPORT_TO_SERVER | false | Whether the MeasureReport should be sent to a FHIR server. |
+| FHIR_REPORT_DESTINATION_SERVER | http://localhost:8080/fhir | The FHIR Server that the MeasureReport should be sent to. |
+| AUTHOR_IDENTIFIER_SYSTEM | http://dsf.dev/sid/organization-identifier | The system of the author organization. |
+| AUTHOR_IDENTIFIER_VALUE | | The code of the author organization. |
+| PROJECT_IDENTIFIER_SYSTEM | http://medizininformatik-initiative.de/sid/project-identifier | The system of the master identifier. |
+| PROJECT_IDENTIFIER_VALUE | | The value of the master identifier. |
## Documentation
diff --git a/docker/.env.default b/docker/.env.default
index 7005a27..9ad7e3e 100644
--- a/docker/.env.default
+++ b/docker/.env.default
@@ -1,11 +1,21 @@
FDE_CONVERT_TO_CSV=true
-FDE_FHIR_SERVER=http://fhir-server:8080/fhir
+FDE_FHIR_SERVER=http://localhost:8080/fhir
FDE_FHIR_USER=
FDE_FHIR_PASSWORD=
FDE_FHIR_MAX_CONNECTIONS=4
FDE_FHIR_MAX_QUEUE_SIZE=500
FDE_FHIR_PAGE_COUNT=1000
FDE_FHIR_BEARER_TOKEN=
+FDE_FHIR_OAUTH_ISSUER_URI=
+FDE_FHIR_OAUTH_CLIENT_ID=
+FDE_FHIR_OAUTH_CLIENT_SECRET=
FDE_MAX_IN_MEMORY_SIZE_MIB=10
FDE_INPUT_MEASURE=../Documentation/example-measures/example-measure-kds.json
-FDE_OUTPUT_DIR=../output
\ No newline at end of file
+FDE_OUTPUT_DIR=../output
+FDE_TZ=Europe/Berlin
+FDE_FHIR_REPORT_DESTINATION_SERVER=http://localhost:8080/fhir
+FDE_SEND_REPORT_TO_SERVER=false
+FDE_AUTHOR_IDENTIFIER_SYSTEM=http://dsf.dev/sid/organization-identifier
+FDE_AUTHOR_IDENTIFIER_VALUE=fde-dic
+FDE_PROJECT_IDENTIFIER_SYSTEM=http://medizininformatik-initiative.de/sid/project-identifier
+FDE_PROJECT_IDENTIFIER_VALUE=fdpg-data-availability-report
diff --git a/docker/Dockerfile b/docker/Dockerfile
index 1b910fa..5e88c10 100644
--- a/docker/Dockerfile
+++ b/docker/Dockerfile
@@ -7,6 +7,13 @@ RUN apt-get update && apt-get upgrade -y && \
COPY /target/fhir-data-evaluator.jar /app/
COPY /src/main/csv-converter.sh /app/csv-converter.sh
+ENV CERTIFICATE_PATH=/app/certs
+ENV TRUSTSTORE_PATH=/app/truststore
+ENV TRUSTSTORE_FILE=self-signed-truststore.jks
+
+RUN mkdir -p $CERTIFICATE_PATH $TRUSTSTORE_PATH
+RUN chown 1001 $CERTIFICATE_PATH $TRUSTSTORE_PATH
+
WORKDIR /app
USER 1001
diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml
index 10cf4e7..2f7792c 100644
--- a/docker/docker-compose.yml
+++ b/docker/docker-compose.yml
@@ -1,21 +1,30 @@
services:
fhir-data-evaluator:
- image: ghcr.io/medizininformatik-initiative/fhir-data-evaluator:develop
+ image: ghcr.io/medizininformatik-initiative/fhir-data-evaluator:1.1.0
environment:
CONVERT_TO_CSV: ${FDE_CONVERT_TO_CSV:-true}
- FHIR_SERVER: ${FDE_FHIR_SERVER:-http://fhir-server:8080/fhir}
+ FHIR_SERVER: ${FDE_FHIR_SERVER:-http://localhost:8080/fhir}
FHIR_USER: ${FDE_FHIR_USER:-}
FHIR_PASSWORD: ${FDE_FHIR_PASSWORD:-}
FHIR_MAX_CONNECTIONS: ${FDE_FHIR_MAX_CONNECTIONS:-4}
FHIR_MAX_QUEUE_SIZE: ${FDE_FHIR_MAX_QUEUE_SIZE:-500}
FHIR_PAGE_COUNT: ${FDE_FHIR_PAGE_COUNT:-1000}
FHIR_BEARER_TOKEN: ${FDE_FHIR_BEARER_TOKEN:-}
+ FHIR_OAUTH_ISSUER_URI: ${FDE_FHIR_OAUTH_ISSUER_URI:-}
+ FHIR_OAUTH_CLIENT_ID: ${FDE_FHIR_OAUTH_CLIENT_ID:-}
+ FHIR_OAUTH_CLIENT_SECRET: ${FDE_FHIR_OAUTH_CLIENT_SECRET:-}
MAX_IN_MEMORY_SIZE_MIB: ${FDE_MAX_IN_MEMORY_SIZE_MIB:-10}
+ SEND_REPORT_TO_SERVER: ${FDE_SEND_REPORT_TO_SERVER:-false}
+ AUTHOR_IDENTIFIER_SYSTEM: ${FDE_AUTHOR_IDENTIFIER_SYSTEM:-http://dsf.dev/sid/organization-identifier}
+ AUTHOR_IDENTIFIER_VALUE: ${FDE_AUTHOR_IDENTIFIER_VALUE:-fde-dic}
+ PROJECT_IDENTIFIER_SYSTEM: ${FDE_PROJECT_IDENTIFIER_SYSTEM:-http://medizininformatik-initiative.de/sid/project-identifier}
+ PROJECT_IDENTIFIER_VALUE: ${FDE_PROJECT_IDENTIFIER_VALUE:-fdpg-data-availability-report}
+ FHIR_REPORT_DESTINATION_SERVER: ${FDE_FHIR_REPORT_DESTINATION_SERVER:-http://localhost:8080/fhir}
+ TZ: ${FDE_TZ:-Europe/Berlin}
+ extra_hosts:
+ - "auth.localhost:host-gateway"
+ - "localhost:host-gateway"
volumes:
- "${FDE_INPUT_MEASURE:-../Documentation/example-measures/example-measure-kds.json}:/app/measure.json"
- "${FDE_OUTPUT_DIR:-../output}:/app/output"
-
-
-
-
-
+ - ../certs:/app/certs
diff --git a/docker/docker-entrypoint.sh b/docker/docker-entrypoint.sh
index 4fce5a2..906b6f0 100644
--- a/docker/docker-entrypoint.sh
+++ b/docker/docker-entrypoint.sh
@@ -1,17 +1,66 @@
#!/bin/bash -e
-if [ ! -w /app/output ]; then
- echo "Missing writing permissions on output directory" >&2
- exit 1
+if [ "${SEND_REPORT_TO_SERVER}" = true ]; then
+ vars_for_upload=(FHIR_REPORT_DESTINATION_SERVER AUTHOR_IDENTIFIER_SYSTEM AUTHOR_IDENTIFIER_VALUE PROJECT_IDENTIFIER_SYSTEM PROJECT_IDENTIFIER_VALUE)
+ for var in "${vars_for_upload[@]}"; do
+ if [[ -z "${!var}" ]]; then
+ echo "In order to upload the MeasureReport to a FHIR server, all following environment variables must be set" \
+ "(but currently are not set): ${vars_for_upload[*]}"
+ exit 1
+ fi
+ done
+else
+ if [ ! -w /app/output ]; then
+ echo "Missing writing permissions on output directory" >&2
+ exit 1
+ fi
fi
-today=$(date +"%Y-%m-%d_%H-%M-%S")
+now="$(date +%s)"
+dateForDirectory="$(date +"%Y-%m-%d_%H-%M-%S" -d "@${now}")"
+dateForBundle="$(date +"%Y-%m-%dT%H:%M:%S%:z" -d "@${now}")"
+
measureName="$(jq -c --raw-output '.name' /app/measure.json)"
-outputDir="$today-$measureName"
-mkdir -p /app/output/"$outputDir"
-cp /app/measure.json /app/output/"$outputDir"/measure.json
+outputDir="$dateForDirectory-$measureName"
+if [ "$SEND_REPORT_TO_SERVER" != true ]; then
+ mkdir -p /app/output/"$outputDir"
+ cp /app/measure.json /app/output/"$outputDir"/measure.json
+fi
+
+TRUSTSTORE_FILE="/app/truststore/self-signed-truststore.jks"
+TRUSTSTORE_PASS=${TRUSTSTORE_PASS:-changeit}
+KEY_PASS=${KEY_PASS:-changeit}
+
+shopt -s nullglob
+IFS=$'\n'
+ca_files=(certs/*.pem)
+
+if [ ! "${#ca_files[@]}" -eq 0 ]; then
+
+ echo "# At least one CA file with extension *.pem found in certs folder -> starting fhir data evaluator with own CAs"
+
+ if [[ -f "$TRUSTSTORE_FILE" ]]; then
+ echo "## Truststore already exists -> resetting truststore"
+ rm "$TRUSTSTORE_FILE"
+ fi
+
+ keytool -genkey -alias self-signed-truststore -keyalg RSA -keystore "$TRUSTSTORE_FILE" -storepass "$TRUSTSTORE_PASS" -keypass "$KEY_PASS" -dname "CN=self-signed,OU=self-signed,O=self-signed,L=self-signed,S=self-signed,C=TE"
+ keytool -delete -alias self-signed-truststore -keystore "$TRUSTSTORE_FILE" -storepass "$TRUSTSTORE_PASS" -noprompt
+
+ for filename in "${ca_files[@]}"; do
+
+ echo "### ADDING CERT: $filename"
+ keytool -delete -alias "$filename" -keystore "$TRUSTSTORE_FILE" -storepass "$TRUSTSTORE_PASS" -noprompt > /dev/null 2>&1
+ keytool -importcert -alias "$filename" -file "$filename" -keystore "$TRUSTSTORE_FILE" -storepass "$TRUSTSTORE_PASS" -noprompt
+
+ done
+
+ java -Djavax.net.ssl.trustStore="$TRUSTSTORE_FILE" -Djavax.net.ssl.trustStorePassword="$TRUSTSTORE_PASS" -jar fhir-data-evaluator.jar "$outputDir" "$dateForBundle"
+else
+ echo "# No CA *.pem cert files found in /app/certs -> starting fhir data evaluator without own CAs"
+ java -jar fhir-data-evaluator.jar "$outputDir" "$dateForBundle"
+fi
-java -jar fhir-data-evaluator.jar "$outputDir"
if [ "${CONVERT_TO_CSV}" = true ]; then
bash /app/csv-converter.sh /app/output/"$outputDir"
diff --git a/pom.xml b/pom.xml
index 22f1bc9..60a70f2 100644
--- a/pom.xml
+++ b/pom.xml
@@ -10,7 +10,7 @@
de.medizininformatikinitiative
fhir_data_evaluator
- 1.0.0
+ 1.1.0
Fhir Data Evaluator
Fhir Data Evaluator
@@ -57,6 +57,10 @@
org.springframework.boot
spring-boot-starter-webflux
+
+ org.springframework.security
+ spring-security-oauth2-client
+
info.picocli
picocli
@@ -90,6 +94,11 @@
reactor-test
test
+
+ org.json
+ json
+ 20240303
+
diff --git a/src/main/java/de/medizininformatikinitiative/fhir_data_evaluator/DataStore.java b/src/main/java/de/medizininformatikinitiative/fhir_data_evaluator/DataStore.java
index 3718859..d0629d4 100644
--- a/src/main/java/de/medizininformatikinitiative/fhir_data_evaluator/DataStore.java
+++ b/src/main/java/de/medizininformatikinitiative/fhir_data_evaluator/DataStore.java
@@ -2,9 +2,11 @@
import ca.uhn.fhir.parser.IParser;
import org.hl7.fhir.r4.model.Bundle;
+import org.hl7.fhir.r4.model.DocumentReference;
import org.hl7.fhir.r4.model.Resource;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatusCode;
+import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.reactive.function.client.WebClientResponseException;
@@ -12,6 +14,7 @@
import reactor.core.publisher.Mono;
import reactor.util.retry.Retry;
+import java.io.IOException;
import java.net.URI;
import java.time.Duration;
import java.util.Optional;
@@ -22,23 +25,25 @@ public class DataStore {
private final WebClient client;
private final IParser parser;
private final int pageCount;
+ private final URI reportDestinationServer;
- public DataStore(WebClient client, IParser parser, @Value("${fhir.pageCount}") int pageCount) {
+ public DataStore(WebClient client, IParser parser, @Value("${fhir.pageCount}") int pageCount,
+ @Value("${fhir.reportDestinationServer}") URI reportDestinationServer) {
this.client = client;
this.parser = parser;
this.pageCount = pageCount;
+ this.reportDestinationServer = reportDestinationServer;
}
-
/**
- * Executes {@code populationQuery} and returns all resources found with that query.
+ * Executes {@code query} and returns all resources found with that query.
*
- * @param populationQuery the fhir search query defining the population
- * @return the resources found with the {@code populationQuery}
+ * @param query the fhir search query
+ * @return the resources found with the {@code query}
*/
- public Flux getPopulation(String populationQuery) {
+ public Flux getResources(String query) {
return client.get()
- .uri(appendPageCount(populationQuery))
+ .uri(appendPageCount(query))
.retrieve()
.bodyToFlux(String.class)
.map(response -> parser.parseResource(Bundle.class, response))
@@ -51,6 +56,29 @@ public Flux getPopulation(String populationQuery) {
.flatMap(bundle -> Flux.fromStream(bundle.getEntry().stream().map(Bundle.BundleEntryComponent::getResource)));
}
+ /**
+ * Posts a FHIR Bundle to a FHIR server.
+ *
+ * @param bundle the Bundle to post to the FHIR server
+ * @return a {@link Mono} that completes when the request is successful, or signals an error Mono on
+ * failure
+ */
+ public Mono postReport(String bundle) {
+ return client.post()
+ .uri(reportDestinationServer)
+ .contentType(MediaType.valueOf("application/fhir+json"))
+ .bodyValue(bundle)
+ .retrieve()
+ .onStatus(status -> status.is4xxClientError() || status.is5xxServerError(), clientResponse ->
+ clientResponse.bodyToMono(String.class)
+ .map(errorBody ->
+ new IOException(String.format("Failed uploading MeasureReport with status " +
+ "code: '%s' and body: '%s'", clientResponse.statusCode(), errorBody)))
+ .switchIfEmpty(Mono.error(new IOException(String.format("Failed uploading MeasureReport " +
+ "with status code: '%s'", clientResponse.statusCode())))))
+ .bodyToMono(Void.class);
+ }
+
private static boolean shouldRetry(HttpStatusCode code) {
return code.is5xxServerError() || code.value() == 404;
}
diff --git a/src/main/java/de/medizininformatikinitiative/fhir_data_evaluator/FhirDataEvaluatorApplication.java b/src/main/java/de/medizininformatikinitiative/fhir_data_evaluator/FhirDataEvaluatorApplication.java
index d149af9..f168846 100644
--- a/src/main/java/de/medizininformatikinitiative/fhir_data_evaluator/FhirDataEvaluatorApplication.java
+++ b/src/main/java/de/medizininformatikinitiative/fhir_data_evaluator/FhirDataEvaluatorApplication.java
@@ -5,11 +5,16 @@
import ca.uhn.fhir.parser.IParser;
import org.hl7.fhir.r4.context.IWorkerContext;
import org.hl7.fhir.r4.hapi.ctx.HapiWorkerContext;
+import org.hl7.fhir.r4.model.DocumentReference;
import org.hl7.fhir.r4.model.Extension;
import org.hl7.fhir.r4.model.Measure;
import org.hl7.fhir.r4.model.MeasureReport;
import org.hl7.fhir.r4.model.Quantity;
import org.hl7.fhir.r4.utils.FHIRPathEngine;
+import org.json.JSONObject;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
@@ -19,27 +24,45 @@
import org.springframework.context.annotation.Profile;
import org.springframework.http.HttpHeaders;
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
+import org.springframework.security.oauth2.client.AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager;
+import org.springframework.security.oauth2.client.InMemoryReactiveOAuth2AuthorizedClientService;
+import org.springframework.security.oauth2.client.registration.ClientRegistrations;
+import org.springframework.security.oauth2.client.registration.InMemoryReactiveClientRegistrationRepository;
+import org.springframework.security.oauth2.client.web.reactive.function.client.ServerOAuth2AuthorizedClientExchangeFilterFunction;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.client.ClientRequest;
+import org.springframework.web.reactive.function.client.ExchangeFilterFunction;
import org.springframework.web.reactive.function.client.ExchangeFilterFunctions;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.netty.http.client.HttpClient;
import reactor.netty.resources.ConnectionProvider;
+import java.io.BufferedReader;
import java.io.FileWriter;
import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
import java.nio.file.Files;
import java.nio.file.Path;
+import java.util.UUID;
+import static org.springframework.security.oauth2.core.AuthorizationGrantType.CLIENT_CREDENTIALS;
@SpringBootApplication
public class FhirDataEvaluatorApplication {
+ private static final String REGISTRATION_ID = "openid-connect";
+
@Bean
FhirContext context() {
return FhirContext.forR4();
}
+ @Bean
+ public Logger logger() {
+ return LoggerFactory.getLogger(FhirDataEvaluatorApplication.class);
+ }
+
@Bean
public IParser parser(FhirContext context) {
return context.newJsonParser();
@@ -64,7 +87,8 @@ public WebClient webClient(@Value("${fhir.server}") String fhirServer,
@Value("${fhir.maxConnections}") int maxConnections,
@Value("${fhir.maxQueueSize}") int maxQueueSize,
@Value("${fhir.bearerToken}") String bearerToken,
- @Value("${maxInMemorySizeMib}") int maxInMemorySizeMib) {
+ @Value("${maxInMemorySizeMib}") int maxInMemorySizeMib,
+ @Qualifier("oauth") ExchangeFilterFunction oauthExchangeFilterFunction) {
ConnectionProvider provider = ConnectionProvider.builder("data-store")
.maxConnections(maxConnections)
.pendingAcquireMaxCount(maxQueueSize)
@@ -84,7 +108,37 @@ public WebClient webClient(@Value("${fhir.server}") String fhirServer,
if (!user.isEmpty() && !password.isEmpty()) {
builder = builder.filter(ExchangeFilterFunctions.basicAuthentication(user, password));
}
- return builder.build();
+ return builder.filter(oauthExchangeFilterFunction).build();
+ }
+
+ @Bean
+ @Qualifier("oauth")
+ ExchangeFilterFunction oauthExchangeFilterFunction(
+ @Value("${fhir.oauth.issuer.uri}") String issuerUri,
+ @Value("${fhir.oauth.client.id}") String clientId,
+ @Value("${fhir.oauth.client.secret}") String clientSecret) {
+ if (!issuerUri.isEmpty() && !clientId.isEmpty() && !clientSecret.isEmpty()) {
+ logger().debug("Enabling OAuth2 authentication (issuer uri: '{}', client id: '{}').",
+ issuerUri, clientId);
+ var clientRegistration = ClientRegistrations.fromIssuerLocation(issuerUri)
+ .registrationId(REGISTRATION_ID)
+ .clientId(clientId)
+ .clientSecret(clientSecret)
+ .authorizationGrantType(CLIENT_CREDENTIALS)
+ .build();
+ var registrations = new InMemoryReactiveClientRegistrationRepository(clientRegistration);
+ var clientService = new InMemoryReactiveOAuth2AuthorizedClientService(registrations);
+ var authorizedClientManager = new AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager(
+ registrations, clientService);
+ var oAuthExchangeFilterFunction = new ServerOAuth2AuthorizedClientExchangeFilterFunction(
+ authorizedClientManager);
+ oAuthExchangeFilterFunction.setDefaultClientRegistrationId(REGISTRATION_ID);
+
+ return oAuthExchangeFilterFunction;
+ } else {
+ logger().debug("Skipping OAuth2 authentication.");
+ return (request, next) -> next.exchange(request);
+ }
}
public static void main(String[] args) {
@@ -92,28 +146,45 @@ public static void main(String[] args) {
app.setWebApplicationType(WebApplicationType.NONE);
app.run(args);
}
-
}
@Component
@Profile("!test")
class EvaluationExecutor implements CommandLineRunner {
+ private final static double NANOS_IN_SECOND = 1_000_000_000.0;
+ private final DataStore dataStore;
+ private final Logger logger = LoggerFactory.getLogger(EvaluationExecutor.class);
+
@Value("${measureFile}")
private String measureFilePath;
@Value("${outputDir}")
private String outputDirectory;
+ @Value("${sendReportToServer}")
+ private boolean sendReportToServer;
+ @Value("${authorIdentifierSystem}")
+ private String authorIdentifierSystem;
+ @Value("${authorIdentifierValue}")
+ private String authorIdentifierValue;
+ @Value("${projectIdentifierSystem}")
+ private String projectIdentifierSystem;
+ @Value("${projectIdentifierValue}")
+ private String projectIdentifierValue;
+ @Value("${fhir.reportDestinationServer}")
+ private String reportDestinationServer;
+ private final String TRANSACTION_BUNDLE_TEMPLATE_FILE = "/transaction-bundle-template.json";
+
private final MeasureEvaluator measureEvaluator;
private final IParser parser;
- private final double NANOS_IN_SECOND = 1_000_000_000.0;
private final Quantity durationQuantity = new Quantity()
.setCode("s")
.setSystem("http://unitsofmeasure.org")
.setUnit("u");
- public EvaluationExecutor(MeasureEvaluator measureEvaluator, IParser parser) {
+ public EvaluationExecutor(MeasureEvaluator measureEvaluator, IParser parser, DataStore dataStore) {
this.measureEvaluator = measureEvaluator;
this.parser = parser;
+ this.dataStore = dataStore;
}
private String getMeasureFile() {
@@ -126,6 +197,75 @@ private String getMeasureFile() {
return readMeasure;
}
+ private String readFromInputStream(InputStream inputStream)
+ throws IOException {
+ StringBuilder resultStringBuilder = new StringBuilder();
+ try (BufferedReader br
+ = new BufferedReader(new InputStreamReader(inputStream))) {
+ String line;
+ while ((line = br.readLine()) != null) {
+ resultStringBuilder.append(line).append("\n");
+ }
+ }
+ return resultStringBuilder.toString();
+ }
+
+ private String getBundleTemplate() {
+ try {
+ return readFromInputStream(EvaluationExecutor.class.getResourceAsStream(TRANSACTION_BUNDLE_TEMPLATE_FILE));
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private String getDocRefId() {
+ var documentReferences = dataStore.getResources(reportDestinationServer + "/DocumentReference")
+ .map(r -> (DocumentReference)r).collectList().block();
+
+ var refsWithSameProjId = documentReferences.stream().filter(r ->
+ r.getMasterIdentifier().getSystem().equals(projectIdentifierSystem) &&
+ r.getMasterIdentifier().getValue().equals(projectIdentifierValue)).toList();
+
+ if (refsWithSameProjId.size() > 1) {
+ throw new RuntimeException(String.format("Multiple DocumentReferences exist for masterIdentifier " +
+ "{system: '%s', code: '%s'} on FHIR server - please delete old DocumentReferences for transfer.",
+ projectIdentifierSystem, projectIdentifierValue));
+ } else if (refsWithSameProjId.size() == 1) {
+ return refsWithSameProjId.get(0).getIdPart();
+ } else {
+ return UUID.randomUUID().toString();
+ }
+ }
+
+ private String createTransactionBundle(String date, String report) {
+ var docRefId = getDocRefId();
+
+ JSONObject jo = new JSONObject(getBundleTemplate());
+
+ var authorIdent = jo.getJSONArray("entry").getJSONObject(0).getJSONObject("resource")
+ .getJSONArray("author").getJSONObject(0).getJSONObject("identifier");
+ var masterIdent = jo.getJSONArray("entry").getJSONObject(0).getJSONObject("resource")
+ .getJSONObject("masterIdentifier");
+
+ authorIdent.put("system", authorIdentifierSystem);
+ authorIdent.put("value", authorIdentifierValue);
+ masterIdent.put("system", projectIdentifierSystem);
+ masterIdent.put("value", projectIdentifierValue);
+
+ jo.getJSONArray("entry").getJSONObject(0).getJSONObject("resource").put("date", date);
+
+ var docRefUrl = "urn::uuid:" + UUID.randomUUID();
+ var reportUrl = "urn::uuid:" + UUID.randomUUID();
+ jo.getJSONArray("entry").getJSONObject(0).getJSONObject("resource").getJSONArray("content")
+ .getJSONObject(0).getJSONObject("attachment").put("url", reportUrl);
+ jo.getJSONArray("entry").getJSONObject(0).put("fullUrl", docRefUrl);
+ jo.getJSONArray("entry").getJSONObject(0).getJSONObject("request").put("url", "DocumentReference/" + docRefId);
+ jo.getJSONArray("entry").getJSONObject(0).getJSONObject("resource").put("id", docRefId);
+
+ jo.getJSONArray("entry").getJSONObject(1).put("fullUrl", reportUrl);
+ jo.getJSONArray("entry").getJSONObject(1).put("resource", new JSONObject(report));
+ return jo.toString();
+ }
public void run(String... args) {
String measureFile = getMeasureFile();
@@ -140,13 +280,28 @@ public void run(String... args) {
.setValue(durationQuantity.setValue(evaluationDuration)));
String directoryAddition = args[0];
+ String dateForBundle = args[1];
- try {
- FileWriter fileWriter = new FileWriter(outputDirectory + directoryAddition + "/measure-report.json");
- fileWriter.write(parser.encodeResourceToString(measureReport));
- fileWriter.close();
- } catch (IOException e) {
- throw new RuntimeException(e);
+ String parsedReport = parser.encodeResourceToString(measureReport);
+
+ if(sendReportToServer) {
+ logger.info("Uploading MeasureReport to FHIR server at {}", reportDestinationServer);
+ try {
+ dataStore.postReport(createTransactionBundle(dateForBundle, parsedReport))
+ .doOnSuccess(v -> logger.info("Successfully uploaded MeasureReport to FHIR server"))
+ .block();
+ } catch (RuntimeException e) {
+ logger.error(e.getMessage());
+ }
+
+ } else {
+ try {
+ FileWriter fileWriter = new FileWriter(outputDirectory + directoryAddition + "/measure-report.json");
+ fileWriter.write(parsedReport);
+ fileWriter.close();
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
}
}
}
diff --git a/src/main/java/de/medizininformatikinitiative/fhir_data_evaluator/GroupEvaluator.java b/src/main/java/de/medizininformatikinitiative/fhir_data_evaluator/GroupEvaluator.java
index 9907d35..af6a122 100644
--- a/src/main/java/de/medizininformatikinitiative/fhir_data_evaluator/GroupEvaluator.java
+++ b/src/main/java/de/medizininformatikinitiative/fhir_data_evaluator/GroupEvaluator.java
@@ -45,7 +45,7 @@ public GroupEvaluator(DataStore dataStore, FHIRPathEngine fhirPathEngine) {
* @throws IllegalArgumentException if the group doesn't have exactly one initial population
*/
public Mono evaluateGroup(Measure.MeasureGroupComponent group) {
- var population = dataStore.getPopulation("/" +
+ var population = dataStore.getResources("/" +
findFhirInitialPopulation(group).getCriteria().getExpressionElement());
var measurePopulationExpression = findMeasurePopulationExpression(group);
diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml
index 6e6a848..e5f8685 100644
--- a/src/main/resources/application.yml
+++ b/src/main/resources/application.yml
@@ -6,6 +6,18 @@ fhir:
maxQueueSize: ${FHIR_MAX_QUEUE_SIZE:500}
pageCount: ${FHIR_PAGE_COUNT:1000}
bearerToken: ${FHIR_BEARER_TOKEN:}
+ oauth:
+ issuer:
+ uri: ${FHIR_OAUTH_ISSUER_URI:}
+ client:
+ id: ${FHIR_OAUTH_CLIENT_ID:}
+ secret: ${FHIR_OAUTH_CLIENT_SECRET:}
+ reportDestinationServer: ${FHIR_REPORT_DESTINATION_SERVER:http://localhost:8080/fhir}
maxInMemorySizeMib: ${MAX_IN_MEMORY_SIZE_MIB:10}
measureFile: ${MEASURE_FILE:/app/measure.json}
outputDir: ${OUTPUT_DIR:/app/output/}
+sendReportToServer: ${SEND_REPORT_TO_SERVER:false}
+authorIdentifierSystem: ${AUTHOR_IDENTIFIER_SYSTEM:http://dsf.dev/sid/organization-identifier}
+authorIdentifierValue: ${AUTHOR_IDENTIFIER_VALUE:}
+projectIdentifierSystem: ${PROJECT_IDENTIFIER_SYSTEM:http://medizininformatik-initiative.de/sid/project-identifier}
+projectIdentifierValue: ${PROJECT_IDENTIFIER_VALUE:}
diff --git a/src/main/resources/transaction-bundle-template.json b/src/main/resources/transaction-bundle-template.json
new file mode 100644
index 0000000..d0327ca
--- /dev/null
+++ b/src/main/resources/transaction-bundle-template.json
@@ -0,0 +1,48 @@
+{
+ "resourceType": "Bundle",
+ "type": "transaction",
+ "entry": [
+ {
+ "resource": {
+ "resourceType": "DocumentReference",
+ "masterIdentifier": {
+ "value": "",
+ "system": ""
+ },
+ "author": [
+ {
+ "type": "Organization",
+ "identifier": {
+ "value": "",
+ "system": ""
+ }
+ }
+ ],
+ "docStatus": "final",
+ "date": "",
+ "content": [
+ {
+ "attachment": {
+ "contentType": "application/fhir+xml",
+ "url": ""
+ }
+ }
+ ],
+ "status": "current"
+ },
+ "fullUrl": "",
+ "request": {
+ "url": "",
+ "method": "PUT"
+ }
+ },
+ {
+ "resource": {},
+ "fullUrl": "",
+ "request": {
+ "url": "MeasureReport",
+ "method": "POST"
+ }
+ }
+ ]
+}
diff --git a/src/test/java/de/medizininformatikinitiative/fhir_data_evaluator/DataStoreTest.java b/src/test/java/de/medizininformatikinitiative/fhir_data_evaluator/DataStoreTest.java
index 0b0af6e..f39c000 100644
--- a/src/test/java/de/medizininformatikinitiative/fhir_data_evaluator/DataStoreTest.java
+++ b/src/test/java/de/medizininformatikinitiative/fhir_data_evaluator/DataStoreTest.java
@@ -1,6 +1,5 @@
package de.medizininformatikinitiative.fhir_data_evaluator;
-
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.parser.IParser;
import okhttp3.mockwebserver.MockResponse;
@@ -9,6 +8,7 @@
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
@@ -19,64 +19,126 @@
import java.io.IOException;
class DataStoreTest {
- private static MockWebServer mockStore;
-
- private DataStore dataStore;
- @BeforeAll
- static void setUp() throws IOException {
- mockStore = new MockWebServer();
- mockStore.start();
+ @Nested
+ class TestGet {
+ private static MockWebServer mockStore;
+
+ private DataStore dataStore;
+
+ @BeforeAll
+ static void setUp() throws IOException {
+ mockStore = new MockWebServer();
+ mockStore.start();
+ }
+
+ @AfterAll
+ static void tearDown() throws IOException {
+ mockStore.shutdown();
+ }
+
+ @BeforeEach
+ void initialize() {
+ WebClient client = WebClient.builder()
+ .baseUrl("http://localhost:%d/fhir".formatted(mockStore.getPort()))
+ .defaultHeader("Accept", "application/fhir+json")
+ .build();
+ IParser parser = FhirContext.forR4().newJsonParser();
+ dataStore = new DataStore(client, parser, 1000, null);
+ }
+
+ @ParameterizedTest
+ @DisplayName("retires the request")
+ @ValueSource(ints = {404, 500, 503, 504})
+ void execute_retry(int statusCode) {
+ mockStore.enqueue(new MockResponse().setResponseCode(statusCode));
+ mockStore.enqueue(new MockResponse().setResponseCode(200)
+ .setBody("{\"resourceType\":\"Bundle\", \"entry\": [{\"resource\": {\"resourceType\":\"Observation\"}}]}"));
+
+ var result = dataStore.getResources("/Observation");
+ StepVerifier.create(result).expectNextCount(1).verifyComplete();
+ }
+
+ @Test
+ @DisplayName("fails after 3 unsuccessful retires")
+ void execute_retry_fails() {
+ mockStore.enqueue(new MockResponse().setResponseCode(500));
+ mockStore.enqueue(new MockResponse().setResponseCode(500));
+ mockStore.enqueue(new MockResponse().setResponseCode(500));
+ mockStore.enqueue(new MockResponse().setResponseCode(500));
+ mockStore.enqueue(new MockResponse().setResponseCode(200));
+
+ var result = dataStore.getResources("/Observation");
+
+ StepVerifier.create(result).expectErrorMessage("Retries exhausted: 3/3").verify();
+ }
+
+ @Test
+ @DisplayName("doesn't retry a 400")
+ void execute_retry_400() {
+ mockStore.enqueue(new MockResponse().setResponseCode(400));
+
+ var result = dataStore.getResources("/Observation");
+
+ StepVerifier.create(result).expectError(WebClientResponseException.BadRequest.class).verify();
+ }
}
- @AfterAll
- static void tearDown() throws IOException {
- mockStore.shutdown();
- }
+ @Nested
+ class TestPost {
+ private static MockWebServer mockStore;
- @BeforeEach
- void initialize() {
- WebClient client = WebClient.builder()
- .baseUrl("http://localhost:%d/fhir".formatted(mockStore.getPort()))
- .defaultHeader("Accept", "application/fhir+json")
- .build();
- IParser parser = FhirContext.forR4().newJsonParser();
- dataStore = new DataStore(client, parser, 1000);
- }
+ private DataStore dataStore;
- @ParameterizedTest
- @DisplayName("retires the request")
- @ValueSource(ints = {404, 500, 503, 504})
- void execute_retry(int statusCode) {
- mockStore.enqueue(new MockResponse().setResponseCode(statusCode));
- mockStore.enqueue(new MockResponse().setResponseCode(200)
- .setBody("{\"resourceType\":\"Bundle\", \"entry\": [{\"resource\": {\"resourceType\":\"Observation\"}}]}"));
+ @BeforeAll
+ static void setUp() throws IOException {
+ mockStore = new MockWebServer();
+ mockStore.start();
+ }
- var result = dataStore.getPopulation("/Observation");
- StepVerifier.create(result).expectNextCount(1).verifyComplete();
- }
+ @AfterAll
+ static void tearDown() throws IOException {
+ mockStore.shutdown();
+ }
- @Test
- @DisplayName("fails after 3 unsuccessful retires")
- void execute_retry_fails() {
- mockStore.enqueue(new MockResponse().setResponseCode(500));
- mockStore.enqueue(new MockResponse().setResponseCode(500));
- mockStore.enqueue(new MockResponse().setResponseCode(500));
- mockStore.enqueue(new MockResponse().setResponseCode(500));
- mockStore.enqueue(new MockResponse().setResponseCode(200));
+ @BeforeEach
+ void initialize() {
+ WebClient client = WebClient.builder()
+ .baseUrl("http://localhost:%d/fhir".formatted(mockStore.getPort()))
+ .defaultHeader("Accept", "application/fhir+json")
+ .build();
+ IParser parser = FhirContext.forR4().newJsonParser();
+ dataStore = new DataStore(client, parser, 1000, null);
+ }
- var result = dataStore.getPopulation("/Observation");
+ @Test
+ void test_errorResponse_400() {
+ mockStore.enqueue(new MockResponse().setResponseCode(400).setBody("error encountered"));
- StepVerifier.create(result).expectErrorMessage("Retries exhausted: 3/3").verify();
- }
+ var result = dataStore.postReport("");
+
+ StepVerifier.create(result).verifyErrorMessage(
+ "Failed uploading MeasureReport with status code: '400 BAD_REQUEST' and body: 'error encountered'");
+ }
+
+ @Test
+ void test_errorResponse_withoutBody() {
+ mockStore.enqueue(new MockResponse().setResponseCode(400));
+
+ var result = dataStore.postReport("");
+
+ StepVerifier.create(result).verifyErrorMessage(
+ "Failed uploading MeasureReport with status code: '400 BAD_REQUEST'");
+ }
- @Test
- @DisplayName("doesn't retry a 400")
- void execute_retry_400() {
- mockStore.enqueue(new MockResponse().setResponseCode(400));
+ @Test
+ void test_errorResponse_500() {
+ mockStore.enqueue(new MockResponse().setResponseCode(500).setBody("error encountered"));
- var result = dataStore.getPopulation("/Observation");
+ var result = dataStore.postReport("");
- StepVerifier.create(result).expectError(WebClientResponseException.BadRequest.class).verify();
+ StepVerifier.create(result).verifyErrorMessage(
+ "Failed uploading MeasureReport with status code: '500 INTERNAL_SERVER_ERROR' and body: 'error encountered'");
+ }
}
}
diff --git a/src/test/java/de/medizininformatikinitiative/fhir_data_evaluator/GroupEvaluatorTest.java b/src/test/java/de/medizininformatikinitiative/fhir_data_evaluator/GroupEvaluatorTest.java
index fa35a11..671a7b7 100644
--- a/src/test/java/de/medizininformatikinitiative/fhir_data_evaluator/GroupEvaluatorTest.java
+++ b/src/test/java/de/medizininformatikinitiative/fhir_data_evaluator/GroupEvaluatorTest.java
@@ -212,7 +212,7 @@ public void test_wrongInitialPopulationLanguage() {
@Test
public void test_componentWithoutCoding() {
- when(dataStore.getPopulation("/" + CONDITION_QUERY)).thenReturn(Flux.fromIterable(List.of(getCondition())));
+ when(dataStore.getResources("/" + CONDITION_QUERY)).thenReturn(Flux.fromIterable(List.of(getCondition())));
Measure.MeasureGroupComponent measureGroup = getMeasureGroup()
.setStratifier(List.of
(new Measure.MeasureGroupStratifierComponent()
@@ -227,7 +227,7 @@ public void test_componentWithoutCoding() {
@Test
public void test_componentWithMultipleCodings() {
- when(dataStore.getPopulation("/" + CONDITION_QUERY)).thenReturn(Flux.fromIterable(List.of(getCondition())));
+ when(dataStore.getResources("/" + CONDITION_QUERY)).thenReturn(Flux.fromIterable(List.of(getCondition())));
Measure.MeasureGroupComponent measureGroup = getMeasureGroup()
.setStratifier(List.of(
new Measure.MeasureGroupStratifierComponent()
@@ -245,7 +245,7 @@ public void test_componentWithMultipleCodings() {
@Test
public void test_componentWithWrongLanguage() {
- when(dataStore.getPopulation("/" + CONDITION_QUERY)).thenReturn(Flux.fromIterable(List.of(getCondition())));
+ when(dataStore.getResources("/" + CONDITION_QUERY)).thenReturn(Flux.fromIterable(List.of(getCondition())));
Measure.MeasureGroupComponent measureGroup = getMeasureGroup()
.setStratifier(List.of(
new Measure.MeasureGroupStratifierComponent().setComponent(List.of(
@@ -491,7 +491,7 @@ class SingleStratifierInGroup_withSingleCriteria {
@Test
public void test_oneStratifierElement_oneResultValue_ignoreOtherPopulations() {
- when(dataStore.getPopulation("/" + CONDITION_QUERY)).thenReturn(Flux.fromIterable(List.of(getCondition())));
+ when(dataStore.getResources("/" + CONDITION_QUERY)).thenReturn(Flux.fromIterable(List.of(getCondition())));
Measure.MeasureGroupComponent measureGroup = getMeasureGroup().setStratifier(List.of(new Measure.MeasureGroupStratifierComponent().setCriteria(COND_CODE_PATH)
.setCode(new CodeableConcept(COND_DEF_CODING))))
.setPopulation(List.of(
@@ -515,7 +515,7 @@ public void test_oneStratifierElement_oneResultValue_ignoreOtherPopulations() {
@Test
public void test_oneStratifierElement_oneResultValue() {
- when(dataStore.getPopulation("/" + CONDITION_QUERY)).thenReturn(Flux.fromIterable(List.of(getCondition())));
+ when(dataStore.getResources("/" + CONDITION_QUERY)).thenReturn(Flux.fromIterable(List.of(getCondition())));
Measure.MeasureGroupComponent measureGroup = getMeasureGroup()
.setStratifier(List.of(
new Measure.MeasureGroupStratifierComponent().setCriteria(COND_CODE_PATH).setCode(new CodeableConcept(COND_DEF_CODING))))
@@ -539,7 +539,7 @@ public void test_oneStratifierElement_oneResultValue() {
@Test
public void test_oneStratifierElement_twoSameResultValues() {
- when(dataStore.getPopulation("/" + CONDITION_QUERY)).thenReturn(Flux.fromIterable(List.of(
+ when(dataStore.getResources("/" + CONDITION_QUERY)).thenReturn(Flux.fromIterable(List.of(
getCondition(),
getCondition())));
Measure.MeasureGroupComponent measureGroup = getMeasureGroup()
@@ -566,7 +566,7 @@ public void test_oneStratifierElement_twoSameResultValues() {
@Test
public void test_oneStratifierElement_twoDifferentResultValues() {
- when(dataStore.getPopulation("/" + CONDITION_QUERY)).thenReturn(Flux.fromIterable(List.of(
+ when(dataStore.getResources("/" + CONDITION_QUERY)).thenReturn(Flux.fromIterable(List.of(
getCondition().setCode(new CodeableConcept(new Coding().setSystem(COND_VALUE_SYSTEM).setCode(COND_VALUE_CODE_1))),
getCondition().setCode(new CodeableConcept(new Coding().setSystem(COND_VALUE_SYSTEM).setCode(COND_VALUE_CODE_2))))));
Measure.MeasureGroupComponent measureGroup = getMeasureGroup()
@@ -598,7 +598,7 @@ public void test_oneStratifierElement_twoDifferentResultValues() {
class FailTests {
@Test
public void test_oneStratifierElement_noValue() {
- when(dataStore.getPopulation("/" + CONDITION_QUERY)).thenReturn(Flux.fromIterable(List.of(new Condition())));
+ when(dataStore.getResources("/" + CONDITION_QUERY)).thenReturn(Flux.fromIterable(List.of(new Condition())));
Measure.MeasureGroupComponent measureGroup = getMeasureGroup()
.setStratifier(List.of(
new Measure.MeasureGroupStratifierComponent().setCriteria(COND_CODE_PATH).setCode(new CodeableConcept(COND_DEF_CODING))))
@@ -624,7 +624,7 @@ public void test_oneStratifierElement_noValue() {
@Test
public void test_oneStratifierElement_tooManyValues() {
Coding condCoding = new Coding().setSystem(COND_VALUE_SYSTEM).setCode(COND_VALUE_CODE);
- when(dataStore.getPopulation("/" + CONDITION_QUERY)).thenReturn(Flux.fromIterable(List.of(new Condition().setCode(new CodeableConcept()
+ when(dataStore.getResources("/" + CONDITION_QUERY)).thenReturn(Flux.fromIterable(List.of(new Condition().setCode(new CodeableConcept()
.addCoding(condCoding)
.addCoding(condCoding)))));
Measure.MeasureGroupComponent measureGroup = getMeasureGroup()
@@ -650,7 +650,7 @@ public void test_oneStratifierElement_tooManyValues() {
@Test
public void test_oneStratifierElement_invalidType() {
- when(dataStore.getPopulation("/" + CONDITION_QUERY)).thenReturn(Flux.fromIterable(List.of(getCondition())));
+ when(dataStore.getResources("/" + CONDITION_QUERY)).thenReturn(Flux.fromIterable(List.of(getCondition())));
Measure.MeasureGroupComponent measureGroup = getMeasureGroup()
.setStratifier(List.of(
new Measure.MeasureGroupStratifierComponent().setCriteria(expressionOfPath("Condition.code")).setCode(new CodeableConcept(COND_DEF_CODING))))
@@ -674,7 +674,7 @@ public void test_oneStratifierElement_invalidType() {
@Test
public void test_oneStratifierElement_missingSystem() {
- when(dataStore.getPopulation("/" + CONDITION_QUERY)).thenReturn(Flux.fromIterable(List.of(new Condition().setCode(new CodeableConcept(new Coding().setCode(COND_VALUE_CODE))))));
+ when(dataStore.getResources("/" + CONDITION_QUERY)).thenReturn(Flux.fromIterable(List.of(new Condition().setCode(new CodeableConcept(new Coding().setCode(COND_VALUE_CODE))))));
Measure.MeasureGroupComponent measureGroup = getMeasureGroup()
.setStratifier(List.of(
new Measure.MeasureGroupStratifierComponent().setCriteria(COND_CODE_PATH).setCode(new CodeableConcept(COND_DEF_CODING))))
@@ -698,7 +698,7 @@ public void test_oneStratifierElement_missingSystem() {
@Test
public void test_oneStratifierElement_missingCode() {
- when(dataStore.getPopulation("/" + CONDITION_QUERY)).thenReturn(Flux.fromIterable(List.of(new Condition().setCode(new CodeableConcept(new Coding().setSystem(COND_VALUE_SYSTEM))))));
+ when(dataStore.getResources("/" + CONDITION_QUERY)).thenReturn(Flux.fromIterable(List.of(new Condition().setCode(new CodeableConcept(new Coding().setSystem(COND_VALUE_SYSTEM))))));
Measure.MeasureGroupComponent measureGroup = getMeasureGroup()
.setStratifier(List.of(
new Measure.MeasureGroupStratifierComponent().setCriteria(COND_CODE_PATH).setCode(new CodeableConcept(COND_DEF_CODING))))
@@ -727,7 +727,7 @@ class MultipleStratifiersInGroup_withSingleCriteria {
@Test
public void test_twoSameStratifierElements() {
- when(dataStore.getPopulation("/" + CONDITION_QUERY)).thenReturn(Flux.fromIterable(List.of(getCondition())));
+ when(dataStore.getResources("/" + CONDITION_QUERY)).thenReturn(Flux.fromIterable(List.of(getCondition())));
Measure.MeasureGroupComponent measureGroup = getMeasureGroup()
.setStratifier(List.of(
new Measure.MeasureGroupStratifierComponent().setCriteria(COND_CODE_PATH).setCode(new CodeableConcept(COND_DEF_CODING)),
@@ -760,7 +760,7 @@ public void test_twoSameStratifierElements() {
@Test
public void test_twoStratifierElements_oneResultValueEach() {
- when(dataStore.getPopulation("/" + CONDITION_QUERY)).thenReturn(Flux.fromIterable(List.of(getCondition()
+ when(dataStore.getResources("/" + CONDITION_QUERY)).thenReturn(Flux.fromIterable(List.of(getCondition()
.setClinicalStatus(new CodeableConcept(new Coding(STATUS_VALUE_SYSTEM, STATUS_VALUE_CODE, SOME_DISPLAY))))));
Measure.MeasureGroupComponent measureGroup = getMeasureGroup()
.setStratifier(List.of(
@@ -801,7 +801,7 @@ class StratifierOfMultipleComponents {
class SingleStratifierInGroup {
@Test
public void test_oneStratifierElement_twoDifferentComponents_oneDifferentResultValueEach() {
- when(dataStore.getPopulation("/" + CONDITION_QUERY)).thenReturn(Flux.fromIterable(List.of(
+ when(dataStore.getResources("/" + CONDITION_QUERY)).thenReturn(Flux.fromIterable(List.of(
getCondition().setClinicalStatus(new CodeableConcept(new Coding(STATUS_VALUE_SYSTEM, STATUS_VALUE_CODE, SOME_DISPLAY))))));
Measure.MeasureGroupComponent measureGroup = getMeasureGroup()
.setStratifier(List.of(
@@ -835,7 +835,7 @@ public void test_oneStratifierElement_twoDifferentComponents_oneDifferentResultV
@Test
public void test_oneStratifierElement_twoDifferentComponents_oneSameResultValueEach() {
- when(dataStore.getPopulation("/" + CONDITION_QUERY)).thenReturn(Flux.fromIterable(List.of(getCondition())));
+ when(dataStore.getResources("/" + CONDITION_QUERY)).thenReturn(Flux.fromIterable(List.of(getCondition())));
Measure.MeasureGroupComponent measureGroup = getMeasureGroup()
.setStratifier(List.of(
new Measure.MeasureGroupStratifierComponent()
@@ -885,7 +885,7 @@ public void test_oneStratifierElement_twoDifferentComponents_twoDifferentResultV
new HashableCoding(STATUS_DEF_SYSTEM, STATUS_DEF_CODE, SOME_DISPLAY),
new HashableCoding(STATUS_VALUE_SYSTEM, "status-value-2", SOME_DISPLAY));
- when(dataStore.getPopulation("/" + CONDITION_QUERY)).thenReturn(Flux.fromIterable(List.of(
+ when(dataStore.getResources("/" + CONDITION_QUERY)).thenReturn(Flux.fromIterable(List.of(
getCondition().setCode(condCoding1).setClinicalStatus(statusCoding1),
getCondition().setCode(condCoding1).setClinicalStatus(statusCoding2),
getCondition().setCode(condCoding2).setClinicalStatus(statusCoding1),
@@ -990,7 +990,7 @@ class Code_ofType_CodeType {
@Test
public void test_quantityCode() {
- when(dataStore.getPopulation("/" + OBSERVATION_QUERY)).thenReturn(Flux.fromIterable(List.of(getObservation(NG_ML))));
+ when(dataStore.getResources("/" + OBSERVATION_QUERY)).thenReturn(Flux.fromIterable(List.of(getObservation(NG_ML))));
Measure.MeasureGroupComponent measureGroup = getMeasureGroup()
.setStratifier(List.of(
new Measure.MeasureGroupStratifierComponent().setCriteria(VALUE_PATH).setCode(new CodeableConcept(QUANTITY_DEF_CODING))))
@@ -1020,7 +1020,7 @@ public void test_quantityCode() {
class Code_ofType_Enumeration {
@Test
public void test_gender() {
- when(dataStore.getPopulation("/" + PATIENT_QUERY)).thenReturn(Flux.fromIterable(List.of(getPatient(GENDER))));
+ when(dataStore.getResources("/" + PATIENT_QUERY)).thenReturn(Flux.fromIterable(List.of(getPatient(GENDER))));
Measure.MeasureGroupComponent measureGroup = getMeasureGroup()
.setStratifier(List.of(
new Measure.MeasureGroupStratifierComponent().setCriteria(GENDER_PATH).setCode(new CodeableConcept(GENDER_DEF_CODING))))
@@ -1046,7 +1046,7 @@ public void test_gender() {
@Test
public void test_code_no_value() {
- when(dataStore.getPopulation("/" + PATIENT_QUERY)).thenReturn(Flux.fromIterable(List.of(getPatient(null))));
+ when(dataStore.getResources("/" + PATIENT_QUERY)).thenReturn(Flux.fromIterable(List.of(getPatient(null))));
Measure.MeasureGroupComponent measureGroup = getMeasureGroup()
.setStratifier(List.of(
new Measure.MeasureGroupStratifierComponent().setCriteria(GENDER_PATH).setCode(new CodeableConcept(GENDER_DEF_CODING))))
@@ -1081,7 +1081,7 @@ class BooleanTypeSimple {
@Test
public void test_code_exists() {
- when(dataStore.getPopulation("/" + CONDITION_QUERY)).thenReturn(Flux.fromIterable(List.of(getCondition())));
+ when(dataStore.getResources("/" + CONDITION_QUERY)).thenReturn(Flux.fromIterable(List.of(getCondition())));
Measure.MeasureGroupComponent measureGroup = getMeasureGroup()
.setStratifier(List.of(
new Measure.MeasureGroupStratifierComponent().setCriteria(COND_CODE_EXISTS_PATH).setCode(new CodeableConcept(COND_DEF_CODING))))
@@ -1105,7 +1105,7 @@ public void test_code_exists() {
@Test
public void test_code_exists_not() {
- when(dataStore.getPopulation("/" + CONDITION_QUERY)).thenReturn(Flux.fromIterable(List.of(getCondition().setCode(null))));
+ when(dataStore.getResources("/" + CONDITION_QUERY)).thenReturn(Flux.fromIterable(List.of(getCondition().setCode(null))));
Measure.MeasureGroupComponent measureGroup = getMeasureGroup()
.setStratifier(List.of(
new Measure.MeasureGroupStratifierComponent().setCriteria(COND_CODE_EXISTS_PATH).setCode(new CodeableConcept(COND_DEF_CODING))))
@@ -1137,7 +1137,7 @@ class UniqueCount {
@Test
@DisplayName("Two same values resulting in unique count '1'")
public void test_twoSameValues() {
- when(dataStore.getPopulation("/" + CONDITION_QUERY)).thenReturn(Flux.fromIterable(List.of(
+ when(dataStore.getResources("/" + CONDITION_QUERY)).thenReturn(Flux.fromIterable(List.of(
getConditionWithSubject(UNIQUE_VAL_1),
getConditionWithSubject(UNIQUE_VAL_1))));
Measure.MeasureGroupComponent measureGroup = getMeasureGroup()
@@ -1173,7 +1173,7 @@ public void test_twoSameValues() {
@Test
@DisplayName("Two different values resulting in unique count '2'")
public void test_twoDifferentValues() {
- when(dataStore.getPopulation("/" + CONDITION_QUERY)).thenReturn(Flux.fromIterable(List.of(
+ when(dataStore.getResources("/" + CONDITION_QUERY)).thenReturn(Flux.fromIterable(List.of(
getConditionWithSubject(UNIQUE_VAL_1),
getConditionWithSubject(UNIQUE_VAL_2))));
Measure.MeasureGroupComponent measureGroup = getMeasureGroup()
@@ -1209,7 +1209,7 @@ public void test_twoDifferentValues() {
@Test
@DisplayName("Two same values part of Measure Population and one different value not part of Measure Population")
public void test_twoSameValues_oneDifferentValue_withDifferentMeasurePopulation() {
- when(dataStore.getPopulation("/" + CONDITION_QUERY)).thenReturn(Flux.fromIterable(List.of(
+ when(dataStore.getResources("/" + CONDITION_QUERY)).thenReturn(Flux.fromIterable(List.of(
getConditionWithSubject(UNIQUE_VAL_1).setClinicalStatus(new CodeableConcept(new Coding(STATUS_VALUE_SYSTEM, STATUS_VALUE_CODE, SOME_DISPLAY))),
getConditionWithSubject(UNIQUE_VAL_1).setClinicalStatus(new CodeableConcept(new Coding(STATUS_VALUE_SYSTEM, STATUS_VALUE_CODE, SOME_DISPLAY))),
getConditionWithSubject(UNIQUE_VAL_2))));
@@ -1246,7 +1246,7 @@ public void test_twoSameValues_oneDifferentValue_withDifferentMeasurePopulation(
@Test
@DisplayName("Two Conditions with same value and one Condition with no value, leading to a different Measure Observation Population count")
public void test_twoSameValues_withDifferentObservationPopulation() {
- when(dataStore.getPopulation("/" + CONDITION_QUERY)).thenReturn(Flux.fromIterable(List.of(
+ when(dataStore.getResources("/" + CONDITION_QUERY)).thenReturn(Flux.fromIterable(List.of(
getConditionWithSubject(UNIQUE_VAL_1),
getConditionWithSubject(UNIQUE_VAL_1),
getCondition())));
@@ -1284,7 +1284,7 @@ public void test_twoSameValues_withDifferentObservationPopulation() {
"and group as a whole has also unique-count '1'")
public void test_twoDifferentStratumValues_withSameUniqueValue() {
- when(dataStore.getPopulation("/" + CONDITION_QUERY)).thenReturn(Flux.fromIterable(List.of(
+ when(dataStore.getResources("/" + CONDITION_QUERY)).thenReturn(Flux.fromIterable(List.of(
getConditionWithSubject(UNIQUE_VAL_1)
.setCode(new CodeableConcept(new Coding().setSystem(COND_VALUE_SYSTEM).setCode(COND_VALUE_CODE_1))),
getConditionWithSubject(UNIQUE_VAL_1)
diff --git a/src/test/java/de/medizininformatikinitiative/fhir_data_evaluator/MeasureEvaluatorIntegrationTest.java b/src/test/java/de/medizininformatikinitiative/fhir_data_evaluator/MeasureEvaluatorIntegrationTest.java
index 90a6f49..1ead70c 100644
--- a/src/test/java/de/medizininformatikinitiative/fhir_data_evaluator/MeasureEvaluatorIntegrationTest.java
+++ b/src/test/java/de/medizininformatikinitiative/fhir_data_evaluator/MeasureEvaluatorIntegrationTest.java
@@ -101,7 +101,7 @@ WebClient webClient() {
@SuppressWarnings("resource")
@Container
- private static final GenericContainer> blaze = new GenericContainer<>("samply/blaze:0.25")
+ private static final GenericContainer> blaze = new GenericContainer<>("samply/blaze:0.30")
.withImagePullPolicy(PullPolicy.alwaysPull())
.withEnv("LOG_LEVEL", "debug")
.withExposedPorts(8080)
diff --git a/src/test/java/de/medizininformatikinitiative/fhir_data_evaluator/MeasureEvaluatorUnitTest.java b/src/test/java/de/medizininformatikinitiative/fhir_data_evaluator/MeasureEvaluatorUnitTest.java
index c150b32..f40ada2 100644
--- a/src/test/java/de/medizininformatikinitiative/fhir_data_evaluator/MeasureEvaluatorUnitTest.java
+++ b/src/test/java/de/medizininformatikinitiative/fhir_data_evaluator/MeasureEvaluatorUnitTest.java
@@ -68,7 +68,7 @@ private void assertInitialPopulation(MeasureReport.StratifierGroupPopulationComp
@Test
void oneGroup_oneStratifier_ofOneComponent() {
- when(dataStore.getPopulation("/" + CONDITION_QUERY)).thenReturn(Flux.fromIterable(List.of(getCondition())));
+ when(dataStore.getResources("/" + CONDITION_QUERY)).thenReturn(Flux.fromIterable(List.of(getCondition())));
Measure.MeasureGroupComponent measureGroup = getMeasureGroup()
.setStratifier(List.of(
new Measure.MeasureGroupStratifierComponent().setComponent(List.of(
@@ -90,7 +90,7 @@ void oneGroup_oneStratifier_ofOneComponent() {
@Test
void oneGroup_oneStratifier_ofTwoComponents() {
- when(dataStore.getPopulation("/" + CONDITION_QUERY)).thenReturn(Flux.fromIterable(List.of(
+ when(dataStore.getResources("/" + CONDITION_QUERY)).thenReturn(Flux.fromIterable(List.of(
getCondition().setClinicalStatus(new CodeableConcept(new Coding(STATUS_VALUE_SYSTEM, STATUS_VALUE_CODE, SOME_DISPLAY))))));
Measure.MeasureGroupComponent measureGroup = getMeasureGroup()
.setStratifier(List.of(
@@ -117,7 +117,7 @@ void oneGroup_oneStratifier_ofTwoComponents() {
@Test
void twoGroups_sameStratifier() {
- when(dataStore.getPopulation("/" + CONDITION_QUERY)).thenReturn(Flux.fromIterable(List.of(getCondition())));
+ when(dataStore.getResources("/" + CONDITION_QUERY)).thenReturn(Flux.fromIterable(List.of(getCondition())));
Measure.MeasureGroupComponent measureGroup_1 = getMeasureGroup()
.setStratifier(List.of(
new Measure.MeasureGroupStratifierComponent()