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()