From 458870898fc5ae5db9f8747b566cebb2b6555410 Mon Sep 17 00:00:00 2001 From: Hauke Hund Date: Wed, 29 Jul 2026 12:10:15 +0200 Subject: [PATCH 01/23] version to 2.1.1-SNAPSHOT --- CITATION.cff | 4 ++-- dsf-bpe/dsf-bpe-process-api-v1-base/pom.xml | 2 +- dsf-bpe/dsf-bpe-process-api-v1-impl/pom.xml | 2 +- dsf-bpe/dsf-bpe-process-api-v1-operaton/pom.xml | 2 +- dsf-bpe/dsf-bpe-process-api-v1/pom.xml | 2 +- dsf-bpe/dsf-bpe-process-api-v2-impl/pom.xml | 2 +- dsf-bpe/dsf-bpe-process-api-v2/pom.xml | 2 +- dsf-bpe/dsf-bpe-process-api/pom.xml | 2 +- dsf-bpe/dsf-bpe-server-jetty/pom.xml | 2 +- dsf-bpe/dsf-bpe-server/pom.xml | 2 +- dsf-bpe/dsf-bpe-test-plugin-v1/pom.xml | 2 +- dsf-bpe/dsf-bpe-test-plugin-v2/pom.xml | 2 +- dsf-bpe/dsf-bpe-test-plugin/pom.xml | 2 +- dsf-bpe/pom.xml | 2 +- dsf-common/dsf-common-auth/pom.xml | 2 +- dsf-common/dsf-common-build-info-reader/pom.xml | 2 +- dsf-common/dsf-common-config/pom.xml | 2 +- dsf-common/dsf-common-db/pom.xml | 2 +- dsf-common/dsf-common-docker-secrets-reader/pom.xml | 2 +- dsf-common/dsf-common-documentation/pom.xml | 2 +- dsf-common/dsf-common-jetty/pom.xml | 2 +- dsf-common/dsf-common-oidc/pom.xml | 2 +- dsf-common/dsf-common-status/pom.xml | 2 +- dsf-common/dsf-common-ui/pom.xml | 2 +- dsf-common/pom.xml | 2 +- dsf-fhir/dsf-fhir-rest-adapter/pom.xml | 2 +- dsf-fhir/dsf-fhir-server-jetty/pom.xml | 2 +- dsf-fhir/dsf-fhir-server/pom.xml | 2 +- dsf-fhir/dsf-fhir-validation/pom.xml | 2 +- dsf-fhir/dsf-fhir-webservice-client/pom.xml | 2 +- dsf-fhir/dsf-fhir-websocket-client/pom.xml | 2 +- dsf-fhir/pom.xml | 2 +- dsf-maven/dsf-maven-plugin/pom.xml | 2 +- dsf-maven/pom.xml | 2 +- pom.xml | 2 +- 35 files changed, 36 insertions(+), 36 deletions(-) diff --git a/CITATION.cff b/CITATION.cff index 4ecc275eb..00d750be0 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -24,8 +24,8 @@ preferred-citation: doi: 10.3233/SHTI210060 type: proceedings title: "Data Sharing Framework (DSF)" -version: 2.1.0 -date-released: 2026-04-14 +version: 2.1.1 +date-released: 2026-08-04 url: https://dsf.dev repository-code: https://github.com/datasharingframework/dsf repository-artifact: https://github.com/datasharingframework/dsf/releases diff --git a/dsf-bpe/dsf-bpe-process-api-v1-base/pom.xml b/dsf-bpe/dsf-bpe-process-api-v1-base/pom.xml index eb44c20f9..a6b2dd82d 100644 --- a/dsf-bpe/dsf-bpe-process-api-v1-base/pom.xml +++ b/dsf-bpe/dsf-bpe-process-api-v1-base/pom.xml @@ -23,7 +23,7 @@ dev.dsf dsf-bpe-pom - 2.1.0 + 2.1.1-SNAPSHOT diff --git a/dsf-bpe/dsf-bpe-process-api-v1-impl/pom.xml b/dsf-bpe/dsf-bpe-process-api-v1-impl/pom.xml index 1323ee3b1..c5d2d4fcb 100644 --- a/dsf-bpe/dsf-bpe-process-api-v1-impl/pom.xml +++ b/dsf-bpe/dsf-bpe-process-api-v1-impl/pom.xml @@ -23,7 +23,7 @@ dev.dsf dsf-bpe-pom - 2.1.0 + 2.1.1-SNAPSHOT diff --git a/dsf-bpe/dsf-bpe-process-api-v1-operaton/pom.xml b/dsf-bpe/dsf-bpe-process-api-v1-operaton/pom.xml index 3e3bae16c..95816cf97 100644 --- a/dsf-bpe/dsf-bpe-process-api-v1-operaton/pom.xml +++ b/dsf-bpe/dsf-bpe-process-api-v1-operaton/pom.xml @@ -23,7 +23,7 @@ dev.dsf dsf-bpe-pom - 2.1.0 + 2.1.1-SNAPSHOT diff --git a/dsf-bpe/dsf-bpe-process-api-v1/pom.xml b/dsf-bpe/dsf-bpe-process-api-v1/pom.xml index cb46d8890..328e41ef5 100644 --- a/dsf-bpe/dsf-bpe-process-api-v1/pom.xml +++ b/dsf-bpe/dsf-bpe-process-api-v1/pom.xml @@ -23,7 +23,7 @@ dev.dsf dsf-bpe-pom - 2.1.0 + 2.1.1-SNAPSHOT diff --git a/dsf-bpe/dsf-bpe-process-api-v2-impl/pom.xml b/dsf-bpe/dsf-bpe-process-api-v2-impl/pom.xml index d5e52227a..cd1ea1a2e 100644 --- a/dsf-bpe/dsf-bpe-process-api-v2-impl/pom.xml +++ b/dsf-bpe/dsf-bpe-process-api-v2-impl/pom.xml @@ -23,7 +23,7 @@ dev.dsf dsf-bpe-pom - 2.1.0 + 2.1.1-SNAPSHOT diff --git a/dsf-bpe/dsf-bpe-process-api-v2/pom.xml b/dsf-bpe/dsf-bpe-process-api-v2/pom.xml index ddc147bc6..0fc973937 100644 --- a/dsf-bpe/dsf-bpe-process-api-v2/pom.xml +++ b/dsf-bpe/dsf-bpe-process-api-v2/pom.xml @@ -23,7 +23,7 @@ dev.dsf dsf-bpe-pom - 2.1.0 + 2.1.1-SNAPSHOT diff --git a/dsf-bpe/dsf-bpe-process-api/pom.xml b/dsf-bpe/dsf-bpe-process-api/pom.xml index ba27d7158..0205d2748 100644 --- a/dsf-bpe/dsf-bpe-process-api/pom.xml +++ b/dsf-bpe/dsf-bpe-process-api/pom.xml @@ -25,7 +25,7 @@ dev.dsf dsf-bpe-pom - 2.1.0 + 2.1.1-SNAPSHOT diff --git a/dsf-bpe/dsf-bpe-server-jetty/pom.xml b/dsf-bpe/dsf-bpe-server-jetty/pom.xml index 94a6b1839..608b48094 100755 --- a/dsf-bpe/dsf-bpe-server-jetty/pom.xml +++ b/dsf-bpe/dsf-bpe-server-jetty/pom.xml @@ -23,7 +23,7 @@ dev.dsf dsf-bpe-pom - 2.1.0 + 2.1.1-SNAPSHOT diff --git a/dsf-bpe/dsf-bpe-server/pom.xml b/dsf-bpe/dsf-bpe-server/pom.xml index 9ba226efb..30c57b695 100755 --- a/dsf-bpe/dsf-bpe-server/pom.xml +++ b/dsf-bpe/dsf-bpe-server/pom.xml @@ -23,7 +23,7 @@ dev.dsf dsf-bpe-pom - 2.1.0 + 2.1.1-SNAPSHOT diff --git a/dsf-bpe/dsf-bpe-test-plugin-v1/pom.xml b/dsf-bpe/dsf-bpe-test-plugin-v1/pom.xml index 512dd8b7b..8192f0a65 100644 --- a/dsf-bpe/dsf-bpe-test-plugin-v1/pom.xml +++ b/dsf-bpe/dsf-bpe-test-plugin-v1/pom.xml @@ -23,7 +23,7 @@ dev.dsf dsf-bpe-pom - 2.1.0 + 2.1.1-SNAPSHOT diff --git a/dsf-bpe/dsf-bpe-test-plugin-v2/pom.xml b/dsf-bpe/dsf-bpe-test-plugin-v2/pom.xml index cddcddb30..7dfcb7880 100644 --- a/dsf-bpe/dsf-bpe-test-plugin-v2/pom.xml +++ b/dsf-bpe/dsf-bpe-test-plugin-v2/pom.xml @@ -23,7 +23,7 @@ dev.dsf dsf-bpe-pom - 2.1.0 + 2.1.1-SNAPSHOT diff --git a/dsf-bpe/dsf-bpe-test-plugin/pom.xml b/dsf-bpe/dsf-bpe-test-plugin/pom.xml index 83fe44e36..61bea458f 100644 --- a/dsf-bpe/dsf-bpe-test-plugin/pom.xml +++ b/dsf-bpe/dsf-bpe-test-plugin/pom.xml @@ -23,7 +23,7 @@ dev.dsf dsf-bpe-pom - 2.1.0 + 2.1.1-SNAPSHOT diff --git a/dsf-bpe/pom.xml b/dsf-bpe/pom.xml index 5d9a7aa0e..08fef2cbb 100755 --- a/dsf-bpe/pom.xml +++ b/dsf-bpe/pom.xml @@ -24,7 +24,7 @@ dev.dsf dsf-pom - 2.1.0 + 2.1.1-SNAPSHOT diff --git a/dsf-common/dsf-common-auth/pom.xml b/dsf-common/dsf-common-auth/pom.xml index a0b85b5db..a7b809b77 100644 --- a/dsf-common/dsf-common-auth/pom.xml +++ b/dsf-common/dsf-common-auth/pom.xml @@ -23,7 +23,7 @@ dev.dsf dsf-common-pom - 2.1.0 + 2.1.1-SNAPSHOT diff --git a/dsf-common/dsf-common-build-info-reader/pom.xml b/dsf-common/dsf-common-build-info-reader/pom.xml index c9df2445a..fade0b103 100644 --- a/dsf-common/dsf-common-build-info-reader/pom.xml +++ b/dsf-common/dsf-common-build-info-reader/pom.xml @@ -23,7 +23,7 @@ dev.dsf dsf-common-pom - 2.1.0 + 2.1.1-SNAPSHOT diff --git a/dsf-common/dsf-common-config/pom.xml b/dsf-common/dsf-common-config/pom.xml index ea99ec487..5f5f2b5b4 100644 --- a/dsf-common/dsf-common-config/pom.xml +++ b/dsf-common/dsf-common-config/pom.xml @@ -23,7 +23,7 @@ dev.dsf dsf-common-pom - 2.1.0 + 2.1.1-SNAPSHOT diff --git a/dsf-common/dsf-common-db/pom.xml b/dsf-common/dsf-common-db/pom.xml index e4ebab9a5..d2d8c3414 100644 --- a/dsf-common/dsf-common-db/pom.xml +++ b/dsf-common/dsf-common-db/pom.xml @@ -23,7 +23,7 @@ dev.dsf dsf-common-pom - 2.1.0 + 2.1.1-SNAPSHOT diff --git a/dsf-common/dsf-common-docker-secrets-reader/pom.xml b/dsf-common/dsf-common-docker-secrets-reader/pom.xml index 53738b0cf..87ac31219 100644 --- a/dsf-common/dsf-common-docker-secrets-reader/pom.xml +++ b/dsf-common/dsf-common-docker-secrets-reader/pom.xml @@ -23,7 +23,7 @@ dev.dsf dsf-common-pom - 2.1.0 + 2.1.1-SNAPSHOT diff --git a/dsf-common/dsf-common-documentation/pom.xml b/dsf-common/dsf-common-documentation/pom.xml index ce16e5f65..1dd75fa0f 100644 --- a/dsf-common/dsf-common-documentation/pom.xml +++ b/dsf-common/dsf-common-documentation/pom.xml @@ -23,7 +23,7 @@ dev.dsf dsf-common-pom - 2.1.0 + 2.1.1-SNAPSHOT diff --git a/dsf-common/dsf-common-jetty/pom.xml b/dsf-common/dsf-common-jetty/pom.xml index a2f4612f7..34f905682 100644 --- a/dsf-common/dsf-common-jetty/pom.xml +++ b/dsf-common/dsf-common-jetty/pom.xml @@ -23,7 +23,7 @@ dev.dsf dsf-common-pom - 2.1.0 + 2.1.1-SNAPSHOT diff --git a/dsf-common/dsf-common-oidc/pom.xml b/dsf-common/dsf-common-oidc/pom.xml index 4113ca264..b42aa9a2a 100644 --- a/dsf-common/dsf-common-oidc/pom.xml +++ b/dsf-common/dsf-common-oidc/pom.xml @@ -25,7 +25,7 @@ dev.dsf dsf-common-pom - 2.1.0 + 2.1.1-SNAPSHOT diff --git a/dsf-common/dsf-common-status/pom.xml b/dsf-common/dsf-common-status/pom.xml index 59f9c67b1..b8a03f6e1 100644 --- a/dsf-common/dsf-common-status/pom.xml +++ b/dsf-common/dsf-common-status/pom.xml @@ -23,7 +23,7 @@ dev.dsf dsf-common-pom - 2.1.0 + 2.1.1-SNAPSHOT diff --git a/dsf-common/dsf-common-ui/pom.xml b/dsf-common/dsf-common-ui/pom.xml index 32f6c8f27..3adb67d79 100644 --- a/dsf-common/dsf-common-ui/pom.xml +++ b/dsf-common/dsf-common-ui/pom.xml @@ -23,7 +23,7 @@ dev.dsf dsf-common-pom - 2.1.0 + 2.1.1-SNAPSHOT diff --git a/dsf-common/pom.xml b/dsf-common/pom.xml index 85108968c..c2fd7997f 100644 --- a/dsf-common/pom.xml +++ b/dsf-common/pom.xml @@ -24,7 +24,7 @@ dev.dsf dsf-pom - 2.1.0 + 2.1.1-SNAPSHOT diff --git a/dsf-fhir/dsf-fhir-rest-adapter/pom.xml b/dsf-fhir/dsf-fhir-rest-adapter/pom.xml index 70f5bb7f9..6f2a8869d 100755 --- a/dsf-fhir/dsf-fhir-rest-adapter/pom.xml +++ b/dsf-fhir/dsf-fhir-rest-adapter/pom.xml @@ -23,7 +23,7 @@ dev.dsf dsf-fhir-pom - 2.1.0 + 2.1.1-SNAPSHOT diff --git a/dsf-fhir/dsf-fhir-server-jetty/pom.xml b/dsf-fhir/dsf-fhir-server-jetty/pom.xml index fb873c6bd..ac31c551b 100755 --- a/dsf-fhir/dsf-fhir-server-jetty/pom.xml +++ b/dsf-fhir/dsf-fhir-server-jetty/pom.xml @@ -23,7 +23,7 @@ dev.dsf dsf-fhir-pom - 2.1.0 + 2.1.1-SNAPSHOT diff --git a/dsf-fhir/dsf-fhir-server/pom.xml b/dsf-fhir/dsf-fhir-server/pom.xml index d269e7be3..5ab54be76 100755 --- a/dsf-fhir/dsf-fhir-server/pom.xml +++ b/dsf-fhir/dsf-fhir-server/pom.xml @@ -23,7 +23,7 @@ dev.dsf dsf-fhir-pom - 2.1.0 + 2.1.1-SNAPSHOT diff --git a/dsf-fhir/dsf-fhir-validation/pom.xml b/dsf-fhir/dsf-fhir-validation/pom.xml index 5abbd43de..f6d6bca09 100644 --- a/dsf-fhir/dsf-fhir-validation/pom.xml +++ b/dsf-fhir/dsf-fhir-validation/pom.xml @@ -23,7 +23,7 @@ dev.dsf dsf-fhir-pom - 2.1.0 + 2.1.1-SNAPSHOT diff --git a/dsf-fhir/dsf-fhir-webservice-client/pom.xml b/dsf-fhir/dsf-fhir-webservice-client/pom.xml index 89031ffa8..0a3dc99eb 100755 --- a/dsf-fhir/dsf-fhir-webservice-client/pom.xml +++ b/dsf-fhir/dsf-fhir-webservice-client/pom.xml @@ -23,7 +23,7 @@ dev.dsf dsf-fhir-pom - 2.1.0 + 2.1.1-SNAPSHOT diff --git a/dsf-fhir/dsf-fhir-websocket-client/pom.xml b/dsf-fhir/dsf-fhir-websocket-client/pom.xml index 71d83c108..f3ab0962d 100755 --- a/dsf-fhir/dsf-fhir-websocket-client/pom.xml +++ b/dsf-fhir/dsf-fhir-websocket-client/pom.xml @@ -23,7 +23,7 @@ dev.dsf dsf-fhir-pom - 2.1.0 + 2.1.1-SNAPSHOT diff --git a/dsf-fhir/pom.xml b/dsf-fhir/pom.xml index 648ff250a..29fc49467 100755 --- a/dsf-fhir/pom.xml +++ b/dsf-fhir/pom.xml @@ -24,7 +24,7 @@ dev.dsf dsf-pom - 2.1.0 + 2.1.1-SNAPSHOT diff --git a/dsf-maven/dsf-maven-plugin/pom.xml b/dsf-maven/dsf-maven-plugin/pom.xml index f8b6a5da5..fa6486a2f 100644 --- a/dsf-maven/dsf-maven-plugin/pom.xml +++ b/dsf-maven/dsf-maven-plugin/pom.xml @@ -26,7 +26,7 @@ dev.dsf dsf-maven-pom - 2.1.0 + 2.1.1-SNAPSHOT diff --git a/dsf-maven/pom.xml b/dsf-maven/pom.xml index d25a615d1..79adadeee 100644 --- a/dsf-maven/pom.xml +++ b/dsf-maven/pom.xml @@ -26,7 +26,7 @@ dev.dsf dsf-pom - 2.1.0 + 2.1.1-SNAPSHOT diff --git a/pom.xml b/pom.xml index 60d592342..192534769 100755 --- a/pom.xml +++ b/pom.xml @@ -20,7 +20,7 @@ dev.dsf dsf-pom - 2.1.0 + 2.1.1-SNAPSHOT pom From c918b95dcd206f24891ccc003d0c9e51a3d3c10f Mon Sep 17 00:00:00 2001 From: Hauke Hund Date: Wed, 29 Jul 2026 12:19:26 +0200 Subject: [PATCH 02/23] backport from develop * java, maven plugin, docker, docker-compose, github actions version upgrades * dev setup network and nginx config * missing license header --- .github/workflows/build.yml | 44 ++++---- .trivyignore | 16 +++ .../dsf-bpe-server-jetty/docker/Dockerfile | 4 +- .../docker-compose.yml | 100 ++++++++++++++---- .../proxy/conf.d/dic1.conf | 2 +- .../proxy/conf.d/dic2.conf | 4 +- .../proxy/conf.d/dic3.conf | 4 +- .../proxy/conf.d/localhost.conf | 28 +++++ .../proxy/conf.d/ttp.conf | 4 +- .../proxy/index.html | 91 ++++++++++++++++ .../proxy/nginx.conf | 4 +- dsf-docker/bpe_proxy/Dockerfile | 2 +- dsf-docker/fhir_proxy/Dockerfile | 2 +- .../dsf-fhir-server-jetty/docker/Dockerfile | 4 +- pom.xml | 33 +++--- 15 files changed, 265 insertions(+), 77 deletions(-) create mode 100644 dsf-docker-dev-setup-3dic-ttp/proxy/conf.d/localhost.conf create mode 100644 dsf-docker-dev-setup-3dic-ttp/proxy/index.html diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 91958b916..39fa386f5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -42,16 +42,16 @@ jobs: name: 'codeql: ${{ matrix.language }}' steps: - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Set up JDK 25 if: ${{ matrix.language == 'java-kotlin' }} - uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0 + uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # v5.6.0 with: distribution: 'zulu' java-version: 25 cache: 'maven' - name: Initialize CodeQL - uses: github/codeql-action/init@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2 + uses: github/codeql-action/init@e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81 # v4.37.3 with: languages: ${{ matrix.language }} build-mode: ${{ matrix.build-mode }} @@ -60,7 +60,7 @@ jobs: if: ${{ matrix.language == 'java-kotlin' }} run: mvn package $MVN_BATCH_MODE_FAIL_AT_END $MVN_SKIP_MOST -DskipTests - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2 + uses: github/codeql-action/analyze@e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81 # v4.37.3 with: category: "/language:${{matrix.language}}" @@ -73,9 +73,9 @@ jobs: main: ${{ steps.main.outputs.main }} steps: - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Set up JDK 25 - uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0 + uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # v5.6.0 with: distribution: 'zulu' java-version: 25 @@ -106,7 +106,7 @@ jobs: id: version run: echo "version=$(mvn -q -Dexec.executable=echo -Dexec.args='${project.version}' --non-recursive org.codehaus.mojo:exec-maven-plugin:3.6.3:exec)" >> $GITHUB_OUTPUT - name: Checkout main branch - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: ref: main - name: Check if ref is main HEAD @@ -142,14 +142,14 @@ jobs: timeout-minutes: 8 steps: - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Download quick-build results uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: quick_build path: ./ - name: Set up JDK 25 - uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0 + uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # v5.6.0 with: distribution: 'zulu' java-version: 25 @@ -178,25 +178,25 @@ jobs: name: 'trivy: ${{ matrix.image.name }}' steps: - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Download quick-build results uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: quick_build path: ./ - name: Set up Buildx - uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 + uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0 - name: Build Docker image run: docker build -t ghcr.io/${{ github.repository_owner }}/${{ matrix.image.name }}:${{ github.sha }} ${{ matrix.image.context }} - name: Scan Docker image with Trivy - uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1 # v0.35.0 + uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0 with: image-ref: ghcr.io/${{ github.repository_owner }}/${{ matrix.image.name }}:${{ github.sha }} format: 'sarif' output: 'trivy-results-${{ matrix.image.name }}.sarif' trivyignores: './.trivyignore' - name: Upload Trivy scan results to GitHub Security tab - uses: github/codeql-action/upload-sarif@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2 + uses: github/codeql-action/upload-sarif@e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81 # v4.37.3 if: always() with: sarif_file: 'trivy-results-${{ matrix.image.name }}.sarif' @@ -225,24 +225,24 @@ jobs: name: 'docker-deploy: ${{ matrix.image.name }}' steps: - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Download quick-build results uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: quick_build path: ./ - name: Set up QEMU - uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0 + uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0 - name: Set up Buildx - uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 + uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0 - name: Login to GitHub Container Registry - uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 + uses: docker/login-action@371161bbe7024a29a25c5e19bfcbc0804fe9ad2c # v4.5.2 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Docker metadata - uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0 + uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0 id: meta with: images: ghcr.io/${{ github.repository_owner }}/${{ matrix.image.name }} @@ -257,7 +257,7 @@ jobs: # latest only for stable releases # develop builds - name: Build and Push - uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0 + uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 id: push with: push: true @@ -273,7 +273,7 @@ jobs: - name: Generate SBOM run: syft ghcr.io/${{ github.repository_owner }}/${{ matrix.image.name }}@${DIGEST} -o cyclonedx-json > sbom.json - name: Set up cosign - uses: sigstore/cosign-installer@cad07c2e89fa2edd6e2d7bab4c1aa38e53f76003 # v4.1.1 + uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2 - name: Attach SBOM run: cosign attest --yes --predicate sbom.json --type cyclonedx ghcr.io/${{ github.repository_owner }}/${{ matrix.image.name }}@${DIGEST} - name: Sign image @@ -287,14 +287,14 @@ jobs: contents: read steps: - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Download quick-build results uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: quick_build path: ./ - name: Set up JDK 25 - uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0 + uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # v5.6.0 with: distribution: 'zulu' java-version: 25 diff --git a/.trivyignore b/.trivyignore index 1e04d74d0..0cd739a94 100644 --- a/.trivyignore +++ b/.trivyignore @@ -1,3 +1,19 @@ +# +# Copyright 2018-2025 Heilbronn University of Applied Sciences +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + # Ignore reason: Vulnerable code not used in DSF # CVE title: FHIR Validator HTTP service has SSRF via /loadIG Chains with startsWith() Credential Leak for Authentication Token Theft CVE-2026-34361 diff --git a/dsf-bpe/dsf-bpe-server-jetty/docker/Dockerfile b/dsf-bpe/dsf-bpe-server-jetty/docker/Dockerfile index 725bfa58f..fc0d51a23 100755 --- a/dsf-bpe/dsf-bpe-server-jetty/docker/Dockerfile +++ b/dsf-bpe/dsf-bpe-server-jetty/docker/Dockerfile @@ -14,7 +14,7 @@ # limitations under the License. # -FROM debian:trixie-slim@sha256:4ffb3a1511099754cddc70eb1b12e50ffdb67619aa0ab6c13fcd800a78ef7c7a AS builder +FROM docker.io/library/debian:trixie-slim@sha256:020c0d20b9880058cbe785a9db107156c3c75c2ac944a6aa7ab59f2add76a7bd AS builder WORKDIR /opt/bpe COPY --chown=root:2202 ./ ./ RUN chown root:2202 ./ && \ @@ -23,7 +23,7 @@ RUN chown root:2202 ./ && \ chmod 1775 ./log -FROM azul/zulu-openjdk:25-jre-headless@sha256:9a30f9ac9b51aba2f0ee76daf6440c03d59a7690b4fd6070e85fba1da9fd32ff +FROM docker.io/azul/zulu-openjdk:25-jre-headless@sha256:44df5eb4a2055e85fff47cb5c6bd4edca4045210a1b05beecd8114822e2f2cdd LABEL org.opencontainers.image.source=https://github.com/datasharingframework/dsf LABEL org.opencontainers.image.description="DSF BPE Server" LABEL org.opencontainers.image.licenses="Apache License, Version 2.0" diff --git a/dsf-docker-dev-setup-3dic-ttp/docker-compose.yml b/dsf-docker-dev-setup-3dic-ttp/docker-compose.yml index 3cd1ac2b8..6f53d56dd 100644 --- a/dsf-docker-dev-setup-3dic-ttp/docker-compose.yml +++ b/dsf-docker-dev-setup-3dic-ttp/docker-compose.yml @@ -16,7 +16,7 @@ services: proxy: - image: nginx:1.29 + image: nginx:1.31 restart: "no" ports: - 127.0.0.1:443:443 @@ -34,23 +34,27 @@ services: source: ./proxy/nginx.conf target: /etc/nginx/nginx.conf read_only: true + - type: bind + source: ./proxy/index.html + target: /usr/share/nginx/html/index.html + read_only: true networks: dic1-fhir-frontend: ipv4_address: 172.20.0.2 dic2-fhir-frontend: - ipv4_address: 172.20.0.10 - dic3-fhir-frontend: ipv4_address: 172.20.0.18 + dic3-fhir-frontend: + ipv4_address: 172.20.0.34 ttp-fhir-frontend: - ipv4_address: 172.20.0.26 + ipv4_address: 172.20.0.50 dic1-bpe-frontend: - ipv4_address: 172.20.0.34 + ipv4_address: 172.20.0.66 dic2-bpe-frontend: - ipv4_address: 172.20.0.42 + ipv4_address: 172.20.0.82 dic3-bpe-frontend: - ipv4_address: 172.20.0.50 + ipv4_address: 172.20.0.98 ttp-bpe-frontend: - ipv4_address: 172.20.0.58 + ipv4_address: 172.20.0.114 internet: aliases: - dic1 @@ -94,7 +98,7 @@ services: read_only: true keycloak: - image: quay.io/keycloak/keycloak:26.3 + image: quay.io/keycloak/keycloak:26.7 restart: "no" ports: - 127.0.0.1:8443:8443 @@ -290,7 +294,7 @@ services: DEV_DSF_SERVER_AUTH_OIDC_CLIENT_SECRET_FILE: /run/secrets/oidc_client_dic2_fhir.secret networks: dic2-fhir-frontend: - ipv4_address: 172.20.0.11 + ipv4_address: 172.20.0.19 dic2-fhir-backend: internet: depends_on: @@ -369,7 +373,7 @@ services: DEV_DSF_SERVER_AUTH_OIDC_CLIENT_SECRET_FILE: /run/secrets/oidc_client_dic3_fhir.secret networks: dic3-fhir-frontend: - ipv4_address: 172.20.0.19 + ipv4_address: 172.20.0.35 dic3-fhir-backend: internet: depends_on: @@ -456,7 +460,7 @@ services: DEV_DSF_SERVER_AUTH_OIDC_CLIENT_SECRET_FILE: /run/secrets/oidc_client_ttp_fhir.secret networks: ttp-fhir-frontend: - ipv4_address: 172.20.0.27 + ipv4_address: 172.20.0.51 ttp-fhir-backend: internet: depends_on: @@ -573,7 +577,7 @@ services: DEV_DSF_BPE_FHIR_CLIENT_CONNECTIONS_CONFIG_DEFAULT_TRUST_SERVER_CERTIFICATE_CAS: /run/secrets/root_ca.crt networks: dic1-bpe-frontend: - ipv4_address: 172.20.0.35 + ipv4_address: 172.20.0.67 dic1-bpe-backend: internet: forward-proxy: @@ -664,7 +668,7 @@ services: DEV_DSF_SERVER_AUTH_OIDC_CLIENT_SECRET_FILE: /run/secrets/oidc_client_dic2_bpe.secret networks: dic2-bpe-frontend: - ipv4_address: 172.20.0.43 + ipv4_address: 172.20.0.83 dic2-bpe-backend: internet: depends_on: @@ -753,7 +757,7 @@ services: DEV_DSF_SERVER_AUTH_OIDC_CLIENT_SECRET_FILE: /run/secrets/oidc_client_dic3_bpe.secret networks: dic3-bpe-frontend: - ipv4_address: 172.20.0.51 + ipv4_address: 172.20.0.99 dic3-bpe-backend: internet: depends_on: @@ -843,7 +847,7 @@ services: DEV_DSF_SERVER_AUTH_OIDC_CLIENT_SECRET_FILE: /run/secrets/oidc_client_ttp_bpe.secret networks: ttp-bpe-frontend: - ipv4_address: 172.20.0.59 + ipv4_address: 172.20.0.115 ttp-bpe-backend: internet: depends_on: @@ -964,57 +968,107 @@ networks: config: - subnet: 172.20.0.0/29 dic1-fhir-backend: - dic2-fhir-frontend: driver: bridge ipam: driver: default config: - subnet: 172.20.0.8/29 + dic2-fhir-frontend: + driver: bridge + ipam: + driver: default + config: + - subnet: 172.20.0.16/29 dic2-fhir-backend: + driver: bridge + ipam: + driver: default + config: + - subnet: 172.20.0.24/29 dic3-fhir-frontend: driver: bridge ipam: driver: default config: - - subnet: 172.20.0.16/29 + - subnet: 172.20.0.32/29 dic3-fhir-backend: + driver: bridge + ipam: + driver: default + config: + - subnet: 172.20.0.40/29 ttp-fhir-frontend: driver: bridge ipam: driver: default config: - - subnet: 172.20.0.24/29 + - subnet: 172.20.0.48/29 ttp-fhir-backend: + driver: bridge + ipam: + driver: default + config: + - subnet: 172.20.0.56/29 dic1-bpe-frontend: driver: bridge ipam: driver: default config: - - subnet: 172.20.0.32/29 + - subnet: 172.20.0.64/29 dic1-bpe-backend: + driver: bridge + ipam: + driver: default + config: + - subnet: 172.20.0.72/29 dic2-bpe-frontend: driver: bridge ipam: driver: default config: - - subnet: 172.20.0.40/29 + - subnet: 172.20.0.80/29 dic2-bpe-backend: + driver: bridge + ipam: + driver: default + config: + - subnet: 172.20.0.88/29 dic3-bpe-frontend: driver: bridge ipam: driver: default config: - - subnet: 172.20.0.48/29 + - subnet: 172.20.0.96/29 dic3-bpe-backend: + driver: bridge + ipam: + driver: default + config: + - subnet: 172.20.0.104/29 ttp-bpe-frontend: driver: bridge ipam: driver: default config: - - subnet: 172.20.0.56/29 + - subnet: 172.20.0.112/29 ttp-bpe-backend: + driver: bridge + ipam: + driver: default + config: + - subnet: 172.20.0.120/29 internet: + driver: bridge + ipam: + driver: default + config: + - subnet: 172.20.0.128/28 forward-proxy: + driver: bridge + ipam: + driver: default + config: + - subnet: 172.20.0.144/29 volumes: postgresql: diff --git a/dsf-docker-dev-setup-3dic-ttp/proxy/conf.d/dic1.conf b/dsf-docker-dev-setup-3dic-ttp/proxy/conf.d/dic1.conf index 0580cae33..e7e86ae66 100644 --- a/dsf-docker-dev-setup-3dic-ttp/proxy/conf.d/dic1.conf +++ b/dsf-docker-dev-setup-3dic-ttp/proxy/conf.d/dic1.conf @@ -38,7 +38,7 @@ server { location /bpe { proxy_set_header X-ClientCert $ssl_client_escaped_cert; - proxy_pass http://172.20.0.35:8080/bpe; + proxy_pass http://172.20.0.67:8080/bpe; proxy_http_version 1.1; proxy_set_header Host $http_host; diff --git a/dsf-docker-dev-setup-3dic-ttp/proxy/conf.d/dic2.conf b/dsf-docker-dev-setup-3dic-ttp/proxy/conf.d/dic2.conf index e7cffb32d..7246c61e8 100644 --- a/dsf-docker-dev-setup-3dic-ttp/proxy/conf.d/dic2.conf +++ b/dsf-docker-dev-setup-3dic-ttp/proxy/conf.d/dic2.conf @@ -22,7 +22,7 @@ server { location /fhir { proxy_set_header X-ClientCert $ssl_client_escaped_cert; - proxy_pass http://172.20.0.11:8080/fhir; + proxy_pass http://172.20.0.19:8080/fhir; proxy_http_version 1.1; proxy_set_header Host $http_host; @@ -38,7 +38,7 @@ server { location /bpe { proxy_set_header X-ClientCert $ssl_client_escaped_cert; - proxy_pass http://172.20.0.43:8080/bpe; + proxy_pass http://172.20.0.83:8080/bpe; proxy_http_version 1.1; proxy_set_header Host $http_host; diff --git a/dsf-docker-dev-setup-3dic-ttp/proxy/conf.d/dic3.conf b/dsf-docker-dev-setup-3dic-ttp/proxy/conf.d/dic3.conf index 2b5668b8a..3b9856925 100644 --- a/dsf-docker-dev-setup-3dic-ttp/proxy/conf.d/dic3.conf +++ b/dsf-docker-dev-setup-3dic-ttp/proxy/conf.d/dic3.conf @@ -22,7 +22,7 @@ server { location /fhir { proxy_set_header X-ClientCert $ssl_client_escaped_cert; - proxy_pass http://172.20.0.19:8080/fhir; + proxy_pass http://172.20.0.35:8080/fhir; proxy_http_version 1.1; proxy_set_header Host $http_host; @@ -38,7 +38,7 @@ server { location /bpe { proxy_set_header X-ClientCert $ssl_client_escaped_cert; - proxy_pass http://172.20.0.51:8080/bpe; + proxy_pass http://172.20.0.99:8080/bpe; proxy_http_version 1.1; proxy_set_header Host $http_host; diff --git a/dsf-docker-dev-setup-3dic-ttp/proxy/conf.d/localhost.conf b/dsf-docker-dev-setup-3dic-ttp/proxy/conf.d/localhost.conf new file mode 100644 index 000000000..0ef58e453 --- /dev/null +++ b/dsf-docker-dev-setup-3dic-ttp/proxy/conf.d/localhost.conf @@ -0,0 +1,28 @@ +# +# Copyright 2018-2025 Heilbronn University of Applied Sciences +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +server { + listen 443 ssl; + listen [::]:443 ssl; + + server_name localhost; + + root /usr/share/nginx/html; + + location / { + try_files /index.html =404; + } +} \ No newline at end of file diff --git a/dsf-docker-dev-setup-3dic-ttp/proxy/conf.d/ttp.conf b/dsf-docker-dev-setup-3dic-ttp/proxy/conf.d/ttp.conf index da3d7d9fc..7fd6b7c49 100644 --- a/dsf-docker-dev-setup-3dic-ttp/proxy/conf.d/ttp.conf +++ b/dsf-docker-dev-setup-3dic-ttp/proxy/conf.d/ttp.conf @@ -22,7 +22,7 @@ server { location /fhir { proxy_set_header X-ClientCert $ssl_client_escaped_cert; - proxy_pass http://172.20.0.27:8080/fhir; + proxy_pass http://172.20.0.51:8080/fhir; proxy_http_version 1.1; proxy_set_header Host $http_host; @@ -38,7 +38,7 @@ server { location /bpe { proxy_set_header X-ClientCert $ssl_client_escaped_cert; - proxy_pass http://172.20.0.59:8080/bpe; + proxy_pass http://172.20.0.115:8080/bpe; proxy_http_version 1.1; proxy_set_header Host $http_host; diff --git a/dsf-docker-dev-setup-3dic-ttp/proxy/index.html b/dsf-docker-dev-setup-3dic-ttp/proxy/index.html new file mode 100644 index 000000000..7676a16f5 --- /dev/null +++ b/dsf-docker-dev-setup-3dic-ttp/proxy/index.html @@ -0,0 +1,91 @@ + + + +localhost + + + +
+image/svg+xml +

3 DIC / TTP Dev Setup

+
+ + + \ No newline at end of file diff --git a/dsf-docker-dev-setup-3dic-ttp/proxy/nginx.conf b/dsf-docker-dev-setup-3dic-ttp/proxy/nginx.conf index 6885c87a0..04fa214ef 100644 --- a/dsf-docker-dev-setup-3dic-ttp/proxy/nginx.conf +++ b/dsf-docker-dev-setup-3dic-ttp/proxy/nginx.conf @@ -28,9 +28,7 @@ http { ssl_certificate /run/secrets/localhost.chain.crt; ssl_certificate_key /run/secrets/localhost.key.plain; - ssl_protocols TLSv1.2 TLSv1.3; - ssl_ciphers TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_GCM_SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256; - ssl_prefer_server_ciphers on; + ssl_protocols TLSv1.3; add_header Strict-Transport-Security "max-age=63072000" always; ssl_client_certificate /run/secrets/issuing_ca.crt; diff --git a/dsf-docker/bpe_proxy/Dockerfile b/dsf-docker/bpe_proxy/Dockerfile index 3a2c06070..557f326f8 100644 --- a/dsf-docker/bpe_proxy/Dockerfile +++ b/dsf-docker/bpe_proxy/Dockerfile @@ -14,7 +14,7 @@ # limitations under the License. # -FROM httpd:2.4-alpine@sha256:8f26f33a7002658050e9ab2cd6b77502619dfc89d0a6ba2e9e4a202e0ef04596 +FROM docker.io/library/httpd:2.4-alpine@sha256:1b766f17b84026429b7cb243317b142921b24432336e798bc881c43f45ed9567 LABEL org.opencontainers.image.source=https://github.com/datasharingframework/dsf LABEL org.opencontainers.image.description="DSF BPE Reverse Proxy" LABEL org.opencontainers.image.licenses="Apache License, Version 2.0" diff --git a/dsf-docker/fhir_proxy/Dockerfile b/dsf-docker/fhir_proxy/Dockerfile index 168144f25..750e9f816 100755 --- a/dsf-docker/fhir_proxy/Dockerfile +++ b/dsf-docker/fhir_proxy/Dockerfile @@ -14,7 +14,7 @@ # limitations under the License. # -FROM httpd:2.4-alpine@sha256:8f26f33a7002658050e9ab2cd6b77502619dfc89d0a6ba2e9e4a202e0ef04596 +FROM docker.io/library/httpd:2.4-alpine@sha256:1b766f17b84026429b7cb243317b142921b24432336e798bc881c43f45ed9567 LABEL org.opencontainers.image.source=https://github.com/datasharingframework/dsf LABEL org.opencontainers.image.description="DSF FHIR Reverse Proxy" LABEL org.opencontainers.image.licenses="Apache License, Version 2.0" diff --git a/dsf-fhir/dsf-fhir-server-jetty/docker/Dockerfile b/dsf-fhir/dsf-fhir-server-jetty/docker/Dockerfile index 22d148838..576b73e4d 100755 --- a/dsf-fhir/dsf-fhir-server-jetty/docker/Dockerfile +++ b/dsf-fhir/dsf-fhir-server-jetty/docker/Dockerfile @@ -14,7 +14,7 @@ # limitations under the License. # -FROM debian:trixie-slim@sha256:4ffb3a1511099754cddc70eb1b12e50ffdb67619aa0ab6c13fcd800a78ef7c7a AS builder +FROM docker.io/library/debian:trixie-slim@sha256:020c0d20b9880058cbe785a9db107156c3c75c2ac944a6aa7ab59f2add76a7bd AS builder WORKDIR /opt/fhir COPY --chown=root:2101 ./ ./ RUN chown root:2101 ./ && \ @@ -23,7 +23,7 @@ RUN chown root:2101 ./ && \ chmod 1775 ./log -FROM azul/zulu-openjdk:25-jre-headless@sha256:9a30f9ac9b51aba2f0ee76daf6440c03d59a7690b4fd6070e85fba1da9fd32ff +FROM docker.io/azul/zulu-openjdk:25-jre-headless@sha256:44df5eb4a2055e85fff47cb5c6bd4edca4045210a1b05beecd8114822e2f2cdd LABEL org.opencontainers.image.source=https://github.com/datasharingframework/dsf LABEL org.opencontainers.image.description="DSF FHIR Server" LABEL org.opencontainers.image.licenses="Apache License, Version 2.0" diff --git a/pom.xml b/pom.xml index 192534769..f98d59295 100755 --- a/pom.xml +++ b/pom.xml @@ -39,18 +39,18 @@ ${project.basedir} 2.0.17 - 2.25.4 - 12.1.8 + 2.25.5 + 12.1.10 3.1.11 2.2.2 - 6.2.17 - 2.21.2 + 6.2.19 + 2.21.5 1.1.1 5.1.0 8.4.2 6.5.27 8.4.2 - 1.83 + 1.84 3.8.0 5.2.1 5.2.1 @@ -183,7 +183,7 @@ org.postgresql postgresql - 42.7.10 + 42.7.13 org.checkerframework @@ -453,7 +453,7 @@ org.thymeleaf thymeleaf - 3.1.3.RELEASE + 3.1.5.RELEASE com.nimbusds @@ -542,7 +542,7 @@ org.apache.maven.plugins maven-jar-plugin - 3.5.0 + 3.5.1 @@ -569,12 +569,12 @@ org.apache.maven.plugins maven-surefire-plugin - 3.5.5 + 3.5.6 org.apache.maven.plugins maven-failsafe-plugin - 3.5.5 + 3.5.6 org.apache.maven.plugins @@ -604,7 +604,7 @@ org.apache.maven.plugins maven-dependency-plugin - 3.10.0 + 3.11.0 org.codehaus.mojo @@ -619,17 +619,17 @@ org.apache.maven.plugins maven-enforcer-plugin - 3.6.2 + 3.6.3 org.apache.maven.plugins maven-site-plugin - 3.21.0 + 3.22.0 com.github.spotbugs spotbugs-maven-plugin - 4.9.8.3 + 4.10.3.0 org.apache.maven.plugins @@ -644,7 +644,7 @@ org.sonatype.central central-publishing-maven-plugin - 0.10.0 + 0.11.0 org.apache.maven.plugins @@ -654,7 +654,7 @@ com.mycila license-maven-plugin - 5.0.0 + 5.1.1 org.apache.maven.plugins @@ -1092,6 +1092,7 @@ SCRIPT_STYLE SCRIPT_STYLE SCRIPT_STYLE + SCRIPT_STYLE From 1812d9590e6bce261ee2d7dd99553bdbc4e95a56 Mon Sep 17 00:00:00 2001 From: Reto Wettstein Date: Fri, 29 May 2026 17:28:54 +0200 Subject: [PATCH 03/23] throw bpmn error if translater is applied --- .../v2/activity/MessageSendTaskDelegate.java | 25 +- .../integration/PluginV2IntegrationTest.java | 6 + .../SendTaskErrorBoundaryEventTestThrow.java | 42 ++ .../dsf/bpe/test/spring/config/Config.java | 15 +- .../src/main/resources/bpe/test.bpmn | 510 ++++++++++-------- 5 files changed, 359 insertions(+), 239 deletions(-) create mode 100644 dsf-bpe/dsf-bpe-test-plugin-v2/src/main/java/dev/dsf/bpe/test/message/SendTaskErrorBoundaryEventTestThrow.java diff --git a/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/activity/MessageSendTaskDelegate.java b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/activity/MessageSendTaskDelegate.java index 7ac1d3470..fa1e2ed14 100644 --- a/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/activity/MessageSendTaskDelegate.java +++ b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/activity/MessageSendTaskDelegate.java @@ -47,13 +47,7 @@ public void execute(DelegateExecution execution) throws Exception // do not stop process execution catch (ErrorBoundaryEvent event) { - MessageSendTaskErrorHandler handler = delegate.getErrorHandler(); - if (handler != null) - event = handler.handleErrorBoundaryEvent(api, variables, event); - - if (event != null) - throw new BpmnError(event.getErrorCode(), event.getErrorMessage(), event); - // else, do nothing if event was absorbed by error handler + handleErrorBoundaryEvent(variables, event); } // stop process execution if exception not absorbed by error handler catch (Exception exception) @@ -62,10 +56,25 @@ public void execute(DelegateExecution execution) throws Exception if (handler != null) exception = handler.handleException(api, variables, sendTaskValues, exception); - if (exception != null) + // do not stop process execution if exception translated to error boundary event + if (exception instanceof ErrorBoundaryEvent event) + handleErrorBoundaryEvent(variables, event); + + else if (exception != null) execution.getProcessEngine().getRuntimeService().deleteProcessInstance(execution.getProcessInstanceId(), exception.getMessage()); // else, do nothing if exception was absorbed by error handler } } + + private void handleErrorBoundaryEvent(Variables variables, ErrorBoundaryEvent event) + { + MessageSendTaskErrorHandler handler = delegate.getErrorHandler(); + if (handler != null) + event = handler.handleErrorBoundaryEvent(api, variables, event); + + // do nothing if event was absorbed by error handler + if (event != null) + throw new BpmnError(event.getErrorCode(), event.getErrorMessage(), event); + } } diff --git a/dsf-bpe/dsf-bpe-server/src/test/java/dev/dsf/bpe/integration/PluginV2IntegrationTest.java b/dsf-bpe/dsf-bpe-server/src/test/java/dev/dsf/bpe/integration/PluginV2IntegrationTest.java index 74751a493..3ce5d2dce 100644 --- a/dsf-bpe/dsf-bpe-server/src/test/java/dev/dsf/bpe/integration/PluginV2IntegrationTest.java +++ b/dsf-bpe/dsf-bpe-server/src/test/java/dev/dsf/bpe/integration/PluginV2IntegrationTest.java @@ -111,6 +111,12 @@ public void startSendTaskTest() throws Exception executePluginTest(createTestTask("SendTaskTest")); } + @Test + public void startSendTaskErrorBoundaryEventTestThrow() throws Exception + { + executePluginTest(createTestTask("SendTaskErrorBoundaryEventTestThrow")); + } + @Test public void startFieldInjectionTest() throws Exception { diff --git a/dsf-bpe/dsf-bpe-test-plugin-v2/src/main/java/dev/dsf/bpe/test/message/SendTaskErrorBoundaryEventTestThrow.java b/dsf-bpe/dsf-bpe-test-plugin-v2/src/main/java/dev/dsf/bpe/test/message/SendTaskErrorBoundaryEventTestThrow.java new file mode 100644 index 000000000..51ef77640 --- /dev/null +++ b/dsf-bpe/dsf-bpe-test-plugin-v2/src/main/java/dev/dsf/bpe/test/message/SendTaskErrorBoundaryEventTestThrow.java @@ -0,0 +1,42 @@ +/* + * Copyright 2018-2025 Heilbronn University of Applied Sciences + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package dev.dsf.bpe.test.message; + +import dev.dsf.bpe.test.AbstractTest; +import dev.dsf.bpe.v2.ProcessPluginApi; +import dev.dsf.bpe.v2.activity.MessageSendTask; +import dev.dsf.bpe.v2.activity.values.SendTaskValues; +import dev.dsf.bpe.v2.error.MessageSendTaskErrorHandler; +import dev.dsf.bpe.v2.error.impl.ExceptionToErrorBoundaryEventTranslationErrorHandler; +import dev.dsf.bpe.v2.variables.Variables; + +public class SendTaskErrorBoundaryEventTestThrow extends AbstractTest implements MessageSendTask +{ + public static final String TEST_ERROR_CODE = "testErrorCode"; + public static final String TEST_ERROR_MESSAGE = "testErrorMessage"; + + @Override + public void execute(ProcessPluginApi api, Variables variables, SendTaskValues sendTask) throws Exception + { + throw new RuntimeException(TEST_ERROR_MESSAGE); + } + + @Override + public MessageSendTaskErrorHandler getErrorHandler() + { + return new ExceptionToErrorBoundaryEventTranslationErrorHandler(_ -> TEST_ERROR_CODE); + } +} diff --git a/dsf-bpe/dsf-bpe-test-plugin-v2/src/main/java/dev/dsf/bpe/test/spring/config/Config.java b/dsf-bpe/dsf-bpe-test-plugin-v2/src/main/java/dev/dsf/bpe/test/spring/config/Config.java index e1577c881..19eb319ac 100644 --- a/dsf-bpe/dsf-bpe-test-plugin-v2/src/main/java/dev/dsf/bpe/test/spring/config/Config.java +++ b/dsf-bpe/dsf-bpe-test-plugin-v2/src/main/java/dev/dsf/bpe/test/spring/config/Config.java @@ -30,6 +30,7 @@ import dev.dsf.bpe.test.listener.StartFieldInjectionTestListener; import dev.dsf.bpe.test.listener.StartSendTaskTestListener; import dev.dsf.bpe.test.message.ContinueSendTestSend; +import dev.dsf.bpe.test.message.SendTaskErrorBoundaryEventTestThrow; import dev.dsf.bpe.test.message.SendTaskTest; import dev.dsf.bpe.test.service.ApiTest; import dev.dsf.bpe.test.service.AutowireTest; @@ -90,13 +91,13 @@ public static ActivityPrototypeBeanCreator activityPrototypeBeanCreator() return new ActivityPrototypeBeanCreator(TestActivitySelector.class, ProxyTest.class, ApiTest.class, OrganizationProviderTest.class, EndpointProviderTest.class, FhirClientProviderTest.class, FhirClientConfigProviderTest.class, StartSendTaskTestListener.class, SendTaskTest.class, - StartFieldInjectionTestListener.class, FieldInjectionTest.class, ErrorBoundaryEventTestThrow.class, - ErrorBoundaryEventTestVerify.class, ExceptionTest.class, CompressionServiceTest.class, - ContinueSendTest.class, ContinueSendTestSend.class, ContinueSendTestEvaluate.class, - JsonVariableTestSet.class, JsonVariableTestGet.class, CryptoServiceTest.class, - MimeTypeServiceTest.class, FhirBinaryVariableTestSet.class, FhirBinaryVariableTestGet.class, - DsfClientTest.class, TargetProviderTest.class, DataLoggerTest.class, AutowireTest.class, - QuestionnaireTestAnswer.class, QuestionnaireTestAnswerCheck.class, + SendTaskErrorBoundaryEventTestThrow.class, StartFieldInjectionTestListener.class, + FieldInjectionTest.class, ErrorBoundaryEventTestThrow.class, ErrorBoundaryEventTestVerify.class, + ExceptionTest.class, CompressionServiceTest.class, ContinueSendTest.class, ContinueSendTestSend.class, + ContinueSendTestEvaluate.class, JsonVariableTestSet.class, JsonVariableTestGet.class, + CryptoServiceTest.class, MimeTypeServiceTest.class, FhirBinaryVariableTestSet.class, + FhirBinaryVariableTestGet.class, DsfClientTest.class, TargetProviderTest.class, DataLoggerTest.class, + AutowireTest.class, QuestionnaireTestAnswer.class, QuestionnaireTestAnswerCheck.class, QuestionnaireTestSetIdentifies.class); } diff --git a/dsf-bpe/dsf-bpe-test-plugin-v2/src/main/resources/bpe/test.bpmn b/dsf-bpe/dsf-bpe-test-plugin-v2/src/main/resources/bpe/test.bpmn index 31e3ceee0..038232d5c 100644 --- a/dsf-bpe/dsf-bpe-test-plugin-v2/src/main/resources/bpe/test.bpmn +++ b/dsf-bpe/dsf-bpe-test-plugin-v2/src/main/resources/bpe/test.bpmn @@ -16,7 +16,7 @@ limitations under the License. --> - + Flow_112zq99 @@ -64,6 +64,7 @@ Flow_0paqotx Flow_1wpvvjm Flow_1ih8var + Flow_1gt50du @@ -99,6 +100,7 @@ Flow_1xuklwp Flow_1u24rv8 Flow_0civp0p + Flow_10cr72e Flow_0a1kwg9 @@ -578,6 +580,35 @@ Flow_1ih8var Flow_1mo1h7u + + + + instantiatesCanonicalValue + + + messageNameValue + + + ${testVariable} + + + + Flow_1gt50du + Flow_063u1ol + + + + Flow_1upv70e + Flow_063u1ol + Flow_10cr72e + + + Flow_1upv70e + + + + + @@ -626,92 +657,92 @@ - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + @@ -719,110 +750,121 @@ - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - - - - + - + - + - + + + + - + - - + + + - - + + + - - + + + + + - + - - + + + + + + + + @@ -896,180 +938,180 @@ - - + + - - + + - - + + - - - - + + + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - - + + + - - + + - - - + + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + @@ -1083,162 +1125,182 @@ - - + + - - + + - - - + + + - - + + - - + + - - - + + + - - + + - - + + - - + + - - + + - - + + - - - + + + - - - + + + - - + + - - + + - - + + - - - + + + - - - + + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - - - - - - - - - - - + + - - + + - - - + + + - - + + - - - + + + - - + + - - + + + + + + + + + + + + - - + + + + + + + + + + + + + + + + + + + + + + From 9b260578d1e93c5e5f33c2caed9e9aecbd908091 Mon Sep 17 00:00:00 2001 From: Hauke Hund Date: Wed, 3 Jun 2026 12:08:30 +0200 Subject: [PATCH 04/23] fixed class and constructor visibility Consumers of the TestAssertException should be able to perform instanceof checks, thus the class needs to be public. Only functions in the PluginTestExecutor need to instantiate TestAssertException. --- .../src/main/java/dev/dsf/bpe/test/PluginTestExecutor.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dsf-bpe/dsf-bpe-test-plugin/src/main/java/dev/dsf/bpe/test/PluginTestExecutor.java b/dsf-bpe/dsf-bpe-test-plugin/src/main/java/dev/dsf/bpe/test/PluginTestExecutor.java index 627ce5714..0b4e690c3 100644 --- a/dsf-bpe/dsf-bpe-test-plugin/src/main/java/dev/dsf/bpe/test/PluginTestExecutor.java +++ b/dsf-bpe/dsf-bpe-test-plugin/src/main/java/dev/dsf/bpe/test/PluginTestExecutor.java @@ -31,11 +31,11 @@ public final class PluginTestExecutor { private static final Logger logger = LoggerFactory.getLogger(PluginTestExecutor.class); - private static final class TestAssertException extends RuntimeException + public static final class TestAssertException extends RuntimeException { private static final long serialVersionUID = 1L; - public TestAssertException(String message) + private TestAssertException(String message) { super(message); } From db2f39c3f46974c505e015b0515c285e549abcf7 Mon Sep 17 00:00:00 2001 From: Hauke Hund Date: Wed, 3 Jun 2026 12:22:34 +0200 Subject: [PATCH 05/23] Simplified SendTaskErrorBoundaryEventTestThrow, added missing condition - Set ID of SendTaskErrorBoundaryEventTestThrow send task for easier debugging. - Removed not needed execution listener from SendTaskErrorBoundaryEventTestThrow send task, mandatory profile field now set via constant. - Added missing condition expression to the flow from the exclusive gateway to the SendTaskErrorBoundaryEventTestThrow send task. - Added default flows to the exclusive gateways of the v1 and v2 test BPMNs to catch missing condition expression errors in the future. --- .../src/main/resources/bpe/test.bpmn | 105 ++- .../src/main/resources/bpe/test.bpmn | 825 +++++++++--------- 2 files changed, 479 insertions(+), 451 deletions(-) diff --git a/dsf-bpe/dsf-bpe-test-plugin-v1/src/main/resources/bpe/test.bpmn b/dsf-bpe/dsf-bpe-test-plugin-v1/src/main/resources/bpe/test.bpmn index c506ff46b..e3b6edbff 100644 --- a/dsf-bpe/dsf-bpe-test-plugin-v1/src/main/resources/bpe/test.bpmn +++ b/dsf-bpe/dsf-bpe-test-plugin-v1/src/main/resources/bpe/test.bpmn @@ -16,7 +16,7 @@ limitations under the License. --> - + Flow_112zq99 @@ -37,13 +37,14 @@ Flow_112zq99 Flow_1bqddk1 - + Flow_1bqddk1 Flow_14rzc0j Flow_0iy74da Flow_1h0pa8u Flow_1hhwby8 Flow_0u49lp5 + Flow_04yuv8k @@ -88,106 +89,118 @@ ${testActivity == 'EnvironmentVariableTest'} + + Flow_04yuv8k + + - + - + - + - + - + - + - + - + - + - + + + + - - + + - - - + + + - - + + - - + + - - - + + + - - + + - - + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + + + + + + diff --git a/dsf-bpe/dsf-bpe-test-plugin-v2/src/main/resources/bpe/test.bpmn b/dsf-bpe/dsf-bpe-test-plugin-v2/src/main/resources/bpe/test.bpmn index 038232d5c..5b5cf6346 100644 --- a/dsf-bpe/dsf-bpe-test-plugin-v2/src/main/resources/bpe/test.bpmn +++ b/dsf-bpe/dsf-bpe-test-plugin-v2/src/main/resources/bpe/test.bpmn @@ -16,7 +16,7 @@ limitations under the License. --> - + Flow_112zq99 @@ -37,7 +37,7 @@ Flow_112zq99 Flow_1bqddk1 - + Flow_1bqddk1 Flow_14rzc0j Flow_0iy74da @@ -65,6 +65,7 @@ Flow_1wpvvjm Flow_1ih8var Flow_1gt50du + Flow_1phdcxi @@ -580,7 +581,7 @@ Flow_1ih8var Flow_1mo1h7u - + instantiatesCanonicalValue @@ -589,718 +590,732 @@ messageNameValue - ${testVariable} + profileValue - Flow_1gt50du Flow_063u1ol - + + ${testActivity == 'SendTaskErrorBoundaryEventTestThrow'} + Flow_1upv70e Flow_063u1ol Flow_10cr72e - + Flow_1upv70e - + + + + Flow_1phdcxi + + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - - + + - + + + + - + - + - + - + - + - + - - - - - - - - - - - + + - - - - - - - + + - - + + + + + + + + + + + + - - + + - - - - + + + - - - - + + + + - - - - - - - - + + + - - - - - - - - + + + - - - - - - - - + + + - - - + + + - - - - + + + + - - - + + + + - - - - - + + + + - - - - + + + + - - - - + + + + - - - - + + + + - - - - + + + + - - - + + + + - - - + + + + - - - + + + + - - - - + + + + - - - + + + + - - - - + + + + - - - - + + + + - - - + + + + - - - - + + + + - - - + + + + - - - - + + + + - - - - + + + + - - - - + + + + - - - - + + + + - - - - + + + + - - - - + + + + - - - + + + + - - - - + + + + - - - - + + + + - - - - + + + + - - - - + + + + - - - - + + + + - - - - + + + + - - - - + + + + - - - - + + + + - - - - + + + + - - - - + + + + - - - - + + + + + + + + + - - - + + + - - - - - - - - + + + - - - + + + - - - - + + + + - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + - - + + + + + + + - - - + + + - - - - - - - - - - - - + + - - + + - - + + - - - + + + - - - + + + - - + + - - + + - - + + - - - + + + - - - + + + - - - - - - - - - - - - - - - - - - - - - - + + - - + + - - + + - - + + + + + + - - + + - - - + + + - - + + - - - + + + - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - + + - - + + - - - - + + + + + From ac28656ef53fd560451632c68107d7dca51ac237 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathias=20R=C3=BChle?= Date: Thu, 2 Jul 2026 15:18:54 +0200 Subject: [PATCH 06/23] Fix check for unsupported JWKS algorithm --- .../src/main/java/dev/dsf/bpe/client/oidc/OidcClientJersey.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/client/oidc/OidcClientJersey.java b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/client/oidc/OidcClientJersey.java index 513d8597e..999eb82d5 100644 --- a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/client/oidc/OidcClientJersey.java +++ b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/client/oidc/OidcClientJersey.java @@ -184,7 +184,7 @@ private DecodedJWT verifyAndDecodeAccessToken(String accessToken, Jwks jwks) thr throw new OidcClientException("Access token key with kid '" + keyId + "' and use 'sig' not in JWKS"); Optional algorithm = key.flatMap(JwksKey::toAlgorithm); - if (key.isEmpty()) + if (algorithm.isEmpty()) { throw new OidcClientException("Access token key with kid '" + keyId + "' has unsupported type (kty) / algorithm (alg) / key-size in JWKS"); From 760c595848bf895ae84305324e693cd9f72e3a42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathias=20R=C3=BChle?= Date: Fri, 3 Jul 2026 00:09:39 +0200 Subject: [PATCH 07/23] URL-encode client id and client secret for Basic Auth RFC 6749 requires the client id and client secret to be url encoded first when using HTTP Basic authentication scheme. see https://datatracker.ietf.org/doc/html/rfc6749#section-2.3.1 --- .../dsf/bpe/client/oidc/OidcClientJersey.java | 8 +- .../bpe/client/oidc/OidcClientJerseyTest.java | 127 +++++++++++++++++- 2 files changed, 129 insertions(+), 6 deletions(-) diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/client/oidc/OidcClientJersey.java b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/client/oidc/OidcClientJersey.java index 999eb82d5..67cf802d8 100644 --- a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/client/oidc/OidcClientJersey.java +++ b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/client/oidc/OidcClientJersey.java @@ -15,6 +15,7 @@ */ package dev.dsf.bpe.client.oidc; +import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.security.KeyStore; import java.time.Duration; @@ -151,9 +152,10 @@ public DecodedJWT getAccessTokenDecoded(OidcConfiguration configuration, Jwks jw Response response = client.target(tokenEndpoint).request(MediaType.APPLICATION_JSON_TYPE) .header(HttpHeaders.AUTHORIZATION, - "Basic " + Base64.getEncoder() - .encodeToString(new StringBuilder().append(clientId).append(':').append(clientSecret) - .toString().getBytes(StandardCharsets.US_ASCII))) + "Basic " + Base64.getEncoder().encodeToString(new StringBuilder() + .append(URLEncoder.encode(clientId, StandardCharsets.US_ASCII)).append(':') + .append(URLEncoder.encode(String.valueOf(clientSecret), StandardCharsets.US_ASCII)) + .toString().getBytes(StandardCharsets.US_ASCII))) .post(Entity.form(new Form().param("grant_type", "client_credentials"))); if (response.getStatus() == Status.OK.getStatusCode()) diff --git a/dsf-bpe/dsf-bpe-server/src/test/java/dev/dsf/bpe/client/oidc/OidcClientJerseyTest.java b/dsf-bpe/dsf-bpe-server/src/test/java/dev/dsf/bpe/client/oidc/OidcClientJerseyTest.java index e9f6360bf..34954bada 100644 --- a/dsf-bpe/dsf-bpe-server/src/test/java/dev/dsf/bpe/client/oidc/OidcClientJerseyTest.java +++ b/dsf-bpe/dsf-bpe-server/src/test/java/dev/dsf/bpe/client/oidc/OidcClientJerseyTest.java @@ -15,22 +15,51 @@ */ package dev.dsf.bpe.client.oidc; +import static java.nio.charset.StandardCharsets.US_ASCII; + +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThrows; +import java.io.IOException; +import java.io.OutputStream; +import java.net.InetSocketAddress; +import java.net.URLEncoder; import java.nio.file.Paths; import java.security.KeyStore; import java.time.Duration; +import java.util.Base64; +import java.util.List; +import java.util.Set; import org.junit.Ignore; +import org.junit.Rule; import org.junit.Test; +import org.junit.rules.ExternalResource; + +import com.sun.net.httpserver.HttpsConfigurator; +import com.sun.net.httpserver.HttpsServer; +import de.hsheilbronn.mi.utils.crypto.context.SSLContextFactory; import de.hsheilbronn.mi.utils.crypto.io.PemReader; import de.hsheilbronn.mi.utils.crypto.keystore.KeyStoreCreator; +import dev.dsf.bpe.api.client.oidc.OidcClientException; +import dev.dsf.bpe.integration.X509Certificates; +import dev.dsf.common.oidc.Jwks; +import dev.dsf.common.oidc.OidcConfiguration; +import jakarta.ws.rs.core.HttpHeaders; -@Ignore -// Needs keycloak service from 3dic-ttp dev setup, "Service accounts roles" needs to be activated for dic1-fhir client public class OidcClientJerseyTest { + @Rule + public final X509Certificates certificates = new X509Certificates(); + + @Rule + public final TokenEndpointServerResource tokenEndpointServerResource = new TokenEndpointServerResource(); + + // Needs keycloak service from 3dic-ttp dev setup, "Service accounts roles" needs to be activated for dic1-fhir + // client + @Ignore @Test public void getAccessToken() throws Exception { @@ -45,4 +74,96 @@ public void getAccessToken() throws Exception char[] accessToken = client.asOidcClientWithDecodedJwt().getAccessToken(); assertNotNull(accessToken); } -} \ No newline at end of file + + @Test + public void basicAuthorizationClientIdAndSecretShouldBeUrlEncoded() throws Exception + { + var clientId = "client/id:+"; + var clientSecret = "client/secret:+"; + var expectedAuthorizationHeaderValue = "Basic ".concat(clientId.transform(id -> URLEncoder.encode(id, US_ASCII)) + .concat(":").concat(clientSecret.transform(secret -> URLEncoder.encode(secret, US_ASCII))) + .transform(s -> Base64.getEncoder().encodeToString(s.getBytes(US_ASCII)))); + var client = new OidcClientJersey("https://localhost:" + tokenEndpointServerResource.getPort(), + "/.well-known/openid-configuration", clientId, clientSecret.toCharArray(), + certificates.getFhirServerCertificate().trustStore(), null, null, null, null, null, "Test Client", + Duration.ofSeconds(10), Duration.ofSeconds(5), true, Duration.ofSeconds(10), null, false); + var config = new OidcConfiguration("https://localhost:" + tokenEndpointServerResource.getPort(), + "https://localhost:" + tokenEndpointServerResource.getPort() + "/token", "https://localhost/jwks", + Set.of("client_credentials")); + + // token is intentionally invalid as it is not the focus of this test + // and to simplify test code + assertThrows(OidcClientException.class, () -> client.getAccessTokenDecoded(config, new Jwks(List.of()))); + + assertEquals(expectedAuthorizationHeaderValue, tokenEndpointServerResource.getAuthorizationHeaderValue()); + } + + private class TokenEndpointServerResource extends ExternalResource + { + private HttpsServer server; + private String authorizationHeaderValue; + + @Override + protected void before() throws Throwable + { + // Server will be created lazily when getPort() is first called + } + + @Override + protected void after() + { + if (server != null) + { + server.stop(0); + } + } + + int getPort() throws Exception + { + if (server == null) + { + server = createServer(); + server.start(); + } + return server.getAddress().getPort(); + } + + String getAuthorizationHeaderValue() + { + return authorizationHeaderValue; + } + + private HttpsServer createServer() throws Exception + { + var newServer = HttpsServer.create(new InetSocketAddress("localhost", 0), 0); + var serverCertificate = certificates.getFhirServerCertificate(); + + var keyStorePassword = "server-password".toCharArray(); + KeyStore keyStore = KeyStoreCreator.jksForPrivateKeyAndCertificateChain(serverCertificate.privateKey(), + keyStorePassword, serverCertificate.certificate(), serverCertificate.caCertificate()); + + newServer.setHttpsConfigurator( + new HttpsConfigurator(SSLContextFactory.createSSLContext(null, keyStore, keyStorePassword, "TLS"))); + + newServer.createContext("/token", exchange -> + { + authorizationHeaderValue = exchange.getRequestHeaders().getFirst(HttpHeaders.AUTHORIZATION); + + byte[] response = "{\"access_token\":\"invalid\",\"expires_in\":300}".getBytes(US_ASCII); + exchange.getResponseHeaders().set(HttpHeaders.CONTENT_TYPE, "application/json"); + exchange.sendResponseHeaders(200, response.length); + try (OutputStream out = exchange.getResponseBody()) + { + out.write(response); + } + catch (IOException e) + { + throw new RuntimeException(e); + } + exchange.close(); + }); + + return newServer; + } + } +} From b62f60bda30e407d3af2bf23dddfa4a7b0f7e175 Mon Sep 17 00:00:00 2001 From: Hauke Hund Date: Sat, 4 Jul 2026 16:03:43 +0200 Subject: [PATCH 08/23] Fixed encoding as defined in RFC 6749 - Appendix B -> UTF-8 --- .../dsf/bpe/client/oidc/OidcClientJersey.java | 9 ++-- .../bpe/client/oidc/OidcClientJerseyTest.java | 47 +++++++++++++++---- 2 files changed, 42 insertions(+), 14 deletions(-) diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/client/oidc/OidcClientJersey.java b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/client/oidc/OidcClientJersey.java index 67cf802d8..7f96d9c16 100644 --- a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/client/oidc/OidcClientJersey.java +++ b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/client/oidc/OidcClientJersey.java @@ -152,10 +152,11 @@ public DecodedJWT getAccessTokenDecoded(OidcConfiguration configuration, Jwks jw Response response = client.target(tokenEndpoint).request(MediaType.APPLICATION_JSON_TYPE) .header(HttpHeaders.AUTHORIZATION, - "Basic " + Base64.getEncoder().encodeToString(new StringBuilder() - .append(URLEncoder.encode(clientId, StandardCharsets.US_ASCII)).append(':') - .append(URLEncoder.encode(String.valueOf(clientSecret), StandardCharsets.US_ASCII)) - .toString().getBytes(StandardCharsets.US_ASCII))) + "Basic " + Base64.getEncoder() + .encodeToString(new StringBuilder() + .append(URLEncoder.encode(clientId, StandardCharsets.UTF_8)).append(':') + .append(URLEncoder.encode(String.valueOf(clientSecret), StandardCharsets.UTF_8)) + .toString().getBytes(StandardCharsets.UTF_8))) .post(Entity.form(new Form().param("grant_type", "client_credentials"))); if (response.getStatus() == Status.OK.getStatusCode()) diff --git a/dsf-bpe/dsf-bpe-server/src/test/java/dev/dsf/bpe/client/oidc/OidcClientJerseyTest.java b/dsf-bpe/dsf-bpe-server/src/test/java/dev/dsf/bpe/client/oidc/OidcClientJerseyTest.java index 34954bada..573879205 100644 --- a/dsf-bpe/dsf-bpe-server/src/test/java/dev/dsf/bpe/client/oidc/OidcClientJerseyTest.java +++ b/dsf-bpe/dsf-bpe-server/src/test/java/dev/dsf/bpe/client/oidc/OidcClientJerseyTest.java @@ -15,16 +15,13 @@ */ package dev.dsf.bpe.client.oidc; -import static java.nio.charset.StandardCharsets.US_ASCII; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertThrows; +import static org.junit.Assert.*; import java.io.IOException; import java.io.OutputStream; import java.net.InetSocketAddress; import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; import java.nio.file.Paths; import java.security.KeyStore; import java.time.Duration; @@ -79,10 +76,11 @@ public void getAccessToken() throws Exception public void basicAuthorizationClientIdAndSecretShouldBeUrlEncoded() throws Exception { var clientId = "client/id:+"; - var clientSecret = "client/secret:+"; - var expectedAuthorizationHeaderValue = "Basic ".concat(clientId.transform(id -> URLEncoder.encode(id, US_ASCII)) - .concat(":").concat(clientSecret.transform(secret -> URLEncoder.encode(secret, US_ASCII))) - .transform(s -> Base64.getEncoder().encodeToString(s.getBytes(US_ASCII)))); + var clientSecret = "client/s€cr€t:+"; + var expectedAuthorizationHeaderValue = "Basic " + .concat(clientId.transform(id -> URLEncoder.encode(id, StandardCharsets.UTF_8)).concat(":") + .concat(clientSecret.transform(secret -> URLEncoder.encode(secret, StandardCharsets.UTF_8))) + .transform(s -> Base64.getEncoder().encodeToString(s.getBytes(StandardCharsets.UTF_8)))); var client = new OidcClientJersey("https://localhost:" + tokenEndpointServerResource.getPort(), "/.well-known/openid-configuration", clientId, clientSecret.toCharArray(), certificates.getFhirServerCertificate().trustStore(), null, null, null, null, null, "Test Client", @@ -149,7 +147,7 @@ private HttpsServer createServer() throws Exception { authorizationHeaderValue = exchange.getRequestHeaders().getFirst(HttpHeaders.AUTHORIZATION); - byte[] response = "{\"access_token\":\"invalid\",\"expires_in\":300}".getBytes(US_ASCII); + byte[] response = "{\"access_token\":\"invalid\",\"expires_in\":300}".getBytes(StandardCharsets.UTF_8); exchange.getResponseHeaders().set(HttpHeaders.CONTENT_TYPE, "application/json"); exchange.sendResponseHeaders(200, response.length); try (OutputStream out = exchange.getResponseBody()) @@ -166,4 +164,33 @@ private HttpsServer createServer() throws Exception return newServer; } } + + @Test + public void testEncoding() throws Exception + { + /* + * RFC 6749, Appendix B - https://datatracker.ietf.org/doc/html/rfc6749#appendix-B + * + * [...] + * + * When parsing data from a payload using this media type, the names and values resulting from reversing the + * name/value encoding consequently need to be treated as octet sequences, to be decoded using the UTF-8 + * character encoding scheme. + * + * For example, the value consisting of the six Unicode code points (1) U+0020 (SPACE), (2) U+0025 (PERCENT + * SIGN), (3) U+0026 (AMPERSAND), (4) U+002B (PLUS SIGN), (5) U+00A3 (POUND SIGN), and (6) U+20AC (EURO SIGN) + * would be encoded into the octet sequence below (using hexadecimal notation): + * + * 20 25 26 2B C2 A3 E2 82 AC + * + * and then represented in the payload as: + * + * +%25%26%2B%C2%A3%E2%82%AC + */ + + String example = "\u0020\u0025\u0026\u002B\u00A3\u20AC"; + String expected = "+%25%26%2B%C2%A3%E2%82%AC"; + + assertEquals(expected, URLEncoder.encode(example, StandardCharsets.UTF_8)); + } } From 6d802fa7421ea1751b72cd5d24cb22d00339b9da Mon Sep 17 00:00:00 2001 From: Hauke Hund Date: Tue, 7 Jul 2026 17:57:44 +0200 Subject: [PATCH 09/23] new test to reproduce issue #535 --- .../dev/dsf/bpe/test/service/JsonVariableTestGet.java | 9 +++++++++ .../dev/dsf/bpe/test/service/JsonVariableTestSet.java | 2 ++ 2 files changed, 11 insertions(+) diff --git a/dsf-bpe/dsf-bpe-test-plugin-v2/src/main/java/dev/dsf/bpe/test/service/JsonVariableTestGet.java b/dsf-bpe/dsf-bpe-test-plugin-v2/src/main/java/dev/dsf/bpe/test/service/JsonVariableTestGet.java index 638db9972..7514efebc 100644 --- a/dsf-bpe/dsf-bpe-test-plugin-v2/src/main/java/dev/dsf/bpe/test/service/JsonVariableTestGet.java +++ b/dsf-bpe/dsf-bpe-test-plugin-v2/src/main/java/dev/dsf/bpe/test/service/JsonVariableTestGet.java @@ -16,6 +16,7 @@ package dev.dsf.bpe.test.service; import static dev.dsf.bpe.test.PluginTestExecutor.expectNotNull; +import static dev.dsf.bpe.test.PluginTestExecutor.expectNull; import static dev.dsf.bpe.test.PluginTestExecutor.expectSame; import dev.dsf.bpe.test.AbstractTest; @@ -62,4 +63,12 @@ public void getIntegerVariable(Variables variables) throws Exception expectNotNull(variable); expectSame(JsonVariableTestSet.TEST_INTEGER, variable); } + + @PluginTest + public void getJsonNullVariable(Variables variables) throws Exception + { + JsonPojo variable = variables.getVariable(JsonVariableTestSet.JSON_NULL_VARIABLE); + + expectNull(variable); + } } diff --git a/dsf-bpe/dsf-bpe-test-plugin-v2/src/main/java/dev/dsf/bpe/test/service/JsonVariableTestSet.java b/dsf-bpe/dsf-bpe-test-plugin-v2/src/main/java/dev/dsf/bpe/test/service/JsonVariableTestSet.java index fe0564455..82d2668f6 100644 --- a/dsf-bpe/dsf-bpe-test-plugin-v2/src/main/java/dev/dsf/bpe/test/service/JsonVariableTestSet.java +++ b/dsf-bpe/dsf-bpe-test-plugin-v2/src/main/java/dev/dsf/bpe/test/service/JsonVariableTestSet.java @@ -26,6 +26,7 @@ public class JsonVariableTestSet implements ServiceTask { public static final String JSON_VARIABLE = "json-variable"; + public static final String JSON_NULL_VARIABLE = "json-null-variable"; public static final String STRING_VARIABLE = "string-variable"; public static final String INTEGER_VARIABLE = "integer-variable"; @@ -41,5 +42,6 @@ public void execute(ProcessPluginApi api, Variables variables) throws ErrorBound variables.setJsonVariable(JSON_VARIABLE, new JsonPojo(TEST_VALUE_1, TEST_VALUE_2, TEST_ZONED_DATE_TIME_VALUE)); variables.setString(STRING_VARIABLE, TEST_STRING); variables.setInteger(INTEGER_VARIABLE, TEST_INTEGER); + variables.setJsonVariable(JSON_NULL_VARIABLE, null); } } From 4985d700dbe8d741db20448047f6f4db586e15cf Mon Sep 17 00:00:00 2001 From: Hauke Hund Date: Tue, 7 Jul 2026 18:03:35 +0200 Subject: [PATCH 10/23] special cases for null json values to fix issue #535 --- .../dev/dsf/bpe/v2/variables/JsonHolder.java | 31 +++++++++++++++---- .../v2/variables/JsonHolderSerializer.java | 3 +- .../dsf/bpe/v2/variables/VariablesImpl.java | 11 +++++-- 3 files changed, 36 insertions(+), 9 deletions(-) diff --git a/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/variables/JsonHolder.java b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/variables/JsonHolder.java index 92ea7188c..f747eda08 100644 --- a/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/variables/JsonHolder.java +++ b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/variables/JsonHolder.java @@ -19,19 +19,33 @@ public class JsonHolder { - private final String dataClassName; - private final byte[] data; - /** * @param dataClassName * not null * @param data * not null + * @return */ - public JsonHolder(String dataClassName, byte[] data) + public static JsonHolder of(String dataClassName, byte[] data) + { + Objects.requireNonNull(dataClassName, "dataClassName"); + Objects.requireNonNull(data, "data"); + + return new JsonHolder(dataClassName, data); + } + + public static JsonHolder empty() + { + return new JsonHolder(null, null); + } + + private final String dataClassName; + private final byte[] data; + + private JsonHolder(String dataClassName, byte[] data) { - this.dataClassName = Objects.requireNonNull(dataClassName, "dataClassName"); - this.data = Objects.requireNonNull(data, "data"); + this.dataClassName = dataClassName; + this.data = data; } public String getDataClassName() @@ -43,4 +57,9 @@ public byte[] getData() { return data; } + + public boolean isEmpty() + { + return dataClassName == null || data == null; + } } diff --git a/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/variables/JsonHolderSerializer.java b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/variables/JsonHolderSerializer.java index 12e55449f..45caed302 100644 --- a/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/variables/JsonHolderSerializer.java +++ b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/variables/JsonHolderSerializer.java @@ -52,7 +52,8 @@ public JsonHolderValue readValue(ValueFields valueFields, boolean asTransientVal String dataClassName = valueFields.getTextValue(); byte[] data = valueFields.getByteArrayValue(); - return JsonHolderValues.create(new JsonHolder(dataClassName, data)); + return JsonHolderValues.create( + dataClassName == null || data == null ? JsonHolder.empty() : JsonHolder.of(dataClassName, data)); } @Override diff --git a/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/variables/VariablesImpl.java b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/variables/VariablesImpl.java index 39b56d71c..f85f9dc65 100644 --- a/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/variables/VariablesImpl.java +++ b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/variables/VariablesImpl.java @@ -107,12 +107,15 @@ public VariablesImpl(DelegateExecution execution, ObjectMapper objectMapper, Dsf private JsonHolder toJsonHolder(Object json) { + if (json == null) + return JsonHolder.empty(); + try { byte[] data = objectMapper.writeValueAsBytes(json); String dataClassName = json.getClass().getName(); - return new JsonHolder(dataClassName, data); + return JsonHolder.of(dataClassName, data); } catch (JsonProcessingException e) { @@ -123,11 +126,15 @@ private JsonHolder toJsonHolder(Object json) @SuppressWarnings("unchecked") private T fromJsonHolder(JsonHolder holder) { + if (holder.isEmpty()) + return null; + try { byte[] data = holder.getData(); - Class dataClass = getClassLoader().loadClass(holder.getDataClassName()); + String dataClassName = holder.getDataClassName(); + Class dataClass = getClassLoader().loadClass(dataClassName); return (T) objectMapper.readValue(data, dataClass); } catch (ClassNotFoundException | IOException e) From b6735d64bbf49e7c81b5692ed58454bca268e338 Mon Sep 17 00:00:00 2001 From: Hauke Hund Date: Wed, 22 Jul 2026 13:58:02 +0200 Subject: [PATCH 11/23] fixed bad handling of input values with index 0, some code cleanup --- .../src/main/resources/fhir/static/form.js | 24 +++++++++---------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/dsf-fhir/dsf-fhir-server/src/main/resources/fhir/static/form.js b/dsf-fhir/dsf-fhir-server/src/main/resources/fhir/static/form.js index 0d96348e3..a598a329d 100644 --- a/dsf-fhir/dsf-fhir-server/src/main/resources/fhir/static/form.js +++ b/dsf-fhir/dsf-fhir-server/src/main/resources/fhir/static/form.js @@ -1108,12 +1108,10 @@ function handlePendingTask() { const count = baseIdCount.get(baseId) || 0 baseIdCount.set(baseId, count + 1) - const suffix = count === 0 ? "" : `|${count}` - if (count > 0) - appendInputRowAfter(baseId) + appendInputRowAfter(`${baseId}|${count - 1}`) - const fullId = (id) => id + suffix + const fullId = (suffix) => `${baseId}|${count}${suffix != null ? `-${suffix}` : ""}` const primitiveValue = values.boolean ?? values.string ?? values.integer ?? @@ -1121,7 +1119,7 @@ function handlePendingTask() { values.dateTime ?? values.instant ?? values.uri if (primitiveValue != null) { - const id = fullId(baseId + (values.boolean !== undefined ? `-${values.boolean}` : "")) + const id = fullId(values.boolean) const input = getInputById(id) @@ -1138,14 +1136,14 @@ function handlePendingTask() { const { reference, identifier } = values.reference if (reference) { - const input = getInputById(fullId(baseId)) + const input = getInputById(fullId()) if (input) input.value = reference } else if (identifier) { const { system, value } = identifier - const inputSystem = getInputById(fullId(baseId + "-system")) - const inputValue = getInputById(fullId(baseId + "-value")) + const inputSystem = getInputById(fullId("system")) + const inputValue = getInputById(fullId("value")) if (inputSystem) inputSystem.value = system if (inputValue) inputValue.value = value @@ -1153,16 +1151,16 @@ function handlePendingTask() { } else if (values.identifier) { const { system, value } = values.identifier - const inputSystem = getInputById(fullId(baseId + "-system")) - const inputValue = getInputById(fullId(baseId + "-value")) + const inputSystem = getInputById(fullId("system")) + const inputValue = getInputById(fullId("value")) if (inputSystem) inputSystem.value = system if (inputValue) inputValue.value = value } else if (values.coding) { const { system, code } = values.coding - const inputSystem = getInputById(fullId(baseId + "-system")) - const inputCode = getInputById(fullId(baseId + "-code")) + const inputSystem = getInputById(fullId("system")) + const inputCode = getInputById(fullId("code")) if (inputSystem) inputSystem.value = system if (inputCode) inputCode.value = code @@ -1173,7 +1171,7 @@ function handlePendingTask() { Object.entries(fields).forEach(([key, val]) => { if (val == null) return - const input = getInputById(fullId(`${baseId}-${key}`)) + const input = getInputById(fullId(key)) if (input) input.value = val }) } From d318dc1e1469783c53bfd515f76ea6144d78f99a Mon Sep 17 00:00:00 2001 From: Hauke Hund Date: Fri, 31 Jul 2026 00:43:17 +0200 Subject: [PATCH 12/23] new tests to verify conditional updates --- .../OrganizationIntegrationTest.java | 96 ++++++++++++++++++ .../fhir/integration/TaskIntegrationTest.java | 97 +++++++++++++++++++ 2 files changed, 193 insertions(+) diff --git a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/integration/OrganizationIntegrationTest.java b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/integration/OrganizationIntegrationTest.java index 3652d13e5..9eb2e6f1f 100755 --- a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/integration/OrganizationIntegrationTest.java +++ b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/integration/OrganizationIntegrationTest.java @@ -21,9 +21,14 @@ import java.util.List; import java.util.Map; +import java.util.UUID; +import java.util.stream.Collectors; +import java.util.stream.IntStream; import org.hl7.fhir.r4.model.Bundle; import org.hl7.fhir.r4.model.Bundle.BundleEntryComponent; +import org.hl7.fhir.r4.model.Bundle.BundleType; +import org.hl7.fhir.r4.model.Bundle.HTTPVerb; import org.hl7.fhir.r4.model.Bundle.SearchEntryMode; import org.hl7.fhir.r4.model.Endpoint; import org.hl7.fhir.r4.model.Extension; @@ -271,4 +276,95 @@ public void testStrictSearchWithUnsupportedRevIncludeParameter() throws Exceptio expectBadRequest(() -> getWebserviceClient().searchWithStrictHandling(Organization.class, Map.of("_revinclude", List.of("Endpoint:foo")))); } + + private Organization prepareForTestIllegalUpdate() + { + Bundle bundle = getWebserviceClient().search(Organization.class, + Map.of("identifier", List.of("http://dsf.dev/sid/organization-identifier|External_Test_Organization"))); + assertNotNull(bundle); + assertNotNull(bundle.getEntry()); + assertEquals(1, bundle.getEntry().size()); + assertNotNull(bundle.getEntry().get(0).getResource()); + assertTrue(bundle.getEntry().get(0).getResource() instanceof Organization); + + Organization o = (Organization) bundle.getEntry().get(0).getResource(); + o.getIdentifierFirstRep().setValue("Test_Organization2"); + o.getExtensionByUrl("http://dsf.dev/fhir/StructureDefinition/extension-certificate-thumbprint") + .setValue(new StringType(IntStream.range(0, 128).mapToObj(_ -> "f").collect(Collectors.joining()))); + return o; + } + + @Test + public void testIllegalUpdate() throws Exception + { + Organization o = prepareForTestIllegalUpdate(); + + expectForbidden(() -> getWebserviceClient().update(o)); + } + + @Test + public void testIllegalConditionalUpdateWithId() throws Exception + { + Organization o = prepareForTestIllegalUpdate(); + + expectForbidden(() -> getWebserviceClient().updateConditionaly(o, + Map.of("_id", List.of(o.getIdElement().getIdPart())))); + } + + @Test + public void testIllegalConditionalUpdateWithoutId() throws Exception + { + Organization o = prepareForTestIllegalUpdate(); + + expectForbidden(() -> getWebserviceClient().updateConditionaly(o, + Map.of("_id", List.of(o.getIdElement().getIdPart())))); + } + + @Test + public void testIllegalUpdateViaBundle() throws Exception + { + Organization o = prepareForTestIllegalUpdate(); + + Bundle b = new Bundle(); + b.setType(BundleType.BATCH); + + BundleEntryComponent entry = b.addEntry() + .setFullUrl(o.getIdElement().withServerBase(getBaseUrl(), "Organization").toVersionless().getValue()); + entry.setResource(o).getRequest().setMethod(HTTPVerb.PUT) + .setUrl("Organization/" + o.getIdElement().getIdPart()); + + Bundle resultBundle = getWebserviceClient().postBundle(b); + assertNotNull(resultBundle); + assertNotNull(resultBundle.getEntry()); + assertEquals(1, resultBundle.getEntry().size()); + assertNotNull(resultBundle.getEntry().get(0)); + assertNotNull(resultBundle.getEntry().get(0).getResponse()); + assertNotNull(resultBundle.getEntry().get(0).getResponse().getStatus()); + assertTrue(resultBundle.getEntry().get(0).getResponse().getStatus().startsWith("403")); + } + + @Test + public void testIllegalConditionalUpdateViaBundle() throws Exception + { + Organization o = prepareForTestIllegalUpdate(); + + Bundle b = new Bundle(); + b.setType(BundleType.BATCH); + + BundleEntryComponent entry = b.addEntry().setFullUrl("urn:uuid:" + UUID.randomUUID().toString()); + entry.setResource(o).getRequest().setMethod(HTTPVerb.PUT) + .setUrl("Organization?_id=" + o.getIdElement().getIdPart()); + + o.setIdElement(null); + o.getMeta().setVersionId(null); + + Bundle resultBundle = getWebserviceClient().postBundle(b); + assertNotNull(resultBundle); + assertNotNull(resultBundle.getEntry()); + assertEquals(1, resultBundle.getEntry().size()); + assertNotNull(resultBundle.getEntry().get(0)); + assertNotNull(resultBundle.getEntry().get(0).getResponse()); + assertNotNull(resultBundle.getEntry().get(0).getResponse().getStatus()); + assertTrue(resultBundle.getEntry().get(0).getResponse().getStatus().startsWith("403")); + } } diff --git a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/integration/TaskIntegrationTest.java b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/integration/TaskIntegrationTest.java index 0678b9c37..28a5216cd 100755 --- a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/integration/TaskIntegrationTest.java +++ b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/integration/TaskIntegrationTest.java @@ -56,6 +56,7 @@ import org.hl7.fhir.r4.model.Task.TaskIntent; import org.hl7.fhir.r4.model.Task.TaskRestrictionComponent; import org.hl7.fhir.r4.model.Task.TaskStatus; +import org.hl7.fhir.r4.model.Type; import org.hl7.fhir.r4.model.ValueSet; import org.junit.Test; import org.slf4j.Logger; @@ -2381,4 +2382,100 @@ public void testSerachTaskWithPractitionerUserByRequester() throws Exception assertEquals(Task.class, entry.getResource().getClass()); assertEquals(createdTask.getIdElement().getIdPart(), entry.getResource().getIdElement().getIdPart()); } + + private Task prepareForTestIllegalUpdate() throws SQLException, IOException + { + OrganizationDao orgDao = getSpringWebApplicationContext().getBean(OrganizationDao.class); + Organization o = new Organization(); + o.getIdentifierFirstRep().setSystem("http://dsf.dev/sid/organization-identifier").setValue("Foo"); + orgDao.create(o); + + Task read = readTestTaskBinary("External_Test_Organization", "Test_Organization"); + Type value = read.getInput().getLast().getValue(); + assertNotNull(value); + assertTrue(value instanceof Reference); + Reference reference = (Reference) value; + reference.setReference("https://localhost:60010/fhir/Binary/941683ea-7670-4d1a-8e0d-75698c433204"); + + Task created = createTaskBinary(read, TaskStatus.REQUESTED, true); + created.setStatus(TaskStatus.INPROGRESS); + created.getRequester().getIdentifier().setValue("Foo"); + return created; + } + + @Test + public void testIllegalUpdate() throws Exception + { + Task created = prepareForTestIllegalUpdate(); + + expectForbidden(() -> getWebserviceClient().update(created)); + } + + @Test + public void testIllegalConditionalUpdateWithId() throws Exception + { + Task created = prepareForTestIllegalUpdate(); + + expectForbidden(() -> getWebserviceClient().updateConditionaly(created, + Map.of("_id", List.of(created.getIdElement().getIdPart())))); + } + + @Test + public void testIllegalConditionalUpdateWithoutId() throws Exception + { + Task created = prepareForTestIllegalUpdate(); + String id = created.getIdElement().getIdPart(); + created.setIdElement(null); + created.getMeta().setVersionId(null); + + expectForbidden(() -> getWebserviceClient().updateConditionaly(created, Map.of("_id", List.of(id)))); + } + + @Test + public void testIllegalUpdateViaBundle() throws Exception + { + Task created = prepareForTestIllegalUpdate(); + + Bundle b = new Bundle(); + b.setType(BundleType.BATCH); + + BundleEntryComponent entry = b.addEntry() + .setFullUrl(created.getIdElement().withServerBase(getBaseUrl(), "Task").toVersionless().getValue()); + entry.setResource(created).getRequest().setMethod(HTTPVerb.PUT) + .setUrl("Task/" + created.getIdElement().getIdPart()); + + Bundle resultBundle = getWebserviceClient().postBundle(b); + assertNotNull(resultBundle); + assertNotNull(resultBundle.getEntry()); + assertEquals(1, resultBundle.getEntry().size()); + assertNotNull(resultBundle.getEntry().get(0)); + assertNotNull(resultBundle.getEntry().get(0).getResponse()); + assertNotNull(resultBundle.getEntry().get(0).getResponse().getStatus()); + assertTrue(resultBundle.getEntry().get(0).getResponse().getStatus().startsWith("403")); + } + + @Test + public void testIllegalConditionalUpdateViaBundle() throws Exception + { + Task created = prepareForTestIllegalUpdate(); + + Bundle b = new Bundle(); + b.setType(BundleType.BATCH); + + BundleEntryComponent entry = b.addEntry().setFullUrl("urn:uuid:" + UUID.randomUUID().toString()); + entry.setResource(created).getRequest().setMethod(HTTPVerb.PUT) + .setUrl("Task?_id=" + created.getIdElement().getIdPart()); + + created.setIdElement(null); + created.getMeta().setVersionId(null); + + Bundle resultBundle = getWebserviceClient().postBundle(b); + assertNotNull(resultBundle); + assertNotNull(resultBundle.getEntry()); + assertEquals(1, resultBundle.getEntry().size()); + assertNotNull(resultBundle.getEntry().get(0)); + assertNotNull(resultBundle.getEntry().get(0).getResponse()); + assertNotNull(resultBundle.getEntry().get(0).getResponse().getStatus()); + assertTrue(resultBundle.getEntry().get(0).getResponse().getStatus().startsWith("403")); + } } From 22743e173b46fdb1d9e6484e955e6a901dd1a531 Mon Sep 17 00:00:00 2001 From: Hauke Hund Date: Fri, 31 Jul 2026 00:48:40 +0200 Subject: [PATCH 13/23] added defensive check, fixed old/new resource mix-up Added defensive check to ensure old and new resource are not the same when running update authorization rules. Fixed new resource used twice instead of the new and old (dbResource) to run update authorization rules. --- .../dsf/fhir/dao/command/AuthorizationHelperImpl.java | 7 +++++++ .../secure/AbstractResourceServiceSecure.java | 10 ++++++++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/command/AuthorizationHelperImpl.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/command/AuthorizationHelperImpl.java index d7e108cb4..6cad0c156 100644 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/command/AuthorizationHelperImpl.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/command/AuthorizationHelperImpl.java @@ -17,6 +17,7 @@ import java.sql.Connection; import java.util.List; +import java.util.Objects; import java.util.Optional; import java.util.stream.Collectors; @@ -117,6 +118,12 @@ public void checkReadAllowed(int index, Connection connection, Identity identity public void checkUpdateAllowed(int index, Connection connection, Identity identity, Resource oldResource, Resource newResource) throws WebApplicationException { + Objects.requireNonNull(newResource, "newResource"); + Objects.requireNonNull(oldResource, "oldResource"); + // intentionally guarding against object identity + if (newResource == oldResource) + throw new IllegalStateException("new resource same object as old resource"); + final String resourceTypeName = getResourceTypeName(oldResource); final String resourceId = oldResource.getIdElement().getIdPart(); final long resourceVersion = oldResource.getIdElement().getVersionIdPartAsLong(); diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/webservice/secure/AbstractResourceServiceSecure.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/webservice/secure/AbstractResourceServiceSecure.java index c9e4a491b..7e879342b 100755 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/webservice/secure/AbstractResourceServiceSecure.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/webservice/secure/AbstractResourceServiceSecure.java @@ -400,6 +400,12 @@ public Response update(String id, R resource, UriInfo uri, HttpHeaders headers) private Response update(String id, R newResource, UriInfo uri, HttpHeaders headers, R oldResource) { + Objects.requireNonNull(newResource, "newResource"); + Objects.requireNonNull(oldResource, "oldResource"); + // intentionally guarding against object identity + if (newResource == oldResource) + throw new IllegalStateException("new resource same object as old resource"); + resolveLiteralInternalRelatedArtifactOrAttachmentUrls(newResource); final String resourceId = oldResource.getIdElement().getIdPart(); @@ -482,7 +488,7 @@ else if (result.getTotal() == 1) { resource.setIdElement(dbResourceId); // more security checks and audit log in update method - return update(resource.getIdElement().getIdPart(), resource, uri, headers, resource); + return update(resource.getIdElement().getIdPart(), resource, uri, headers, dbResource); } // update: resource has same id @@ -494,7 +500,7 @@ else if (resource.hasId() && dbResourceId.getIdPart().equals(resource.getIdElement().getIdPart())) { // more security checks and audit log in update method - return update(resource.getIdElement().getIdPart(), resource, uri, headers, resource); + return update(resource.getIdElement().getIdPart(), resource, uri, headers, dbResource); } // update resource has different id -> 400 Bad Request From a696810b3db0ad0dd89f9c68352e05bfdae6229b Mon Sep 17 00:00:00 2001 From: Hauke Hund Date: Fri, 31 Jul 2026 02:41:02 +0200 Subject: [PATCH 14/23] Adds limits for max allowed plain text and crypt text stream lengths Plain text limited to 250 MiB, crypt text limited to 250 MiB + 1024 Bytes. Additional 1024 Bytes for crypt text to accommodate the protocol header. --- .../dsf/bpe/v2/service/CryptoServiceImpl.java | 8 +- .../v2/service/stream/LimitedInputStream.java | 165 +++++++++++ .../dsf/bpe/v2/service/CryptoServiceTest.java | 100 +++++++ .../stream/LimitedInputStreamTest.java | 263 ++++++++++++++++++ .../dev/dsf/bpe/v2/service/CryptoService.java | 4 +- 5 files changed, 537 insertions(+), 3 deletions(-) create mode 100644 dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/service/stream/LimitedInputStream.java create mode 100644 dsf-bpe/dsf-bpe-process-api-v2-impl/src/test/java/dev/dsf/bpe/v2/service/CryptoServiceTest.java create mode 100644 dsf-bpe/dsf-bpe-process-api-v2-impl/src/test/java/dev/dsf/bpe/v2/service/stream/LimitedInputStreamTest.java diff --git a/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/service/CryptoServiceImpl.java b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/service/CryptoServiceImpl.java index ec6cd03f2..47ff33c8b 100644 --- a/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/service/CryptoServiceImpl.java +++ b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/service/CryptoServiceImpl.java @@ -47,11 +47,15 @@ import de.hsheilbronn.mi.utils.crypto.keypair.KeyPairGeneratorFactory; import de.hsheilbronn.mi.utils.crypto.keypair.KeyPairValidator; import de.hsheilbronn.mi.utils.crypto.keystore.KeyStoreCreator; +import dev.dsf.bpe.v2.service.stream.LimitedInputStream; public class CryptoServiceImpl implements CryptoService { public static final class KemDelegate implements Kem { + public static final long ENCRYPT_LIMIT = 250 * 1024 * 1024; // 250 MiB + public static final long DENCRYPT_LIMIT = (250 * 1024 * 1024) + 1024; // 250 MiB + 1024 Bytes + private final AbstractKemAesGcm delegate; public KemDelegate(AbstractKemAesGcm delegate) @@ -63,7 +67,7 @@ public KemDelegate(AbstractKemAesGcm delegate) public InputStream encrypt(InputStream data, PublicKey publicKey) throws NoSuchAlgorithmException, InvalidKeyException, NoSuchPaddingException, InvalidAlgorithmParameterException { - return delegate.encrypt(data, publicKey); + return delegate.encrypt(new LimitedInputStream(data, ENCRYPT_LIMIT), publicKey); } @Override @@ -71,7 +75,7 @@ public InputStream decrypt(InputStream encrypted, PrivateKey privateKey) throws IOException, NoSuchAlgorithmException, InvalidKeyException, DecapsulateException, NoSuchPaddingException, InvalidAlgorithmParameterException { - return delegate.decrypt(encrypted, privateKey); + return delegate.decrypt(new LimitedInputStream(encrypted, DENCRYPT_LIMIT), privateKey); } } diff --git a/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/service/stream/LimitedInputStream.java b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/service/stream/LimitedInputStream.java new file mode 100644 index 000000000..b2bc76c45 --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/service/stream/LimitedInputStream.java @@ -0,0 +1,165 @@ +/* + * Copyright 2018-2025 Heilbronn University of Applied Sciences + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package dev.dsf.bpe.v2.service.stream; + +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Objects; + +/** + * An InputStream wrapper that exposes at most a configured number of bytes from the underlying stream. + * + *

+ * Unlike a regular end-of-stream condition, exhausting the configured limit is treated as a resource limit violation. + * Once the configured limit has been exhausted, every subsequent call to {@link #read()} or {@link #skip(long)} throws + * {@link LimitExceededException}, regardless of whether the underlying stream has reached end-of-stream. + *

+ * + *

+ * If the underlying stream supports mark/reset, this stream supports it as well. The remaining byte limit is restored + * when {@link #reset()} is called. + *

+ * + * This class is not safe for concurrent use by multiple threads. + */ +public final class LimitedInputStream extends FilterInputStream +{ + private final long maxBytes; + + private long remainingBytes; + private long markedRemaining; + + /** + * Creates a stream that allows reading at most {@code maxBytes} bytes. + * + * @param in + * the underlying input stream + * @param maxBytes + * the maximum number of bytes that may be read + * @throws NullPointerException + * if {@code in} is null + * @throws IllegalArgumentException + * if {@code maxBytes} is negative + */ + public LimitedInputStream(InputStream in, long maxBytes) + { + super(Objects.requireNonNull(in, "in")); + + if (maxBytes < 0) + throw new IllegalArgumentException("maxBytes must be >= 0"); + + this.maxBytes = maxBytes; + + this.remainingBytes = maxBytes; + this.markedRemaining = maxBytes; + } + + /** + * Returns the number of bytes that may still be consumed. + * + * @return remaining byte allowance + */ + public long getRemainingBytes() + { + return remainingBytes; + } + + @Override + public int read() throws IOException + { + ensureRemaining(); + + int b = super.read(); + if (b != -1) + remainingBytes--; + + return b; + } + + @Override + public int read(byte[] b, int off, int len) throws IOException + { + Objects.checkFromIndexSize(off, len, b.length); + + if (len == 0) + return 0; + + ensureRemaining(); + + int allowed = (int) Math.min(len, remainingBytes); + int count = super.read(b, off, allowed); + + if (count > 0) + remainingBytes -= count; + + return count; + } + + /** + * Skipping bytes beyond the configured limit is treated as an attempt to consume data beyond the allowed resource + * boundary. + * + * @throws LimitExceededException + * if the limit has already been exhausted + */ + @Override + public long skip(long n) throws IOException + { + if (n <= 0) + return 0; + + ensureRemaining(); + + long skipped = super.skip(Math.min(n, remainingBytes)); + remainingBytes -= skipped; + + return skipped; + } + + /** + * Returns the smaller of the underlying available bytes and the remaining byte allowance. This value represents + * bytes available within this wrapper's limit, not necessarily bytes immediately readable. + */ + @Override + public int available() throws IOException + { + return (int) Math.min(super.available(), remainingBytes); + } + + @Override + public synchronized void mark(int readlimit) + { + if (markSupported()) + { + super.mark(readlimit); + markedRemaining = remainingBytes; + } + } + + @Override + public synchronized void reset() throws IOException + { + super.reset(); + remainingBytes = markedRemaining; + } + + private void ensureRemaining() throws IOException + { + if (remainingBytes == 0) + throw new IOException("Stream limit of " + maxBytes + " byte" + (maxBytes != 1 ? "s" : "") + " exceeded"); + } +} \ No newline at end of file diff --git a/dsf-bpe/dsf-bpe-process-api-v2-impl/src/test/java/dev/dsf/bpe/v2/service/CryptoServiceTest.java b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/test/java/dev/dsf/bpe/v2/service/CryptoServiceTest.java new file mode 100644 index 000000000..1c7ed0c4a --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/test/java/dev/dsf/bpe/v2/service/CryptoServiceTest.java @@ -0,0 +1,100 @@ +/* + * Copyright 2018-2025 Heilbronn University of Applied Sciences + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package dev.dsf.bpe.v2.service; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +import java.io.IOException; +import java.io.InputStream; +import java.security.KeyPair; + +import org.apache.commons.io.input.NullInputStream; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; +import org.junit.runners.Parameterized.Parameters; + +import de.hsheilbronn.mi.utils.crypto.kem.AbstractKemAesGcm; +import de.hsheilbronn.mi.utils.crypto.kem.EcDhKemAesGcm; +import de.hsheilbronn.mi.utils.crypto.kem.RsaKemAesGcm; +import de.hsheilbronn.mi.utils.crypto.keypair.KeyPairGeneratorFactory; +import dev.dsf.bpe.v2.service.CryptoService.Kem; +import dev.dsf.bpe.v2.service.CryptoServiceImpl.KemDelegate; +import net.sourceforge.plantuml.NullOutputStream; + +@RunWith(Parameterized.class) +public class CryptoServiceTest +{ + @Parameters + public static Object[][] data() + { + CryptoService cryptoService = new CryptoServiceImpl(); + + return new Object[][] { + { cryptoService.createKeyPairGeneratorX25519AndInitialize().genKeyPair(), cryptoService.createEcDhKem(), + new EcDhKemAesGcm() }, + { cryptoService.createKeyPairGeneratorX448AndInitialize().genKeyPair(), cryptoService.createEcDhKem(), + new EcDhKemAesGcm() }, + { KeyPairGeneratorFactory.rsa1024().initialize().generateKeyPair(), cryptoService.createRsaKem(), + new RsaKemAesGcm() }, + { cryptoService.createKeyPairGeneratorRsa4096AndInitialize().generateKeyPair(), + cryptoService.createRsaKem(), new RsaKemAesGcm() } }; + } + + @Parameter(0) + public KeyPair keyPair; + + @Parameter(1) + public Kem kem; + + @Parameter(2) + public AbstractKemAesGcm noLimitKem; + + @Test + public void testEncryptionLimit() throws Exception + { + try + { + kem.encrypt(new NullInputStream(300 * 1024 * 1024), keyPair.getPublic()).transferTo(new NullOutputStream()); + + fail("Expected IOException"); + } + catch (IOException e) + { + assertEquals("Stream limit of " + KemDelegate.ENCRYPT_LIMIT + " bytes exceeded", e.getMessage()); + } + } + + @Test + public void testDecryptLimit() throws Exception + { + try + { + InputStream encrypted = noLimitKem.encrypt(new NullInputStream(KemDelegate.DENCRYPT_LIMIT + 1), + keyPair.getPublic()); + + kem.decrypt(encrypted, keyPair.getPrivate()).transferTo(new NullOutputStream()); + + fail("Expected IOException"); + } + catch (IOException e) + { + assertEquals("Stream limit of " + KemDelegate.DENCRYPT_LIMIT + " bytes exceeded", e.getMessage()); + } + } +} diff --git a/dsf-bpe/dsf-bpe-process-api-v2-impl/src/test/java/dev/dsf/bpe/v2/service/stream/LimitedInputStreamTest.java b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/test/java/dev/dsf/bpe/v2/service/stream/LimitedInputStreamTest.java new file mode 100644 index 000000000..20aaaf357 --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/test/java/dev/dsf/bpe/v2/service/stream/LimitedInputStreamTest.java @@ -0,0 +1,263 @@ +/* + * Copyright 2018-2025 Heilbronn University of Applied Sciences + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package dev.dsf.bpe.v2.service.stream; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; + +import org.junit.Test; + +public class LimitedInputStreamTest +{ + @SuppressWarnings("resource") + @Test(expected = NullPointerException.class) + public void constructorRejectsNullInputStream() + { + new LimitedInputStream(null, 1); + } + + @Test(expected = IllegalArgumentException.class) + public void constructorRejectsNegativeLimit() + { + new LimitedInputStream(new ByteArrayInputStream(new byte[0]), -1); + } + + @Test + public void initialRemainingBytesEqualsConfiguredLimit() + { + LimitedInputStream in = new LimitedInputStream(new ByteArrayInputStream(new byte[] { 1, 2, 3 }), 2); + + assertEquals(2, in.getRemainingBytes()); + } + + @Test + public void readSingleBytesConsumesLimit() throws Exception + { + LimitedInputStream in = new LimitedInputStream(new ByteArrayInputStream(new byte[] { 10, 20, 30 }), 2); + + assertEquals(10, in.read()); + assertEquals(1, in.getRemainingBytes()); + + assertEquals(20, in.read()); + assertEquals(0, in.getRemainingBytes()); + + try + { + in.read(); + fail("Expected IOException"); + } + catch (IOException e) + { + assertEquals("Stream limit of 2 bytes exceeded", e.getMessage()); + } + } + + @Test + public void readReturnsEndOfStreamWithoutConsumingRemainingLimit() throws Exception + { + LimitedInputStream in = new LimitedInputStream(new ByteArrayInputStream(new byte[] { 1 }), 5); + + assertEquals(1, in.read()); + assertEquals(4, in.getRemainingBytes()); + + assertEquals(-1, in.read()); + assertEquals(4, in.getRemainingBytes()); + + assertEquals(-1, in.read()); + assertEquals(4, in.getRemainingBytes()); + } + + @Test + public void bulkReadRespectsConfiguredLimit() throws Exception + { + LimitedInputStream in = new LimitedInputStream(new ByteArrayInputStream(new byte[] { 1, 2, 3, 4, 5 }), 3); + + byte[] buffer = new byte[10]; + + int count = in.read(buffer, 2, 5); + + assertEquals(3, count); + assertArrayEquals(new byte[] { 0, 0, 1, 2, 3, 0, 0, 0, 0, 0 }, buffer); + + assertEquals(0, in.getRemainingBytes()); + + try + { + in.read(buffer, 0, 1); + fail("Expected IOException"); + } + catch (IOException e) + { + assertEquals("Stream limit of 3 bytes exceeded", e.getMessage()); + } + } + + @Test + public void zeroLengthReadReturnsZeroWithoutCheckingLimit() throws Exception + { + LimitedInputStream in = new LimitedInputStream(new ByteArrayInputStream(new byte[] { 1 }), 0); + + byte[] buffer = new byte[4]; + + assertEquals(0, in.read(buffer, 0, 0)); + assertEquals(0, in.getRemainingBytes()); + } + + @Test(expected = IndexOutOfBoundsException.class) + public void bulkReadValidatesRange() throws Exception + { + LimitedInputStream in = new LimitedInputStream(new ByteArrayInputStream(new byte[1]), 1); + + in.read(new byte[2], 1, 2); + } + + @Test + public void skipConsumesRemainingLimit() throws Exception + { + LimitedInputStream in = new LimitedInputStream(new ByteArrayInputStream(new byte[] { 1, 2, 3, 4 }), 3); + + assertEquals(2, in.skip(2)); + assertEquals(1, in.getRemainingBytes()); + + assertEquals(1, in.skip(5)); + assertEquals(0, in.getRemainingBytes()); + + try + { + in.skip(1); + fail("Expected IOException"); + } + catch (IOException e) + { + assertEquals("Stream limit of 3 bytes exceeded", e.getMessage()); + } + } + + @Test + public void skipWithNonPositiveValueReturnsZero() throws Exception + { + LimitedInputStream in = new LimitedInputStream(new ByteArrayInputStream(new byte[] { 1, 2, 3 }), 3); + + assertEquals(0, in.skip(0)); + assertEquals(0, in.skip(-5)); + assertEquals(3, in.getRemainingBytes()); + } + + @Test + public void availableIsLimitedByRemainingBytes() throws Exception + { + LimitedInputStream in = new LimitedInputStream(new ByteArrayInputStream(new byte[] { 1, 2, 3, 4, 5 }), 3); + + assertEquals(3, in.available()); + + in.read(); + + assertEquals(2, in.available()); + } + + @Test + public void markAndResetRestoreRemainingBytes() throws Exception + { + ByteArrayInputStream source = new ByteArrayInputStream(new byte[] { 1, 2, 3, 4 }); + + LimitedInputStream in = new LimitedInputStream(source, 4); + + assertTrue(in.markSupported()); + + in.read(); + assertEquals(3, in.getRemainingBytes()); + + in.mark(10); + + in.read(); + in.read(); + + assertEquals(1, in.getRemainingBytes()); + + in.reset(); + + assertEquals(3, in.getRemainingBytes()); + assertEquals(2, in.read()); + assertEquals(2, in.getRemainingBytes()); + } + + @Test + public void markDoesNothingWhenUnderlyingStreamDoesNotSupportMark() throws Exception + { + InputStream source = new InputStream() + { + private final byte[] data = { 1, 2, 3 }; + private int index; + + @Override + public int read() + { + return index < data.length ? data[index++] : -1; + } + + @Override + public boolean markSupported() + { + return false; + } + }; + + @SuppressWarnings("resource") + LimitedInputStream in = new LimitedInputStream(source, 3); + + assertFalse(in.markSupported()); + + in.read(); + assertEquals(2, in.getRemainingBytes()); + + in.mark(100); + + try + { + in.reset(); + fail("Expected IOException"); + } + catch (IOException expected) + { + // Expected from FilterInputStream/InputStream because mark is unsupported. + } + } + + @Test + public void exceptionMessageUsesSingularForOneByteLimit() throws Exception + { + LimitedInputStream in = new LimitedInputStream(new ByteArrayInputStream(new byte[] { 1 }), 1); + + assertEquals(1, in.read()); + + try + { + in.read(); + fail("Expected IOException"); + } + catch (IOException e) + { + assertEquals("Stream limit of 1 byte exceeded", e.getMessage()); + } + } +} diff --git a/dsf-bpe/dsf-bpe-process-api-v2/src/main/java/dev/dsf/bpe/v2/service/CryptoService.java b/dsf-bpe/dsf-bpe-process-api-v2/src/main/java/dev/dsf/bpe/v2/service/CryptoService.java index bfe0c7dc6..23d100b47 100644 --- a/dsf-bpe/dsf-bpe-process-api-v2/src/main/java/dev/dsf/bpe/v2/service/CryptoService.java +++ b/dsf-bpe/dsf-bpe-process-api-v2/src/main/java/dev/dsf/bpe/v2/service/CryptoService.java @@ -63,7 +63,9 @@ public interface CryptoService { /** - * Key encapsulation mechanism with encrypt and decrypt methods. + * Key encapsulation mechanism with encrypt and decrypt methods.
+ *
+ * Starting with DSF 2.1.1 plain-texts are limited to 250 MiB */ public interface Kem { From 5ad2a825469afa5a8e5d007f18e48efdb8f724f8 Mon Sep 17 00:00:00 2001 From: Hauke Hund Date: Fri, 31 Jul 2026 03:01:10 +0200 Subject: [PATCH 15/23] new tests to verify $snapshot operations enforce authorization rules Adds a GET method to the webservice client for retrieving a snapshot for existing StructureDefinition resource: .../StructureDefinition/id/$snapshot --- .../StructureDefinitionIntegrationTest.java | 114 +++++++++++++++++- ...BasicFhirWebserviceCientWithRetryImpl.java | 6 + .../client/BasicFhirWebserviceClient.java | 2 + .../client/FhirWebserviceClientJersey.java | 16 +++ 4 files changed, 136 insertions(+), 2 deletions(-) diff --git a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/integration/StructureDefinitionIntegrationTest.java b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/integration/StructureDefinitionIntegrationTest.java index e85b540c8..ef7c02f2e 100644 --- a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/integration/StructureDefinitionIntegrationTest.java +++ b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/integration/StructureDefinitionIntegrationTest.java @@ -30,6 +30,7 @@ import java.util.List; import java.util.Optional; import java.util.UUID; +import java.util.function.Consumer; import java.util.function.Function; import org.hl7.fhir.r4.model.Bundle; @@ -49,13 +50,23 @@ public class StructureDefinitionIntegrationTest extends AbstractIntegrationTest { private static final Path PROFILE_FOLDER = Paths.get("src/test/resources/integration/structuredefinition"); - private void testCreateWithoutSnapshot(Function createOp) throws Exception + private StructureDefinition testCreateWithoutSnapshot(Function createOp) + throws Exception + { + return testCreateWithoutSnapshot(createOp, _ -> + {}); + } + + private StructureDefinition testCreateWithoutSnapshot(Function createOp, + Consumer profileModifier) throws Exception { EventManager eventManager = getSpringWebApplicationContext().getBean(EventManager.class); List events = new ArrayList<>(); eventManager.addHandler(events::add); StructureDefinition profile = readProfile(PROFILE_FOLDER.resolve("dsf-task-test.xml")); + profileModifier.accept(profile); + StructureDefinition created = createOp.apply(profile); assertNotNull(created); assertTrue(created.hasIdElement()); @@ -76,15 +87,27 @@ private void testCreateWithoutSnapshot(Function createOp) throws Exception + private StructureDefinition testCreateWithSnapshot(Function createOp) + throws Exception + { + return testCreateWithSnapshot(createOp, _ -> + {}); + } + + private StructureDefinition testCreateWithSnapshot(Function createOp, + Consumer profileModifier) throws Exception { EventManager eventManager = getSpringWebApplicationContext().getBean(EventManager.class); List events = new ArrayList<>(); eventManager.addHandler(events::add); StructureDefinition profile = readProfile(PROFILE_FOLDER.resolve("dsf-task-test-snapshot.xml")); + profileModifier.accept(profile); + StructureDefinition created = createOp.apply(profile); assertNotNull(created); assertTrue(created.hasIdElement()); @@ -105,6 +128,8 @@ private void testCreateWithSnapshot(Function getExternalWebserviceClient().read(StructureDefinition.class, + created.getIdElement().getIdPart())); + + expectForbidden( + () -> getExternalWebserviceClient().generateSnapshot(created.getUrl() + "|" + created.getVersion())); + + expectForbidden(() -> getExternalWebserviceClient().getSnapshot(created.getIdElement().getIdPart())); + } + + @Test + public void testGetSnapshotAllowedWithoutSnapshotInCreatedResource() throws Exception + { + StructureDefinition created = testCreateWithoutSnapshot(getWebserviceClient()::create, + getReadAccessHelper()::addLocal); + assertFalse(created.hasSnapshot()); + + StructureDefinition read = getWebserviceClient().read(StructureDefinition.class, + created.getIdElement().getIdPart()); + assertNotNull(read); + assertFalse(read.hasSnapshot()); + + StructureDefinition snapshot1 = getWebserviceClient() + .generateSnapshot(created.getUrl() + "|" + created.getVersion()); + assertNotNull(snapshot1); + assertTrue(snapshot1.hasSnapshot()); + + StructureDefinition snapshot2 = getWebserviceClient().getSnapshot(created.getIdElement().getIdPart()); + assertNotNull(snapshot2); + assertTrue(snapshot2.hasSnapshot()); + } + + @Test + public void testGenerateSnapshotForResource() throws Exception + { + StructureDefinition profile = readProfile(PROFILE_FOLDER.resolve("dsf-task-test.xml")); + profile.getMeta().setTag(null); + + StructureDefinition snapshot1 = getWebserviceClient().generateSnapshot(profile); + assertNotNull(snapshot1); + assertTrue(snapshot1.hasSnapshot()); + + StructureDefinition snapshot2 = getExternalWebserviceClient().generateSnapshot(profile); + assertNotNull(snapshot2); + assertTrue(snapshot2.hasSnapshot()); + + StructureDefinition profileWithSnapshot = readProfile(PROFILE_FOLDER.resolve("dsf-task-test-snapshot.xml")); + profileWithSnapshot.getMeta().setTag(null); + + StructureDefinition snapshot3 = getWebserviceClient().generateSnapshot(profileWithSnapshot); + assertNotNull(snapshot3); + assertTrue(snapshot3.hasSnapshot()); + + StructureDefinition snapshot4 = getExternalWebserviceClient().generateSnapshot(profileWithSnapshot); + assertNotNull(snapshot4); + assertTrue(snapshot4.hasSnapshot()); + } } diff --git a/dsf-fhir/dsf-fhir-webservice-client/src/main/java/dev/dsf/fhir/client/BasicFhirWebserviceCientWithRetryImpl.java b/dsf-fhir/dsf-fhir-webservice-client/src/main/java/dev/dsf/fhir/client/BasicFhirWebserviceCientWithRetryImpl.java index eb764ccd3..de0d8c139 100644 --- a/dsf-fhir/dsf-fhir-webservice-client/src/main/java/dev/dsf/fhir/client/BasicFhirWebserviceCientWithRetryImpl.java +++ b/dsf-fhir/dsf-fhir-webservice-client/src/main/java/dev/dsf/fhir/client/BasicFhirWebserviceCientWithRetryImpl.java @@ -154,6 +154,12 @@ public CapabilityStatement getConformance() return retry(() -> delegate.getConformance()); } + @Override + public StructureDefinition getSnapshot(String id) + { + return retry(() -> delegate.getSnapshot(id)); + } + @Override public StructureDefinition generateSnapshot(StructureDefinition differential) { diff --git a/dsf-fhir/dsf-fhir-webservice-client/src/main/java/dev/dsf/fhir/client/BasicFhirWebserviceClient.java b/dsf-fhir/dsf-fhir-webservice-client/src/main/java/dev/dsf/fhir/client/BasicFhirWebserviceClient.java index b288c5eca..ed687e375 100644 --- a/dsf-fhir/dsf-fhir-webservice-client/src/main/java/dev/dsf/fhir/client/BasicFhirWebserviceClient.java +++ b/dsf-fhir/dsf-fhir-webservice-client/src/main/java/dev/dsf/fhir/client/BasicFhirWebserviceClient.java @@ -177,6 +177,8 @@ BinaryInputStream readBinary(String id, String version, MediaType mediaType, Lon CapabilityStatement getConformance(); + StructureDefinition getSnapshot(String id); + StructureDefinition generateSnapshot(String url); StructureDefinition generateSnapshot(StructureDefinition differential); diff --git a/dsf-fhir/dsf-fhir-webservice-client/src/main/java/dev/dsf/fhir/client/FhirWebserviceClientJersey.java b/dsf-fhir/dsf-fhir-webservice-client/src/main/java/dev/dsf/fhir/client/FhirWebserviceClientJersey.java index ff6e90bfa..897eae502 100755 --- a/dsf-fhir/dsf-fhir-webservice-client/src/main/java/dev/dsf/fhir/client/FhirWebserviceClientJersey.java +++ b/dsf-fhir/dsf-fhir-webservice-client/src/main/java/dev/dsf/fhir/client/FhirWebserviceClientJersey.java @@ -833,6 +833,22 @@ public CapabilityStatement getConformance() throw handleError(response); } + @Override + public StructureDefinition getSnapshot(String id) + { + Objects.requireNonNull(id, "id"); + + Response response = getResource().path(StructureDefinition.class.getAnnotation(ResourceDef.class).name()) + .path(id).path("$snapshot").request().accept(Constants.CT_FHIR_JSON_NEW).get(); + + logger.debug("HTTP {}: {}", response.getStatusInfo().getStatusCode(), + response.getStatusInfo().getReasonPhrase()); + if (Status.OK.getStatusCode() == response.getStatus()) + return response.readEntity(StructureDefinition.class); + else + throw handleError(response); + } + @Override public StructureDefinition generateSnapshot(String url) { From d29e157e9eb99965196494c7537aed64b279bfdb Mon Sep 17 00:00:00 2001 From: Hauke Hund Date: Fri, 31 Jul 2026 03:01:49 +0200 Subject: [PATCH 16/23] improved $snapshot implementation, added missing authorization checks --- .../impl/StructureDefinitionServiceImpl.java | 32 +++++++++---------- .../StructureDefinitionServiceSecure.java | 28 +++++++++++++--- 2 files changed, 40 insertions(+), 20 deletions(-) diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/webservice/impl/StructureDefinitionServiceImpl.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/webservice/impl/StructureDefinitionServiceImpl.java index 42a056c23..7aa25a449 100755 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/webservice/impl/StructureDefinitionServiceImpl.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/webservice/impl/StructureDefinitionServiceImpl.java @@ -30,6 +30,7 @@ import org.hl7.fhir.r4.model.Parameters; import org.hl7.fhir.r4.model.Parameters.ParametersParameterComponent; import org.hl7.fhir.r4.model.PrimitiveType; +import org.hl7.fhir.r4.model.Resource; import org.hl7.fhir.r4.model.StringType; import org.hl7.fhir.r4.model.StructureDefinition; import org.hl7.fhir.r4.model.Type; @@ -211,12 +212,13 @@ private void afterDelete(String id) @Override public Response postSnapshotNew(String snapshotPath, Parameters parameters, UriInfo uri, HttpHeaders headers) { - ParametersParameterComponent param = parameters.getParameter("url"); - Type urlType = param.getValue(); - Optional resource = parameters.getParameter().stream() - .filter(p -> "resource".equals(p.getName())).findFirst(); + ParametersParameterComponent urlParam = parameters.getParameter("url"); + Type urlType = urlParam == null ? null : urlParam.getValue(); - if (urlType != null && resource.isEmpty()) + ParametersParameterComponent resourceParam = parameters.getParameter("resource"); + Resource resource = resourceParam == null ? null : resourceParam.getResource(); + + if (urlType != null && resource == null) { if (!(urlType instanceof StringType || urlType instanceof UriType)) return Response.status(Status.BAD_REQUEST).build(); // TODO OperationOutcome @@ -228,25 +230,24 @@ public Response postSnapshotNew(String snapshotPath, Parameters parameters, UriI return getSnapshot(url.getValue(), uri, headers); } - else if (urlType == null && resource.isPresent() && resource.get().getResource() != null) + else if (urlType == null && resource != null) { - if (!(resource.get().getResource() instanceof StructureDefinition)) + if (!(resource instanceof StructureDefinition)) return Response.status(Status.BAD_REQUEST).build(); // TODO OperationOutcome - StructureDefinition sd = (StructureDefinition) resource.get().getResource(); - - logger.trace("Parameters with StructureDefinition resource url {}", sd.getUrl()); + StructureDefinition sd = (StructureDefinition) resource; - if (!sd.hasDifferential()) - return Response.status(Status.BAD_REQUEST).build(); // TODO OperationOutcome + logger.trace("Parameters with StructureDefinition.url {}", sd.getUrl()); if (sd.hasSnapshot()) return responseGenerator .response(Status.OK, sd, parameterConverter.getMediaTypeThrowIfNotSupported(uri, headers)) .build(); - else + else if (sd.hasDifferential()) return responseGenerator.response(Status.OK, generateSnapshot(sd), parameterConverter.getMediaTypeThrowIfNotSupported(uri, headers)).build(); + else + return Response.status(Status.BAD_REQUEST).build(); // TODO OperationOutcome } else { @@ -257,8 +258,7 @@ else if (urlType == null && resource.isPresent() && resource.get().getResource() private Response getSnapshot(String url, UriInfo uri, HttpHeaders headers) { - SearchQuery query = snapshotDao.createSearchQuery(getCurrentIdentity(), - PageAndCount.single()); + SearchQuery query = snapshotDao.createSearchQueryWithoutUserFilter(PageAndCount.single()); Map> searchParameters = new HashMap<>(); searchParameters.put(StructureDefinitionUrl.PARAMETER_NAME, List.of(url)); searchParameters.put(SearchQuery.PARAMETER_SORT, List.of("-" + ResourceLastUpdated.PARAMETER_NAME)); @@ -295,7 +295,7 @@ public Response getSnapshotExisting(String snapshotPath, String id, UriInfo uri, () -> snapshotDao.read(parameterConverter.toUuid(resourceTypeName, id)), Optional::empty, Optional::empty); - if (snapshot.isPresent()) + if (snapshot.isPresent() && snapshot.get().hasSnapshot()) return snapshot.map(d -> responseGenerator.response(Status.OK, d, parameterConverter.getMediaTypeThrowIfNotSupported(uri, headers))).get().build(); diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/webservice/secure/StructureDefinitionServiceSecure.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/webservice/secure/StructureDefinitionServiceSecure.java index 7a70db6b7..2e7fd4990 100755 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/webservice/secure/StructureDefinitionServiceSecure.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/webservice/secure/StructureDefinitionServiceSecure.java @@ -16,7 +16,10 @@ package dev.dsf.fhir.webservice.secure; import org.hl7.fhir.r4.model.Parameters; +import org.hl7.fhir.r4.model.Parameters.ParametersParameterComponent; +import org.hl7.fhir.r4.model.Resource; import org.hl7.fhir.r4.model.StructureDefinition; +import org.hl7.fhir.r4.model.Type; import dev.dsf.fhir.authorization.AuthorizationRule; import dev.dsf.fhir.dao.StructureDefinitionDao; @@ -53,24 +56,41 @@ public StructureDefinitionServiceSecure(StructureDefinitionService delegate, Str @Override public Response postSnapshotNew(String snapshotPath, Parameters parameters, UriInfo uri, HttpHeaders headers) { - return delegate.postSnapshotNew(snapshotPath, parameters, uri, headers); + Response response = delegate.postSnapshotNew(snapshotPath, parameters, uri, headers); + + ParametersParameterComponent urlParam = parameters.getParameter("url"); + Type urlType = urlParam == null ? null : urlParam.getValue(); + + ParametersParameterComponent resourceParam = parameters.getParameter("resource"); + Resource resource = resourceParam == null ? null : resourceParam.getResource(); + + if (urlType != null && resource == null) + return checkRead(response); + else + return response; } @Override public Response getSnapshotNew(String snapshotPath, UriInfo uri, HttpHeaders headers) { - return delegate.getSnapshotNew(snapshotPath, uri, headers); + Response response = delegate.getSnapshotNew(snapshotPath, uri, headers); + + return checkRead(response); } @Override public Response postSnapshotExisting(String snapshotPath, String id, UriInfo uri, HttpHeaders headers) { - return delegate.postSnapshotExisting(snapshotPath, id, uri, headers); + Response response = delegate.postSnapshotExisting(snapshotPath, id, uri, headers); + + return checkRead(response); } @Override public Response getSnapshotExisting(String snapshotPath, String id, UriInfo uri, HttpHeaders headers) { - return delegate.getSnapshotExisting(snapshotPath, id, uri, headers); + Response response = delegate.getSnapshotExisting(snapshotPath, id, uri, headers); + + return checkRead(response); } } From d8cc90c213f186170b3c8ecc043489d258054a49 Mon Sep 17 00:00:00 2001 From: Hauke Hund Date: Fri, 31 Jul 2026 03:07:53 +0200 Subject: [PATCH 17/23] code formatting --- .../dev/dsf/fhir/integration/OrganizationIntegrationTest.java | 4 ++-- .../java/dev/dsf/fhir/integration/TaskIntegrationTest.java | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/integration/OrganizationIntegrationTest.java b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/integration/OrganizationIntegrationTest.java index 9eb2e6f1f..556f8e2b1 100755 --- a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/integration/OrganizationIntegrationTest.java +++ b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/integration/OrganizationIntegrationTest.java @@ -315,11 +315,11 @@ public void testIllegalConditionalUpdateWithId() throws Exception public void testIllegalConditionalUpdateWithoutId() throws Exception { Organization o = prepareForTestIllegalUpdate(); - + expectForbidden(() -> getWebserviceClient().updateConditionaly(o, Map.of("_id", List.of(o.getIdElement().getIdPart())))); } - + @Test public void testIllegalUpdateViaBundle() throws Exception { diff --git a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/integration/TaskIntegrationTest.java b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/integration/TaskIntegrationTest.java index 28a5216cd..db730bcd6 100755 --- a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/integration/TaskIntegrationTest.java +++ b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/integration/TaskIntegrationTest.java @@ -2396,7 +2396,7 @@ private Task prepareForTestIllegalUpdate() throws SQLException, IOException assertTrue(value instanceof Reference); Reference reference = (Reference) value; reference.setReference("https://localhost:60010/fhir/Binary/941683ea-7670-4d1a-8e0d-75698c433204"); - + Task created = createTaskBinary(read, TaskStatus.REQUESTED, true); created.setStatus(TaskStatus.INPROGRESS); created.getRequester().getIdentifier().setValue("Foo"); From 703c9b409c0293514473e0354465bd808ed256b4 Mon Sep 17 00:00:00 2001 From: Hauke Hund Date: Fri, 31 Jul 2026 03:18:51 +0200 Subject: [PATCH 18/23] javadoc fix --- .../dev/dsf/bpe/v2/service/stream/LimitedInputStream.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/service/stream/LimitedInputStream.java b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/service/stream/LimitedInputStream.java index b2bc76c45..3b462af3f 100644 --- a/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/service/stream/LimitedInputStream.java +++ b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/service/stream/LimitedInputStream.java @@ -26,7 +26,7 @@ *

* Unlike a regular end-of-stream condition, exhausting the configured limit is treated as a resource limit violation. * Once the configured limit has been exhausted, every subsequent call to {@link #read()} or {@link #skip(long)} throws - * {@link LimitExceededException}, regardless of whether the underlying stream has reached end-of-stream. + * {@link IOException}, regardless of whether the underlying stream has reached end-of-stream. *

* *

@@ -113,7 +113,7 @@ public int read(byte[] b, int off, int len) throws IOException * Skipping bytes beyond the configured limit is treated as an attempt to consume data beyond the allowed resource * boundary. * - * @throws LimitExceededException + * @throws IOException * if the limit has already been exhausted */ @Override From fb3877bab446134ecdff35c73f786e8dca226f9b Mon Sep 17 00:00:00 2001 From: Hauke Hund Date: Sun, 2 Aug 2026 20:35:54 +0200 Subject: [PATCH 19/23] new security service and authorization rule unit tests --- .../AbstractAuthorizationRuleTest.java | 188 +++ .../AbstractMetaTagAuthorizationRuleTest.java | 91 ++ ...tivityDefinitionAuthorizationRuleTest.java | 46 + .../BinaryAuthorizationRuleTest.java | 42 + .../BundleAuthorizationRuleTest.java | 42 + .../CodeSystemAuthorizationRuleTest.java | 42 + ...ocumentReferenceAuthorizationRuleTest.java | 42 + .../EndpointAuthorizationRuleTest.java | 42 + .../GroupAuthorizationRuleTest.java | 42 + ...ealthcareServiceAuthorizationRuleTest.java | 42 + .../LibraryAuthorizationRuleTest.java | 42 + .../LocationAuthorizationRuleTest.java | 42 + .../MeasureAuthorizationRuleTest.java | 42 + .../MeasureReportAuthorizationRuleTest.java | 42 + .../NamingSystemAuthorizationRuleTest.java | 42 + ...ationAffiliationAuthorizationRuleTest.java | 43 + .../OrganizationAuthorizationRuleTest.java | 42 + .../PatientAuthorizationRuleTest.java | 42 + .../PractitionerAuthorizationRuleTest.java | 42 + ...PractitionerRoleAuthorizationRuleTest.java | 42 + .../ProvenanceAuthorizationRuleTest.java | 42 + .../QuestionnaireAuthorizationRuleTest.java | 42 + ...ionnaireResponseAuthorizationRuleTest.java | 194 +++ .../ResearchStudyAuthorizationRuleTest.java | 42 + ...uctureDefinitionAuthorizationRuleTest.java | 42 + .../SubscriptionAuthorizationRuleTest.java | 42 + .../TaskAuthorizationRuleTest.java | 171 +++ .../ValueSetAuthorizationRuleTest.java | 42 + .../filter/HistoryIdentityFilterTest.java | 93 ++ .../filter/TaskHistoryIdentityFilterTest.java | 74 ++ .../AbstractResourceServiceSecureTest.java | 1163 +++++++++++++++++ .../secure/BinaryServiceSecureTest.java | 73 ++ .../secure/ResourceServiceSecureTest.java | 206 +++ .../secure/RootServiceSecureTest.java | 153 +++ .../StructureDefinitionServiceSecureTest.java | 110 ++ 35 files changed, 3529 insertions(+) create mode 100644 dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/AbstractAuthorizationRuleTest.java create mode 100644 dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/AbstractMetaTagAuthorizationRuleTest.java create mode 100644 dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/ActivityDefinitionAuthorizationRuleTest.java create mode 100644 dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/BinaryAuthorizationRuleTest.java create mode 100644 dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/BundleAuthorizationRuleTest.java create mode 100644 dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/CodeSystemAuthorizationRuleTest.java create mode 100644 dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/DocumentReferenceAuthorizationRuleTest.java create mode 100644 dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/EndpointAuthorizationRuleTest.java create mode 100644 dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/GroupAuthorizationRuleTest.java create mode 100644 dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/HealthcareServiceAuthorizationRuleTest.java create mode 100644 dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/LibraryAuthorizationRuleTest.java create mode 100644 dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/LocationAuthorizationRuleTest.java create mode 100644 dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/MeasureAuthorizationRuleTest.java create mode 100644 dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/MeasureReportAuthorizationRuleTest.java create mode 100644 dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/NamingSystemAuthorizationRuleTest.java create mode 100644 dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/OrganizationAffiliationAuthorizationRuleTest.java create mode 100644 dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/OrganizationAuthorizationRuleTest.java create mode 100644 dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/PatientAuthorizationRuleTest.java create mode 100644 dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/PractitionerAuthorizationRuleTest.java create mode 100644 dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/PractitionerRoleAuthorizationRuleTest.java create mode 100644 dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/ProvenanceAuthorizationRuleTest.java create mode 100644 dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/QuestionnaireAuthorizationRuleTest.java create mode 100644 dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/QuestionnaireResponseAuthorizationRuleTest.java create mode 100644 dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/ResearchStudyAuthorizationRuleTest.java create mode 100644 dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/StructureDefinitionAuthorizationRuleTest.java create mode 100644 dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/SubscriptionAuthorizationRuleTest.java create mode 100644 dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/TaskAuthorizationRuleTest.java create mode 100644 dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/ValueSetAuthorizationRuleTest.java create mode 100644 dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/history/filter/HistoryIdentityFilterTest.java create mode 100644 dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/history/filter/TaskHistoryIdentityFilterTest.java create mode 100644 dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/webservice/secure/AbstractResourceServiceSecureTest.java create mode 100644 dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/webservice/secure/BinaryServiceSecureTest.java create mode 100644 dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/webservice/secure/ResourceServiceSecureTest.java create mode 100644 dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/webservice/secure/RootServiceSecureTest.java create mode 100644 dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/webservice/secure/StructureDefinitionServiceSecureTest.java diff --git a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/AbstractAuthorizationRuleTest.java b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/AbstractAuthorizationRuleTest.java new file mode 100644 index 000000000..e454d8e13 --- /dev/null +++ b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/AbstractAuthorizationRuleTest.java @@ -0,0 +1,188 @@ +/* + * Copyright 2018-2025 Heilbronn University of Applied Sciences + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package dev.dsf.fhir.authorization; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + +import java.sql.Connection; +import java.util.UUID; +import java.util.function.Predicate; + +import org.hl7.fhir.r4.model.Resource; +import org.junit.Before; +import org.junit.Test; + +import dev.dsf.common.auth.conf.DsfRole; +import dev.dsf.common.auth.conf.Identity; +import dev.dsf.fhir.authentication.FhirServerRoleImpl; +import dev.dsf.fhir.authentication.OrganizationProvider; +import dev.dsf.fhir.authorization.read.ReadAccessHelper; +import dev.dsf.fhir.dao.provider.DaoProvider; +import dev.dsf.fhir.help.ParameterConverter; +import dev.dsf.fhir.service.ReferenceResolver; + +public abstract class AbstractAuthorizationRuleTest +{ + protected static final String SERVER_BASE = "https://dsf.test/fhir"; + protected static final UUID RESOURCE_UUID = UUID.fromString("d1e2f3a4-b5c6-4d7e-8f90-123456789abc"); + + protected DaoProvider daoProvider; + protected ReferenceResolver referenceResolver; + protected OrganizationProvider organizationProvider; + protected ReadAccessHelper readAccessHelper; + protected ParameterConverter parameterConverter; + + protected AbstractAuthorizationRule rule; + + @Before + public void setUpBase() + { + daoProvider = mock(DaoProvider.class); + referenceResolver = mock(ReferenceResolver.class); + organizationProvider = mock(OrganizationProvider.class); + readAccessHelper = mock(ReadAccessHelper.class); + parameterConverter = mock(ParameterConverter.class); + lenient().when(parameterConverter.toUuid(any(), any())).thenReturn(RESOURCE_UUID); + + rule = createRule(); + } + + protected abstract AbstractAuthorizationRule createRule(); + + protected abstract Class expectedResourceType(); + + protected abstract R newResource(); + + protected static Identity identity(boolean localIdentity, boolean hasRole) + { + return identity(localIdentity, _ -> hasRole); + } + + protected static Identity identity(boolean localIdentity, Predicate hasRole) + { + Identity identity = mock(Identity.class); + lenient().when(identity.isLocalIdentity()).thenReturn(localIdentity); + lenient().when(identity.hasDsfRole(any())).then(invocation -> hasRole.test(invocation.getArgument(0))); + + return identity; + } + + @Test + public void getResourceTypeMatchesExpected() + { + assertEquals(expectedResourceType(), rule.getResourceType()); + } + + @Test + public void afterPropertiesSetSucceedsWithAllDependencies() throws Exception + { + rule.afterPropertiesSet(); + } + + @Test + public void searchAllowedWhenIdentityHasSearchRole() + { + assertTrue(rule.reasonSearchAllowed(identity(true, true)).isPresent()); + } + + @Test + public void searchDeniedWhenIdentityHasNoSearchRole() + { + assertTrue(rule.reasonSearchAllowed(identity(true, false)).isEmpty()); + } + + @Test + public void historyAllowedWhenIdentityHasHistoryRole() + { + assertTrue(rule.reasonHistoryAllowed(identity(true, true)).isPresent()); + } + + @Test + public void historyDeniedWhenIdentityHasNoHistoryRole() + { + assertTrue(rule.reasonHistoryAllowed(identity(true, false)).isEmpty()); + } + + @Test + public void websocketAllowedForLocalIdentityWithWebsocketRole() throws Exception + { + when(daoProvider.newReadOnlyAutoCommitTransaction()).thenReturn(mock(Connection.class)); + + assertTrue(rule.reasonWebsocketAllowed(identity(true, true), newResource()).isPresent()); + } + + @Test + public void websocketDeniedForNonLocalIdentity() throws Exception + { + when(daoProvider.newReadOnlyAutoCommitTransaction()).thenReturn(mock(Connection.class)); + + assertTrue(rule.reasonWebsocketAllowed(identity(false, true), newResource()).isEmpty()); + } + + @Test + public void websocketDeniedWithoutWebsocketRole() throws Exception + { + when(daoProvider.newReadOnlyAutoCommitTransaction()).thenReturn(mock(Connection.class)); + + assertTrue(rule.reasonWebsocketAllowed(identity(true, false), newResource()).isEmpty()); + } + + @Test + public void permanentDeleteDeniedForNonLocalIdentity() + { + Connection connection = mock(Connection.class); + + assertTrue(rule.reasonPermanentDeleteAllowed(connection, identity(false, true), newResource()).isEmpty()); + verifyNoInteractions(connection); + } + + @Test + public void permanentDeleteDeniedWithoutPermanentDeleteRole() + { + Connection connection = mock(Connection.class); + + assertTrue(rule.reasonPermanentDeleteAllowed(connection, identity(true, false), newResource()).isEmpty()); + verifyNoInteractions(connection); + } + + @Test + public void permanentDeleteForLocalIdentityWithPermanentDeleteRoleAndDeleteRoleAllowed() throws Exception + { + when(daoProvider.newReadOnlyAutoCommitTransaction()).thenReturn(mock(Connection.class)); + + assertTrue(rule.reasonPermanentDeleteAllowed(identity(true, true), newResource()).isPresent()); + } + + @Test + public void permanentDeleteForLocalIdentityWithPermanentDeleteRoleAndNoDeleteRoleDenied() throws Exception + { + when(daoProvider.newReadOnlyAutoCommitTransaction()).thenReturn(mock(Connection.class)); + + assertTrue(rule.reasonPermanentDeleteAllowed(identity(true, role -> + { + if (FhirServerRoleImpl.delete(expectedResourceType()).equals(role)) + return false; + else + return true; + }), newResource()).isEmpty()); + } +} diff --git a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/AbstractMetaTagAuthorizationRuleTest.java b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/AbstractMetaTagAuthorizationRuleTest.java new file mode 100644 index 000000000..4fe47fe7d --- /dev/null +++ b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/AbstractMetaTagAuthorizationRuleTest.java @@ -0,0 +1,91 @@ +/* + * Copyright 2018-2025 Heilbronn University of Applied Sciences + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package dev.dsf.fhir.authorization; + +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verifyNoInteractions; + +import java.sql.Connection; + +import org.hl7.fhir.r4.model.Resource; +import org.junit.Test; + +public abstract class AbstractMetaTagAuthorizationRuleTest extends AbstractAuthorizationRuleTest +{ + @Test + public void createDeniedWhenIdentityNotLocal() + { + Connection connection = mock(Connection.class); + + assertTrue(rule.reasonCreateAllowed(connection, identity(false, true), newResource()).isEmpty()); + verifyNoInteractions(connection); + } + + @Test + public void createDeniedWhenIdentityHasNoCreateRole() + { + Connection connection = mock(Connection.class); + + assertTrue(rule.reasonCreateAllowed(connection, identity(true, false), newResource()).isEmpty()); + verifyNoInteractions(connection); + } + + @Test + public void readDeniedWhenIdentityHasNoReadRole() + { + Connection connection = mock(Connection.class); + + assertTrue(rule.reasonReadAllowed(connection, identity(true, false), newResource()).isEmpty()); + verifyNoInteractions(connection); + } + + @Test + public void updateDeniedWhenIdentityNotLocal() + { + Connection connection = mock(Connection.class); + + assertTrue(rule.reasonUpdateAllowed(connection, identity(false, true), newResource(), newResource()).isEmpty()); + verifyNoInteractions(connection); + } + + @Test + public void updateDeniedWhenIdentityHasNoUpdateRole() + { + Connection connection = mock(Connection.class); + + assertTrue(rule.reasonUpdateAllowed(connection, identity(true, false), newResource(), newResource()).isEmpty()); + verifyNoInteractions(connection); + } + + @Test + public void deleteDeniedWhenIdentityNotLocal() + { + Connection connection = mock(Connection.class); + + assertTrue(rule.reasonDeleteAllowed(connection, identity(false, true), newResource()).isEmpty()); + verifyNoInteractions(connection); + } + + @Test + public void deleteDeniedWhenIdentityHasNoDeleteRole() + { + Connection connection = mock(Connection.class); + + assertTrue(rule.reasonDeleteAllowed(connection, identity(true, false), newResource()).isEmpty()); + verifyNoInteractions(connection); + } +} diff --git a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/ActivityDefinitionAuthorizationRuleTest.java b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/ActivityDefinitionAuthorizationRuleTest.java new file mode 100644 index 000000000..df925ad6b --- /dev/null +++ b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/ActivityDefinitionAuthorizationRuleTest.java @@ -0,0 +1,46 @@ +/* + * Copyright 2018-2025 Heilbronn University of Applied Sciences + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package dev.dsf.fhir.authorization; + +import static org.mockito.Mockito.mock; + +import org.hl7.fhir.r4.model.ActivityDefinition; + +import dev.dsf.fhir.authorization.process.ProcessAuthorizationHelper; + +public class ActivityDefinitionAuthorizationRuleTest extends AbstractMetaTagAuthorizationRuleTest +{ + @Override + protected ActivityDefinitionAuthorizationRule createRule() + { + return new ActivityDefinitionAuthorizationRule(daoProvider, SERVER_BASE, referenceResolver, + organizationProvider, readAccessHelper, parameterConverter, mock(ProcessAuthorizationHelper.class)); + } + + @Override + protected Class expectedResourceType() + { + return ActivityDefinition.class; + } + + @Override + protected ActivityDefinition newResource() + { + ActivityDefinition resource = new ActivityDefinition(); + resource.setId("ActivityDefinition/d1e2f3a4-b5c6-4d7e-8f90-123456789abc/_history/1"); + return resource; + } +} diff --git a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/BinaryAuthorizationRuleTest.java b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/BinaryAuthorizationRuleTest.java new file mode 100644 index 000000000..e48052941 --- /dev/null +++ b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/BinaryAuthorizationRuleTest.java @@ -0,0 +1,42 @@ +/* + * Copyright 2018-2025 Heilbronn University of Applied Sciences + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package dev.dsf.fhir.authorization; + +import org.hl7.fhir.r4.model.Binary; + +public class BinaryAuthorizationRuleTest extends AbstractMetaTagAuthorizationRuleTest +{ + @Override + protected BinaryAuthorizationRule createRule() + { + return new BinaryAuthorizationRule(daoProvider, SERVER_BASE, referenceResolver, organizationProvider, + readAccessHelper, parameterConverter); + } + + @Override + protected Class expectedResourceType() + { + return Binary.class; + } + + @Override + protected Binary newResource() + { + Binary resource = new Binary(); + resource.setId("Binary/d1e2f3a4-b5c6-4d7e-8f90-123456789abc/_history/1"); + return resource; + } +} diff --git a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/BundleAuthorizationRuleTest.java b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/BundleAuthorizationRuleTest.java new file mode 100644 index 000000000..3e3ae3f2e --- /dev/null +++ b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/BundleAuthorizationRuleTest.java @@ -0,0 +1,42 @@ +/* + * Copyright 2018-2025 Heilbronn University of Applied Sciences + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package dev.dsf.fhir.authorization; + +import org.hl7.fhir.r4.model.Bundle; + +public class BundleAuthorizationRuleTest extends AbstractMetaTagAuthorizationRuleTest +{ + @Override + protected BundleAuthorizationRule createRule() + { + return new BundleAuthorizationRule(daoProvider, SERVER_BASE, referenceResolver, organizationProvider, + readAccessHelper, parameterConverter); + } + + @Override + protected Class expectedResourceType() + { + return Bundle.class; + } + + @Override + protected Bundle newResource() + { + Bundle resource = new Bundle(); + resource.setId("Bundle/d1e2f3a4-b5c6-4d7e-8f90-123456789abc/_history/1"); + return resource; + } +} diff --git a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/CodeSystemAuthorizationRuleTest.java b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/CodeSystemAuthorizationRuleTest.java new file mode 100644 index 000000000..1fa46bc54 --- /dev/null +++ b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/CodeSystemAuthorizationRuleTest.java @@ -0,0 +1,42 @@ +/* + * Copyright 2018-2025 Heilbronn University of Applied Sciences + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package dev.dsf.fhir.authorization; + +import org.hl7.fhir.r4.model.CodeSystem; + +public class CodeSystemAuthorizationRuleTest extends AbstractMetaTagAuthorizationRuleTest +{ + @Override + protected CodeSystemAuthorizationRule createRule() + { + return new CodeSystemAuthorizationRule(daoProvider, SERVER_BASE, referenceResolver, organizationProvider, + readAccessHelper, parameterConverter); + } + + @Override + protected Class expectedResourceType() + { + return CodeSystem.class; + } + + @Override + protected CodeSystem newResource() + { + CodeSystem resource = new CodeSystem(); + resource.setId("CodeSystem/d1e2f3a4-b5c6-4d7e-8f90-123456789abc/_history/1"); + return resource; + } +} diff --git a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/DocumentReferenceAuthorizationRuleTest.java b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/DocumentReferenceAuthorizationRuleTest.java new file mode 100644 index 000000000..0f21ac8d1 --- /dev/null +++ b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/DocumentReferenceAuthorizationRuleTest.java @@ -0,0 +1,42 @@ +/* + * Copyright 2018-2025 Heilbronn University of Applied Sciences + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package dev.dsf.fhir.authorization; + +import org.hl7.fhir.r4.model.DocumentReference; + +public class DocumentReferenceAuthorizationRuleTest extends AbstractMetaTagAuthorizationRuleTest +{ + @Override + protected DocumentReferenceAuthorizationRule createRule() + { + return new DocumentReferenceAuthorizationRule(daoProvider, SERVER_BASE, referenceResolver, organizationProvider, + readAccessHelper, parameterConverter); + } + + @Override + protected Class expectedResourceType() + { + return DocumentReference.class; + } + + @Override + protected DocumentReference newResource() + { + DocumentReference resource = new DocumentReference(); + resource.setId("DocumentReference/d1e2f3a4-b5c6-4d7e-8f90-123456789abc/_history/1"); + return resource; + } +} diff --git a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/EndpointAuthorizationRuleTest.java b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/EndpointAuthorizationRuleTest.java new file mode 100644 index 000000000..a01fa4b9c --- /dev/null +++ b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/EndpointAuthorizationRuleTest.java @@ -0,0 +1,42 @@ +/* + * Copyright 2018-2025 Heilbronn University of Applied Sciences + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package dev.dsf.fhir.authorization; + +import org.hl7.fhir.r4.model.Endpoint; + +public class EndpointAuthorizationRuleTest extends AbstractMetaTagAuthorizationRuleTest +{ + @Override + protected EndpointAuthorizationRule createRule() + { + return new EndpointAuthorizationRule(daoProvider, SERVER_BASE, referenceResolver, organizationProvider, + readAccessHelper, parameterConverter); + } + + @Override + protected Class expectedResourceType() + { + return Endpoint.class; + } + + @Override + protected Endpoint newResource() + { + Endpoint resource = new Endpoint(); + resource.setId("Endpoint/d1e2f3a4-b5c6-4d7e-8f90-123456789abc/_history/1"); + return resource; + } +} diff --git a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/GroupAuthorizationRuleTest.java b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/GroupAuthorizationRuleTest.java new file mode 100644 index 000000000..253cad708 --- /dev/null +++ b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/GroupAuthorizationRuleTest.java @@ -0,0 +1,42 @@ +/* + * Copyright 2018-2025 Heilbronn University of Applied Sciences + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package dev.dsf.fhir.authorization; + +import org.hl7.fhir.r4.model.Group; + +public class GroupAuthorizationRuleTest extends AbstractMetaTagAuthorizationRuleTest +{ + @Override + protected GroupAuthorizationRule createRule() + { + return new GroupAuthorizationRule(daoProvider, SERVER_BASE, referenceResolver, organizationProvider, + readAccessHelper, parameterConverter); + } + + @Override + protected Class expectedResourceType() + { + return Group.class; + } + + @Override + protected Group newResource() + { + Group resource = new Group(); + resource.setId("Group/d1e2f3a4-b5c6-4d7e-8f90-123456789abc/_history/1"); + return resource; + } +} diff --git a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/HealthcareServiceAuthorizationRuleTest.java b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/HealthcareServiceAuthorizationRuleTest.java new file mode 100644 index 000000000..35349aaea --- /dev/null +++ b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/HealthcareServiceAuthorizationRuleTest.java @@ -0,0 +1,42 @@ +/* + * Copyright 2018-2025 Heilbronn University of Applied Sciences + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package dev.dsf.fhir.authorization; + +import org.hl7.fhir.r4.model.HealthcareService; + +public class HealthcareServiceAuthorizationRuleTest extends AbstractMetaTagAuthorizationRuleTest +{ + @Override + protected HealthcareServiceAuthorizationRule createRule() + { + return new HealthcareServiceAuthorizationRule(daoProvider, SERVER_BASE, referenceResolver, organizationProvider, + readAccessHelper, parameterConverter); + } + + @Override + protected Class expectedResourceType() + { + return HealthcareService.class; + } + + @Override + protected HealthcareService newResource() + { + HealthcareService resource = new HealthcareService(); + resource.setId("HealthcareService/d1e2f3a4-b5c6-4d7e-8f90-123456789abc/_history/1"); + return resource; + } +} diff --git a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/LibraryAuthorizationRuleTest.java b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/LibraryAuthorizationRuleTest.java new file mode 100644 index 000000000..513e03a51 --- /dev/null +++ b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/LibraryAuthorizationRuleTest.java @@ -0,0 +1,42 @@ +/* + * Copyright 2018-2025 Heilbronn University of Applied Sciences + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package dev.dsf.fhir.authorization; + +import org.hl7.fhir.r4.model.Library; + +public class LibraryAuthorizationRuleTest extends AbstractMetaTagAuthorizationRuleTest +{ + @Override + protected LibraryAuthorizationRule createRule() + { + return new LibraryAuthorizationRule(daoProvider, SERVER_BASE, referenceResolver, organizationProvider, + readAccessHelper, parameterConverter); + } + + @Override + protected Class expectedResourceType() + { + return Library.class; + } + + @Override + protected Library newResource() + { + Library resource = new Library(); + resource.setId("Library/d1e2f3a4-b5c6-4d7e-8f90-123456789abc/_history/1"); + return resource; + } +} diff --git a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/LocationAuthorizationRuleTest.java b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/LocationAuthorizationRuleTest.java new file mode 100644 index 000000000..485b25f7c --- /dev/null +++ b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/LocationAuthorizationRuleTest.java @@ -0,0 +1,42 @@ +/* + * Copyright 2018-2025 Heilbronn University of Applied Sciences + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package dev.dsf.fhir.authorization; + +import org.hl7.fhir.r4.model.Location; + +public class LocationAuthorizationRuleTest extends AbstractMetaTagAuthorizationRuleTest +{ + @Override + protected LocationAuthorizationRule createRule() + { + return new LocationAuthorizationRule(daoProvider, SERVER_BASE, referenceResolver, organizationProvider, + readAccessHelper, parameterConverter); + } + + @Override + protected Class expectedResourceType() + { + return Location.class; + } + + @Override + protected Location newResource() + { + Location resource = new Location(); + resource.setId("Location/d1e2f3a4-b5c6-4d7e-8f90-123456789abc/_history/1"); + return resource; + } +} diff --git a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/MeasureAuthorizationRuleTest.java b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/MeasureAuthorizationRuleTest.java new file mode 100644 index 000000000..86d1a33ce --- /dev/null +++ b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/MeasureAuthorizationRuleTest.java @@ -0,0 +1,42 @@ +/* + * Copyright 2018-2025 Heilbronn University of Applied Sciences + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package dev.dsf.fhir.authorization; + +import org.hl7.fhir.r4.model.Measure; + +public class MeasureAuthorizationRuleTest extends AbstractMetaTagAuthorizationRuleTest +{ + @Override + protected MeasureAuthorizationRule createRule() + { + return new MeasureAuthorizationRule(daoProvider, SERVER_BASE, referenceResolver, organizationProvider, + readAccessHelper, parameterConverter); + } + + @Override + protected Class expectedResourceType() + { + return Measure.class; + } + + @Override + protected Measure newResource() + { + Measure resource = new Measure(); + resource.setId("Measure/d1e2f3a4-b5c6-4d7e-8f90-123456789abc/_history/1"); + return resource; + } +} diff --git a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/MeasureReportAuthorizationRuleTest.java b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/MeasureReportAuthorizationRuleTest.java new file mode 100644 index 000000000..734702b96 --- /dev/null +++ b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/MeasureReportAuthorizationRuleTest.java @@ -0,0 +1,42 @@ +/* + * Copyright 2018-2025 Heilbronn University of Applied Sciences + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package dev.dsf.fhir.authorization; + +import org.hl7.fhir.r4.model.MeasureReport; + +public class MeasureReportAuthorizationRuleTest extends AbstractMetaTagAuthorizationRuleTest +{ + @Override + protected MeasureReportAuthorizationRule createRule() + { + return new MeasureReportAuthorizationRule(daoProvider, SERVER_BASE, referenceResolver, organizationProvider, + readAccessHelper, parameterConverter); + } + + @Override + protected Class expectedResourceType() + { + return MeasureReport.class; + } + + @Override + protected MeasureReport newResource() + { + MeasureReport resource = new MeasureReport(); + resource.setId("MeasureReport/d1e2f3a4-b5c6-4d7e-8f90-123456789abc/_history/1"); + return resource; + } +} diff --git a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/NamingSystemAuthorizationRuleTest.java b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/NamingSystemAuthorizationRuleTest.java new file mode 100644 index 000000000..c4120876a --- /dev/null +++ b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/NamingSystemAuthorizationRuleTest.java @@ -0,0 +1,42 @@ +/* + * Copyright 2018-2025 Heilbronn University of Applied Sciences + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package dev.dsf.fhir.authorization; + +import org.hl7.fhir.r4.model.NamingSystem; + +public class NamingSystemAuthorizationRuleTest extends AbstractMetaTagAuthorizationRuleTest +{ + @Override + protected NamingSystemAuthorizationRule createRule() + { + return new NamingSystemAuthorizationRule(daoProvider, SERVER_BASE, referenceResolver, organizationProvider, + readAccessHelper, parameterConverter); + } + + @Override + protected Class expectedResourceType() + { + return NamingSystem.class; + } + + @Override + protected NamingSystem newResource() + { + NamingSystem resource = new NamingSystem(); + resource.setId("NamingSystem/d1e2f3a4-b5c6-4d7e-8f90-123456789abc/_history/1"); + return resource; + } +} diff --git a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/OrganizationAffiliationAuthorizationRuleTest.java b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/OrganizationAffiliationAuthorizationRuleTest.java new file mode 100644 index 000000000..34ef4912b --- /dev/null +++ b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/OrganizationAffiliationAuthorizationRuleTest.java @@ -0,0 +1,43 @@ +/* + * Copyright 2018-2025 Heilbronn University of Applied Sciences + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package dev.dsf.fhir.authorization; + +import org.hl7.fhir.r4.model.OrganizationAffiliation; + +public class OrganizationAffiliationAuthorizationRuleTest + extends AbstractMetaTagAuthorizationRuleTest +{ + @Override + protected OrganizationAffiliationAuthorizationRule createRule() + { + return new OrganizationAffiliationAuthorizationRule(daoProvider, SERVER_BASE, referenceResolver, + organizationProvider, readAccessHelper, parameterConverter); + } + + @Override + protected Class expectedResourceType() + { + return OrganizationAffiliation.class; + } + + @Override + protected OrganizationAffiliation newResource() + { + OrganizationAffiliation resource = new OrganizationAffiliation(); + resource.setId("OrganizationAffiliation/d1e2f3a4-b5c6-4d7e-8f90-123456789abc/_history/1"); + return resource; + } +} diff --git a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/OrganizationAuthorizationRuleTest.java b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/OrganizationAuthorizationRuleTest.java new file mode 100644 index 000000000..ef9efb435 --- /dev/null +++ b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/OrganizationAuthorizationRuleTest.java @@ -0,0 +1,42 @@ +/* + * Copyright 2018-2025 Heilbronn University of Applied Sciences + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package dev.dsf.fhir.authorization; + +import org.hl7.fhir.r4.model.Organization; + +public class OrganizationAuthorizationRuleTest extends AbstractMetaTagAuthorizationRuleTest +{ + @Override + protected OrganizationAuthorizationRule createRule() + { + return new OrganizationAuthorizationRule(daoProvider, SERVER_BASE, referenceResolver, organizationProvider, + readAccessHelper, parameterConverter); + } + + @Override + protected Class expectedResourceType() + { + return Organization.class; + } + + @Override + protected Organization newResource() + { + Organization resource = new Organization(); + resource.setId("Organization/d1e2f3a4-b5c6-4d7e-8f90-123456789abc/_history/1"); + return resource; + } +} diff --git a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/PatientAuthorizationRuleTest.java b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/PatientAuthorizationRuleTest.java new file mode 100644 index 000000000..48aad4287 --- /dev/null +++ b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/PatientAuthorizationRuleTest.java @@ -0,0 +1,42 @@ +/* + * Copyright 2018-2025 Heilbronn University of Applied Sciences + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package dev.dsf.fhir.authorization; + +import org.hl7.fhir.r4.model.Patient; + +public class PatientAuthorizationRuleTest extends AbstractMetaTagAuthorizationRuleTest +{ + @Override + protected PatientAuthorizationRule createRule() + { + return new PatientAuthorizationRule(daoProvider, SERVER_BASE, referenceResolver, organizationProvider, + readAccessHelper, parameterConverter); + } + + @Override + protected Class expectedResourceType() + { + return Patient.class; + } + + @Override + protected Patient newResource() + { + Patient resource = new Patient(); + resource.setId("Patient/d1e2f3a4-b5c6-4d7e-8f90-123456789abc/_history/1"); + return resource; + } +} diff --git a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/PractitionerAuthorizationRuleTest.java b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/PractitionerAuthorizationRuleTest.java new file mode 100644 index 000000000..4ca8f3c34 --- /dev/null +++ b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/PractitionerAuthorizationRuleTest.java @@ -0,0 +1,42 @@ +/* + * Copyright 2018-2025 Heilbronn University of Applied Sciences + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package dev.dsf.fhir.authorization; + +import org.hl7.fhir.r4.model.Practitioner; + +public class PractitionerAuthorizationRuleTest extends AbstractMetaTagAuthorizationRuleTest +{ + @Override + protected PractitionerAuthorizationRule createRule() + { + return new PractitionerAuthorizationRule(daoProvider, SERVER_BASE, referenceResolver, organizationProvider, + readAccessHelper, parameterConverter); + } + + @Override + protected Class expectedResourceType() + { + return Practitioner.class; + } + + @Override + protected Practitioner newResource() + { + Practitioner resource = new Practitioner(); + resource.setId("Practitioner/d1e2f3a4-b5c6-4d7e-8f90-123456789abc/_history/1"); + return resource; + } +} diff --git a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/PractitionerRoleAuthorizationRuleTest.java b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/PractitionerRoleAuthorizationRuleTest.java new file mode 100644 index 000000000..b392646e1 --- /dev/null +++ b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/PractitionerRoleAuthorizationRuleTest.java @@ -0,0 +1,42 @@ +/* + * Copyright 2018-2025 Heilbronn University of Applied Sciences + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package dev.dsf.fhir.authorization; + +import org.hl7.fhir.r4.model.PractitionerRole; + +public class PractitionerRoleAuthorizationRuleTest extends AbstractMetaTagAuthorizationRuleTest +{ + @Override + protected PractitionerRoleAuthorizationRule createRule() + { + return new PractitionerRoleAuthorizationRule(daoProvider, SERVER_BASE, referenceResolver, organizationProvider, + readAccessHelper, parameterConverter); + } + + @Override + protected Class expectedResourceType() + { + return PractitionerRole.class; + } + + @Override + protected PractitionerRole newResource() + { + PractitionerRole resource = new PractitionerRole(); + resource.setId("PractitionerRole/d1e2f3a4-b5c6-4d7e-8f90-123456789abc/_history/1"); + return resource; + } +} diff --git a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/ProvenanceAuthorizationRuleTest.java b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/ProvenanceAuthorizationRuleTest.java new file mode 100644 index 000000000..883660dcb --- /dev/null +++ b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/ProvenanceAuthorizationRuleTest.java @@ -0,0 +1,42 @@ +/* + * Copyright 2018-2025 Heilbronn University of Applied Sciences + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package dev.dsf.fhir.authorization; + +import org.hl7.fhir.r4.model.Provenance; + +public class ProvenanceAuthorizationRuleTest extends AbstractMetaTagAuthorizationRuleTest +{ + @Override + protected ProvenanceAuthorizationRule createRule() + { + return new ProvenanceAuthorizationRule(daoProvider, SERVER_BASE, referenceResolver, organizationProvider, + readAccessHelper, parameterConverter); + } + + @Override + protected Class expectedResourceType() + { + return Provenance.class; + } + + @Override + protected Provenance newResource() + { + Provenance resource = new Provenance(); + resource.setId("Provenance/d1e2f3a4-b5c6-4d7e-8f90-123456789abc/_history/1"); + return resource; + } +} diff --git a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/QuestionnaireAuthorizationRuleTest.java b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/QuestionnaireAuthorizationRuleTest.java new file mode 100644 index 000000000..6aac8eacc --- /dev/null +++ b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/QuestionnaireAuthorizationRuleTest.java @@ -0,0 +1,42 @@ +/* + * Copyright 2018-2025 Heilbronn University of Applied Sciences + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package dev.dsf.fhir.authorization; + +import org.hl7.fhir.r4.model.Questionnaire; + +public class QuestionnaireAuthorizationRuleTest extends AbstractMetaTagAuthorizationRuleTest +{ + @Override + protected QuestionnaireAuthorizationRule createRule() + { + return new QuestionnaireAuthorizationRule(daoProvider, SERVER_BASE, referenceResolver, organizationProvider, + readAccessHelper, parameterConverter); + } + + @Override + protected Class expectedResourceType() + { + return Questionnaire.class; + } + + @Override + protected Questionnaire newResource() + { + Questionnaire resource = new Questionnaire(); + resource.setId("Questionnaire/d1e2f3a4-b5c6-4d7e-8f90-123456789abc/_history/1"); + return resource; + } +} diff --git a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/QuestionnaireResponseAuthorizationRuleTest.java b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/QuestionnaireResponseAuthorizationRuleTest.java new file mode 100644 index 000000000..0d0c50092 --- /dev/null +++ b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/QuestionnaireResponseAuthorizationRuleTest.java @@ -0,0 +1,194 @@ +/* + * Copyright 2018-2025 Heilbronn University of Applied Sciences + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package dev.dsf.fhir.authorization; + +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + +import java.sql.Connection; + +import org.hl7.fhir.r4.model.QuestionnaireResponse; +import org.hl7.fhir.r4.model.QuestionnaireResponse.QuestionnaireResponseStatus; +import org.junit.Ignore; +import org.junit.Test; + +import dev.dsf.common.auth.conf.OrganizationIdentity; + +public class QuestionnaireResponseAuthorizationRuleTest extends AbstractAuthorizationRuleTest +{ + @Override + protected QuestionnaireResponseAuthorizationRule createRule() + { + return new QuestionnaireResponseAuthorizationRule(daoProvider, SERVER_BASE, referenceResolver, + organizationProvider, readAccessHelper, parameterConverter); + } + + @Override + protected Class expectedResourceType() + { + return QuestionnaireResponse.class; + } + + @Override + protected QuestionnaireResponse newResource() + { + return response(QuestionnaireResponseStatus.INPROGRESS); + } + + private static QuestionnaireResponse response(QuestionnaireResponseStatus status) + { + QuestionnaireResponse questionnaireResponse = new QuestionnaireResponse(); + questionnaireResponse.setId("QuestionnaireResponse/d1e2f3a4-b5c6-4d7e-8f90-123456789abc/_history/1"); + questionnaireResponse.setStatus(status); + return questionnaireResponse; + } + + private static OrganizationIdentity localOrganizationIdentity() + { + OrganizationIdentity identity = mock(OrganizationIdentity.class); + when(identity.isLocalIdentity()).thenReturn(true); + when(identity.hasDsfRole(any())).thenReturn(true); + return identity; + } + + @Test + public void createDeniedWhenIdentityHasNoCreateRole() + { + Connection connection = mock(Connection.class); + + assertTrue(rule.reasonCreateAllowed(connection, identity(true, false), + response(QuestionnaireResponseStatus.INPROGRESS)).isEmpty()); + verifyNoInteractions(connection); + } + + @Test + public void createDeniedWhenIdentityNotLocalOrganizationOrDsfAdmin() + { + Connection connection = mock(Connection.class); + + assertTrue(rule + .reasonCreateAllowed(connection, identity(true, true), response(QuestionnaireResponseStatus.INPROGRESS)) + .isEmpty()); + verifyNoInteractions(connection); + } + + @Test + public void createDeniedWhenStatusNotInProgress() + { + Connection connection = mock(Connection.class); + + assertTrue(rule.reasonCreateAllowed(connection, localOrganizationIdentity(), + response(QuestionnaireResponseStatus.STOPPED)).isEmpty()); + verifyNoInteractions(connection); + } + + @Test + public void readAllowedForLocalOrganization() + { + Connection connection = mock(Connection.class); + + assertTrue(rule.reasonReadAllowed(connection, localOrganizationIdentity(), + response(QuestionnaireResponseStatus.COMPLETED)).isPresent()); + verifyNoInteractions(connection); + } + + @Test + public void readDeniedWhenIdentityHasNoReadRole() + { + Connection connection = mock(Connection.class); + + assertTrue(rule + .reasonReadAllowed(connection, identity(true, false), response(QuestionnaireResponseStatus.COMPLETED)) + .isEmpty()); + verifyNoInteractions(connection); + } + + @Test + public void readDeniedForIdentityNotLocalOrganizationAndNotAuthorizedPractitioner() + { + Connection connection = mock(Connection.class); + + assertTrue(rule + .reasonReadAllowed(connection, identity(true, true), response(QuestionnaireResponseStatus.COMPLETED)) + .isEmpty()); + verifyNoInteractions(connection); + } + + @Test + public void updateDeniedWhenIdentityHasNoUpdateRole() + { + Connection connection = mock(Connection.class); + + assertTrue(rule.reasonUpdateAllowed(connection, identity(true, false), + response(QuestionnaireResponseStatus.INPROGRESS), response(QuestionnaireResponseStatus.COMPLETED)) + .isEmpty()); + verifyNoInteractions(connection); + } + + @Test + public void updateDeniedWhenIdentityNotLocal() + { + Connection connection = mock(Connection.class); + + assertTrue(rule.reasonUpdateAllowed(connection, identity(false, true), + response(QuestionnaireResponseStatus.INPROGRESS), response(QuestionnaireResponseStatus.COMPLETED)) + .isEmpty()); + verifyNoInteractions(connection); + } + + @Test + public void deleteAllowedForLocalOrganization() + { + Connection connection = mock(Connection.class); + + assertTrue(rule.reasonDeleteAllowed(connection, localOrganizationIdentity(), + response(QuestionnaireResponseStatus.COMPLETED)).isPresent()); + verifyNoInteractions(connection); + } + + @Test + public void deleteDeniedWhenIdentityHasNoDeleteRole() + { + Connection connection = mock(Connection.class); + + assertTrue(rule + .reasonDeleteAllowed(connection, identity(true, false), response(QuestionnaireResponseStatus.COMPLETED)) + .isEmpty()); + verifyNoInteractions(connection); + } + + @Test + public void deleteDeniedWhenIdentityNotLocalOrganizationOrDsfAdmin() + { + Connection connection = mock(Connection.class); + + assertTrue(rule + .reasonDeleteAllowed(connection, identity(true, true), response(QuestionnaireResponseStatus.COMPLETED)) + .isEmpty()); + verifyNoInteractions(connection); + } + + @Test + @Ignore + @Override + public void permanentDeleteForLocalIdentityWithPermanentDeleteRoleAndDeleteRoleAllowed() throws Exception + { + super.permanentDeleteForLocalIdentityWithPermanentDeleteRoleAndDeleteRoleAllowed(); + } +} diff --git a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/ResearchStudyAuthorizationRuleTest.java b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/ResearchStudyAuthorizationRuleTest.java new file mode 100644 index 000000000..6fcd2473f --- /dev/null +++ b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/ResearchStudyAuthorizationRuleTest.java @@ -0,0 +1,42 @@ +/* + * Copyright 2018-2025 Heilbronn University of Applied Sciences + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package dev.dsf.fhir.authorization; + +import org.hl7.fhir.r4.model.ResearchStudy; + +public class ResearchStudyAuthorizationRuleTest extends AbstractMetaTagAuthorizationRuleTest +{ + @Override + protected ResearchStudyAuthorizationRule createRule() + { + return new ResearchStudyAuthorizationRule(daoProvider, SERVER_BASE, referenceResolver, organizationProvider, + readAccessHelper, parameterConverter); + } + + @Override + protected Class expectedResourceType() + { + return ResearchStudy.class; + } + + @Override + protected ResearchStudy newResource() + { + ResearchStudy resource = new ResearchStudy(); + resource.setId("ResearchStudy/d1e2f3a4-b5c6-4d7e-8f90-123456789abc/_history/1"); + return resource; + } +} diff --git a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/StructureDefinitionAuthorizationRuleTest.java b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/StructureDefinitionAuthorizationRuleTest.java new file mode 100644 index 000000000..4bb7397b3 --- /dev/null +++ b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/StructureDefinitionAuthorizationRuleTest.java @@ -0,0 +1,42 @@ +/* + * Copyright 2018-2025 Heilbronn University of Applied Sciences + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package dev.dsf.fhir.authorization; + +import org.hl7.fhir.r4.model.StructureDefinition; + +public class StructureDefinitionAuthorizationRuleTest extends AbstractMetaTagAuthorizationRuleTest +{ + @Override + protected StructureDefinitionAuthorizationRule createRule() + { + return new StructureDefinitionAuthorizationRule(daoProvider, SERVER_BASE, referenceResolver, + organizationProvider, readAccessHelper, parameterConverter); + } + + @Override + protected Class expectedResourceType() + { + return StructureDefinition.class; + } + + @Override + protected StructureDefinition newResource() + { + StructureDefinition resource = new StructureDefinition(); + resource.setId("StructureDefinition/d1e2f3a4-b5c6-4d7e-8f90-123456789abc/_history/1"); + return resource; + } +} diff --git a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/SubscriptionAuthorizationRuleTest.java b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/SubscriptionAuthorizationRuleTest.java new file mode 100644 index 000000000..64846e995 --- /dev/null +++ b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/SubscriptionAuthorizationRuleTest.java @@ -0,0 +1,42 @@ +/* + * Copyright 2018-2025 Heilbronn University of Applied Sciences + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package dev.dsf.fhir.authorization; + +import org.hl7.fhir.r4.model.Subscription; + +public class SubscriptionAuthorizationRuleTest extends AbstractMetaTagAuthorizationRuleTest +{ + @Override + protected SubscriptionAuthorizationRule createRule() + { + return new SubscriptionAuthorizationRule(daoProvider, SERVER_BASE, referenceResolver, organizationProvider, + readAccessHelper, parameterConverter); + } + + @Override + protected Class expectedResourceType() + { + return Subscription.class; + } + + @Override + protected Subscription newResource() + { + Subscription resource = new Subscription(); + resource.setId("Subscription/d1e2f3a4-b5c6-4d7e-8f90-123456789abc/_history/1"); + return resource; + } +} diff --git a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/TaskAuthorizationRuleTest.java b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/TaskAuthorizationRuleTest.java new file mode 100644 index 000000000..c0abdce2d --- /dev/null +++ b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/TaskAuthorizationRuleTest.java @@ -0,0 +1,171 @@ +/* + * Copyright 2018-2025 Heilbronn University of Applied Sciences + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package dev.dsf.fhir.authorization; + +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + +import java.sql.Connection; + +import org.hl7.fhir.r4.model.Task; +import org.hl7.fhir.r4.model.Task.TaskStatus; +import org.junit.Ignore; +import org.junit.Test; + +import ca.uhn.fhir.context.FhirContext; +import dev.dsf.common.auth.conf.OrganizationIdentity; +import dev.dsf.fhir.authentication.EndpointProvider; +import dev.dsf.fhir.authorization.process.ProcessAuthorizationHelper; + +public class TaskAuthorizationRuleTest extends AbstractAuthorizationRuleTest +{ + private static final FhirContext FHIR_CONTEXT = FhirContext.forR4(); + + private final ProcessAuthorizationHelper processAuthorizationHelper = mock(ProcessAuthorizationHelper.class); + private final EndpointProvider endpointProvider = mock(EndpointProvider.class); + + @Override + protected TaskAuthorizationRule createRule() + { + return newRule(processAuthorizationHelper, endpointProvider); + } + + private TaskAuthorizationRule newRule(ProcessAuthorizationHelper processAuthorizationHelper, + EndpointProvider endpointProvider) + { + return new TaskAuthorizationRule(daoProvider, SERVER_BASE, referenceResolver, organizationProvider, + readAccessHelper, parameterConverter, processAuthorizationHelper, FHIR_CONTEXT, endpointProvider); + } + + @Override + protected Class expectedResourceType() + { + return Task.class; + } + + @Override + protected Task newResource() + { + return task(TaskStatus.DRAFT); + } + + private static Task task(TaskStatus status) + { + Task task = new Task(); + task.setId("Task/d1e2f3a4-b5c6-4d7e-8f90-123456789abc/_history/1"); + task.setStatus(status); + return task; + } + + private static OrganizationIdentity localOrganizationIdentity() + { + OrganizationIdentity identity = mock(OrganizationIdentity.class); + when(identity.isLocalIdentity()).thenReturn(true); + when(identity.hasDsfRole(any())).thenReturn(true); + return identity; + } + + @Test + public void afterPropertiesSetThrowsWhenProcessAuthorizationHelperNull() + { + TaskAuthorizationRule ruleWithNull = newRule(null, endpointProvider); + assertThrows(NullPointerException.class, ruleWithNull::afterPropertiesSet); + } + + @Test + public void afterPropertiesSetThrowsWhenEndpointProviderNull() + { + TaskAuthorizationRule ruleWithNull = newRule(processAuthorizationHelper, null); + assertThrows(NullPointerException.class, ruleWithNull::afterPropertiesSet); + } + + @Test + public void createDeniedWhenIdentityHasNoCreateRole() + { + Connection connection = mock(Connection.class); + + assertTrue(rule.reasonCreateAllowed(connection, identity(true, false), task(TaskStatus.REQUESTED)).isEmpty()); + verifyNoInteractions(connection); + } + + @Test + public void createDeniedWhenTaskStatusNotDraftOrRequested() + { + Connection connection = mock(Connection.class); + + assertTrue(rule.reasonCreateAllowed(connection, identity(true, true), task(TaskStatus.COMPLETED)).isEmpty()); + verifyNoInteractions(connection); + } + + @Test + public void createDraftDeniedWhenIdentityNotLocalOrganizationOrDsfAdmin() + { + Connection connection = mock(Connection.class); + + assertTrue(rule.reasonCreateAllowed(connection, identity(true, true), task(TaskStatus.DRAFT)).isEmpty()); + verifyNoInteractions(connection); + } + + @Test + public void deleteAllowedForLocalOrganizationWhenTaskStatusDraft() + { + Connection connection = mock(Connection.class); + + assertTrue( + rule.reasonDeleteAllowed(connection, localOrganizationIdentity(), task(TaskStatus.DRAFT)).isPresent()); + verifyNoInteractions(connection); + } + + @Test + public void deleteDeniedWhenIdentityHasNoDeleteRole() + { + Connection connection = mock(Connection.class); + + assertTrue(rule.reasonDeleteAllowed(connection, identity(true, false), task(TaskStatus.DRAFT)).isEmpty()); + verifyNoInteractions(connection); + } + + @Test + public void deleteDeniedWhenIdentityNotLocalOrganizationOrDsfAdmin() + { + Connection connection = mock(Connection.class); + + assertTrue(rule.reasonDeleteAllowed(connection, identity(true, true), task(TaskStatus.DRAFT)).isEmpty()); + verifyNoInteractions(connection); + } + + @Test + public void deleteDeniedWhenTaskStatusNotDraft() + { + Connection connection = mock(Connection.class); + + assertTrue(rule.reasonDeleteAllowed(connection, localOrganizationIdentity(), task(TaskStatus.REQUESTED)) + .isEmpty()); + verifyNoInteractions(connection); + } + + @Test + @Ignore + @Override + public void permanentDeleteForLocalIdentityWithPermanentDeleteRoleAndDeleteRoleAllowed() throws Exception + { + super.permanentDeleteForLocalIdentityWithPermanentDeleteRoleAndDeleteRoleAllowed(); + } +} diff --git a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/ValueSetAuthorizationRuleTest.java b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/ValueSetAuthorizationRuleTest.java new file mode 100644 index 000000000..165eca1ad --- /dev/null +++ b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/ValueSetAuthorizationRuleTest.java @@ -0,0 +1,42 @@ +/* + * Copyright 2018-2025 Heilbronn University of Applied Sciences + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package dev.dsf.fhir.authorization; + +import org.hl7.fhir.r4.model.ValueSet; + +public class ValueSetAuthorizationRuleTest extends AbstractMetaTagAuthorizationRuleTest +{ + @Override + protected ValueSetAuthorizationRule createRule() + { + return new ValueSetAuthorizationRule(daoProvider, SERVER_BASE, referenceResolver, organizationProvider, + readAccessHelper, parameterConverter); + } + + @Override + protected Class expectedResourceType() + { + return ValueSet.class; + } + + @Override + protected ValueSet newResource() + { + ValueSet resource = new ValueSet(); + resource.setId("ValueSet/d1e2f3a4-b5c6-4d7e-8f90-123456789abc/_history/1"); + return resource; + } +} diff --git a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/history/filter/HistoryIdentityFilterTest.java b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/history/filter/HistoryIdentityFilterTest.java new file mode 100644 index 000000000..af9ecbea9 --- /dev/null +++ b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/history/filter/HistoryIdentityFilterTest.java @@ -0,0 +1,93 @@ +/* + * Copyright 2018-2025 Heilbronn University of Applied Sciences + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package dev.dsf.fhir.history.filter; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; + +import java.util.List; +import java.util.function.Function; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; +import org.junit.runners.Parameterized.Parameters; + +import dev.dsf.common.auth.conf.Identity; + +@RunWith(Parameterized.class) +public class HistoryIdentityFilterTest +{ + @Parameters(name = "{1}") + public static List data() + { + return List.of(row(ActivityDefinitionHistoryIdentityFilter::new, "ActivityDefinition"), + row(BinaryHistoryIdentityFilter::new, "Binary"), row(BundleHistoryIdentityFilter::new, "Bundle"), + row(CodeSystemHistoryIdentityFilter::new, "CodeSystem"), + row(DocumentReferenceHistoryIdentityFilter::new, "DocumentReference"), + row(EndpointHistoryIdentityFilter::new, "Endpoint"), row(GroupHistoryIdentityFilter::new, "Group"), + row(HealthcareServiceHistoryIdentityFilter::new, "HealthcareService"), + row(LibraryHistoryIdentityFilter::new, "Library"), row(LocationHistoryIdentityFilter::new, "Location"), + row(MeasureHistoryIdentityFilter::new, "Measure"), + row(MeasureReportHistoryIdentityFilter::new, "MeasureReport"), + row(NamingSystemHistoryIdentityFilter::new, "NamingSystem"), + row(OrganizationAffiliationHistoryIdentityFilter::new, "OrganizationAffiliation"), + row(OrganizationHistoryIdentityFilter::new, "Organization"), + row(PatientHistoryIdentityFilter::new, "Patient"), + row(PractitionerHistoryIdentityFilter::new, "Practitioner"), + row(PractitionerRoleHistoryIdentityFilter::new, "PractitionerRole"), + row(ProvenanceHistoryIdentityFilter::new, "Provenance"), + row(QuestionnaireHistoryIdentityFilter::new, "Questionnaire"), + row(QuestionnaireResponseHistoryIdentityFilter::new, "QuestionnaireResponse"), + row(ResearchStudyHistoryIdentityFilter::new, "ResearchStudy"), + row(StructureDefinitionHistoryIdentityFilter::new, "StructureDefinition"), + row(SubscriptionHistoryIdentityFilter::new, "Subscription"), + row(TaskHistoryIdentityFilter::new, "Task"), row(ValueSetHistoryIdentityFilter::new, "ValueSet")); + } + + private static Object[] row(Function factory, String resourceType) + { + return new Object[] { factory, resourceType }; + } + + @Parameter(0) + public Function filterFactory; + + @Parameter(1) + public String resourceType; + + @Test + public void filterQueryScopesToResourceType() + { + HistoryIdentityFilter filter = filterFactory.apply(mock(Identity.class)); + + String query = filter.getFilterQuery(); + assertNotNull(query); + assertTrue("filter query must scope to type '" + resourceType + "' but was: " + query, + query.contains("type = '" + resourceType + "'")); + } + + @Test + public void filterQueryIsDefinedAndParameterCountNonNegative() + { + HistoryIdentityFilter filter = filterFactory.apply(mock(Identity.class)); + + assertTrue(filter.isDefined()); + assertTrue(filter.getSqlParameterCount() >= 0); + } +} diff --git a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/history/filter/TaskHistoryIdentityFilterTest.java b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/history/filter/TaskHistoryIdentityFilterTest.java new file mode 100644 index 000000000..67cdd70c0 --- /dev/null +++ b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/history/filter/TaskHistoryIdentityFilterTest.java @@ -0,0 +1,74 @@ +/* + * Copyright 2018-2025 Heilbronn University of Applied Sciences + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package dev.dsf.fhir.history.filter; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import org.junit.Test; + +import dev.dsf.common.auth.conf.Identity; +import dev.dsf.common.auth.conf.OrganizationIdentity; + +public class TaskHistoryIdentityFilterTest +{ + @Test + public void identityWithoutRoleSeesNothing() + { + TaskHistoryIdentityFilter filter = new TaskHistoryIdentityFilter(mock(Identity.class)); + + String query = filter.getFilterQuery(); + assertTrue(query.contains("type = 'Task'")); + assertTrue(query.contains("FALSE")); + assertEquals(0, filter.getSqlParameterCount()); + } + + @Test + public void localOrganizationSeesTasksWhereItIsRecipient() + { + OrganizationIdentity identity = mock(OrganizationIdentity.class); + when(identity.hasDsfRole(any())).thenReturn(true); + when(identity.isLocalIdentity()).thenReturn(true); + + TaskHistoryIdentityFilter filter = new TaskHistoryIdentityFilter(identity); + + String query = filter.getFilterQuery(); + assertTrue(query.contains("type = 'Task'")); + assertTrue(query.contains("recipient")); + assertFalse(query.contains("FALSE")); + assertEquals(1, filter.getSqlParameterCount()); + } + + @Test + public void remoteOrganizationSeesTasksWhereItIsRequester() + { + OrganizationIdentity identity = mock(OrganizationIdentity.class); + when(identity.hasDsfRole(any())).thenReturn(true); + when(identity.isLocalIdentity()).thenReturn(false); + + TaskHistoryIdentityFilter filter = new TaskHistoryIdentityFilter(identity); + + String query = filter.getFilterQuery(); + assertTrue(query.contains("type = 'Task'")); + assertTrue(query.contains("requester")); + assertFalse(query.contains("FALSE")); + assertEquals(1, filter.getSqlParameterCount()); + } +} diff --git a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/webservice/secure/AbstractResourceServiceSecureTest.java b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/webservice/secure/AbstractResourceServiceSecureTest.java new file mode 100644 index 000000000..01b31e1ca --- /dev/null +++ b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/webservice/secure/AbstractResourceServiceSecureTest.java @@ -0,0 +1,1163 @@ +/* + * Copyright 2018-2025 Heilbronn University of Applied Sciences + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package dev.dsf.fhir.webservice.secure; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +import java.lang.reflect.Method; +import java.util.List; +import java.util.Optional; +import java.util.UUID; +import java.util.function.Supplier; + +import org.hl7.fhir.r4.model.IdType; +import org.hl7.fhir.r4.model.OperationOutcome; +import org.hl7.fhir.r4.model.Resource; +import org.junit.Before; +import org.junit.Test; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.model.api.annotation.ResourceDef; +import ca.uhn.fhir.validation.ResultSeverityEnum; +import ca.uhn.fhir.validation.SingleValidationMessage; +import ca.uhn.fhir.validation.ValidationResult; +import dev.dsf.common.auth.conf.Identity; +import dev.dsf.fhir.authentication.CurrentIdentityProvider; +import dev.dsf.fhir.authorization.AuthorizationRule; +import dev.dsf.fhir.dao.ResourceDao; +import dev.dsf.fhir.help.ExceptionHandler; +import dev.dsf.fhir.help.ParameterConverter; +import dev.dsf.fhir.help.ResponseGenerator; +import dev.dsf.fhir.search.PageAndCount; +import dev.dsf.fhir.search.PartialResult; +import dev.dsf.fhir.search.SearchQuery; +import dev.dsf.fhir.search.SearchQueryParameterError; +import dev.dsf.fhir.search.SearchQueryParameterError.SearchQueryParameterErrorType; +import dev.dsf.fhir.service.DefaultProfileProvider; +import dev.dsf.fhir.service.ReferenceCleaner; +import dev.dsf.fhir.service.ReferenceExtractor; +import dev.dsf.fhir.service.ReferenceResolver; +import dev.dsf.fhir.validation.ResourceValidator; +import dev.dsf.fhir.validation.ValidationRules; +import dev.dsf.fhir.webservice.specification.BasicResourceService; +import jakarta.ws.rs.WebApplicationException; +import jakarta.ws.rs.core.HttpHeaders; +import jakarta.ws.rs.core.MultivaluedMap; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.core.Response.Status; +import jakarta.ws.rs.core.UriInfo; + +public abstract class AbstractResourceServiceSecureTest, D extends ResourceDao> +{ + @FunctionalInterface + public interface ResourceServiceSecureFactory, D extends ResourceDao> + { + S create(S delegate, String serverBase, ResponseGenerator responseGenerator, + ReferenceResolver referenceResolver, ReferenceCleaner referenceCleaner, + ReferenceExtractor referenceExtractor, D binaryDao, ExceptionHandler exceptionHandler, + ParameterConverter parameterConverter, AuthorizationRule authorizationRule, + ResourceValidator resourceValidator, ValidationRules validationRules, + DefaultProfileProvider defaultProfileProvider); + } + + protected static final String SERVER_BASE = "https://dsf.test/fhir"; + private static final String PERMANENT_DELETE_PATH = "$permanent-delete"; + + protected static final FhirContext FHIR_CONTEXT = FhirContext.forR4(); + + protected final Class resourceClass; + protected final Class serviceClass; + protected final Class daoClass; + protected final Supplier resouceSupplier; + protected final ResourceServiceSecureFactory resourceServiceSecureFactory; + + protected final ValidationRules validationRules = new ValidationRules(SERVER_BASE); + protected final ResponseGenerator responseGenerator = new ResponseGenerator(SERVER_BASE); + protected final ExceptionHandler exceptionHandler = new ExceptionHandler(responseGenerator); + protected final ResourceValidator resourceValidator = mock(ResourceValidator.class); + protected final ReferenceCleaner referenceCleaner = mock(ReferenceCleaner.class); + @SuppressWarnings("unchecked") + protected final AuthorizationRule authorizationRule = mock(AuthorizationRule.class); + protected final CurrentIdentityProvider currentIdentityProvider = mock(CurrentIdentityProvider.class); + protected final Response responseOkWithResourceIdVersion = mock(Response.class); + protected final Response responseForbiddenWithOperationOutcome = mock(Response.class); + protected final Response responseNotModifiedWithResourceIdVersion = mock(Response.class); + protected final Response responsePreconditionFailedWithResourceIdVersion = mock(Response.class); + + protected final S delegate; + protected final D dao; + + protected S resourceServiceSecure; + + public AbstractResourceServiceSecureTest(Class resourceClass, Class serviceClass, Class daoClass, + Supplier resouceSupplier, ResourceServiceSecureFactory resourceServiceSecureFactory) + { + this.resourceClass = resourceClass; + this.serviceClass = serviceClass; + this.daoClass = daoClass; + this.resouceSupplier = resouceSupplier; + this.resourceServiceSecureFactory = resourceServiceSecureFactory; + + delegate = mock(serviceClass); + dao = mock(daoClass); + } + + @Before + public void before() throws Exception + { + resourceServiceSecure = createResourceServiceSecure(); + resourceServiceSecure.setCurrentIdentityProvider(currentIdentityProvider); + + Method afterPropertiesSet = resourceServiceSecure.getClass().getMethod("afterPropertiesSet"); + afterPropertiesSet.invoke(resourceServiceSecure); + + Identity identity = mock(Identity.class); + when(identity.getName()).thenReturn("Test Identity"); + when(currentIdentityProvider.getCurrentIdentity()).thenReturn(identity); + + when(referenceCleaner.cleanLiteralReferences(any(resourceClass))) + .thenAnswer(invocation -> invocation.getArgument(0)); + + when(responseOkWithResourceIdVersion.getStatusInfo()).thenReturn(Status.OK); + when(responseOkWithResourceIdVersion.getStatus()).thenReturn(Status.OK.getStatusCode()); + when(responseOkWithResourceIdVersion.hasEntity()).thenReturn(true); + when(responseOkWithResourceIdVersion.getEntity()).thenReturn(createResourceWithIdAndVersion()); + + when(responseForbiddenWithOperationOutcome.getStatusInfo()).thenReturn(Status.FORBIDDEN); + when(responseForbiddenWithOperationOutcome.getStatus()).thenReturn(Status.FORBIDDEN.getStatusCode()); + when(responseForbiddenWithOperationOutcome.hasEntity()).thenReturn(true); + when(responseForbiddenWithOperationOutcome.getEntity()).thenReturn(new OperationOutcome()); + + when(responseNotModifiedWithResourceIdVersion.getStatusInfo()).thenReturn(Status.NOT_MODIFIED); + when(responseNotModifiedWithResourceIdVersion.getStatus()).thenReturn(Status.NOT_MODIFIED.getStatusCode()); + when(responseNotModifiedWithResourceIdVersion.hasEntity()).thenReturn(true); + when(responseNotModifiedWithResourceIdVersion.getEntity()).thenReturn(createResourceWithIdAndVersion()); + + when(responsePreconditionFailedWithResourceIdVersion.getStatusInfo()).thenReturn(Status.PRECONDITION_FAILED); + when(responsePreconditionFailedWithResourceIdVersion.getStatus()) + .thenReturn(Status.PRECONDITION_FAILED.getStatusCode()); + when(responsePreconditionFailedWithResourceIdVersion.hasEntity()).thenReturn(true); + when(responsePreconditionFailedWithResourceIdVersion.getEntity()).thenReturn(createResourceWithIdAndVersion()); + } + + protected final S createResourceServiceSecure() + { + return resourceServiceSecureFactory.create(delegate, SERVER_BASE, responseGenerator, + mock(ReferenceResolver.class), referenceCleaner, mock(ReferenceExtractor.class), dao, exceptionHandler, + mock(ParameterConverter.class), authorizationRule, resourceValidator, validationRules, + mock(DefaultProfileProvider.class)); + } + + protected final R createResource() + { + return resouceSupplier.get(); + } + + protected final R createResourceWithIdAndVersion() + { + R resource = createResource(); + resource.setIdElement( + new IdType(resourceClass.getAnnotation(ResourceDef.class).name(), UUID.randomUUID().toString(), "1")); + resource.getMeta().setVersionId("1"); + + return resource; + } + + @Test + public void readMustEnforceReadAuthorization() + { + when(delegate.read(anyString(), any(), any())).thenReturn(responseOkWithResourceIdVersion); + + resourceServiceSecure.read("some-id", mock(UriInfo.class), mock(HttpHeaders.class)); + + verify(authorizationRule).reasonReadAllowed(any(), any()); + } + + @Test + public void vreadMustEnforceReadAuthorization() + { + when(delegate.vread(anyString(), anyLong(), any(), any())).thenReturn(responseOkWithResourceIdVersion); + + resourceServiceSecure.vread("some-id", 1, mock(UriInfo.class), mock(HttpHeaders.class)); + + verify(authorizationRule).reasonReadAllowed(any(), any()); + } + + @Test + public void expectForbiddenReadNotAllowed() throws Exception + { + when(delegate.read(anyString(), any(), any())).thenReturn(responseOkWithResourceIdVersion); + when(authorizationRule.reasonReadAllowed(any(), any())).thenReturn(Optional.empty()); + + Response response = resourceServiceSecure.read("some-id", mock(UriInfo.class), mock(HttpHeaders.class)); + assertEquals(Status.FORBIDDEN, response.getStatusInfo()); + } + + @Test + public void expectForbiddenVRreadNotAllowed() throws Exception + { + when(delegate.vread(anyString(), anyLong(), any(), any())).thenReturn(responseOkWithResourceIdVersion); + when(authorizationRule.reasonReadAllowed(any(), any())).thenReturn(Optional.empty()); + + Response response = resourceServiceSecure.vread("some-id", 1, mock(UriInfo.class), mock(HttpHeaders.class)); + assertEquals(Status.FORBIDDEN, response.getStatusInfo()); + } + + @Test + public void expectOkReadAllowedWithStatusOk() throws Exception + { + when(delegate.read(anyString(), any(), any())).thenReturn(responseOkWithResourceIdVersion); + when(authorizationRule.reasonReadAllowed(any(), any())).thenReturn(Optional.of("Test Reason")); + + Response response = resourceServiceSecure.read("some-id", mock(UriInfo.class), mock(HttpHeaders.class)); + assertEquals(Status.OK, response.getStatusInfo()); + assertTrue(response.hasEntity()); + assertEquals(resourceClass, response.getEntity().getClass()); + + verify(authorizationRule).reasonReadAllowed(any(), any()); + } + + @Test + public void expectOkVReadAllowedWithStatusOk() throws Exception + { + when(delegate.vread(anyString(), anyLong(), any(), any())).thenReturn(responseOkWithResourceIdVersion); + when(authorizationRule.reasonReadAllowed(any(), any())).thenReturn(Optional.of("Test Reason")); + + Response response = resourceServiceSecure.vread("some-id", 1, mock(UriInfo.class), mock(HttpHeaders.class)); + assertEquals(Status.OK, response.getStatusInfo()); + assertTrue(response.hasEntity()); + assertEquals(resourceClass, response.getEntity().getClass()); + + verify(authorizationRule).reasonReadAllowed(any(), any()); + } + + @Test + public void expectNoEntityAndStatusNotModifiedReadAllowedWithStatusNotModified() throws Exception + { + when(delegate.read(anyString(), any(), any())).thenReturn(responseNotModifiedWithResourceIdVersion); + when(authorizationRule.reasonReadAllowed(any(), any())).thenReturn(Optional.of("Test Reason")); + + Response response = resourceServiceSecure.read("some-id", mock(UriInfo.class), mock(HttpHeaders.class)); + assertEquals(Status.NOT_MODIFIED, response.getStatusInfo()); + assertFalse(response.hasEntity()); + + verify(authorizationRule).reasonReadAllowed(any(), any()); + } + + @Test + public void expectNoEntityAndStatusNotModifiedVReadAllowedWithStatusNotModified() throws Exception + { + when(delegate.vread(anyString(), anyLong(), any(), any())).thenReturn(responseNotModifiedWithResourceIdVersion); + when(authorizationRule.reasonReadAllowed(any(), any())).thenReturn(Optional.of("Test Reason")); + + Response response = resourceServiceSecure.vread("some-id", 1, mock(UriInfo.class), mock(HttpHeaders.class)); + assertEquals(Status.NOT_MODIFIED, response.getStatusInfo()); + assertFalse(response.hasEntity()); + + verify(authorizationRule).reasonReadAllowed(any(), any()); + } + + @Test + public void expectNoEntityAndStatusPreconditionFailedReadAllowedWithStatusPreconditionFailed() throws Exception + { + when(delegate.read(anyString(), any(), any())).thenReturn(responsePreconditionFailedWithResourceIdVersion); + when(authorizationRule.reasonReadAllowed(any(), any())).thenReturn(Optional.of("Test Reason")); + + Response response = resourceServiceSecure.read("some-id", mock(UriInfo.class), mock(HttpHeaders.class)); + assertEquals(Status.PRECONDITION_FAILED, response.getStatusInfo()); + assertFalse(response.hasEntity()); + + verify(authorizationRule).reasonReadAllowed(any(), any()); + } + + @Test + public void expectNoEntityAndStatusPreconditionFailedVReadAllowedWithStatusPreconditionFailed() throws Exception + { + when(delegate.vread(anyString(), anyLong(), any(), any())) + .thenReturn(responsePreconditionFailedWithResourceIdVersion); + when(authorizationRule.reasonReadAllowed(any(), any())).thenReturn(Optional.of("Test Reason")); + + Response response = resourceServiceSecure.vread("some-id", 1, mock(UriInfo.class), mock(HttpHeaders.class)); + assertEquals(Status.PRECONDITION_FAILED, response.getStatusInfo()); + assertFalse(response.hasEntity()); + + verify(authorizationRule).reasonReadAllowed(any(), any()); + } + + @Test + public void expectOperationOutcomeReadNoAuthorizationRuleCallOperationOutcome() throws Exception + { + when(delegate.read(anyString(), any(), any())).thenReturn(responseForbiddenWithOperationOutcome); + + Response response = resourceServiceSecure.read("some-id", mock(UriInfo.class), mock(HttpHeaders.class)); + assertEquals(Status.FORBIDDEN, response.getStatusInfo()); + assertTrue(response.hasEntity()); + assertEquals(OperationOutcome.class, response.getEntity().getClass()); + + verifyNoInteractions(authorizationRule); + } + + @Test + public void expectOperationOutcomeVReadNoAuthorizationRuleCallOperationOutcome() throws Exception + { + when(delegate.vread(anyString(), anyLong(), any(), any())).thenReturn(responseForbiddenWithOperationOutcome); + + Response response = resourceServiceSecure.vread("some-id", 1, mock(UriInfo.class), mock(HttpHeaders.class)); + assertEquals(Status.FORBIDDEN, response.getStatusInfo()); + assertTrue(response.hasEntity()); + assertEquals(OperationOutcome.class, response.getEntity().getClass()); + + verifyNoInteractions(authorizationRule); + } + + @Test + public void expectForbiddenReadNoAuthorizationRuleCallUnexpectedResourceType() throws Exception + { + Response unexpectedResourceTpye = mock(Response.class); + when(unexpectedResourceTpye.getStatusInfo()).thenReturn(Status.OK); + when(unexpectedResourceTpye.getStatus()).thenReturn(Status.OK.getStatusCode()); + when(unexpectedResourceTpye.getEntity()).thenReturn("Unexpected Resource Type"); + when(unexpectedResourceTpye.hasEntity()).thenReturn(true); + when(delegate.read(anyString(), any(), any())).thenReturn(unexpectedResourceTpye); + + Response response = resourceServiceSecure.read("some-id", mock(UriInfo.class), mock(HttpHeaders.class)); + assertEquals(Status.FORBIDDEN, response.getStatusInfo()); + assertTrue(response.hasEntity()); + assertEquals(OperationOutcome.class, response.getEntity().getClass()); + + verifyNoInteractions(authorizationRule); + } + + @Test + public void expectForbiddenVReadNoAuthorizationRuleCallUnexpectedResourceType() throws Exception + { + Response unexpectedResourceTpye = mock(Response.class); + when(unexpectedResourceTpye.getStatusInfo()).thenReturn(Status.OK); + when(unexpectedResourceTpye.getStatus()).thenReturn(Status.OK.getStatusCode()); + when(unexpectedResourceTpye.getEntity()).thenReturn("Unexpected Resource Type"); + when(unexpectedResourceTpye.hasEntity()).thenReturn(true); + when(delegate.vread(anyString(), anyLong(), any(), any())).thenReturn(unexpectedResourceTpye); + + Response response = resourceServiceSecure.vread("some-id", 1, mock(UriInfo.class), mock(HttpHeaders.class)); + assertEquals(Status.FORBIDDEN, response.getStatusInfo()); + assertTrue(response.hasEntity()); + assertEquals(OperationOutcome.class, response.getEntity().getClass()); + + verifyNoInteractions(authorizationRule); + } + + @Test + public void expectStatusCodeReadNoAuthorizationRuleCallStatusCodeOnly() throws Exception + { + Response statusCodeOnly = mock(Response.class); + when(statusCodeOnly.getStatusInfo()).thenReturn(Status.PAYMENT_REQUIRED); + when(statusCodeOnly.getStatus()).thenReturn(Status.PAYMENT_REQUIRED.getStatusCode()); + + when(delegate.read(anyString(), any(), any())).thenReturn(statusCodeOnly); + + Response response = resourceServiceSecure.read("some-id", mock(UriInfo.class), mock(HttpHeaders.class)); + assertEquals(Status.PAYMENT_REQUIRED, response.getStatusInfo()); + assertFalse(response.hasEntity()); + + verifyNoInteractions(authorizationRule); + } + + @Test + public void expectStatusCodeVReadNoAuthorizationRuleCallStatusCodeOnly() throws Exception + { + Response statusCodeOnly = mock(Response.class); + when(statusCodeOnly.getStatusInfo()).thenReturn(Status.PAYMENT_REQUIRED); + when(statusCodeOnly.getStatus()).thenReturn(Status.PAYMENT_REQUIRED.getStatusCode()); + + when(delegate.vread(anyString(), anyLong(), any(), any())).thenReturn(statusCodeOnly); + + Response response = resourceServiceSecure.vread("some-id", 1, mock(UriInfo.class), mock(HttpHeaders.class)); + assertEquals(Status.PAYMENT_REQUIRED, response.getStatusInfo()); + assertFalse(response.hasEntity()); + + verifyNoInteractions(authorizationRule); + } + + @Test + public void createMustEnforceCreateAuthorization() + { + resourceServiceSecure.create(createResourceWithIdAndVersion(), mock(UriInfo.class), mock(HttpHeaders.class)); + + verify(authorizationRule).reasonCreateAllowed(any(), any()); + } + + @Test + public void expectForbiddenCreateNotAllowed() throws Exception + { + when(authorizationRule.reasonCreateAllowed(any(), any())).thenReturn(Optional.empty()); + + Response response = resourceServiceSecure.create(createResourceWithIdAndVersion(), mock(UriInfo.class), + mock(HttpHeaders.class)); + assertEquals(Status.FORBIDDEN, response.getStatusInfo()); + + verify(authorizationRule).reasonCreateAllowed(any(), any()); + } + + @Test + public void expectCreatedCreateAllowed() throws Exception + { + R resource = createResourceWithIdAndVersion(); + + when(resourceValidator.validate(any())).thenReturn(new ValidationResult(FHIR_CONTEXT, List.of())); + when(authorizationRule.reasonCreateAllowed(any(), any())).thenReturn(Optional.of("Test Reason")); + + Response responseCreated = mock(Response.class); + when(responseCreated.getStatusInfo()).thenReturn(Status.CREATED); + when(responseCreated.getStatus()).thenReturn(Status.CREATED.getStatusCode()); + when(responseCreated.getEntity()).thenReturn(resource); + when(responseCreated.hasEntity()).thenReturn(true); + when(delegate.create(any(), any(), any())).thenReturn(responseCreated); + + Response response = resourceServiceSecure.create(resource, mock(UriInfo.class), mock(HttpHeaders.class)); + assertEquals(Status.CREATED, response.getStatusInfo()); + assertTrue(response.hasEntity()); + assertEquals(resourceClass, response.getEntity().getClass()); + + verify(resourceValidator).validate(any()); + verify(authorizationRule).reasonCreateAllowed(any(), any()); + } + + @Test + public void expectForbiddenCreateAllowedNonValidResouce() throws Exception + { + R resource = createResourceWithIdAndVersion(); + + SingleValidationMessage validationMessage = new SingleValidationMessage(); + validationMessage.setSeverity(ResultSeverityEnum.ERROR); + + when(resourceValidator.validate(any())) + .thenReturn(new ValidationResult(FHIR_CONTEXT, List.of(validationMessage))); + when(authorizationRule.reasonCreateAllowed(any(), any())).thenReturn(Optional.of("Test Reason")); + + Response response = resourceServiceSecure.create(resource, mock(UriInfo.class), mock(HttpHeaders.class)); + assertEquals(Status.FORBIDDEN, response.getStatusInfo()); + assertTrue(response.hasEntity()); + assertEquals(OperationOutcome.class, response.getEntity().getClass()); + + verify(resourceValidator).validate(any()); + verify(authorizationRule).reasonCreateAllowed(any(), any()); + } + + @Test + public void expectOkCreateAllowedOneExists() throws Exception + { + R resource = createResourceWithIdAndVersion(); + + when(resourceValidator.validate(any())).thenReturn(new ValidationResult(FHIR_CONTEXT, List.of())); + when(authorizationRule.reasonCreateAllowed(any(), any())).thenReturn(Optional.of("Test Reason")); + + Response responseOk = responseGenerator.oneExists(resource, "Test Criteria"); + when(delegate.create(any(), any(), any())).thenReturn(responseOk); + + Response response = resourceServiceSecure.create(resource, mock(UriInfo.class), mock(HttpHeaders.class)); + assertEquals(Status.OK, response.getStatusInfo()); + + verify(resourceValidator).validate(any()); + verify(authorizationRule).reasonCreateAllowed(any(), any()); + } + + @Test + public void expectPreconditionFailedCreateAllowedMultipleExists() throws Exception + { + R resource = createResourceWithIdAndVersion(); + + when(resourceValidator.validate(any())).thenReturn(new ValidationResult(FHIR_CONTEXT, List.of())); + when(authorizationRule.reasonCreateAllowed(any(), any())).thenReturn(Optional.of("Test Reason")); + + Response responsePreconditionFailed = responseGenerator + .multipleExists(resourceClass.getAnnotation(ResourceDef.class).name(), "Test Criteria"); + when(delegate.create(any(), any(), any())).thenReturn(responsePreconditionFailed); + + Response response = resourceServiceSecure.create(resource, mock(UriInfo.class), mock(HttpHeaders.class)); + assertEquals(Status.PRECONDITION_FAILED, response.getStatusInfo()); + + verify(resourceValidator).validate(any()); + verify(authorizationRule).reasonCreateAllowed(any(), any()); + } + + @Test + public void historyBaseMustEnforceHistoryAuthorization() + { + when(delegate.history(any(), any())).thenReturn(mock(Response.class)); + + resourceServiceSecure.history(mock(UriInfo.class), mock(HttpHeaders.class)); + + verify(authorizationRule).reasonHistoryAllowed(any()); + } + + @Test + public void historyResouceMustEnforceHistoryAuthorization() + { + when(delegate.history(anyString(), any(), any())).thenReturn(mock(Response.class)); + + resourceServiceSecure.history("some-id", mock(UriInfo.class), mock(HttpHeaders.class)); + + verify(authorizationRule).reasonHistoryAllowed(any()); + } + + @Test + public void expectOkHistoryBaseAllowed() throws Exception + { + when(authorizationRule.reasonHistoryAllowed(any())).thenReturn(Optional.of("Test Reason")); + + Response responseOk = mock(Response.class); + when(responseOk.getStatusInfo()).thenReturn(Status.OK); + when(responseOk.getStatus()).thenReturn(Status.OK.getStatusCode()); + when(delegate.history(any(), any())).thenReturn(responseOk); + + Response response = resourceServiceSecure.history(mock(UriInfo.class), mock(HttpHeaders.class)); + assertEquals(Status.OK, response.getStatusInfo()); + + verify(authorizationRule).reasonHistoryAllowed(any()); + } + + @Test + public void expectOkHistoryResourceAllowed() throws Exception + { + when(authorizationRule.reasonHistoryAllowed(any())).thenReturn(Optional.of("Test Reason")); + + Response responseOk = mock(Response.class); + when(responseOk.getStatusInfo()).thenReturn(Status.OK); + when(responseOk.getStatus()).thenReturn(Status.OK.getStatusCode()); + when(delegate.history(anyString(), any(), any())).thenReturn(responseOk); + + Response response = resourceServiceSecure.history("some-id", mock(UriInfo.class), mock(HttpHeaders.class)); + assertEquals(Status.OK, response.getStatusInfo()); + + verify(authorizationRule).reasonHistoryAllowed(any()); + } + + @Test + public void expectBadRequesHistoryBaseAllowedInvalidRequest() throws Exception + { + when(authorizationRule.reasonHistoryAllowed(any())).thenReturn(Optional.of("Test Reason")); + + Response responseOk = mock(Response.class); + when(responseOk.getStatusInfo()).thenReturn(Status.BAD_REQUEST); + when(responseOk.getStatus()).thenReturn(Status.BAD_REQUEST.getStatusCode()); + when(delegate.history(any(), any())).thenReturn(responseOk); + + Response response = resourceServiceSecure.history(mock(UriInfo.class), mock(HttpHeaders.class)); + assertEquals(Status.BAD_REQUEST, response.getStatusInfo()); + + verify(authorizationRule).reasonHistoryAllowed(any()); + } + + @Test + public void expectBadRequestHistoryResourceAllowedInvalidRequest() throws Exception + { + when(authorizationRule.reasonHistoryAllowed(any())).thenReturn(Optional.of("Test Reason")); + + Response responseBadRequest = mock(Response.class); + when(responseBadRequest.getStatusInfo()).thenReturn(Status.BAD_REQUEST); + when(responseBadRequest.getStatus()).thenReturn(Status.BAD_REQUEST.getStatusCode()); + when(delegate.history(anyString(), any(), any())).thenReturn(responseBadRequest); + + Response response = resourceServiceSecure.history("some-id", mock(UriInfo.class), mock(HttpHeaders.class)); + assertEquals(Status.BAD_REQUEST, response.getStatusInfo()); + + verify(authorizationRule).reasonHistoryAllowed(any()); + } + + @Test + public void updateConditionalFoundResourceNoIdMustEnforceUpdateAuthorization() throws Exception + { + @SuppressWarnings("unchecked") + SearchQuery searchQuery = mock(SearchQuery.class); + when(dao.createSearchQueryWithoutUserFilter(eq(PageAndCount.single()))).thenReturn(searchQuery); + @SuppressWarnings("unchecked") + PartialResult partialResult = mock(PartialResult.class); + when(partialResult.getTotal()).thenReturn(1); + when(partialResult.getPartialResult()).thenReturn(List.of(createResourceWithIdAndVersion())); + when(dao.search(any())).thenReturn(partialResult); + when(delegate.update(any(), any(), any())).thenReturn(mock(Response.class)); + + UriInfo uriInfo = mock(UriInfo.class); + @SuppressWarnings("unchecked") + MultivaluedMap parameters = mock(MultivaluedMap.class); + when(uriInfo.getQueryParameters()).thenReturn(parameters); + + resourceServiceSecure.update(createResource(), uriInfo, mock(HttpHeaders.class)); + + verify(dao).createSearchQueryWithoutUserFilter(any()); + verify(dao).search(any()); + verify(authorizationRule).reasonUpdateAllowed(any(Identity.class), any(resourceClass), any(resourceClass)); + } + + @Test + public void updateConditionalFoundResourceSameIdMustEnforceUpdateAuthorization() throws Exception + { + R existingResource = createResourceWithIdAndVersion(); + R updateResource = createResource(); + updateResource.setIdElement(existingResource.getIdElement().toVersionless()); + + @SuppressWarnings("unchecked") + SearchQuery searchQuery = mock(SearchQuery.class); + when(dao.createSearchQueryWithoutUserFilter(eq(PageAndCount.single()))).thenReturn(searchQuery); + @SuppressWarnings("unchecked") + PartialResult partialResult = mock(PartialResult.class); + when(partialResult.getTotal()).thenReturn(1); + when(partialResult.getPartialResult()).thenReturn(List.of(existingResource)); + when(dao.search(any())).thenReturn(partialResult); + when(delegate.update(any(), any(), any())).thenReturn(mock(Response.class)); + + UriInfo uriInfo = mock(UriInfo.class); + @SuppressWarnings("unchecked") + MultivaluedMap parameters = mock(MultivaluedMap.class); + when(uriInfo.getQueryParameters()).thenReturn(parameters); + + resourceServiceSecure.update(updateResource, uriInfo, mock(HttpHeaders.class)); + + verify(dao).createSearchQueryWithoutUserFilter(any()); + verify(dao).search(any()); + verify(authorizationRule).reasonUpdateAllowed(any(Identity.class), any(resourceClass), any(resourceClass)); + } + + @Test + public void updateConditionalNotFoundMustEnforceCreateAuthorization() throws Exception + { + @SuppressWarnings("unchecked") + SearchQuery searchQuery = mock(SearchQuery.class); + when(dao.createSearchQueryWithoutUserFilter(eq(PageAndCount.single()))).thenReturn(searchQuery); + @SuppressWarnings("unchecked") + PartialResult partialResult = mock(PartialResult.class); + when(partialResult.getTotal()).thenReturn(0); + when(dao.search(any())).thenReturn(partialResult); + when(delegate.update(any(), any(), any())).thenReturn(mock(Response.class)); + + UriInfo uriInfo = mock(UriInfo.class); + @SuppressWarnings("unchecked") + MultivaluedMap parameters = mock(MultivaluedMap.class); + when(uriInfo.getQueryParameters()).thenReturn(parameters); + + resourceServiceSecure.update(createResource(), uriInfo, mock(HttpHeaders.class)); + + verify(dao).createSearchQueryWithoutUserFilter(any()); + verify(dao).search(any()); + verify(authorizationRule).reasonCreateAllowed(any(Identity.class), any(resourceClass)); + } + + @Test + public void expectMethodNotAllowedConditionalUpdateNoAuthorizationRuleCallNoMathcingResourceInDbButResourceHasId() + throws Exception + { + @SuppressWarnings("unchecked") + SearchQuery searchQuery = mock(SearchQuery.class); + when(dao.createSearchQueryWithoutUserFilter(eq(PageAndCount.single()))).thenReturn(searchQuery); + @SuppressWarnings("unchecked") + PartialResult partialResult = mock(PartialResult.class); + when(partialResult.getTotal()).thenReturn(0); + when(dao.search(any())).thenReturn(partialResult); + when(delegate.update(any(), any(), any())).thenReturn(mock(Response.class)); + + UriInfo uriInfo = mock(UriInfo.class); + @SuppressWarnings("unchecked") + MultivaluedMap parameters = mock(MultivaluedMap.class); + when(uriInfo.getQueryParameters()).thenReturn(parameters); + + Response response = resourceServiceSecure.update(createResourceWithIdAndVersion(), uriInfo, + mock(HttpHeaders.class)); + assertEquals(Status.METHOD_NOT_ALLOWED, response.getStatusInfo()); + assertTrue(response.hasEntity()); + assertEquals(OperationOutcome.class, response.getEntity().getClass()); + + verify(dao).createSearchQueryWithoutUserFilter(any()); + verify(dao).search(any()); + verifyNoInteractions(authorizationRule); + } + + @Test + public void expectBadRequestConditionalUpdateNoAuthorizationRuleCallFoundResourceDifferentId() throws Exception + { + R existingResource = createResourceWithIdAndVersion(); + R updateResource = createResourceWithIdAndVersion(); + + @SuppressWarnings("unchecked") + SearchQuery searchQuery = mock(SearchQuery.class); + when(dao.createSearchQueryWithoutUserFilter(eq(PageAndCount.single()))).thenReturn(searchQuery); + @SuppressWarnings("unchecked") + PartialResult partialResult = mock(PartialResult.class); + when(partialResult.getTotal()).thenReturn(1); + when(partialResult.getPartialResult()).thenReturn(List.of(existingResource)); + when(dao.search(any())).thenReturn(partialResult); + when(delegate.update(any(), any(), any())).thenReturn(mock(Response.class)); + + UriInfo uriInfo = mock(UriInfo.class); + @SuppressWarnings("unchecked") + MultivaluedMap parameters = mock(MultivaluedMap.class); + when(uriInfo.getQueryParameters()).thenReturn(parameters); + + Response response = resourceServiceSecure.update(updateResource, uriInfo, mock(HttpHeaders.class)); + assertEquals(Status.BAD_REQUEST, response.getStatusInfo()); + assertTrue(response.hasEntity()); + assertEquals(OperationOutcome.class, response.getEntity().getClass()); + + verify(dao).createSearchQueryWithoutUserFilter(any()); + verify(dao).search(any()); + verifyNoInteractions(authorizationRule); + } + + @Test + public void expectBadRequestConditionalUpdateNoAuthorizationRuleCallUnsupportedQueryParameter() throws Exception + { + R updateResource = createResourceWithIdAndVersion(); + + @SuppressWarnings("unchecked") + SearchQuery searchQuery = mock(SearchQuery.class); + when(searchQuery.getUnsupportedQueryParameters()) + .thenReturn(List.of(new SearchQueryParameterError(SearchQueryParameterErrorType.UNSUPPORTED_PARAMETER, + "ParameterTestName", "Parameter Test Value"))); + when(dao.createSearchQueryWithoutUserFilter(eq(PageAndCount.single()))).thenReturn(searchQuery); + when(delegate.update(any(), any(), any())).thenReturn(mock(Response.class)); + + UriInfo uriInfo = mock(UriInfo.class); + @SuppressWarnings("unchecked") + MultivaluedMap parameters = mock(MultivaluedMap.class); + when(uriInfo.getQueryParameters()).thenReturn(parameters); + + try + { + resourceServiceSecure.update(updateResource, uriInfo, mock(HttpHeaders.class)); + fail("WebApplicationException expected"); + } + catch (WebApplicationException e) + { + assertEquals(Status.BAD_REQUEST, e.getResponse().getStatusInfo()); + assertTrue(e.getResponse().hasEntity()); + assertEquals(OperationOutcome.class, e.getResponse().getEntity().getClass()); + } + + verify(dao).createSearchQueryWithoutUserFilter(any()); + verifyNoMoreInteractions(dao); + verifyNoInteractions(authorizationRule); + } + + @Test + public void expectPreconditionFailedConditionalUpdateNoAuthorizationRuleCallFoundResourceDifferentId() + throws Exception + { + R updateResource = createResourceWithIdAndVersion(); + + @SuppressWarnings("unchecked") + SearchQuery searchQuery = mock(SearchQuery.class); + when(dao.createSearchQueryWithoutUserFilter(eq(PageAndCount.single()))).thenReturn(searchQuery); + @SuppressWarnings("unchecked") + PartialResult partialResult = mock(PartialResult.class); + when(partialResult.getTotal()).thenReturn(2); + when(dao.search(any())).thenReturn(partialResult); + when(delegate.update(any(), any(), any())).thenReturn(mock(Response.class)); + + UriInfo uriInfo = mock(UriInfo.class); + @SuppressWarnings("unchecked") + MultivaluedMap parameters = mock(MultivaluedMap.class); + when(uriInfo.getQueryParameters()).thenReturn(parameters); + + Response response = resourceServiceSecure.update(updateResource, uriInfo, mock(HttpHeaders.class)); + assertEquals(Status.PRECONDITION_FAILED, response.getStatusInfo()); + assertTrue(response.hasEntity()); + assertEquals(OperationOutcome.class, response.getEntity().getClass()); + + verify(dao).createSearchQueryWithoutUserFilter(any()); + verify(dao).search(any()); + verifyNoInteractions(authorizationRule); + } + + @Test + public void updateMustEnforceUpdateAuthorization() throws Exception + { + when(dao.read(any())).thenReturn(Optional.of(createResourceWithIdAndVersion())); + when(delegate.update(anyString(), any(resourceClass), any(), any())).thenReturn(mock(Response.class)); + + resourceServiceSecure.update("some-id", createResource(), mock(UriInfo.class), mock(HttpHeaders.class)); + + verify(dao).read(any()); + verify(authorizationRule).reasonUpdateAllowed(any(Identity.class), any(resourceClass), any(resourceClass)); + } + + @Test + public void expectMethodNotAllowedUpdateNoAuthorizationRuleCallResourceNotInDb() throws Exception + { + when(dao.read(any())).thenReturn(Optional.empty()); + + Response response = resourceServiceSecure.update("some-id", createResource(), mock(UriInfo.class), + mock(HttpHeaders.class)); + assertEquals(Status.METHOD_NOT_ALLOWED, response.getStatusInfo()); + assertTrue(response.hasEntity()); + assertEquals(OperationOutcome.class, response.getEntity().getClass()); + + verify(dao).read(any()); + verifyNoInteractions(authorizationRule); + } + + @Test + public void expectOkUpdateAllowed() throws Exception + { + when(dao.read(any())).thenReturn(Optional.of(createResourceWithIdAndVersion())); + when(authorizationRule.reasonUpdateAllowed(any(), any(resourceClass), any(resourceClass))) + .thenReturn(Optional.of("Test Reason")); + when(resourceValidator.validate(any())).thenReturn(new ValidationResult(FHIR_CONTEXT, List.of())); + + when(delegate.update(anyString(), any(resourceClass), any(), any())) + .thenReturn(responseOkWithResourceIdVersion); + + Response response = resourceServiceSecure.update("some-id", createResource(), mock(UriInfo.class), + mock(HttpHeaders.class)); + assertEquals(Status.OK, response.getStatusInfo()); + + verify(dao).read(any()); + verify(authorizationRule).reasonUpdateAllowed(any(Identity.class), any(resourceClass), any(resourceClass)); + } + + @Test(expected = IllegalStateException.class) + public void expectIllegalStateExceptionUpdateSameResources() throws Exception + { + R resource = createResourceWithIdAndVersion(); + + when(dao.read(any())).thenReturn(Optional.of(resource)); + + try + { + resourceServiceSecure.update("some-id", resource, mock(UriInfo.class), mock(HttpHeaders.class)); + } + finally + { + verify(dao).read(any()); + verifyNoInteractions(authorizationRule); + } + } + + @Test + public void expectForbiddenUpdateAllowedDelegateDuplicateResource() throws Exception + { + when(dao.read(any())).thenReturn(Optional.of(createResourceWithIdAndVersion())); + when(authorizationRule.reasonUpdateAllowed(any(), any(resourceClass), any(resourceClass))) + .thenReturn(Optional.of("Test Reason")); + when(resourceValidator.validate(any())).thenReturn(new ValidationResult(FHIR_CONTEXT, List.of())); + + when(delegate.update(anyString(), any(resourceClass), any(), any())) + .thenThrow(new WebApplicationException(Response.status(Status.FORBIDDEN).build())); + + try + { + resourceServiceSecure.update("some-id", createResource(), mock(UriInfo.class), mock(HttpHeaders.class)); + fail("WebApplicationException expected"); + } + catch (WebApplicationException e) + { + assertEquals(Status.FORBIDDEN, e.getResponse().getStatusInfo()); + } + + verify(dao).read(any()); + verify(authorizationRule).reasonUpdateAllowed(any(Identity.class), any(resourceClass), any(resourceClass)); + } + + @Test + public void deleteMustEnforceDeleteAuthorization() throws Exception + { + when(dao.readIncludingDeleted(any())).thenReturn(Optional.of(createResourceWithIdAndVersion())); + when(delegate.delete(anyString(), any(), any())).thenReturn(mock(Response.class)); + + resourceServiceSecure.delete("some-id", mock(UriInfo.class), mock(HttpHeaders.class)); + + verify(authorizationRule).reasonDeleteAllowed(any(Identity.class), any(resourceClass)); + } + + @Test + public void expectNotFoundDeleteNoAuthorizationRuleCallResourceNotInDb() throws Exception + { + when(dao.readIncludingDeleted(any())).thenReturn(Optional.empty()); + + Response response = resourceServiceSecure.delete("some-id", mock(UriInfo.class), mock(HttpHeaders.class)); + assertEquals(Status.NOT_FOUND, response.getStatusInfo()); + assertTrue(response.hasEntity()); + assertEquals(OperationOutcome.class, response.getEntity().getClass()); + + verifyNoInteractions(authorizationRule); + } + + @Test + public void expectOkDeleteAllowed() throws Exception + { + when(dao.readIncludingDeleted(any())).thenReturn(Optional.of(createResourceWithIdAndVersion())); + when(authorizationRule.reasonDeleteAllowed(any(Identity.class), any(resourceClass))) + .thenReturn(Optional.of("Test Reason")); + + Response responseOk = mock(Response.class); + when(responseOk.getStatusInfo()).thenReturn(Status.OK); + when(responseOk.getStatus()).thenReturn(Status.OK.getStatusCode()); + + when(delegate.delete(anyString(), any(), any())).thenReturn(responseOk); + + Response response = resourceServiceSecure.delete("some-id", mock(UriInfo.class), mock(HttpHeaders.class)); + assertEquals(Status.OK, response.getStatusInfo()); + + verify(dao).readIncludingDeleted(any()); + verify(authorizationRule).reasonDeleteAllowed(any(Identity.class), any(resourceClass)); + } + + @Test + public void expectNotFoundDeleteAllowedNotFoundWhileDeleting() throws Exception + { + when(dao.readIncludingDeleted(any())).thenReturn(Optional.of(createResourceWithIdAndVersion())); + when(authorizationRule.reasonDeleteAllowed(any(Identity.class), any(resourceClass))) + .thenReturn(Optional.of("Test Reason")); + + when(delegate.delete(anyString(), any(), any())) + .thenThrow(new WebApplicationException(Response.status(Status.NOT_FOUND).build())); + + try + { + resourceServiceSecure.delete("some-id", mock(UriInfo.class), mock(HttpHeaders.class)); + fail("WebApplicationException expected"); + } + catch (WebApplicationException e) + { + assertEquals(Status.NOT_FOUND, e.getResponse().getStatusInfo()); + } + + verify(dao).readIncludingDeleted(any()); + verify(authorizationRule).reasonDeleteAllowed(any(Identity.class), any(resourceClass)); + } + + @Test + public void deleteConditionalMustEnforceDeleteAuthorization() throws Exception + { + @SuppressWarnings("unchecked") + SearchQuery searchQuery = mock(SearchQuery.class); + when(dao.createSearchQueryWithoutUserFilter(eq(PageAndCount.single()))).thenReturn(searchQuery); + @SuppressWarnings("unchecked") + PartialResult partialResult = mock(PartialResult.class); + when(partialResult.getTotal()).thenReturn(1); + when(partialResult.getPartialResult()).thenReturn(List.of(createResourceWithIdAndVersion())); + when(dao.search(any())).thenReturn(partialResult); + when(dao.readIncludingDeleted(any())).thenReturn(Optional.of(createResourceWithIdAndVersion())); + + when(delegate.delete(anyString(), any(), any())).thenReturn(mock(Response.class)); + + UriInfo uriInfo = mock(UriInfo.class); + @SuppressWarnings("unchecked") + MultivaluedMap parameters = mock(MultivaluedMap.class); + when(uriInfo.getQueryParameters()).thenReturn(parameters); + + resourceServiceSecure.delete(uriInfo, mock(HttpHeaders.class)); + + verify(dao).createSearchQueryWithoutUserFilter(any()); + verify(dao).search(any()); + verify(authorizationRule).reasonDeleteAllowed(any(Identity.class), any(resourceClass)); + } + + @Test + public void expectBadRequestConditionalDeleteNoAuthorizationRuleCallUnsupportedQueryParameter() throws Exception + { + @SuppressWarnings("unchecked") + SearchQuery searchQuery = mock(SearchQuery.class); + when(searchQuery.getUnsupportedQueryParameters()) + .thenReturn(List.of(new SearchQueryParameterError(SearchQueryParameterErrorType.UNSUPPORTED_PARAMETER, + "ParameterTestName", "Parameter Test Value"))); + when(dao.createSearchQueryWithoutUserFilter(eq(PageAndCount.single()))).thenReturn(searchQuery); + + UriInfo uriInfo = mock(UriInfo.class); + @SuppressWarnings("unchecked") + MultivaluedMap parameters = mock(MultivaluedMap.class); + when(uriInfo.getQueryParameters()).thenReturn(parameters); + + Response response = resourceServiceSecure.delete(uriInfo, mock(HttpHeaders.class)); + assertEquals(Status.BAD_REQUEST, response.getStatusInfo()); + assertTrue(response.hasEntity()); + assertEquals(OperationOutcome.class, response.getEntity().getClass()); + + verify(dao).createSearchQueryWithoutUserFilter(any()); + verifyNoMoreInteractions(dao); + verifyNoInteractions(authorizationRule); + } + + @Test + public void expectPreconditionFailedDeleteAllowedMultipleExists() throws Exception + { + @SuppressWarnings("unchecked") + SearchQuery searchQuery = mock(SearchQuery.class); + when(dao.createSearchQueryWithoutUserFilter(eq(PageAndCount.single()))).thenReturn(searchQuery); + @SuppressWarnings("unchecked") + PartialResult partialResult = mock(PartialResult.class); + when(partialResult.getTotal()).thenReturn(2); + when(dao.search(any())).thenReturn(partialResult); + + UriInfo uriInfo = mock(UriInfo.class); + @SuppressWarnings("unchecked") + MultivaluedMap parameters = mock(MultivaluedMap.class); + when(uriInfo.getQueryParameters()).thenReturn(parameters); + + Response response = resourceServiceSecure.delete(uriInfo, mock(HttpHeaders.class)); + assertEquals(Status.PRECONDITION_FAILED, response.getStatusInfo()); + assertTrue(response.hasEntity()); + assertEquals(OperationOutcome.class, response.getEntity().getClass()); + + verify(dao).createSearchQueryWithoutUserFilter(any()); + verify(dao).search(any()); + verifyNoMoreInteractions(dao); + verifyNoInteractions(authorizationRule); + } + + @Test + public void expectNoContentDeleteAlloweNoneExists() throws Exception + { + @SuppressWarnings("unchecked") + SearchQuery searchQuery = mock(SearchQuery.class); + when(dao.createSearchQueryWithoutUserFilter(eq(PageAndCount.single()))).thenReturn(searchQuery); + @SuppressWarnings("unchecked") + PartialResult partialResult = mock(PartialResult.class); + when(partialResult.getTotal()).thenReturn(0); + when(dao.search(any())).thenReturn(partialResult); + + UriInfo uriInfo = mock(UriInfo.class); + @SuppressWarnings("unchecked") + MultivaluedMap parameters = mock(MultivaluedMap.class); + when(uriInfo.getQueryParameters()).thenReturn(parameters); + + Response response = resourceServiceSecure.delete(uriInfo, mock(HttpHeaders.class)); + assertEquals(Status.NO_CONTENT, response.getStatusInfo()); + + verify(dao).createSearchQueryWithoutUserFilter(any()); + verify(dao).search(any()); + verifyNoMoreInteractions(dao); + verifyNoInteractions(authorizationRule); + } + + @Test + public void searchMustEnforceSearchAuthorization() throws Exception + { + when(delegate.search(any(), any())).thenReturn(mock(Response.class)); + + resourceServiceSecure.search(mock(UriInfo.class), mock(HttpHeaders.class)); + + verify(authorizationRule).reasonSearchAllowed(any(Identity.class)); + } + + @Test + public void expectOkSearchAllowed() throws Exception + { + when(authorizationRule.reasonSearchAllowed(any(Identity.class))).thenReturn(Optional.of("Test Reason")); + when(delegate.search(any(), any())).thenReturn(Response.ok().build()); + + Response response = resourceServiceSecure.search(mock(UriInfo.class), mock(HttpHeaders.class)); + assertEquals(Status.OK, response.getStatusInfo()); + + verify(authorizationRule).reasonSearchAllowed(any(Identity.class)); + } + + @Test + public void expectBadRequestSearchAllowedUnsupportedParameter() throws Exception + { + when(authorizationRule.reasonSearchAllowed(any(Identity.class))).thenReturn(Optional.of("Test Reason")); + when(delegate.search(any(), any())).thenReturn(Response.status(Status.BAD_REQUEST).build()); + + Response response = resourceServiceSecure.search(mock(UriInfo.class), mock(HttpHeaders.class)); + assertEquals(Status.BAD_REQUEST, response.getStatusInfo()); + + verify(authorizationRule).reasonSearchAllowed(any(Identity.class)); + } + + @Test + public void deletePermanentlyMustEnforcePermanentDeleteAuthorization() throws Exception + { + when(dao.readIncludingDeleted(any())).thenReturn(Optional.of(createResourceWithIdAndVersion())); + when(delegate.deletePermanently(anyString(), anyString(), any(), any())).thenReturn(mock(Response.class)); + + resourceServiceSecure.deletePermanently(PERMANENT_DELETE_PATH, "some-id", mock(UriInfo.class), + mock(HttpHeaders.class)); + + verify(dao).readIncludingDeleted(any()); + verify(authorizationRule).reasonPermanentDeleteAllowed(any(Identity.class), any(resourceClass)); + } + + @Test + public void expectNotFoundDeletePermanentlyNoAuthorizationRuleCallResourceNotInDb() throws Exception + { + when(dao.readIncludingDeleted(any())).thenReturn(Optional.empty()); + + Response response = resourceServiceSecure.deletePermanently(PERMANENT_DELETE_PATH, "some-id", + mock(UriInfo.class), mock(HttpHeaders.class)); + assertEquals(Status.NOT_FOUND, response.getStatusInfo()); + assertTrue(response.hasEntity()); + assertEquals(OperationOutcome.class, response.getEntity().getClass()); + + verifyNoInteractions(authorizationRule); + } + + @Test + public void expectOkDeletePermanentlyAllowed() throws Exception + { + when(dao.readIncludingDeleted(any())).thenReturn(Optional.of(createResourceWithIdAndVersion())); + when(authorizationRule.reasonPermanentDeleteAllowed(any(Identity.class), any(resourceClass))) + .thenReturn(Optional.of("Test Reason")); + + Response responseOk = mock(Response.class); + when(responseOk.getStatusInfo()).thenReturn(Status.OK); + when(responseOk.getStatus()).thenReturn(Status.OK.getStatusCode()); + + when(delegate.deletePermanently(anyString(), anyString(), any(), any())).thenReturn(responseOk); + + Response response = resourceServiceSecure.deletePermanently(PERMANENT_DELETE_PATH, "some-id", + mock(UriInfo.class), mock(HttpHeaders.class)); + assertEquals(Status.OK, response.getStatusInfo()); + + verify(dao).readIncludingDeleted(any()); + verify(authorizationRule).reasonPermanentDeleteAllowed(any(Identity.class), any(resourceClass)); + } + + + @Test + public void expectNotFoundDeletePermanentlyAllowedNotFoundWhileDeletingPermanently() throws Exception + { + when(dao.readIncludingDeleted(any())).thenReturn(Optional.of(createResourceWithIdAndVersion())); + when(authorizationRule.reasonPermanentDeleteAllowed(any(Identity.class), any(resourceClass))) + .thenReturn(Optional.of("Test Reason")); + + when(delegate.deletePermanently(anyString(), anyString(), any(), any())) + .thenThrow(new WebApplicationException(Response.status(Status.NOT_FOUND).build())); + + try + { + resourceServiceSecure.deletePermanently(PERMANENT_DELETE_PATH, "some-id", mock(UriInfo.class), + mock(HttpHeaders.class)); + fail("WebApplicationException expected"); + } + catch (WebApplicationException e) + { + assertEquals(Status.NOT_FOUND, e.getResponse().getStatusInfo()); + } + + verify(dao).readIncludingDeleted(any()); + verify(authorizationRule).reasonPermanentDeleteAllowed(any(Identity.class), any(resourceClass)); + } +} diff --git a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/webservice/secure/BinaryServiceSecureTest.java b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/webservice/secure/BinaryServiceSecureTest.java new file mode 100644 index 000000000..42ea0dcf4 --- /dev/null +++ b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/webservice/secure/BinaryServiceSecureTest.java @@ -0,0 +1,73 @@ +/* + * Copyright 2018-2025 Heilbronn University of Applied Sciences + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package dev.dsf.fhir.webservice.secure; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.io.InputStream; + +import org.hl7.fhir.r4.model.Binary; +import org.junit.Test; + +import dev.dsf.fhir.dao.BinaryDao; +import dev.dsf.fhir.webservice.specification.BinaryService; +import jakarta.ws.rs.core.HttpHeaders; +import jakarta.ws.rs.core.UriInfo; + +public class BinaryServiceSecureTest extends AbstractResourceServiceSecureTest +{ + public BinaryServiceSecureTest() + { + super(Binary.class, BinaryService.class, BinaryDao.class, Binary::new, BinaryServiceSecure::new); + } + + @Test + public void readHeadMustEnforceReadAuthorization() + { + when(delegate.readHead(anyString(), any(), any())).thenReturn(responseOkWithResourceIdVersion); + + resourceServiceSecure.readHead("some-id", mock(UriInfo.class), mock(HttpHeaders.class)); + + verify(authorizationRule).reasonReadAllowed(any(), any()); + } + + @Test + public void vreadHeadMustEnforceReadAuthorization() + { + when(delegate.vreadHead(anyString(), anyLong(), any(), any())).thenReturn(responseOkWithResourceIdVersion); + + resourceServiceSecure.vreadHead("some-id", 1, mock(UriInfo.class), mock(HttpHeaders.class)); + + verify(authorizationRule).reasonReadAllowed(any(), any()); + } + + @Test(expected = UnsupportedOperationException.class) + public void expectUnsupportedOperationExceptionCreateInputStream() throws Exception + { + resourceServiceSecure.create(mock(InputStream.class), mock(UriInfo.class), mock(HttpHeaders.class)); + } + + @Test(expected = UnsupportedOperationException.class) + public void expectUnsupportedOperationExceptionUpdateInputStream() throws Exception + { + resourceServiceSecure.update("some-id", mock(InputStream.class), mock(UriInfo.class), mock(HttpHeaders.class)); + } +} diff --git a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/webservice/secure/ResourceServiceSecureTest.java b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/webservice/secure/ResourceServiceSecureTest.java new file mode 100644 index 000000000..8235ddc7c --- /dev/null +++ b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/webservice/secure/ResourceServiceSecureTest.java @@ -0,0 +1,206 @@ +/* + * Copyright 2018-2025 Heilbronn University of Applied Sciences + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package dev.dsf.fhir.webservice.secure; + +import java.util.Collection; +import java.util.List; +import java.util.function.Supplier; + +import org.hl7.fhir.r4.model.ActivityDefinition; +import org.hl7.fhir.r4.model.Bundle; +import org.hl7.fhir.r4.model.CodeSystem; +import org.hl7.fhir.r4.model.DocumentReference; +import org.hl7.fhir.r4.model.Endpoint; +import org.hl7.fhir.r4.model.Group; +import org.hl7.fhir.r4.model.HealthcareService; +import org.hl7.fhir.r4.model.Library; +import org.hl7.fhir.r4.model.Location; +import org.hl7.fhir.r4.model.Measure; +import org.hl7.fhir.r4.model.MeasureReport; +import org.hl7.fhir.r4.model.NamingSystem; +import org.hl7.fhir.r4.model.Organization; +import org.hl7.fhir.r4.model.OrganizationAffiliation; +import org.hl7.fhir.r4.model.Patient; +import org.hl7.fhir.r4.model.Practitioner; +import org.hl7.fhir.r4.model.PractitionerRole; +import org.hl7.fhir.r4.model.Provenance; +import org.hl7.fhir.r4.model.Questionnaire; +import org.hl7.fhir.r4.model.QuestionnaireResponse; +import org.hl7.fhir.r4.model.ResearchStudy; +import org.hl7.fhir.r4.model.Resource; +import org.hl7.fhir.r4.model.Subscription; +import org.hl7.fhir.r4.model.ValueSet; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +import dev.dsf.fhir.dao.ActivityDefinitionDao; +import dev.dsf.fhir.dao.BundleDao; +import dev.dsf.fhir.dao.CodeSystemDao; +import dev.dsf.fhir.dao.DocumentReferenceDao; +import dev.dsf.fhir.dao.EndpointDao; +import dev.dsf.fhir.dao.GroupDao; +import dev.dsf.fhir.dao.HealthcareServiceDao; +import dev.dsf.fhir.dao.LibraryDao; +import dev.dsf.fhir.dao.LocationDao; +import dev.dsf.fhir.dao.MeasureDao; +import dev.dsf.fhir.dao.MeasureReportDao; +import dev.dsf.fhir.dao.NamingSystemDao; +import dev.dsf.fhir.dao.OrganizationAffiliationDao; +import dev.dsf.fhir.dao.OrganizationDao; +import dev.dsf.fhir.dao.PatientDao; +import dev.dsf.fhir.dao.PractitionerDao; +import dev.dsf.fhir.dao.PractitionerRoleDao; +import dev.dsf.fhir.dao.ProvenanceDao; +import dev.dsf.fhir.dao.QuestionnaireDao; +import dev.dsf.fhir.dao.QuestionnaireResponseDao; +import dev.dsf.fhir.dao.ResearchStudyDao; +import dev.dsf.fhir.dao.ResourceDao; +import dev.dsf.fhir.dao.SubscriptionDao; +import dev.dsf.fhir.dao.ValueSetDao; +import dev.dsf.fhir.webservice.specification.ActivityDefinitionService; +import dev.dsf.fhir.webservice.specification.BasicResourceService; +import dev.dsf.fhir.webservice.specification.BundleService; +import dev.dsf.fhir.webservice.specification.CodeSystemService; +import dev.dsf.fhir.webservice.specification.DocumentReferenceService; +import dev.dsf.fhir.webservice.specification.EndpointService; +import dev.dsf.fhir.webservice.specification.GroupService; +import dev.dsf.fhir.webservice.specification.HealthcareServiceService; +import dev.dsf.fhir.webservice.specification.LibraryService; +import dev.dsf.fhir.webservice.specification.LocationService; +import dev.dsf.fhir.webservice.specification.MeasureReportService; +import dev.dsf.fhir.webservice.specification.MeasureService; +import dev.dsf.fhir.webservice.specification.NamingSystemService; +import dev.dsf.fhir.webservice.specification.OrganizationAffiliationService; +import dev.dsf.fhir.webservice.specification.OrganizationService; +import dev.dsf.fhir.webservice.specification.PatientService; +import dev.dsf.fhir.webservice.specification.PractitionerRoleService; +import dev.dsf.fhir.webservice.specification.PractitionerService; +import dev.dsf.fhir.webservice.specification.ProvenanceService; +import dev.dsf.fhir.webservice.specification.QuestionnaireResponseService; +import dev.dsf.fhir.webservice.specification.QuestionnaireService; +import dev.dsf.fhir.webservice.specification.ResearchStudyService; +import dev.dsf.fhir.webservice.specification.SubscriptionService; +import dev.dsf.fhir.webservice.specification.ValueSetService; + +@RunWith(Parameterized.class) +public class ResourceServiceSecureTest + extends AbstractResourceServiceSecureTest, ResourceDao> +{ + @Parameters(name = "{0}") + public static Collection data() + { + return List.of(new Object[][] { + + { "ActivityDefinition", ActivityDefinition.class, ActivityDefinitionService.class, + ActivityDefinitionDao.class, (Supplier) ActivityDefinition::new, + (ResourceServiceSecureFactory) ActivityDefinitionServiceSecure::new }, + + { "Bundle", Bundle.class, BundleService.class, BundleDao.class, (Supplier) Bundle::new, + (ResourceServiceSecureFactory) BundleServiceSecure::new }, + + { "CodeSystem", CodeSystem.class, CodeSystemService.class, CodeSystemDao.class, + (Supplier) CodeSystem::new, + (ResourceServiceSecureFactory) CodeSystemServiceSecure::new }, + + { "DocumentReference", DocumentReference.class, DocumentReferenceService.class, + DocumentReferenceDao.class, (Supplier) DocumentReference::new, + (ResourceServiceSecureFactory) DocumentReferenceServiceSecure::new }, + + { "Endpoint", Endpoint.class, EndpointService.class, EndpointDao.class, + (Supplier) Endpoint::new, + (ResourceServiceSecureFactory) EndpointServiceSecure::new }, + + { "Group", Group.class, GroupService.class, GroupDao.class, (Supplier) Group::new, + (ResourceServiceSecureFactory) GroupServiceSecure::new }, + + { "HealthcareService", HealthcareService.class, HealthcareServiceService.class, + HealthcareServiceDao.class, (Supplier) HealthcareService::new, + (ResourceServiceSecureFactory) HealthcareServiceServiceSecure::new }, + + { "Library", Library.class, LibraryService.class, LibraryDao.class, (Supplier) Library::new, + (ResourceServiceSecureFactory) LibraryServiceSecure::new }, + + { "Location", Location.class, LocationService.class, LocationDao.class, + (Supplier) Location::new, + (ResourceServiceSecureFactory) LocationServiceSecure::new }, + + { "Measure", Measure.class, MeasureService.class, MeasureDao.class, (Supplier) Measure::new, + (ResourceServiceSecureFactory) MeasureServiceSecure::new }, + + { "MeasureReport", MeasureReport.class, MeasureReportService.class, MeasureReportDao.class, + (Supplier) MeasureReport::new, + (ResourceServiceSecureFactory) MeasureReportServiceSecure::new }, + + { "NamingSystem", NamingSystem.class, NamingSystemService.class, NamingSystemDao.class, + (Supplier) NamingSystem::new, + (ResourceServiceSecureFactory) NamingSystemServiceSecure::new }, + + { "OrganizationAffiliation", OrganizationAffiliation.class, OrganizationAffiliationService.class, + OrganizationAffiliationDao.class, + (Supplier) OrganizationAffiliation::new, + (ResourceServiceSecureFactory) OrganizationAffiliationServiceSecure::new }, + + { "Organization", Organization.class, OrganizationService.class, OrganizationDao.class, + (Supplier) Organization::new, + (ResourceServiceSecureFactory) OrganizationServiceSecure::new }, + + { "Patient", Patient.class, PatientService.class, PatientDao.class, (Supplier) Patient::new, + (ResourceServiceSecureFactory) PatientServiceSecure::new }, + + { "Practitioner", Practitioner.class, PractitionerService.class, PractitionerDao.class, + (Supplier) Practitioner::new, + (ResourceServiceSecureFactory) PractitionerServiceSecure::new }, + + { "PractitionerRole", PractitionerRole.class, PractitionerRoleService.class, PractitionerRoleDao.class, + (Supplier) PractitionerRole::new, + (ResourceServiceSecureFactory) PractitionerRoleServiceSecure::new }, + + { "Provenance", Provenance.class, ProvenanceService.class, ProvenanceDao.class, + (Supplier) Provenance::new, + (ResourceServiceSecureFactory) ProvenanceServiceSecure::new }, + + { "Questionnaire", Questionnaire.class, QuestionnaireService.class, QuestionnaireDao.class, + (Supplier) Questionnaire::new, + (ResourceServiceSecureFactory) QuestionnaireServiceSecure::new }, + + { "QuestionnaireResponse", QuestionnaireResponse.class, QuestionnaireResponseService.class, + QuestionnaireResponseDao.class, (Supplier) QuestionnaireResponse::new, + (ResourceServiceSecureFactory) QuestionnaireResponseServiceSecure::new }, + + { "ResearchStudy", ResearchStudy.class, ResearchStudyService.class, ResearchStudyDao.class, + (Supplier) ResearchStudy::new, + (ResourceServiceSecureFactory) ResearchStudyServiceSecure::new }, + + { "Subscription", Subscription.class, SubscriptionService.class, SubscriptionDao.class, + (Supplier) Subscription::new, + (ResourceServiceSecureFactory) SubscriptionServiceSecure::new }, + + { "ValueSet", ValueSet.class, ValueSetService.class, ValueSetDao.class, + (Supplier) ValueSet::new, + (ResourceServiceSecureFactory) ValueSetServiceSecure::new }, + + }); + } + + public ResourceServiceSecureTest(String label, Class resourceClass, + Class> serviceClass, Class> daoClass, + Supplier resouceSupplier, + ResourceServiceSecureFactory, ResourceDao> resourceServiceSecureFactory) + { + super(resourceClass, serviceClass, daoClass, resouceSupplier, resourceServiceSecureFactory); + } +} diff --git a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/webservice/secure/RootServiceSecureTest.java b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/webservice/secure/RootServiceSecureTest.java new file mode 100644 index 000000000..907607f65 --- /dev/null +++ b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/webservice/secure/RootServiceSecureTest.java @@ -0,0 +1,153 @@ +/* + * Copyright 2018-2025 Heilbronn University of Applied Sciences + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package dev.dsf.fhir.webservice.secure; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + +import java.util.EnumSet; +import java.util.Optional; + +import org.hl7.fhir.r4.model.Bundle; +import org.hl7.fhir.r4.model.Bundle.BundleType; +import org.hl7.fhir.r4.model.OperationOutcome; +import org.hl7.fhir.r4.model.Resource; +import org.junit.Before; +import org.junit.Test; + +import dev.dsf.common.auth.conf.Identity; +import dev.dsf.fhir.authentication.CurrentIdentityProvider; +import dev.dsf.fhir.authorization.AuthorizationRule; +import dev.dsf.fhir.help.ResponseGenerator; +import dev.dsf.fhir.service.ReferenceResolver; +import dev.dsf.fhir.webservice.specification.RootService; +import jakarta.ws.rs.core.HttpHeaders; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.core.Response.Status; +import jakarta.ws.rs.core.UriInfo; + +public class RootServiceSecureTest +{ + private static final String SERVER_BASE = "https://dsf.test/fhir"; + + private final RootService delegate = mock(RootService.class); + + @SuppressWarnings("unchecked") + private final AuthorizationRule authorizationRule = mock(AuthorizationRule.class); + private final CurrentIdentityProvider currentIdentityProvider = mock(CurrentIdentityProvider.class); + + private RootServiceSecure rootServiceSecure; + + @Before + public void before() throws Exception + { + rootServiceSecure = new RootServiceSecure(delegate, SERVER_BASE, new ResponseGenerator(SERVER_BASE), + mock(ReferenceResolver.class), authorizationRule); + rootServiceSecure.afterPropertiesSet(); + + rootServiceSecure.setCurrentIdentityProvider(currentIdentityProvider); + + Identity identity = mock(Identity.class); + when(identity.getName()).thenReturn("Test Identity"); + when(currentIdentityProvider.getCurrentIdentity()).thenReturn(identity); + } + + @Test + public void getAllowedForAll() throws Exception + { + when(delegate.root(any(), any())).thenReturn(mock(Response.class)); + + rootServiceSecure.root(mock(UriInfo.class), mock(HttpHeaders.class)); + + verifyNoInteractions(authorizationRule); + } + + @Test + public void handleBundleAllowedBatch() + { + when(delegate.handleBundle(any(), any(), any())).thenReturn(mock(Response.class)); + + Bundle b = new Bundle(); + b.setType(BundleType.BATCH); + + rootServiceSecure.handleBundle(b, mock(UriInfo.class), mock(HttpHeaders.class)); + + verifyNoInteractions(authorizationRule); + } + + @Test + public void handleBundleAllowedTransaction() + { + when(delegate.handleBundle(any(), any(), any())).thenReturn(mock(Response.class)); + + Bundle b = new Bundle(); + b.setType(BundleType.TRANSACTION); + + rootServiceSecure.handleBundle(b, mock(UriInfo.class), mock(HttpHeaders.class)); + + verifyNoInteractions(authorizationRule); + } + + @Test + public void expectForbiddenHandleBundleNotBatchNotTransaction() + { + EnumSet forbiddenTypes = EnumSet.complementOf(EnumSet.of(BundleType.BATCH, BundleType.TRANSACTION)); + for (BundleType type : forbiddenTypes) + { + Bundle b = new Bundle(); + b.setType(type); + + Response response = rootServiceSecure.handleBundle(b, mock(UriInfo.class), mock(HttpHeaders.class)); + assertEquals(Status.FORBIDDEN, response.getStatusInfo()); + assertTrue(response.hasEntity()); + assertEquals(OperationOutcome.class, response.getEntity().getClass()); + + } + + verifyNoInteractions(authorizationRule); + } + + @Test + public void historyMustEnforceHistoryAuthorization() + { + when(delegate.history(any(), any())).thenReturn(mock(Response.class)); + + rootServiceSecure.history(mock(UriInfo.class), mock(HttpHeaders.class)); + + verify(authorizationRule).reasonHistoryAllowed(any()); + } + + @Test + public void expectOkHistoryAllowed() throws Exception + { + when(authorizationRule.reasonHistoryAllowed(any())).thenReturn(Optional.of("Test Reason")); + + Response responseOk = mock(Response.class); + when(responseOk.getStatusInfo()).thenReturn(Status.OK); + when(responseOk.getStatus()).thenReturn(Status.OK.getStatusCode()); + when(delegate.history(any(), any())).thenReturn(responseOk); + + Response response = rootServiceSecure.history(mock(UriInfo.class), mock(HttpHeaders.class)); + assertEquals(Status.OK, response.getStatusInfo()); + + verify(authorizationRule).reasonHistoryAllowed(any()); + } +} diff --git a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/webservice/secure/StructureDefinitionServiceSecureTest.java b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/webservice/secure/StructureDefinitionServiceSecureTest.java new file mode 100644 index 000000000..a4dff0d10 --- /dev/null +++ b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/webservice/secure/StructureDefinitionServiceSecureTest.java @@ -0,0 +1,110 @@ +/* + * Copyright 2018-2025 Heilbronn University of Applied Sciences + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package dev.dsf.fhir.webservice.secure; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + +import org.hl7.fhir.r4.model.Parameters; +import org.hl7.fhir.r4.model.Parameters.ParametersParameterComponent; +import org.hl7.fhir.r4.model.StructureDefinition; +import org.hl7.fhir.r4.model.UrlType; +import org.junit.Test; + +import dev.dsf.fhir.dao.StructureDefinitionDao; +import dev.dsf.fhir.webservice.specification.StructureDefinitionService; +import jakarta.ws.rs.core.HttpHeaders; +import jakarta.ws.rs.core.UriInfo; + +public class StructureDefinitionServiceSecureTest extends + AbstractResourceServiceSecureTest +{ + private static final String SNAPSHOT_PATH = "$snapshot"; + + public StructureDefinitionServiceSecureTest() + { + super(StructureDefinition.class, StructureDefinitionService.class, StructureDefinitionDao.class, + StructureDefinition::new, StructureDefinitionServiceSecure::new); + } + + @Test + public void getSnapshotExistingMustEnforceReadAuthorization() + { + when(delegate.getSnapshotExisting(anyString(), anyString(), any(), any())) + .thenReturn(responseOkWithResourceIdVersion); + + resourceServiceSecure.getSnapshotExisting(SNAPSHOT_PATH, "some-id", mock(UriInfo.class), + mock(HttpHeaders.class)); + + verify(authorizationRule).reasonReadAllowed(any(), any()); + } + + @Test + public void postSnapshotExistingMustEnforceReadAuthorization() + { + when(delegate.postSnapshotExisting(anyString(), anyString(), any(), any())) + .thenReturn(responseOkWithResourceIdVersion); + + resourceServiceSecure.postSnapshotExisting(SNAPSHOT_PATH, "some-id", mock(UriInfo.class), + mock(HttpHeaders.class)); + + verify(authorizationRule).reasonReadAllowed(any(), any()); + } + + @Test + public void getSnapshotNewMustEnforceReadAuthorization() + { + when(delegate.getSnapshotNew(anyString(), any(), any())).thenReturn(responseOkWithResourceIdVersion); + + resourceServiceSecure.getSnapshotNew(SNAPSHOT_PATH, mock(UriInfo.class), mock(HttpHeaders.class)); + + verify(authorizationRule).reasonReadAllowed(any(), any()); + } + + @Test + public void postSnapshotNewMustEnforceReadAuthorizationIfInvokedWithUrlParameter() + { + Parameters parameters = mock(Parameters.class); + when(parameters.getParameter("url")).thenReturn(new ParametersParameterComponent().setValue(new UrlType())); + + when(delegate.postSnapshotNew(anyString(), eq(parameters), any(), any())) + .thenReturn(responseOkWithResourceIdVersion); + + resourceServiceSecure.postSnapshotNew(SNAPSHOT_PATH, parameters, mock(UriInfo.class), mock(HttpHeaders.class)); + + verify(authorizationRule).reasonReadAllowed(any(), any()); + } + + @Test + public void postSnapshotNewMustNotEnforceReadAuthorizationIfInvokedWithResourceParameter() + { + Parameters parameters = mock(Parameters.class); + when(parameters.getParameter("resource")) + .thenReturn(new ParametersParameterComponent().setResource(new StructureDefinition())); + + when(delegate.postSnapshotNew(anyString(), eq(parameters), any(), any())) + .thenReturn(responseOkWithResourceIdVersion); + + resourceServiceSecure.postSnapshotNew(SNAPSHOT_PATH, parameters, mock(UriInfo.class), mock(HttpHeaders.class)); + + verifyNoInteractions(authorizationRule); + } +} From 662e33672309f6d7501d30058927a1a9a5b9794a Mon Sep 17 00:00:00 2001 From: Hauke Hund Date: Sun, 2 Aug 2026 20:37:14 +0200 Subject: [PATCH 20/23] additional draft task integration tests --- .../fhir/integration/TaskIntegrationTest.java | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/integration/TaskIntegrationTest.java b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/integration/TaskIntegrationTest.java index db730bcd6..83853b7cf 100755 --- a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/integration/TaskIntegrationTest.java +++ b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/integration/TaskIntegrationTest.java @@ -2478,4 +2478,48 @@ public void testIllegalConditionalUpdateViaBundle() throws Exception assertNotNull(resultBundle.getEntry().get(0).getResponse().getStatus()); assertTrue(resultBundle.getEntry().get(0).getResponse().getStatus().startsWith("403")); } + + @Test + public void testDeleteDraftTaskAllowedLocalOrganization() throws Exception + { + ActivityDefinition ad = readActivityDefinition("dsf-test-activity-definition14-1.0.xml"); + getWebserviceClient().create(ad); + + StructureDefinition profile = readTestTaskProfile(); + getWebserviceClient().create(profile); + + Task t = readTestTask("Test_Organization", null, "Test_Organization"); + t.addIdentifier().setSystem("http://dsf.dev/sid/task-identifier").setValue("delete-allowed"); + t.setStatus(TaskStatus.DRAFT); + Task createdT = getWebserviceClient().create(t); + assertNotNull(createdT); + String id = createdT.getIdElement().getIdPart(); + assertNotNull(id); + + getWebserviceClient().delete(Task.class, id); + + Bundle searchResult = getWebserviceClient().search(Task.class, Map.of()); + assertNotNull(searchResult); + assertEquals(0, searchResult.getTotal()); + } + + @Test + public void testDeleteDraftTaskForbiddenExternalOrganization() throws Exception + { + ActivityDefinition ad = readActivityDefinition("dsf-test-activity-definition14-1.0.xml"); + getWebserviceClient().create(ad); + + StructureDefinition profile = readTestTaskProfile(); + getWebserviceClient().create(profile); + + Task t = readTestTask("Test_Organization", null, "Test_Organization"); + t.addIdentifier().setSystem("http://dsf.dev/sid/task-identifier").setValue("delete-forbidden"); + t.setStatus(TaskStatus.DRAFT); + Task createdT = getWebserviceClient().create(t); + assertNotNull(createdT); + String id = createdT.getIdElement().getIdPart(); + assertNotNull(id); + + expectForbidden(() -> getExternalWebserviceClient().delete(Task.class, id)); + } } From 5ae983eb0284a5649a4ecaf37dcfda1c3d6811a2 Mon Sep 17 00:00:00 2001 From: Hauke Hund Date: Sun, 2 Aug 2026 20:42:58 +0200 Subject: [PATCH 21/23] some refactoring and harmonization Modified the search construction of conditional delete to run without user filter, the same as conditional update. The delete authorization rule checks resource access. Using a user filter in the search may have suppressed a "Precondition Failed" response status, for a unspecific delete criteria. --- .../secure/AbstractResourceServiceSecure.java | 34 +++++++++++-------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/webservice/secure/AbstractResourceServiceSecure.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/webservice/secure/AbstractResourceServiceSecure.java index 7e879342b..e3742372b 100755 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/webservice/secure/AbstractResourceServiceSecure.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/webservice/secure/AbstractResourceServiceSecure.java @@ -94,7 +94,6 @@ public AbstractResourceServiceSecure(S delegate, String serverBase, ResponseGene this.referenceCleaner = referenceCleaner; this.referenceExtractor = referenceExtractor; this.resourceType = resourceType; - this.defaultProfileProvider = defaultProfileProvider; this.resourceTypeName = resourceType.getAnnotation(ResourceDef.class).name(); this.dao = dao; this.exceptionHandler = exceptionHandler; @@ -102,6 +101,7 @@ public AbstractResourceServiceSecure(S delegate, String serverBase, ResponseGene this.authorizationRule = authorizationRule; this.resourceValidator = resourceValidator; this.validationRules = validationRules; + this.defaultProfileProvider = defaultProfileProvider; } @Override @@ -460,20 +460,24 @@ public Response update(R resource, UriInfo uri, HttpHeaders headers) Map> queryParameters = uri.getQueryParameters(); PartialResult result = getExisting(queryParameters); - // No matches, no id provided: The server creates the resource. - if (result.getTotal() <= 0 && !resource.hasId()) + // No matches + if (result.getTotal() <= 0) { - // more security checks and audit log in create method - return create(resource, uri, headers); - } + // no id provided: The server creates the resource. + if (!resource.hasId()) + { + // more security checks and audit log in create method + return create(resource, uri, headers); + } - // No matches, id provided: The server treats the interaction as an Update as Create interaction (or rejects it, - // if it does not support Update as Create) -> reject - else if (result.getTotal() <= 0 && resource.hasId()) - { - audit.info("Create as update of non existing {} denied for identity '{}'", resourceTypeName, - getCurrentIdentity().getName()); - return responseGenerator.updateAsCreateNotAllowed(resourceTypeName); + // id provided: The server treats the interaction as an Update as Create interaction (or rejects it, if it + // does not support Update as Create) -> reject + else + { + audit.info("Create as update of non existing {} denied for identity '{}'", resourceTypeName, + getCurrentIdentity().getName()); + return responseGenerator.updateAsCreateNotAllowed(resourceTypeName); + } } // One Match, no resource id provided OR (resource id provided and it matches the found resource): @@ -519,7 +523,7 @@ else if (resource.hasId() // Multiple matches: The server returns a 412 Precondition Failed error indicating the client's criteria were // not selective enough preferably with an OperationOutcome - else // if (result.getOverallCount() > 1) + else { audit.info( "Update of {} denied for identity '{}', conditional update criteria not selective enough, multiple matches", @@ -623,7 +627,7 @@ public Response delete(UriInfo uri, HttpHeaders headers) .collect(Collectors.toMap(Entry::getKey, Entry::getValue)); } - SearchQuery query = dao.createSearchQuery(getCurrentIdentity(), PageAndCount.single()); + SearchQuery query = dao.createSearchQueryWithoutUserFilter(PageAndCount.single()); query.configureParameters(queryParameters); List unsupportedQueryParameters = query.getUnsupportedQueryParameters(); From d4901e6c5f186495a97b20e9b3538eab70d4fb81 Mon Sep 17 00:00:00 2001 From: Hauke Hund Date: Tue, 4 Aug 2026 09:34:58 +0200 Subject: [PATCH 22/23] 2.1.1 release --- dsf-bpe/dsf-bpe-process-api-v1-base/pom.xml | 2 +- dsf-bpe/dsf-bpe-process-api-v1-impl/pom.xml | 2 +- dsf-bpe/dsf-bpe-process-api-v1-operaton/pom.xml | 2 +- dsf-bpe/dsf-bpe-process-api-v1/pom.xml | 2 +- dsf-bpe/dsf-bpe-process-api-v2-impl/pom.xml | 2 +- dsf-bpe/dsf-bpe-process-api-v2/pom.xml | 2 +- dsf-bpe/dsf-bpe-process-api/pom.xml | 2 +- dsf-bpe/dsf-bpe-server-jetty/pom.xml | 2 +- dsf-bpe/dsf-bpe-server/pom.xml | 2 +- dsf-bpe/dsf-bpe-test-plugin-v1/pom.xml | 2 +- dsf-bpe/dsf-bpe-test-plugin-v2/pom.xml | 2 +- dsf-bpe/dsf-bpe-test-plugin/pom.xml | 2 +- dsf-bpe/pom.xml | 2 +- dsf-common/dsf-common-auth/pom.xml | 2 +- dsf-common/dsf-common-build-info-reader/pom.xml | 2 +- dsf-common/dsf-common-config/pom.xml | 2 +- dsf-common/dsf-common-db/pom.xml | 2 +- dsf-common/dsf-common-docker-secrets-reader/pom.xml | 2 +- dsf-common/dsf-common-documentation/pom.xml | 2 +- dsf-common/dsf-common-jetty/pom.xml | 2 +- dsf-common/dsf-common-oidc/pom.xml | 2 +- dsf-common/dsf-common-status/pom.xml | 2 +- dsf-common/dsf-common-ui/pom.xml | 2 +- dsf-common/pom.xml | 2 +- dsf-fhir/dsf-fhir-rest-adapter/pom.xml | 2 +- dsf-fhir/dsf-fhir-server-jetty/pom.xml | 2 +- dsf-fhir/dsf-fhir-server/pom.xml | 2 +- dsf-fhir/dsf-fhir-validation/pom.xml | 2 +- dsf-fhir/dsf-fhir-webservice-client/pom.xml | 2 +- dsf-fhir/dsf-fhir-websocket-client/pom.xml | 2 +- dsf-fhir/pom.xml | 2 +- dsf-maven/dsf-maven-plugin/pom.xml | 2 +- dsf-maven/pom.xml | 2 +- pom.xml | 2 +- 34 files changed, 34 insertions(+), 34 deletions(-) diff --git a/dsf-bpe/dsf-bpe-process-api-v1-base/pom.xml b/dsf-bpe/dsf-bpe-process-api-v1-base/pom.xml index a6b2dd82d..37692cccd 100644 --- a/dsf-bpe/dsf-bpe-process-api-v1-base/pom.xml +++ b/dsf-bpe/dsf-bpe-process-api-v1-base/pom.xml @@ -23,7 +23,7 @@ dev.dsf dsf-bpe-pom - 2.1.1-SNAPSHOT + 2.1.1 diff --git a/dsf-bpe/dsf-bpe-process-api-v1-impl/pom.xml b/dsf-bpe/dsf-bpe-process-api-v1-impl/pom.xml index c5d2d4fcb..a17965649 100644 --- a/dsf-bpe/dsf-bpe-process-api-v1-impl/pom.xml +++ b/dsf-bpe/dsf-bpe-process-api-v1-impl/pom.xml @@ -23,7 +23,7 @@ dev.dsf dsf-bpe-pom - 2.1.1-SNAPSHOT + 2.1.1 diff --git a/dsf-bpe/dsf-bpe-process-api-v1-operaton/pom.xml b/dsf-bpe/dsf-bpe-process-api-v1-operaton/pom.xml index 95816cf97..385862593 100644 --- a/dsf-bpe/dsf-bpe-process-api-v1-operaton/pom.xml +++ b/dsf-bpe/dsf-bpe-process-api-v1-operaton/pom.xml @@ -23,7 +23,7 @@ dev.dsf dsf-bpe-pom - 2.1.1-SNAPSHOT + 2.1.1 diff --git a/dsf-bpe/dsf-bpe-process-api-v1/pom.xml b/dsf-bpe/dsf-bpe-process-api-v1/pom.xml index 328e41ef5..d50519934 100644 --- a/dsf-bpe/dsf-bpe-process-api-v1/pom.xml +++ b/dsf-bpe/dsf-bpe-process-api-v1/pom.xml @@ -23,7 +23,7 @@ dev.dsf dsf-bpe-pom - 2.1.1-SNAPSHOT + 2.1.1 diff --git a/dsf-bpe/dsf-bpe-process-api-v2-impl/pom.xml b/dsf-bpe/dsf-bpe-process-api-v2-impl/pom.xml index cd1ea1a2e..43ff4da45 100644 --- a/dsf-bpe/dsf-bpe-process-api-v2-impl/pom.xml +++ b/dsf-bpe/dsf-bpe-process-api-v2-impl/pom.xml @@ -23,7 +23,7 @@ dev.dsf dsf-bpe-pom - 2.1.1-SNAPSHOT + 2.1.1 diff --git a/dsf-bpe/dsf-bpe-process-api-v2/pom.xml b/dsf-bpe/dsf-bpe-process-api-v2/pom.xml index 0fc973937..c0a4f5371 100644 --- a/dsf-bpe/dsf-bpe-process-api-v2/pom.xml +++ b/dsf-bpe/dsf-bpe-process-api-v2/pom.xml @@ -23,7 +23,7 @@ dev.dsf dsf-bpe-pom - 2.1.1-SNAPSHOT + 2.1.1 diff --git a/dsf-bpe/dsf-bpe-process-api/pom.xml b/dsf-bpe/dsf-bpe-process-api/pom.xml index 0205d2748..41e5ad506 100644 --- a/dsf-bpe/dsf-bpe-process-api/pom.xml +++ b/dsf-bpe/dsf-bpe-process-api/pom.xml @@ -25,7 +25,7 @@ dev.dsf dsf-bpe-pom - 2.1.1-SNAPSHOT + 2.1.1 diff --git a/dsf-bpe/dsf-bpe-server-jetty/pom.xml b/dsf-bpe/dsf-bpe-server-jetty/pom.xml index 608b48094..e5d4ceb80 100755 --- a/dsf-bpe/dsf-bpe-server-jetty/pom.xml +++ b/dsf-bpe/dsf-bpe-server-jetty/pom.xml @@ -23,7 +23,7 @@ dev.dsf dsf-bpe-pom - 2.1.1-SNAPSHOT + 2.1.1 diff --git a/dsf-bpe/dsf-bpe-server/pom.xml b/dsf-bpe/dsf-bpe-server/pom.xml index 30c57b695..0c16bb5b5 100755 --- a/dsf-bpe/dsf-bpe-server/pom.xml +++ b/dsf-bpe/dsf-bpe-server/pom.xml @@ -23,7 +23,7 @@ dev.dsf dsf-bpe-pom - 2.1.1-SNAPSHOT + 2.1.1 diff --git a/dsf-bpe/dsf-bpe-test-plugin-v1/pom.xml b/dsf-bpe/dsf-bpe-test-plugin-v1/pom.xml index 8192f0a65..6e56d3616 100644 --- a/dsf-bpe/dsf-bpe-test-plugin-v1/pom.xml +++ b/dsf-bpe/dsf-bpe-test-plugin-v1/pom.xml @@ -23,7 +23,7 @@ dev.dsf dsf-bpe-pom - 2.1.1-SNAPSHOT + 2.1.1 diff --git a/dsf-bpe/dsf-bpe-test-plugin-v2/pom.xml b/dsf-bpe/dsf-bpe-test-plugin-v2/pom.xml index 7dfcb7880..acafc571c 100644 --- a/dsf-bpe/dsf-bpe-test-plugin-v2/pom.xml +++ b/dsf-bpe/dsf-bpe-test-plugin-v2/pom.xml @@ -23,7 +23,7 @@ dev.dsf dsf-bpe-pom - 2.1.1-SNAPSHOT + 2.1.1 diff --git a/dsf-bpe/dsf-bpe-test-plugin/pom.xml b/dsf-bpe/dsf-bpe-test-plugin/pom.xml index 61bea458f..ed8460e43 100644 --- a/dsf-bpe/dsf-bpe-test-plugin/pom.xml +++ b/dsf-bpe/dsf-bpe-test-plugin/pom.xml @@ -23,7 +23,7 @@ dev.dsf dsf-bpe-pom - 2.1.1-SNAPSHOT + 2.1.1 diff --git a/dsf-bpe/pom.xml b/dsf-bpe/pom.xml index 08fef2cbb..61331eee3 100755 --- a/dsf-bpe/pom.xml +++ b/dsf-bpe/pom.xml @@ -24,7 +24,7 @@ dev.dsf dsf-pom - 2.1.1-SNAPSHOT + 2.1.1 diff --git a/dsf-common/dsf-common-auth/pom.xml b/dsf-common/dsf-common-auth/pom.xml index a7b809b77..1d4a06c4e 100644 --- a/dsf-common/dsf-common-auth/pom.xml +++ b/dsf-common/dsf-common-auth/pom.xml @@ -23,7 +23,7 @@ dev.dsf dsf-common-pom - 2.1.1-SNAPSHOT + 2.1.1 diff --git a/dsf-common/dsf-common-build-info-reader/pom.xml b/dsf-common/dsf-common-build-info-reader/pom.xml index fade0b103..4c488df4e 100644 --- a/dsf-common/dsf-common-build-info-reader/pom.xml +++ b/dsf-common/dsf-common-build-info-reader/pom.xml @@ -23,7 +23,7 @@ dev.dsf dsf-common-pom - 2.1.1-SNAPSHOT + 2.1.1 diff --git a/dsf-common/dsf-common-config/pom.xml b/dsf-common/dsf-common-config/pom.xml index 5f5f2b5b4..87d975213 100644 --- a/dsf-common/dsf-common-config/pom.xml +++ b/dsf-common/dsf-common-config/pom.xml @@ -23,7 +23,7 @@ dev.dsf dsf-common-pom - 2.1.1-SNAPSHOT + 2.1.1 diff --git a/dsf-common/dsf-common-db/pom.xml b/dsf-common/dsf-common-db/pom.xml index d2d8c3414..b725486fa 100644 --- a/dsf-common/dsf-common-db/pom.xml +++ b/dsf-common/dsf-common-db/pom.xml @@ -23,7 +23,7 @@ dev.dsf dsf-common-pom - 2.1.1-SNAPSHOT + 2.1.1 diff --git a/dsf-common/dsf-common-docker-secrets-reader/pom.xml b/dsf-common/dsf-common-docker-secrets-reader/pom.xml index 87ac31219..3a5e14119 100644 --- a/dsf-common/dsf-common-docker-secrets-reader/pom.xml +++ b/dsf-common/dsf-common-docker-secrets-reader/pom.xml @@ -23,7 +23,7 @@ dev.dsf dsf-common-pom - 2.1.1-SNAPSHOT + 2.1.1 diff --git a/dsf-common/dsf-common-documentation/pom.xml b/dsf-common/dsf-common-documentation/pom.xml index 1dd75fa0f..35e40bcf7 100644 --- a/dsf-common/dsf-common-documentation/pom.xml +++ b/dsf-common/dsf-common-documentation/pom.xml @@ -23,7 +23,7 @@ dev.dsf dsf-common-pom - 2.1.1-SNAPSHOT + 2.1.1 diff --git a/dsf-common/dsf-common-jetty/pom.xml b/dsf-common/dsf-common-jetty/pom.xml index 34f905682..7af404be1 100644 --- a/dsf-common/dsf-common-jetty/pom.xml +++ b/dsf-common/dsf-common-jetty/pom.xml @@ -23,7 +23,7 @@ dev.dsf dsf-common-pom - 2.1.1-SNAPSHOT + 2.1.1 diff --git a/dsf-common/dsf-common-oidc/pom.xml b/dsf-common/dsf-common-oidc/pom.xml index b42aa9a2a..bd3277728 100644 --- a/dsf-common/dsf-common-oidc/pom.xml +++ b/dsf-common/dsf-common-oidc/pom.xml @@ -25,7 +25,7 @@ dev.dsf dsf-common-pom - 2.1.1-SNAPSHOT + 2.1.1 diff --git a/dsf-common/dsf-common-status/pom.xml b/dsf-common/dsf-common-status/pom.xml index b8a03f6e1..80ffcb0ac 100644 --- a/dsf-common/dsf-common-status/pom.xml +++ b/dsf-common/dsf-common-status/pom.xml @@ -23,7 +23,7 @@ dev.dsf dsf-common-pom - 2.1.1-SNAPSHOT + 2.1.1 diff --git a/dsf-common/dsf-common-ui/pom.xml b/dsf-common/dsf-common-ui/pom.xml index 3adb67d79..836398642 100644 --- a/dsf-common/dsf-common-ui/pom.xml +++ b/dsf-common/dsf-common-ui/pom.xml @@ -23,7 +23,7 @@ dev.dsf dsf-common-pom - 2.1.1-SNAPSHOT + 2.1.1 diff --git a/dsf-common/pom.xml b/dsf-common/pom.xml index c2fd7997f..6e0c5ff67 100644 --- a/dsf-common/pom.xml +++ b/dsf-common/pom.xml @@ -24,7 +24,7 @@ dev.dsf dsf-pom - 2.1.1-SNAPSHOT + 2.1.1 diff --git a/dsf-fhir/dsf-fhir-rest-adapter/pom.xml b/dsf-fhir/dsf-fhir-rest-adapter/pom.xml index 6f2a8869d..923fbd5a6 100755 --- a/dsf-fhir/dsf-fhir-rest-adapter/pom.xml +++ b/dsf-fhir/dsf-fhir-rest-adapter/pom.xml @@ -23,7 +23,7 @@ dev.dsf dsf-fhir-pom - 2.1.1-SNAPSHOT + 2.1.1 diff --git a/dsf-fhir/dsf-fhir-server-jetty/pom.xml b/dsf-fhir/dsf-fhir-server-jetty/pom.xml index ac31c551b..95ab01081 100755 --- a/dsf-fhir/dsf-fhir-server-jetty/pom.xml +++ b/dsf-fhir/dsf-fhir-server-jetty/pom.xml @@ -23,7 +23,7 @@ dev.dsf dsf-fhir-pom - 2.1.1-SNAPSHOT + 2.1.1 diff --git a/dsf-fhir/dsf-fhir-server/pom.xml b/dsf-fhir/dsf-fhir-server/pom.xml index 5ab54be76..0faa4391d 100755 --- a/dsf-fhir/dsf-fhir-server/pom.xml +++ b/dsf-fhir/dsf-fhir-server/pom.xml @@ -23,7 +23,7 @@ dev.dsf dsf-fhir-pom - 2.1.1-SNAPSHOT + 2.1.1 diff --git a/dsf-fhir/dsf-fhir-validation/pom.xml b/dsf-fhir/dsf-fhir-validation/pom.xml index f6d6bca09..1af0f8144 100644 --- a/dsf-fhir/dsf-fhir-validation/pom.xml +++ b/dsf-fhir/dsf-fhir-validation/pom.xml @@ -23,7 +23,7 @@ dev.dsf dsf-fhir-pom - 2.1.1-SNAPSHOT + 2.1.1 diff --git a/dsf-fhir/dsf-fhir-webservice-client/pom.xml b/dsf-fhir/dsf-fhir-webservice-client/pom.xml index 0a3dc99eb..00e545bc8 100755 --- a/dsf-fhir/dsf-fhir-webservice-client/pom.xml +++ b/dsf-fhir/dsf-fhir-webservice-client/pom.xml @@ -23,7 +23,7 @@ dev.dsf dsf-fhir-pom - 2.1.1-SNAPSHOT + 2.1.1 diff --git a/dsf-fhir/dsf-fhir-websocket-client/pom.xml b/dsf-fhir/dsf-fhir-websocket-client/pom.xml index f3ab0962d..fe5c554bb 100755 --- a/dsf-fhir/dsf-fhir-websocket-client/pom.xml +++ b/dsf-fhir/dsf-fhir-websocket-client/pom.xml @@ -23,7 +23,7 @@ dev.dsf dsf-fhir-pom - 2.1.1-SNAPSHOT + 2.1.1 diff --git a/dsf-fhir/pom.xml b/dsf-fhir/pom.xml index 29fc49467..51e6671dc 100755 --- a/dsf-fhir/pom.xml +++ b/dsf-fhir/pom.xml @@ -24,7 +24,7 @@ dev.dsf dsf-pom - 2.1.1-SNAPSHOT + 2.1.1 diff --git a/dsf-maven/dsf-maven-plugin/pom.xml b/dsf-maven/dsf-maven-plugin/pom.xml index fa6486a2f..f805842b8 100644 --- a/dsf-maven/dsf-maven-plugin/pom.xml +++ b/dsf-maven/dsf-maven-plugin/pom.xml @@ -26,7 +26,7 @@ dev.dsf dsf-maven-pom - 2.1.1-SNAPSHOT + 2.1.1 diff --git a/dsf-maven/pom.xml b/dsf-maven/pom.xml index 79adadeee..4b1b53587 100644 --- a/dsf-maven/pom.xml +++ b/dsf-maven/pom.xml @@ -26,7 +26,7 @@ dev.dsf dsf-pom - 2.1.1-SNAPSHOT + 2.1.1 diff --git a/pom.xml b/pom.xml index f98d59295..bd2f03633 100755 --- a/pom.xml +++ b/pom.xml @@ -20,7 +20,7 @@ dev.dsf dsf-pom - 2.1.1-SNAPSHOT + 2.1.1 pom From 3eac36a1eb3f2935003f6b77972a7e62527fee95 Mon Sep 17 00:00:00 2001 From: Hauke Hund Date: Tue, 4 Aug 2026 09:57:16 +0200 Subject: [PATCH 23/23] fixed typo, decreased visibility --- .../main/java/dev/dsf/bpe/v2/service/CryptoServiceImpl.java | 6 +++--- .../test/java/dev/dsf/bpe/v2/service/CryptoServiceTest.java | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/service/CryptoServiceImpl.java b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/service/CryptoServiceImpl.java index 47ff33c8b..687e37332 100644 --- a/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/service/CryptoServiceImpl.java +++ b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/service/CryptoServiceImpl.java @@ -53,8 +53,8 @@ public class CryptoServiceImpl implements CryptoService { public static final class KemDelegate implements Kem { - public static final long ENCRYPT_LIMIT = 250 * 1024 * 1024; // 250 MiB - public static final long DENCRYPT_LIMIT = (250 * 1024 * 1024) + 1024; // 250 MiB + 1024 Bytes + protected static final long ENCRYPT_LIMIT = 250 * 1024 * 1024; // 250 MiB + protected static final long DECRYPT_LIMIT = (250 * 1024 * 1024) + 1024; // 250 MiB + 1024 Bytes private final AbstractKemAesGcm delegate; @@ -75,7 +75,7 @@ public InputStream decrypt(InputStream encrypted, PrivateKey privateKey) throws IOException, NoSuchAlgorithmException, InvalidKeyException, DecapsulateException, NoSuchPaddingException, InvalidAlgorithmParameterException { - return delegate.decrypt(new LimitedInputStream(encrypted, DENCRYPT_LIMIT), privateKey); + return delegate.decrypt(new LimitedInputStream(encrypted, DECRYPT_LIMIT), privateKey); } } diff --git a/dsf-bpe/dsf-bpe-process-api-v2-impl/src/test/java/dev/dsf/bpe/v2/service/CryptoServiceTest.java b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/test/java/dev/dsf/bpe/v2/service/CryptoServiceTest.java index 1c7ed0c4a..ba19071df 100644 --- a/dsf-bpe/dsf-bpe-process-api-v2-impl/src/test/java/dev/dsf/bpe/v2/service/CryptoServiceTest.java +++ b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/test/java/dev/dsf/bpe/v2/service/CryptoServiceTest.java @@ -85,7 +85,7 @@ public void testDecryptLimit() throws Exception { try { - InputStream encrypted = noLimitKem.encrypt(new NullInputStream(KemDelegate.DENCRYPT_LIMIT + 1), + InputStream encrypted = noLimitKem.encrypt(new NullInputStream(KemDelegate.DECRYPT_LIMIT + 1), keyPair.getPublic()); kem.decrypt(encrypted, keyPair.getPrivate()).transferTo(new NullOutputStream()); @@ -94,7 +94,7 @@ public void testDecryptLimit() throws Exception } catch (IOException e) { - assertEquals("Stream limit of " + KemDelegate.DENCRYPT_LIMIT + " bytes exceeded", e.getMessage()); + assertEquals("Stream limit of " + KemDelegate.DECRYPT_LIMIT + " bytes exceeded", e.getMessage()); } } }