├── .github ├── PULL_REQUEST_TEMPLATE.md ├── dependabot.yml └── workflows │ ├── codecov.yml │ └── codeql.yml ├── .gitignore ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE ├── OWNERS ├── README.md ├── build.sh ├── coverage └── pom.xml ├── doc ├── .gitignore ├── README.md ├── developers.md ├── docs │ ├── concepts.md │ ├── contribute.md │ ├── design.md │ ├── getting_started.md │ ├── images │ │ ├── Info_Gateway_Overview.png │ │ ├── Info_Gateway_Use_Cases.png │ │ ├── flow.png │ │ ├── integrated.png │ │ ├── separate.png │ │ └── summary.png │ ├── index.md │ ├── release_process.md │ ├── support.md │ ├── tutorial_docker.md │ ├── tutorial_first_access_checker.md │ └── tutorials.md ├── mkdocs.yml └── requirements.txt ├── docker ├── .env ├── README.md ├── hapi-proxy-compose.yaml └── keycloak │ ├── .env │ ├── Dockerfile │ ├── README.md │ ├── config-compose.yaml │ ├── keycloak_events.txt │ ├── keycloak_events_list.txt │ └── keycloak_setup.sh ├── e2e-test ├── clients.py ├── e2e.py ├── e2e.sh └── obs.json ├── exec ├── README.md ├── pom.xml └── src │ ├── main │ └── java │ │ └── com │ │ └── google │ │ └── fhir │ │ └── gateway │ │ ├── CustomFhirEndpointExample.java │ │ ├── CustomGenericEndpointExample.java │ │ └── MainApp.java │ └── test │ └── java │ └── com │ └── google │ └── fhir │ └── gateway │ └── MainAppTest.java ├── kokoro ├── README.md └── gcp_ubuntu │ ├── continuous.cfg │ ├── kokoro_build.sh │ └── presubmit.cfg ├── license-header.txt ├── plugins ├── README.md ├── pom.xml └── src │ ├── main │ └── java │ │ └── com │ │ └── google │ │ └── fhir │ │ └── gateway │ │ └── plugin │ │ ├── AccessGrantedAndUpdateList.java │ │ ├── ListAccessChecker.java │ │ ├── PatientAccessChecker.java │ │ ├── SmartFhirScope.java │ │ └── SmartScopeChecker.java │ └── test │ ├── java │ └── com │ │ └── google │ │ └── fhir │ │ └── gateway │ │ └── plugin │ │ ├── AccessCheckerTestBase.java │ │ ├── AccessGrantedAndUpdateListTest.java │ │ ├── ListAccessCheckerTest.java │ │ ├── PatientAccessCheckerTest.java │ │ ├── SmartScopeCheckerTest.java │ │ └── TestUtil.java │ └── resources │ ├── bundle_empty.json │ ├── bundle_list_patient_item.json │ ├── bundle_transaction_delete_multiple_patient.json │ ├── bundle_transaction_delete_non_patient.json │ ├── bundle_transaction_delete_patient.json │ ├── bundle_transaction_delete_patient_unauthorized.json │ ├── bundle_transaction_get_multiple_with_null_patient.json │ ├── bundle_transaction_get_non_patient_authorized.json │ ├── bundle_transaction_get_non_patient_multiple_authorized.json │ ├── bundle_transaction_get_non_patient_unauthorized.json │ ├── bundle_transaction_get_patient_unauthorized.json │ ├── bundle_transaction_no_patient_in_url.json │ ├── bundle_transaction_no_patient_ref.json │ ├── bundle_transaction_no_resource_field.json │ ├── bundle_transaction_non_patients.json │ ├── bundle_transaction_patch_authorized.json │ ├── bundle_transaction_patch_not_binary.json │ ├── bundle_transaction_patch_unauthorized.json │ ├── bundle_transaction_patient_and_non_patients.json │ ├── bundle_transaction_post_patient.json │ ├── bundle_transaction_put_authorized_patient.json │ ├── bundle_transaction_put_patient.json │ ├── bundle_transaction_put_unauthorized.json │ ├── capability.json │ ├── patient-list-example.json │ ├── patient_id_search.json │ ├── patient_id_search_single.json │ ├── test_obs.json │ ├── test_obs_no_subject.json │ ├── test_obs_patch.json │ ├── test_obs_patch_no_reference.json │ ├── test_obs_patch_unauthorized_no_patient_id.json │ ├── test_obs_patch_unauthorized_patient.json │ ├── test_obs_patch_unauthorized_remove.json │ ├── test_obs_performers.json │ ├── test_obs_unauthorized.json │ └── test_patient.json ├── pom.xml ├── resources ├── README.md ├── fhir_access_proxy.png ├── hapi_page_url_allowed_queries.json └── patient-list-example.json └── server ├── pom.xml └── src ├── main ├── java │ └── com │ │ └── google │ │ └── fhir │ │ └── gateway │ │ ├── AllowedQueriesChecker.java │ │ ├── AllowedQueriesConfig.java │ │ ├── BearerAuthorizationInterceptor.java │ │ ├── BundlePatients.java │ │ ├── CapabilityPostProcessor.java │ │ ├── ExceptionUtil.java │ │ ├── FhirClientFactory.java │ │ ├── FhirProxyServer.java │ │ ├── FhirUtil.java │ │ ├── GcpFhirClient.java │ │ ├── GenericFhirClient.java │ │ ├── HttpFhirClient.java │ │ ├── HttpUtil.java │ │ ├── JwtUtil.java │ │ ├── PatientFinderImp.java │ │ ├── PermissiveAccessChecker.java │ │ ├── ProxyConstants.java │ │ ├── RequestDetailsToReader.java │ │ ├── TokenVerifier.java │ │ └── interfaces │ │ ├── AccessChecker.java │ │ ├── AccessCheckerFactory.java │ │ ├── AccessDecision.java │ │ ├── NoOpAccessDecision.java │ │ ├── PatientFinder.java │ │ ├── RequestDetailsReader.java │ │ └── RequestMutation.java ├── resources │ ├── CompartmentDefinition-patient.json │ ├── README.md │ ├── logback.xml │ └── patient_paths.json └── webapp │ └── WEB-INF │ └── web.xml └── test ├── java └── com │ └── google │ └── fhir │ └── gateway │ ├── AllowedQueriesCheckerTest.java │ ├── BearerAuthorizationInterceptorTest.java │ ├── FhirUtilTest.java │ ├── GcpFhirClientTest.java │ ├── GenericFhirClientTest.java │ ├── HttpFhirClientTest.java │ ├── TestUtil.java │ └── TokenVerifierTest.java └── resources ├── allowed_queries_with_no_extra_params.json ├── allowed_queries_with_path_type.json ├── allowed_unauthenticated_queries.json ├── capability.json ├── error_operation_outcome.json ├── hapi_page_url_allowed_queries.json ├── idp_keycloak_config.json ├── malformed_allowed_queries.json ├── no_path_allowed_queries.json ├── patient-list-example.json ├── patient_id_search.json └── test_patient.json /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Description of what I changed 4 | 5 | 6 | 7 | 8 | 11 | 12 | ## E2E test 13 | 14 | 16 | 17 | TESTED: 18 | 19 | Please replace this with a description of how you tested your PR beyond the 20 | automated e2e/unit tests. 21 | 22 | ## Checklist: I completed these to help reviewers :) 23 | 24 | 25 | 26 | 27 | - [ ] I have read and will follow the [review process](https://github.com/GoogleCloudPlatform/openmrs-fhir-analytics/blob/master/doc/review_process.md). 28 | - [ ] I am familiar with Google Style Guides for the language I have coded in. 29 | 30 | No? Please take some time and review [Java](https://google.github.io/styleguide/javaguide.html) and [Python](https://google.github.io/styleguide/pyguide.html) style guides. 31 | 32 | - [ ] My IDE is configured to follow the Google [**code styles**](https://google.github.io/styleguide/). 33 | 34 | No? Unsure? -> [configure your IDE](https://github.com/google/google-java-format). 35 | 36 | - [ ] I have **added tests** to cover my changes. (If you refactored existing code that was well tested you do not have to add tests) 37 | - [ ] I ran `mvn clean package` right before creating this pull request and added all formatting changes to my commit. 38 | - [ ] All new and existing **tests passed**. 39 | - [ ] My pull request is **based on the latest changes** of the master branch. 40 | 41 | No? Unsure? -> execute command `git pull --rebase upstream master` 42 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "maven" 9 | directory: "/" 10 | schedule: 11 | interval: "weekly" 12 | # Allow up to 30 open pull requests at the same time 13 | open-pull-requests-limit: 30 14 | -------------------------------------------------------------------------------- /.github/workflows/codecov.yml: -------------------------------------------------------------------------------- 1 | # This is based on: 2 | # https://github.com/codecov/example-java-maven/blob/main/.github/workflows/ci.yml 3 | name: Codecov 4 | on: [push, pull_request] 5 | jobs: 6 | run: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Checkout 10 | uses: actions/checkout@v4 11 | - name: Set up JDK 11 12 | uses: actions/setup-java@v1 13 | with: 14 | java-version: 17 15 | - name: Install dependencies 16 | run: mvn install -DskipTests=true -Dmaven.javadoc.skip=true -B -V 17 | - name: Run tests and collect coverage 18 | run: mvn -B test 19 | - name: Upload coverage to Codecov 20 | uses: codecov/codecov-action@v4 21 | with: 22 | # To find this token, and how it is stored in the repo, see: 23 | # https://docs.codecov.com/docs/adding-the-codecov-token 24 | token: ${{ secrets.CODECOV_TOKEN }} 25 | slug: google/fhir-gateway -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ "main" ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ "main" ] 20 | schedule: 21 | - cron: '25 21 * * 6' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} 27 | timeout-minutes: ${{ (matrix.language == 'swift' && 120) || 360 }} 28 | permissions: 29 | actions: read 30 | contents: read 31 | security-events: write 32 | 33 | strategy: 34 | fail-fast: false 35 | matrix: 36 | language: [ 'java', 'python' ] 37 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby', 'swift' ] 38 | # Use only 'java' to analyze code written in Java, Kotlin or both 39 | # Use only 'javascript' to analyze code written in JavaScript, TypeScript or both 40 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support 41 | 42 | steps: 43 | - uses: actions/setup-java@v4 44 | with: 45 | distribution: 'temurin' 46 | java-version: '17' 47 | 48 | - name: Checkout repository 49 | uses: actions/checkout@v3 50 | 51 | # Initializes the CodeQL tools for scanning. 52 | - name: Initialize CodeQL 53 | uses: github/codeql-action/init@v2 54 | with: 55 | languages: ${{ matrix.language }} 56 | # If you wish to specify custom queries, you can do so here or in a config file. 57 | # By default, queries listed here will override any specified in a config file. 58 | # Prefix the list here with "+" to use these queries and those in the config file. 59 | 60 | # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 61 | # queries: security-extended,security-and-quality 62 | 63 | 64 | # Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java). 65 | # If this step fails, then you should remove it and run the build manually (see below) 66 | - name: Autobuild 67 | uses: github/codeql-action/autobuild@v2 68 | 69 | # ℹ️ Command-line programs to run using the OS shell. 70 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 71 | 72 | # If the Autobuild fails above, remove it and uncomment the following three lines. 73 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. 74 | 75 | # - run: | 76 | # echo "Run, Build Application using script" 77 | # ./location_of_script_within_repo/buildscript.sh 78 | 79 | - name: Perform CodeQL Analysis 80 | uses: github/codeql-action/analyze@v2 81 | with: 82 | category: "/language:${{matrix.language}}" 83 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/target/ 2 | /tmp/ 3 | 4 | # IntelliJ 5 | .idea/ 6 | *.iml 7 | 8 | # Local configuration file (sdk path, etc) 9 | local.properties 10 | 11 | # VSCode/Eclipse project files 12 | .vscode 13 | .classpath 14 | .project 15 | .settings 16 | bin/ 17 | gen/ 18 | 19 | # Python cache files 20 | __pycache__/ 21 | 22 | # MacOS 23 | .DS_Store 24 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to Contribute 2 | 3 | We'd love to accept your patches and contributions to this project. There are 4 | just a few small guidelines you need to follow. 5 | 6 | ## Contributor License Agreement 7 | 8 | Contributions to this project must be accompanied by a Contributor License 9 | Agreement (CLA). You (or your employer) retain the copyright to your 10 | contribution; this simply gives us permission to use and redistribute your 11 | contributions as part of the project. Head over to 12 | to see your current agreements on file or 13 | to sign a new one. 14 | 15 | You generally only need to submit a CLA once, so if you've already submitted one 16 | (even if it was for a different project), you probably don't need to do it 17 | again. 18 | 19 | ## Code reviews 20 | 21 | All submissions by non-project members, require review. We use GitHub pull 22 | requests for this purpose. Consult 23 | [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more 24 | information on using pull requests. We use GitHub for issue tracking. 25 | 26 | ## Community Guidelines 27 | 28 | This project follows 29 | [Google's Open Source Community Guidelines](https://opensource.google/conduct/). 30 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2021-2024 Google LLC 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | # Image for building and running tests against the source code of 18 | # the FHIR Gateway. 19 | FROM maven:3.8.7-eclipse-temurin-17-focal as build 20 | 21 | RUN apt-get update && apt-get install -y nodejs npm 22 | RUN npm cache clean -f && npm install -g n && n stable 23 | 24 | WORKDIR /app 25 | 26 | COPY server/src ./server/src 27 | COPY server/pom.xml ./server/ 28 | COPY plugins/src ./plugins/src 29 | COPY plugins/pom.xml ./plugins/ 30 | COPY exec/src ./exec/src 31 | COPY exec/pom.xml ./exec/ 32 | COPY coverage/pom.xml ./coverage/ 33 | COPY license-header.txt . 34 | COPY pom.xml . 35 | 36 | RUN mvn spotless:check 37 | # Updating license will fail in e2e and there is no point doing it here anyways. 38 | RUN mvn --batch-mode package -Dlicense.skip=true 39 | 40 | 41 | # Image for FHIR Gateway binary with configuration knobs as environment vars. 42 | FROM eclipse-temurin:17-jdk-focal as main 43 | 44 | COPY --from=build /app/exec/target/fhir-gateway-exec.jar / 45 | COPY resources/hapi_page_url_allowed_queries.json resources/hapi_page_url_allowed_queries.json 46 | 47 | ENV PROXY_PORT=8080 48 | ENV TOKEN_ISSUER="http://localhost/auth/realms/test" 49 | ENV PROXY_TO="http://localhost:8099/fhir" 50 | ENV BACKEND_TYPE="HAPI" 51 | 52 | # If ACCESS_CHECKER is set to a non-empty value, patient level access checks 53 | # are enabled; otherwise any valid token issued by TOKEN_ISSUER can be used 54 | # for full access to the FHIR store. 55 | ENV ACCESS_CHECKER="list" 56 | ENV RUN_MODE="PROD" 57 | 58 | ENTRYPOINT java -jar fhir-gateway-exec.jar --server.port=${PROXY_PORT} 59 | -------------------------------------------------------------------------------- /OWNERS: -------------------------------------------------------------------------------- 1 | bashir@google.com 2 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Copyright 2021-2023 Google LLC 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | # Fail on any error. 19 | set -e 20 | # Display commands being run. 21 | # WARNING: please only enable 'set -x' if necessary for debugging, and be very 22 | # careful if you handle credentials (e.g. from Keystore) with 'set -x': 23 | # statements like "export VAR=$(cat /tmp/keystore/credentials)" will result in 24 | # the credentials being printed in build logs. 25 | # Additionally, recursive invocation with credentials as command-line 26 | # parameters, will print the full command, with credentials, in the build logs. 27 | # set -x 28 | export BUILD_ID=${KOKORO_BUILD_ID:-local} 29 | gcloud auth configure-docker us-docker.pkg.dev 30 | ./e2e-test/e2e.sh 31 | docker push us-docker.pkg.dev/fhir-proxy-build/stable/fhir-gateway:${BUILD_ID} 32 | -------------------------------------------------------------------------------- /coverage/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 19 | 23 | 4.0.0 24 | 25 | 26 | com.google.fhir.gateway 27 | fhir-gateway 28 | 0.4.1-SNAPSHOT 29 | 30 | 31 | com.google.fhir.gateway 32 | coverage 33 | coverage 34 | Compute aggregated test code coverage 35 | pom 36 | 37 | 38 | true 39 | ${project.parent.basedir} 40 | 41 | 42 | 43 | 44 | com.google.fhir.gateway 45 | server 46 | ${project.parent.version} 47 | 48 | 49 | 50 | com.google.fhir.gateway 51 | plugins 52 | ${project.parent.version} 53 | 54 | 55 | 56 | 57 | 58 | 59 | org.jacoco 60 | jacoco-maven-plugin 61 | 62 | 63 | report-aggregate 64 | test 65 | 66 | report-aggregate 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /doc/.gitignore: -------------------------------------------------------------------------------- 1 | site/** 2 | -------------------------------------------------------------------------------- /doc/README.md: -------------------------------------------------------------------------------- 1 | To generate the documentation web-site content from `docs/` see instructions 2 | [here](https://github.com/google/fhir-data-pipes/tree/master/doc#documentation-site). 3 | -------------------------------------------------------------------------------- /doc/developers.md: -------------------------------------------------------------------------------- 1 | # Developer Tips 2 | 3 | ## Setting up IDE to use google-java-format by default 4 | 5 | - If your IDE is IntelliJ, follow the instructions 6 | [here](https://github.com/google/google-java-format#intellij-android-studio-and-other-jetbrains-ides) 7 | 8 | - If your IDE is Eclipse, follow the instructions 9 | [here](https://github.com/google/google-java-format#eclipse) 10 | -------------------------------------------------------------------------------- /doc/docs/contribute.md: -------------------------------------------------------------------------------- 1 | # How to Contribute 2 | 3 | We'd love to accept your patches and contributions to this project. There are 4 | just a few small guidelines you need to follow. 5 | 6 | ## Contributor License Agreement 7 | 8 | Contributions to this project must be accompanied by a Contributor License 9 | Agreement (CLA). You (or your employer) retain the copyright to your 10 | contribution; this simply gives us permission to use and redistribute your 11 | contributions as part of the project. Head over to 12 | to see your current agreements on file or 13 | to sign a new one. 14 | 15 | You generally only need to submit a CLA once, so if you've already submitted one 16 | (even if it was for a different project), you probably don't need to do it 17 | again. 18 | 19 | ## Code reviews 20 | 21 | All code changes require review. We use GitHub pull-requests for this purpose. 22 | 23 | - Consult [GitHub Help](https://help.github.com/articles/about-pull-requests/) 24 | for more information on using pull requests. 25 | 26 | - We use GitHub for issue tracking. 27 | 28 | ## Community Guidelines 29 | 30 | This project follows 31 | [Google's Open Source Community Guidelines](https://opensource.google/conduct/). 32 | -------------------------------------------------------------------------------- /doc/docs/images/Info_Gateway_Overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/fhir-gateway/bfd3cb7b2aa4376bda9ddd8cc4175b295e671e54/doc/docs/images/Info_Gateway_Overview.png -------------------------------------------------------------------------------- /doc/docs/images/Info_Gateway_Use_Cases.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/fhir-gateway/bfd3cb7b2aa4376bda9ddd8cc4175b295e671e54/doc/docs/images/Info_Gateway_Use_Cases.png -------------------------------------------------------------------------------- /doc/docs/images/flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/fhir-gateway/bfd3cb7b2aa4376bda9ddd8cc4175b295e671e54/doc/docs/images/flow.png -------------------------------------------------------------------------------- /doc/docs/images/integrated.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/fhir-gateway/bfd3cb7b2aa4376bda9ddd8cc4175b295e671e54/doc/docs/images/integrated.png -------------------------------------------------------------------------------- /doc/docs/images/separate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/fhir-gateway/bfd3cb7b2aa4376bda9ddd8cc4175b295e671e54/doc/docs/images/separate.png -------------------------------------------------------------------------------- /doc/docs/images/summary.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/fhir-gateway/bfd3cb7b2aa4376bda9ddd8cc4175b295e671e54/doc/docs/images/summary.png -------------------------------------------------------------------------------- /doc/docs/index.md: -------------------------------------------------------------------------------- 1 | # FHIR Info Gateway 2 | 3 | The Info Gateway is a reverse proxy which controls client access to FHIR 4 | resources on a server. It works by inspecting FHIR requests and verifying that 5 | the client is authorized to access the requested resources. 6 | 7 | It makes it easier for developers to enforce various forms of authorization 8 | policies including organizational role based access control (RBAC) policies when 9 | working with FHIR data. 10 | 11 | - To enable authorization and access-control (ACL) policy enforcement between a 12 | client application and a FHIR server, the Info Gateway is used along with an 13 | Identity Provider (IDP) and Authorization server (AuthZ). 14 | - The IDP can be a generic OpenID Connect (OIDC) compliant service or a special 15 | purpose one. 16 | - The IDP+AuthZ should provide a JSON Web Token (JWT) to the client. The client 17 | uses this as a Bearer access-token (AT) when sending FHIR requests. 18 | - A sample end-to-end implementation with Keycloak as the IDP+AuthZ service is 19 | provided and has been tested with HAPI FHIR and Google Cloud Healthcare 20 | FHIR-store as the FHIR server. 21 | 22 | ![FHIR Info Gateway](images/Info_Gateway_Overview.png) 23 | 24 | ## Key Features 25 | 26 | Key features of the Info Gateway include: 27 | 28 | - A stand-alone service that can work with any FHIR compliant servers. 29 | - A pluggable architecture for defining an access-checker to allow for 30 | implementation configurability. 31 | - Query filtering to block/allow specific queries. 32 | - Post-processing of the results returned by the FHIR-server, for example to 33 | remove sensitive information. 34 | - A generic interface for implementing custom endpoints, e.g., a sync endpoint 35 | to return updates for all patients assigned to a health-worker. 36 | 37 | ## Common use cases 38 | 39 | The Info Gateway is designed to solve for a generic problem, that is, access 40 | control for **any client** and **any FHIR server**. 41 | 42 | Common access-check use-cases include: 43 | 44 | 1. For a mobile app used by community based front-line health workers possibly 45 | with offline support 46 | 2. Web based dashboard used by program admins 47 | 3. For a personal health record app used by patients or caregivers 48 | 4. To enable SMART-on-FHIR apps for patient or system level scopes 49 | 50 | FHIR Info Gateway is implemented as a "FHIR facade", i.e., it is a FHIR server 51 | itself which is implemented using the 52 | [HAPI FHIR Plain Server](https://hapifhir.io/hapi-fhir/docs/server_plain/introduction.html) 53 | library: 54 | 55 | ![FHIR Info Gateway](images/Info_Gateway_Use_Cases.png) 56 | -------------------------------------------------------------------------------- /doc/docs/release_process.md: -------------------------------------------------------------------------------- 1 | # Semantic versioning 2 | 3 | FHIR Info Gateway artifacts are released on 4 | [Maven](https://central.sonatype.com/namespace/com.google.fhir.gateway). A 5 | docker image is also published on 6 | [GCP Artifact Registry](https://console.cloud.google.com/artifacts/docker/fhir-proxy-build/us/stable/fhir-gateway?project=fhir-proxy-build). 7 | 8 | Versioning across all Open Health Stack components is based on the 9 | major.minor.patch scheme and respects Semantic Versioning. 10 | 11 | Respecting Semantic Versioning is important for multiple reasons: 12 | 13 | - It guarantees simple minor version upgrades, as long as you only use the 14 | public APIs. 15 | - A new major version is an opportunity to thoroughly document breaking changes. 16 | - A new major/minor version is an opportunity to communicate new features 17 | through a blog post. 18 | 19 | ## Major versions 20 | 21 | The major version number is incremented on every breaking change. 22 | 23 | Whenever a new major version is released, we publish: 24 | 25 | - a blog post with feature highlights, major bug fixes, breaking changes, and 26 | upgrade instructions. 27 | - an exhaustive changelog entry via the release notes 28 | 29 | ## Minor versions 30 | 31 | The minor version number is incremented on every significant retro-compatible 32 | change. 33 | 34 | Whenever a new minor version is released, we publish: 35 | 36 | - an exhaustive changelog entry via the release notes. 37 | 38 | ## Patch versions 39 | 40 | The patch version number is incremented on bugfixes releases. 41 | 42 | Whenever a new patch version is released, we publish: 43 | 44 | - an exhaustive changelog entry. 45 | -------------------------------------------------------------------------------- /doc/docs/support.md: -------------------------------------------------------------------------------- 1 | # Support 2 | 3 | On this page we've listed some ways you can get technical support along with 4 | Open Health Stack communities and forums that you can be a part of. 5 | 6 | Before participating please read our 7 | [code of conduct](https://opensource.google/conduct) that we expect all 8 | community members to adhere too. 9 | 10 | ## Developer calls 11 | 12 | There are weekly Open Health Stack developer calls that you are welcome to join. 13 | 14 | - Calls are on Thursdays and **alternate** between Android FHIR SDK and OHS 15 | server side components (FHIR Data Pipes and Info Gateway). 16 | - See the schedule below for more details. 17 | - To be added to the calls, please email: `hello-ohs[at]google.com`. 18 | 19 | **Developer call schedule** 20 | 21 | | OHS Developers Call | GMT | East Africa | India | 22 | | :------------------------- | :------: | :-----------: | :---------: | 23 | | Android FHIR SDK | 10:00 UK | 12:00 Nairobi | 14:30 Delhi | 24 | | Analytics and Info Gateway | 13:00 UK | 15:00 Nairobi | 17:30 Delhi | 25 | 26 | ## Discussion forums 27 | 28 | We are in the process of setting up a dedicated discussion forum for Open Health 29 | Stack. In the meantime, you can reach out to `hello-ohs[at]google.com`. 30 | 31 | ## Stack Overflow 32 | 33 | Stack Overflow is a popular forum to ask code-level questions or if you're stuck 34 | with a specific error. It would be nice to tag your question with 35 | `open-health-stack`! 36 | 37 | ## Bugs or Feature requests 38 | 39 | Before submitting a bug or filing a feature reqeust, please review the open 40 | issues on our 41 | [github repository](https://github.com/google/fhir-data-pipes/issues). 42 | 43 | If your issue is there, please add a comment. Otherwise, create a new issue to 44 | file a bug or submit a new feature request. 45 | 46 | Please review the [contributing section](contribute.md). 47 | -------------------------------------------------------------------------------- /doc/docs/tutorial_first_access_checker.md: -------------------------------------------------------------------------------- 1 | # Create an access checker plugin 2 | 3 | In this guide you will create your own access checker plugin. 4 | 5 | ## Implement the `AccessCheckerFactory` interface 6 | 7 | To create your own access checker plugin, create an implementation of the 8 | [`AccessCheckerFactory` interface](https://github.com/google/fhir-gateway/blob/main/server/src/main/java/com/google/fhir/gateway/interfaces/AccessCheckerFactory.java) 9 | annotated with a `@Named(value = "name")` annotation defining the name of the 10 | plugin. 11 | 12 | The most important parts are to implement a custom 13 | [`AccessChecker`](https://github.com/google/fhir-gateway/blob/main/server/src/main/java/com/google/fhir/gateway/interfaces/AccessChecker.java) 14 | to be returned by the factory and its `checkAccess` function which specifies if 15 | access is granted or not by returning an 16 | [`AccessDecision`](https://github.com/google/fhir-gateway/blob/main/server/src/main/java/com/google/fhir/gateway/interfaces/AccessDecision.java). 17 | 18 | ## Create a new class 19 | 20 | The simplest way to create your own access checker is to make a new class file 21 | in the `plugins/src/main/java/com/google/fhir/gateway/plugin` directory, next to 22 | the existing sample plugins. The following code can be used as a starting 23 | template for a minimal access checker: 24 | 25 | ```java 26 | package com.google.fhir.gateway.plugin; 27 | 28 | import ca.uhn.fhir.context.FhirContext; 29 | import com.auth0.jwt.interfaces.DecodedJWT; 30 | import com.google.fhir.gateway.FhirUtil; 31 | import com.google.fhir.gateway.HttpFhirClient; 32 | import com.google.fhir.gateway.JwtUtil; 33 | import com.google.fhir.gateway.interfaces.AccessChecker; 34 | import com.google.fhir.gateway.interfaces.AccessCheckerFactory; 35 | import com.google.fhir.gateway.interfaces.AccessDecision; 36 | import com.google.fhir.gateway.interfaces.NoOpAccessDecision; 37 | import com.google.fhir.gateway.interfaces.PatientFinder; 38 | import com.google.fhir.gateway.interfaces.RequestDetailsReader; 39 | import javax.inject.Named; 40 | 41 | public class MyAccessChecker implements AccessChecker { 42 | 43 | private final FhirContext fhirContext; 44 | private final HttpFhirClient httpFhirClient; 45 | private final String claim; 46 | private final PatientFinder patientFinder; 47 | 48 | // We're not using any of the parameters here, but real access checkers 49 | // would likely use some/all. 50 | private MyAccessChecker( 51 | HttpFhirClient httpFhirClient, 52 | String claim, 53 | FhirContext fhirContext, 54 | PatientFinder patientFinder) { 55 | this.fhirContext = fhirContext; 56 | this.claim = claim; 57 | this.httpFhirClient = httpFhirClient; 58 | this.patientFinder = patientFinder; 59 | } 60 | 61 | @Override 62 | public AccessDecision checkAccess(RequestDetailsReader requestDetails) { 63 | // Implement your access logic here. 64 | return NoOpAccessDecision.accessGranted(); 65 | } 66 | 67 | // The factory must be thread-safe but the AccessChecker instances it returns 68 | // do not need to be thread-safe. 69 | @Named(value = "sample") 70 | public static class Factory implements AccessCheckerFactory { 71 | 72 | static final String CLAIM = "sub"; 73 | 74 | private String getClaim(DecodedJWT jwt) { 75 | return FhirUtil.checkIdOrFail(JwtUtil.getClaimOrDie(jwt, CLAIM)); 76 | } 77 | 78 | @Override 79 | public AccessChecker create( 80 | DecodedJWT jwt, 81 | HttpFhirClient httpFhirClient, 82 | FhirContext fhirContext, 83 | PatientFinder patientFinder) { 84 | String claim = getClaim(jwt); 85 | return new MyAccessChecker(httpFhirClient, claim, fhirContext, patientFinder); 86 | } 87 | } 88 | } 89 | 90 | ``` 91 | 92 | ## Rebuild to include the plugin 93 | 94 | Once you're done implementing your access checker plugin, rebuild using 95 | `mvn package` from the root of the project to include the plugin, set the 96 | access-checker using e.g. `export ACCESS_CHECKER=sample` 97 | 98 | ## Run the gateway 99 | 100 | Run the gateway using e.g. 101 | `java -jar exec/target/exec-0.1.0.jar --server.port=8080`. 102 | -------------------------------------------------------------------------------- /doc/docs/tutorials.md: -------------------------------------------------------------------------------- 1 | # Tutorials 2 | 3 | Explore the developer resources to learn how to get started with the Info 4 | Gateway 5 | -------------------------------------------------------------------------------- /doc/mkdocs.yml: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2021-2025 Google LLC 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | site_name: FHIR Info Gateway Docs 18 | theme: 19 | name: material 20 | features: 21 | # - navigation.tabs 22 | - navigation.tabs.sticky 23 | - navigation.section 24 | - toc.follow 25 | # - toc.integrate 26 | - navigation.top 27 | - navigation.path 28 | - search.suggest 29 | - search.highlight 30 | - content.tabs.link 31 | - content.code.annotation 32 | - content.code.copy 33 | - navigation.footer 34 | language: en 35 | palette: 36 | - scheme: default 37 | toggle: 38 | icon: material/toggle-switch-off-outline 39 | name: Switch to dark mode 40 | primary: indigo 41 | accent: purple 42 | - scheme: slate 43 | toggle: 44 | icon: material/toggle-switch 45 | name: Switch to light mode 46 | primary: indigo 47 | accent: lime 48 | icon: 49 | repo: fontawesome/brands/github 50 | 51 | font: 52 | text: inter 53 | 54 | 55 | plugins: 56 | - search 57 | 58 | site_url: https://example.com/info-gateway/ 59 | 60 | repo_url: https://github.com/google/fhir-gateway 61 | 62 | repo_name: FHIR Info Gateway 63 | 64 | nav: 65 | - Home: 'index.md' 66 | - Concepts: 'concepts.md' 67 | - Getting Started: 'getting_started.md' 68 | - Tutorials: 69 | - 'Run the Info Gateway in Docker' : 'tutorial_docker.md' 70 | - 'Create an access checker' : 'tutorial_first_access_checker.md' 71 | - Design: 'design.md' 72 | - Community: 73 | - 'Support' : 'support.md' 74 | - 'Contributing': 'contribute.md' 75 | - 'Release process': 'release_process.md' 76 | 77 | markdown_extensions: 78 | - pymdownx.highlight: 79 | anchor_linenums: true 80 | - pymdownx.inlinehilite 81 | - pymdownx.snippets 82 | - admonition 83 | - pymdownx.arithmatex: 84 | generic: true 85 | - footnotes 86 | - pymdownx.details 87 | - pymdownx.superfences 88 | - pymdownx.tabbed: 89 | alternate_style: true 90 | - pymdownx.mark 91 | - attr_list 92 | - pymdownx.emoji: 93 | emoji_index: !!python/name:materialx.emoji.twemoji 94 | emoji_generator: !!python/name:materialx.emoji.to_svg 95 | 96 | copyright: | 97 | © 2024 Google Health Open Health Stack -------------------------------------------------------------------------------- /doc/requirements.txt: -------------------------------------------------------------------------------- 1 | mkdocs 2 | mkdocs-material 3 | -------------------------------------------------------------------------------- /docker/.env: -------------------------------------------------------------------------------- 1 | TOKEN_ISSUER="http://localhost:9080/auth/realms/test" 2 | PROXY_TO="http://localhost:8099/fhir" 3 | BACKEND_TYPE="HAPI" 4 | ACCESS_CHECKER="list" 5 | RUN_MODE="PROD" 6 | ALLOWED_QUERIES_FILE="" -------------------------------------------------------------------------------- /docker/README.md: -------------------------------------------------------------------------------- 1 | # Docker Compose YAMLs 2 | 3 | This directory contains two Docker Compose YAML files. 4 | [hapi-proxy-compose.yaml](./hapi-proxy-compose.yaml) sets up the FHIR Proxy and 5 | a HAPI FHIR Server with synthetic data pre-loaded (more details below). 6 | [keycloak/config-compose.yaml](./keycloak/config-compose.yaml) sets up a test 7 | Keycloak instance that can support both a list based access control and a 8 | single-patient based SMART-on-FHIR app (in two separate realms). 9 | 10 | ## Pre-loaded HAPI Server 11 | 12 | The 13 | [us-docker.pkg.dev/fhir-proxy-build/stable/hapi-synthea:latest](https://console.cloud.google.com/gcr/images/fhir-sdk/global/synthetic-data) 14 | image is based on the HAPI FHIR 15 | [image](https://hub.docker.com/r/hapiproject/hapi) with the 16 | `1K Sample Synthetic Patient Records, FHIR R4` dataset from 17 | [Synthea](https://synthea.mitre.org/downloads) stored in the container itself. 18 | To load this dataset into the HAPI FHIR image, do the following: 19 | 20 | 1. Run a local version of the HAPI FHIR server: 21 | 22 | ``` 23 | docker run --rm -d -p 8080:8080 --name hapi_fhir hapiproject/hapi:latest 24 | ``` 25 | 26 | 2. Download the `1K Sample Synthetic Patient Records, FHIR R4` dataset: 27 | 28 | ``` 29 | wget https://synthetichealth.github.io/synthea-sample-data/downloads/synthea_sample_data_fhir_r4_sep2019.zip \ 30 | -O fhir.zip 31 | ``` 32 | 33 | 3. Unzip the file, a directory named `fhir` should be created containig JSON 34 | files: 35 | 36 | ``` 37 | unzip fhir.zip 38 | ``` 39 | 40 | 4. Use the Synthetic Data Uploader from the 41 | [FHIR Analytics](https://github.com/GoogleCloudPlatform/openmrs-fhir-analytics/tree/master/synthea-hiv) 42 | repo to upload the files into the HAPI FHIR container 43 | `docker run -it --network=host \ -e SINK_TYPE="HAPI" \ -e FHIR_ENDPOINT=http://localhost:8080/fhir \ -e INPUT_DIR="/workspace/output/fhir" \ -e CORES="--cores 1" \ -v $(pwd)/fhir:/workspace/output/fhir \ us-docker.pkg.dev/cloud-build-fhir/fhir-analytics/synthea-uploader:latest` 44 | 45 | 5. As the uploader uses `POST` to upload the JSON files, the server will create 46 | the ID used to refer to resources. We would like to upload a patient list 47 | example, but to do so, we need to fetch the IDs from the server. To do so, 48 | run: 49 | 50 | ``` 51 | curl http://localhost:8080/fhir/Patient?_elements=fullUrl 52 | ``` 53 | 54 | 6. Choose two Patient IDs (the two picked here are 2522 and 2707), then run the 55 | following to upload the list into the server 56 | 57 | ``` 58 | PATIENT_ID1=2522 59 | PATIENT_ID2=2707 60 | curl -X PUT -H "Content-Type: application/json" \ 61 | "http://localhost:8080/fhir/List/patient-list-example" \ 62 | -d '{ 63 | "resourceType": "List", 64 | "id": "patient-list-example", 65 | "status": "current", 66 | "mode": "working", 67 | "entry": [ 68 | { 69 | "item": { 70 | "reference": "Patient/'"${PATIENT_ID1}"'" 71 | } 72 | }, 73 | { 74 | "item": { 75 | "reference": "Patient/'"${PATIENT_ID2}"'" 76 | } 77 | } 78 | ] 79 | }' 80 | ``` 81 | 82 | 7. Commit the Docker container. This saves its state into a new image 83 | 84 | ``` 85 | docker commit hapi_fhir us-docker.pkg.dev/fhir-proxy-build/stable/hapi-synthea:latest 86 | ``` 87 | 88 | 8. Push the image 89 | ``` 90 | docker push us-docker.pkg.dev/fhir-proxy-build/stable/hapi-synthea:latest 91 | ``` 92 | -------------------------------------------------------------------------------- /docker/hapi-proxy-compose.yaml: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2021-2023 Google LLC 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | # This is for easy creation and setup of a FHIR Proxy and HAPI FHIR Server 18 | # 19 | # The relevant environment variables are (see `.env` file for default values): 20 | # 21 | # TOKEN_ISSUER: URL of the Identity Provider server; 22 | # default: "http://localhost:9080/auth/realms/test" 23 | # 24 | # PROXY_TO: URL of the FHIR backend the proxy connects to; 25 | # default: "http://localhost:8099/fhir" 26 | # 27 | # BACKEND_TYPE: the FHIR backend type. Either GCP or HAPI; default: "HAPI" 28 | # 29 | # ACCESS_CHECKER: access checker to use when running the proxy; default: "list" 30 | # 31 | # RUN_MODE: Enforces proxy's token issuer check when set to "PROD". To bypass 32 | # the check, set to "DEV"; default: "PROD" 33 | 34 | version: "3.0" 35 | 36 | services: 37 | fhir-proxy: 38 | image: us-docker.pkg.dev/fhir-proxy-build/stable/fhir-gateway:${BUILD_ID:-latest} 39 | environment: 40 | - TOKEN_ISSUER 41 | - PROXY_TO 42 | - BACKEND_TYPE 43 | - ACCESS_CHECKER 44 | - RUN_MODE 45 | - ALLOWED_QUERIES_FILE 46 | network_mode: "host" 47 | healthcheck: 48 | # As hapi-server does not support curl, we check here that the 49 | # hapi-server is ready to accept requests 50 | test: curl --fail http://localhost:8099/fhir/metadata > /dev/null 51 | start_period: 35s 52 | interval: 10s 53 | retries: 5 54 | timeout: 10s 55 | 56 | hapi-server: 57 | image: us-docker.pkg.dev/fhir-proxy-build/stable/hapi-synthea:latest 58 | ports: 59 | - "8099:8080" 60 | -------------------------------------------------------------------------------- /docker/keycloak/.env: -------------------------------------------------------------------------------- 1 | HTTP_PORT="9080" 2 | HTTPS_PORT="9443" 3 | KEYCLOAK_USER="admin" 4 | KEYCLOAK_PASSWORD="adminpass" 5 | TEST_REALM="test" 6 | TEST_USER="testuser" 7 | TEST_PASS="testpass" 8 | SMART_REALM="test-smart" 9 | SMART_PATIENT_ID="8eb95e44-627f-4899-9ea3-097d4f7be57b" 10 | -------------------------------------------------------------------------------- /docker/keycloak/Dockerfile: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2021-2022 Google LLC 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | 18 | 19 | # This is to craete a docker wrapper around an example configuration script. 20 | # The configuration set the requirements needed for a patient list based access. 21 | 22 | # Note the image is just for the configurator script which relies on `kcadm.sh` 23 | # tool of Keycloak. It is not tied to `smart-keycloak` and should work for 24 | # other Keycloak containers too. We use smart-keycloak as the base package 25 | # for convenience. 26 | # FROM quay.io/keycloak/keycloak 27 | FROM quay.io/alvearie/smart-keycloak 28 | 29 | ENV KEYCLOAK_USER="admin" 30 | ENV KEYCLOAK_PASSWORD="adminpass" 31 | 32 | # By default, a test realm is created for list-based access controls. 33 | ENV TEST_REALM="test" 34 | ENV TEST_USER="testuser" 35 | ENV TEST_PASS="testpass" 36 | 37 | # Setting SMART_REALM makes config changes in that realm for the sample 38 | # Growth Chart app; this includes adding a user, client, etc. For simplicity, 39 | # it uses the same username/password as the above realm. It is assumed that 40 | # this realm is already created by the alvearie/keycloak-config image. 41 | ENV SMART_REALM="" 42 | # This should be the logical id of a Patient resource in the target FHIR store. 43 | # The above test Keycloak user is mapped to this FHIR Patient. 44 | ENV SMART_PATIENT_ID="8eb95e44-627f-4899-9ea3-097d4f7be57b" 45 | 46 | COPY keycloak_setup.sh /opt/jboss/keycloak/bin/ 47 | COPY keycloak_events_list.txt /opt/jboss/keycloak/bin/ 48 | 49 | # Another option is to bring up the base Keycloak image separately then run 50 | # the setup script in a separate `docker run`, e.g., using docker-compose. 51 | ENTRYPOINT [ "/opt/jboss/keycloak/bin/keycloak_setup.sh" ] 52 | -------------------------------------------------------------------------------- /docker/keycloak/README.md: -------------------------------------------------------------------------------- 1 | # Sample Identity Provider and Authorization Server 2 | 3 | This directory contains the docker configuration for a sample 4 | [Keycloak](https://www.keycloak.org/) server that can be used as the Identity 5 | Provider (IDP) and Authorization server (AuthZ) companions of the FHIR proxy. 6 | 7 | **NOTE:** This is just for test purposes; never use this configuration in 8 | production without addressing security issues, in particular SSL access. 9 | 10 | There are three components involved here which are all combined in 11 | [config-compose.yaml](config-compose.yaml): 12 | 13 | - The 14 | [Alvearie SMART Keycloak](https://github.com/Alvearie/keycloak-extensions-for-fhir). 15 | We could also use the base Keycloak image if the access-checker does not care 16 | about [SMART on FHIR](http://www.hl7.org/fhir/smart-app-launch/) spec (for 17 | example the 18 | [`list` access-checker](../../plugins/src/main/java/com/google/fhir/gateway/plugin/ListAccessChecker.java)). 19 | The 20 | [`patient` access-checker](../../plugins/src/main/java/com/google/fhir/gateway/plugin/PatientAccessChecker.java) 21 | is intended for a SMART on FHIR app with patient scopes. 22 | 23 | - The `alvearie/keycloak-config:latest` docker image to configure a SMART 24 | enabled realm. This is useful for the `patient` access-checker. 25 | 26 | - The `us-docker.pkg.dev/fhir-proxy-build/stable/keycloak-config:latest` docker 27 | image to configure a realm for the `list` access-checker. 28 | 29 | You can change the configuration parameters by changing environment variables 30 | passed to the docker images. By default, the values in [`.env`](.env) is used. 31 | To run all above components: 32 | 33 | ```shell 34 | docker-compose -f config-compose.yaml up 35 | ``` 36 | -------------------------------------------------------------------------------- /docker/keycloak/config-compose.yaml: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2021-2022 Google LLC 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | # This is for easy creation and setup of a test Keycloak instance that can 18 | # support both a list based access control and a single-patient based 19 | # SMART-on-FHIR app (in two separate realms). 20 | # 21 | # The Keycloak instance uses port 9080 for http and 9443 for https traffic by 22 | # default (configurable through environment variables, see below). Note 23 | # there is no valid certificate set for this instance and hence it should never 24 | # be used in production. 25 | # 26 | # The relevant environment variables are (see `keycloak_setup.sh` for details 27 | # and `.env` file for default values): 28 | # 29 | # HTTP_PORT: the port on the host machine mapped to Keycloak's http port; 30 | # default: 9080 31 | # 32 | # HTTPS_PORT: same as above for https; default: 9443 33 | # 34 | # KEYCLOAK_USER: the admin user for the Keycloak instance; default: "admin" 35 | # 36 | # KEYCLOAK_PASSWORD: the password for the admin user; default: "adminpass" 37 | # 38 | # TEST_REALM: the name of the realm for list based access; default: "test" 39 | # 40 | # TEST_USER: the username used for both realms; default: "testuser" 41 | # 42 | # TEST_PASS: the password for the test user; default: "testpass" 43 | # 44 | # SMART_REALM: if non-empty, it is the name of the SoF realm; leave this unset 45 | # if SoF support is needed; note there is still a default SoF realm configured 46 | # in this case which can be ignored; default: "test-smart" 47 | # 48 | # SMART_PATIENT_ID: the id of the Patient resource for the created user; 49 | # default: "8eb95e44-627f-4899-9ea3-097d4f7be57b" 50 | 51 | version: "3.0" 52 | 53 | services: 54 | keycloak: 55 | image: quay.io/alvearie/smart-keycloak:latest 56 | ports: 57 | - "${HTTP_PORT}:8080" 58 | - "${HTTPS_PORT}:8443" 59 | environment: 60 | - KEYCLOAK_USER 61 | - KEYCLOAK_PASSWORD 62 | healthcheck: 63 | test: curl --fail http://localhost:8080/auth/realms/master 64 | start_period: 35s 65 | interval: 10s 66 | retries: 5 67 | timeout: 10s 68 | 69 | smart-config: 70 | image: alvearie/keycloak-config 71 | depends_on: 72 | keycloak: 73 | condition: service_healthy 74 | environment: 75 | - KEYCLOAK_BASE_URL=http://keycloak:8080/auth 76 | - KEYCLOAK_USER 77 | - KEYCLOAK_PASSWORD 78 | - KEYCLOAK_REALM=${SMART_REALM:-dummy-smart} 79 | 80 | proxy-config: 81 | image: us-docker.pkg.dev/fhir-proxy-build/stable/keycloak-config:latest 82 | depends_on: 83 | smart-config: 84 | condition: service_completed_successfully 85 | environment: 86 | - KEYCLOAK_BASE_URL=http://keycloak:8080/auth 87 | - KEYCLOAK_USER 88 | - KEYCLOAK_PASSWORD 89 | - TEST_REALM 90 | - TEST_USER 91 | - TEST_PASS 92 | - SMART_REALM 93 | -------------------------------------------------------------------------------- /docker/keycloak/keycloak_events.txt: -------------------------------------------------------------------------------- 1 | # This list is from Keycloak documentation here with deprecated ones removed: 2 | # https://www.keycloak.org/docs-api/15.0/javadocs/org/keycloak/events/EventType.html 3 | # To generate the keycloak_events_list.txt from this, use: 4 | # $ cat keycloak_events.txt | awk '!/^#/{ if (NF == 1) printf("\"%s\"\n", $1) }' | paste -sd "," > keycloak_events_list.txt 5 | AUTHREQID_TO_TOKEN 6 | AUTHREQID_TO_TOKEN_ERROR 7 | CLIENT_DELETE 8 | CLIENT_DELETE_ERROR 9 | CLIENT_INFO 10 | CLIENT_INFO_ERROR 11 | CLIENT_INITIATED_ACCOUNT_LINKING 12 | CLIENT_INITIATED_ACCOUNT_LINKING_ERROR 13 | CLIENT_LOGIN 14 | CLIENT_LOGIN_ERROR 15 | CLIENT_REGISTER 16 | CLIENT_REGISTER_ERROR 17 | CLIENT_UPDATE 18 | CLIENT_UPDATE_ERROR 19 | CODE_TO_TOKEN 20 | CODE_TO_TOKEN_ERROR 21 | CUSTOM_REQUIRED_ACTION 22 | CUSTOM_REQUIRED_ACTION_ERROR 23 | DELETE_ACCOUNT 24 | DELETE_ACCOUNT_ERROR 25 | EXECUTE_ACTION_TOKEN 26 | EXECUTE_ACTION_TOKEN_ERROR 27 | EXECUTE_ACTIONS 28 | EXECUTE_ACTIONS_ERROR 29 | FEDERATED_IDENTITY_LINK 30 | FEDERATED_IDENTITY_LINK_ERROR 31 | GRANT_CONSENT 32 | GRANT_CONSENT_ERROR 33 | IDENTITY_PROVIDER_FIRST_LOGIN 34 | IDENTITY_PROVIDER_FIRST_LOGIN_ERROR 35 | IDENTITY_PROVIDER_LINK_ACCOUNT 36 | IDENTITY_PROVIDER_LINK_ACCOUNT_ERROR 37 | IDENTITY_PROVIDER_LOGIN 38 | IDENTITY_PROVIDER_LOGIN_ERROR 39 | IDENTITY_PROVIDER_POST_LOGIN 40 | IDENTITY_PROVIDER_POST_LOGIN_ERROR 41 | IDENTITY_PROVIDER_RESPONSE 42 | IDENTITY_PROVIDER_RESPONSE_ERROR 43 | IDENTITY_PROVIDER_RETRIEVE_TOKEN 44 | IDENTITY_PROVIDER_RETRIEVE_TOKEN_ERROR 45 | IMPERSONATE 46 | IMPERSONATE_ERROR 47 | INTROSPECT_TOKEN 48 | INTROSPECT_TOKEN_ERROR 49 | INVALID_SIGNATURE 50 | INVALID_SIGNATURE_ERROR 51 | LOGIN 52 | LOGIN_ERROR 53 | LOGOUT 54 | LOGOUT_ERROR 55 | OAUTH2_DEVICE_AUTH 56 | OAUTH2_DEVICE_AUTH_ERROR 57 | OAUTH2_DEVICE_CODE_TO_TOKEN 58 | OAUTH2_DEVICE_CODE_TO_TOKEN_ERROR 59 | OAUTH2_DEVICE_VERIFY_USER_CODE 60 | OAUTH2_DEVICE_VERIFY_USER_CODE_ERROR 61 | PERMISSION_TOKEN 62 | PERMISSION_TOKEN_ERROR 63 | PUSHED_AUTHORIZATION_REQUEST 64 | PUSHED_AUTHORIZATION_REQUEST_ERROR 65 | REFRESH_TOKEN 66 | REFRESH_TOKEN_ERROR 67 | REGISTER 68 | REGISTER_ERROR 69 | REGISTER_NODE 70 | REGISTER_NODE_ERROR 71 | REMOVE_FEDERATED_IDENTITY 72 | REMOVE_FEDERATED_IDENTITY_ERROR 73 | REMOVE_TOTP 74 | REMOVE_TOTP_ERROR 75 | RESET_PASSWORD 76 | RESET_PASSWORD_ERROR 77 | RESTART_AUTHENTICATION 78 | RESTART_AUTHENTICATION_ERROR 79 | REVOKE_GRANT 80 | REVOKE_GRANT_ERROR 81 | SEND_IDENTITY_PROVIDER_LINK 82 | SEND_IDENTITY_PROVIDER_LINK_ERROR 83 | SEND_RESET_PASSWORD 84 | SEND_RESET_PASSWORD_ERROR 85 | SEND_VERIFY_EMAIL 86 | SEND_VERIFY_EMAIL_ERROR 87 | TOKEN_EXCHANGE 88 | TOKEN_EXCHANGE_ERROR 89 | UNREGISTER_NODE 90 | UNREGISTER_NODE_ERROR 91 | UPDATE_CONSENT 92 | UPDATE_CONSENT_ERROR 93 | UPDATE_EMAIL 94 | UPDATE_EMAIL_ERROR 95 | UPDATE_PASSWORD 96 | UPDATE_PASSWORD_ERROR 97 | UPDATE_PROFILE 98 | UPDATE_PROFILE_ERROR 99 | UPDATE_TOTP 100 | UPDATE_TOTP_ERROR 101 | USER_INFO_REQUEST 102 | USER_INFO_REQUEST_ERROR 103 | VERIFY_EMAIL 104 | VERIFY_EMAIL_ERROR 105 | VERIFY_PROFILE 106 | VERIFY_PROFILE_ERROR 107 | -------------------------------------------------------------------------------- /docker/keycloak/keycloak_events_list.txt: -------------------------------------------------------------------------------- 1 | "AUTHREQID_TO_TOKEN","AUTHREQID_TO_TOKEN_ERROR","CLIENT_DELETE","CLIENT_DELETE_ERROR","CLIENT_INFO","CLIENT_INFO_ERROR","CLIENT_INITIATED_ACCOUNT_LINKING","CLIENT_INITIATED_ACCOUNT_LINKING_ERROR","CLIENT_LOGIN","CLIENT_LOGIN_ERROR","CLIENT_REGISTER","CLIENT_REGISTER_ERROR","CLIENT_UPDATE","CLIENT_UPDATE_ERROR","CODE_TO_TOKEN","CODE_TO_TOKEN_ERROR","CUSTOM_REQUIRED_ACTION","CUSTOM_REQUIRED_ACTION_ERROR","DELETE_ACCOUNT","DELETE_ACCOUNT_ERROR","EXECUTE_ACTION_TOKEN","EXECUTE_ACTION_TOKEN_ERROR","EXECUTE_ACTIONS","EXECUTE_ACTIONS_ERROR","FEDERATED_IDENTITY_LINK","FEDERATED_IDENTITY_LINK_ERROR","GRANT_CONSENT","GRANT_CONSENT_ERROR","IDENTITY_PROVIDER_FIRST_LOGIN","IDENTITY_PROVIDER_FIRST_LOGIN_ERROR","IDENTITY_PROVIDER_LINK_ACCOUNT","IDENTITY_PROVIDER_LINK_ACCOUNT_ERROR","IDENTITY_PROVIDER_LOGIN","IDENTITY_PROVIDER_LOGIN_ERROR","IDENTITY_PROVIDER_POST_LOGIN","IDENTITY_PROVIDER_POST_LOGIN_ERROR","IDENTITY_PROVIDER_RESPONSE","IDENTITY_PROVIDER_RESPONSE_ERROR","IDENTITY_PROVIDER_RETRIEVE_TOKEN","IDENTITY_PROVIDER_RETRIEVE_TOKEN_ERROR","IMPERSONATE","IMPERSONATE_ERROR","INTROSPECT_TOKEN","INTROSPECT_TOKEN_ERROR","INVALID_SIGNATURE","INVALID_SIGNATURE_ERROR","LOGIN","LOGIN_ERROR","LOGOUT","LOGOUT_ERROR","OAUTH2_DEVICE_AUTH","OAUTH2_DEVICE_AUTH_ERROR","OAUTH2_DEVICE_CODE_TO_TOKEN","OAUTH2_DEVICE_CODE_TO_TOKEN_ERROR","OAUTH2_DEVICE_VERIFY_USER_CODE","OAUTH2_DEVICE_VERIFY_USER_CODE_ERROR","PERMISSION_TOKEN","PERMISSION_TOKEN_ERROR","PUSHED_AUTHORIZATION_REQUEST","PUSHED_AUTHORIZATION_REQUEST_ERROR","REFRESH_TOKEN","REFRESH_TOKEN_ERROR","REGISTER","REGISTER_ERROR","REGISTER_NODE","REGISTER_NODE_ERROR","REMOVE_FEDERATED_IDENTITY","REMOVE_FEDERATED_IDENTITY_ERROR","REMOVE_TOTP","REMOVE_TOTP_ERROR","RESET_PASSWORD","RESET_PASSWORD_ERROR","RESTART_AUTHENTICATION","RESTART_AUTHENTICATION_ERROR","REVOKE_GRANT","REVOKE_GRANT_ERROR","SEND_IDENTITY_PROVIDER_LINK","SEND_IDENTITY_PROVIDER_LINK_ERROR","SEND_RESET_PASSWORD","SEND_RESET_PASSWORD_ERROR","SEND_VERIFY_EMAIL","SEND_VERIFY_EMAIL_ERROR","TOKEN_EXCHANGE","TOKEN_EXCHANGE_ERROR","UNREGISTER_NODE","UNREGISTER_NODE_ERROR","UPDATE_CONSENT","UPDATE_CONSENT_ERROR","UPDATE_EMAIL","UPDATE_EMAIL_ERROR","UPDATE_PASSWORD","UPDATE_PASSWORD_ERROR","UPDATE_PROFILE","UPDATE_PROFILE_ERROR","UPDATE_TOTP","UPDATE_TOTP_ERROR","USER_INFO_REQUEST","USER_INFO_REQUEST_ERROR","VERIFY_EMAIL","VERIFY_EMAIL_ERROR","VERIFY_PROFILE","VERIFY_PROFILE_ERROR" 2 | -------------------------------------------------------------------------------- /e2e-test/e2e.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2021-2022 Google LLC 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | """End-to-end tests using the FHIR Proxy, HAPI Server, and AuthZ Server.""" 18 | 19 | import logging 20 | import time 21 | from typing import List, Tuple 22 | 23 | import clients 24 | 25 | 26 | def test_proxy_and_server_equal_count( 27 | patient_list: List[str], 28 | resource_search_pairs: List[Tuple[str, str]], 29 | hapi: clients.HapiClient, 30 | fhir_proxy: clients.FhirProxyClient, 31 | auth: clients.AuthClient, 32 | ) -> None: 33 | """Checks number of resources are the same via the Proxy or HAPI.""" 34 | token = auth.get_auth_token() 35 | for patient in patient_list: 36 | for resource_search_pair in resource_search_pairs: 37 | value_from_server = hapi.get_resource_count( 38 | resource_search_pair[0], patient 39 | ) 40 | value_from_proxy = fhir_proxy.get_resource_count( 41 | token, resource_search_pair, patient 42 | ) 43 | logging.info( 44 | "%s resources returned for %s from: \n\tServer: %s, Proxy: %s", 45 | resource_search_pair[0], 46 | patient, 47 | value_from_server, 48 | value_from_proxy, 49 | ) 50 | 51 | if value_from_server != value_from_proxy: 52 | error_msg = "Number of resources do not match\n\tServer: {}, Proxy: {}".format( 53 | value_from_server, value_from_proxy 54 | ) 55 | raise ValueError(error_msg) 56 | 57 | 58 | def test_post_resource_increase_count( 59 | resource_search_pair: Tuple[str, str], 60 | file_name: str, 61 | patient_id: str, 62 | hapi: clients.HapiClient, 63 | fhir_proxy: clients.FhirProxyClient, 64 | auth: clients.AuthClient, 65 | ) -> None: 66 | """Test to add a resource to the backend via the Proxy.""" 67 | token = auth_client.get_auth_token() 68 | value_from_server = hapi.get_resource_count(resource_search_pair[0], patient_id) 69 | value_from_proxy = fhir_proxy.get_resource_count( 70 | token, resource_search_pair, patient_id 71 | ) 72 | 73 | if value_from_server != value_from_proxy: 74 | error_msg = "Number of resources do not match\n\tServer: {}, Proxy: {}".format( 75 | value_from_server, value_from_proxy 76 | ) 77 | raise ValueError(error_msg) 78 | 79 | current_value = value_from_proxy 80 | logging.info( 81 | "%s %ss returned for %s", current_value, resource_search_pair[0], patient_id, 82 | ) 83 | 84 | logging.info("Adding one %s for %s", resource_search_pair[0], patient_id) 85 | fhir_proxy.post_resource(resource_search_pair[0], file_name, token) 86 | 87 | while value_from_proxy != (current_value + 1): 88 | token = auth.get_auth_token() 89 | value_from_proxy = fhir_proxy.get_resource_count( 90 | token, resource_search_pair, patient_id 91 | ) 92 | time.sleep(10) 93 | 94 | logging.info("Added one %s for %s", resource_search_pair[0], patient_id) 95 | logging.info( 96 | "%s %ss returned for %s", value_from_proxy, resource_search_pair[0], patient_id, 97 | ) 98 | 99 | 100 | if __name__ == "__main__": 101 | logging.basicConfig(level=logging.INFO) 102 | 103 | patients = ["Patient/75270", "Patient/3810"] 104 | resources = [("Encounter", "patient"), ("Observation", "subject")] 105 | auth_client = clients.AuthClient() 106 | fhir_proxy_client = clients.FhirProxyClient() 107 | hapi_client = clients.HapiClient() 108 | 109 | logging.info("Testing proxy and server resource counts ...") 110 | test_proxy_and_server_equal_count( 111 | patients, resources, hapi_client, fhir_proxy_client, auth_client 112 | ) 113 | logging.info("Testing post resource ...") 114 | test_post_resource_increase_count( 115 | ("Observation", "subject"), 116 | "e2e-test/obs.json", 117 | "Patient/75270", 118 | hapi_client, 119 | fhir_proxy_client, 120 | auth_client, 121 | ) 122 | -------------------------------------------------------------------------------- /e2e-test/e2e.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Copyright 2021-2023 Google LLC 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | # Fail on any error. 19 | set -e 20 | 21 | export BUILD_ID=${KOKORO_BUILD_ID:-local} 22 | 23 | function setup() { 24 | docker build -t us-docker.pkg.dev/fhir-proxy-build/stable/fhir-gateway:${BUILD_ID} . 25 | docker-compose -f docker/keycloak/config-compose.yaml \ 26 | up --force-recreate --remove-orphans -d --quiet-pull 27 | # TODO find a way to expose docker container logs in the output; currently 28 | # with -d we don't get any logs and it makes debugging failures difficult. 29 | # Note `--wait` is an option in the new version of `docker compose` to make 30 | # sure that all services are up in a healthy state (old version fails). 31 | docker-compose -f docker/hapi-proxy-compose.yaml \ 32 | up --force-recreate --remove-orphans -d --quiet-pull --wait 33 | } 34 | 35 | setup 36 | python3 e2e-test/e2e.py 37 | -------------------------------------------------------------------------------- /e2e-test/obs.json: -------------------------------------------------------------------------------- 1 | { 2 | "resourceType": "Observation", 3 | "id": "12345", 4 | "meta": { 5 | "versionId": "1", 6 | "lastUpdated": "2022-04-05T17:01:56.616+00:00", 7 | "source": "#BSiazk9B4TZgS3cm" 8 | }, 9 | "status": "final", 10 | "category": [ { 11 | "coding": [ { 12 | "system": "http://terminology.hl7.org/CodeSystem/observation-category", 13 | "code": "vital-signs", 14 | "display": "vital-signs" 15 | } ] 16 | } ], 17 | "code": { 18 | "coding": [ { 19 | "system": "http://loinc.org", 20 | "code": "29463-7", 21 | "display": "Body Weight" 22 | } ], 23 | "text": "Body Weight" 24 | }, 25 | "subject": { 26 | "reference": "Patient/75270" 27 | }, 28 | "encounter": { 29 | "reference": "Encounter/76366" 30 | }, 31 | "effectiveDateTime": "2015-07-05T10:11:41-04:00", 32 | "issued": "2015-07-05T10:11:41.981-04:00", 33 | "valueQuantity": { 34 | "value": 99.13907455532309, 35 | "unit": "kg", 36 | "system": "http://unitsofmeasure.org", 37 | "code": "kg" 38 | } 39 | } -------------------------------------------------------------------------------- /exec/README.md: -------------------------------------------------------------------------------- 1 | # Sample application 2 | 3 | This module is to show simple examples of how to use the FHIR Gateway. The 4 | minimal application is 5 | [MainApp](src/main/java/com/google/fhir/gateway/MainApp.java). With this single 6 | class, you can create an executable app with the Gateway [server](../server) and 7 | all of the `AccessChecker` [plugins](../plugins), namely 8 | [ListAccessChecker](../plugins/src/main/java/com/google/fhir/gateway/plugin/ListAccessChecker.java) 9 | and 10 | [PatientAccessChecker](../plugins/src/main/java/com/google/fhir/gateway/plugin/PatientAccessChecker.java). 11 | 12 | Two other classes are provided to show how to implement custom endpoints. 13 | -------------------------------------------------------------------------------- /exec/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 19 | 20 | 4.0.0 21 | 22 | com.google.fhir.gateway 23 | fhir-gateway 24 | 0.4.1-SNAPSHOT 25 | 26 | 27 | exec 28 | jar 29 | 30 | 31 | ${project.parent.basedir} 32 | 3.4.5 33 | 34 | true 35 | 36 | 37 | 38 | 39 | 43 | 44 | org.springframework.boot 45 | spring-boot-dependencies 46 | ${spring-boot.version} 47 | pom 48 | import 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | ${project.groupId} 57 | server 58 | ${project.version} 59 | 60 | 61 | 62 | ${project.groupId} 63 | plugins 64 | ${project.version} 65 | 66 | 67 | 68 | 69 | org.springframework.boot 70 | spring-boot-starter-web 71 | 72 | 73 | 74 | org.springframework.boot 75 | spring-boot-starter-test 76 | test 77 | 78 | 79 | 80 | 81 | 82 | 83 | ${project.parent.artifactId}-${project.artifactId} 84 | 85 | 86 | 87 | 88 | org.springframework.boot 89 | spring-boot-maven-plugin 90 | ${spring-boot.version} 91 | 92 | 93 | repackage 94 | 95 | repackage 96 | 97 | 98 | com.google.fhir.gateway.MainApp 99 | 107 | ZIP 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | -------------------------------------------------------------------------------- /exec/src/main/java/com/google/fhir/gateway/CustomGenericEndpointExample.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021-2024 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.google.fhir.gateway; 17 | 18 | import jakarta.servlet.annotation.WebServlet; 19 | import jakarta.servlet.http.HttpServlet; 20 | import jakarta.servlet.http.HttpServletRequest; 21 | import jakarta.servlet.http.HttpServletResponse; 22 | import java.io.IOException; 23 | import org.apache.http.HttpStatus; 24 | 25 | /** 26 | * This is an example servlet that can be used for any custom endpoint. It does not make any 27 | * assumptions about authorization headers or accessing a FHIR server. 28 | */ 29 | @WebServlet("/custom/*") 30 | public class CustomGenericEndpointExample extends HttpServlet { 31 | 32 | @Override 33 | protected void doGet(HttpServletRequest request, HttpServletResponse resp) throws IOException { 34 | String uri = request.getRequestURI(); 35 | // For a real production case, `uri` needs to be escaped. 36 | resp.getOutputStream().print("Successful request to the custom endpoint " + uri); 37 | resp.setStatus(HttpStatus.SC_OK); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /exec/src/main/java/com/google/fhir/gateway/MainApp.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021-2023 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.google.fhir.gateway; 17 | 18 | import org.springframework.boot.SpringApplication; 19 | import org.springframework.boot.autoconfigure.SpringBootApplication; 20 | import org.springframework.boot.web.servlet.ServletComponentScan; 21 | 22 | /** 23 | * This class shows the minimum that is required to create a FHIR Gateway with all AccessChecker 24 | * plugins defined in "com.google.fhir.gateway.plugin". 25 | */ 26 | @SpringBootApplication(scanBasePackages = {"com.google.fhir.gateway.plugin"}) 27 | @ServletComponentScan(basePackages = "com.google.fhir.gateway") 28 | public class MainApp { 29 | 30 | public static void main(String[] args) { 31 | SpringApplication.run(MainApp.class, args); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /exec/src/test/java/com/google/fhir/gateway/MainAppTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021-2024 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.google.fhir.gateway; 17 | 18 | // TODO change this test to fail if the expected plugins cannot be found. 19 | 20 | // TODO uncomment this test possibly with adding the option of passing 21 | // TOKEN_ISSUER name through system properties (in addition to env vars). 22 | // Currently in our e2e tests, we verify that the sample app can start with 23 | // proper TOKEN_ISSUER env var. The behaviour of this test has changed in 24 | // recent versions of Spring and that's why it is commented out temporarily. 25 | // 26 | // @RunWith(SpringRunner.class) 27 | // @SpringBootTest 28 | // public class MainAppTest { 29 | // 30 | // 31 | // @Test 32 | // public void contextLoads() { 33 | // } 34 | // } 35 | -------------------------------------------------------------------------------- /kokoro/README.md: -------------------------------------------------------------------------------- 1 | ## Kokoro Infrastructure 2 | 3 | The files in this directory serve as plumbing for running tests under Kokoro, 4 | our internal CI. If there are any changes required to these config files, please 5 | file an issue. 6 | -------------------------------------------------------------------------------- /kokoro/gcp_ubuntu/continuous.cfg: -------------------------------------------------------------------------------- 1 | # -*- protobuffer -*- 2 | # proto-file: google3/devtools/kokoro/config/proto/build.proto 3 | # proto-message: BuildConfig 4 | 5 | # Location of the bash script. Should have value /. 6 | # github_scm.name is specified in the job configuration. 7 | build_file: "fhir-gateway/kokoro/gcp_ubuntu/kokoro_build.sh" 8 | -------------------------------------------------------------------------------- /kokoro/gcp_ubuntu/kokoro_build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Copyright 2021-2023 Google LLC 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | # Fail on any error. 19 | set -e 20 | # Display commands being run. 21 | # WARNING: please only enable 'set -x' if necessary for debugging, and be very 22 | # careful if you handle credentials (e.g. from Keystore) with 'set -x': 23 | # statements like "export VAR=$(cat /tmp/keystore/credentials)" will result in 24 | # the credentials being printed in build logs. 25 | # Additionally, recursive invocation with credentials as command-line 26 | # parameters, will print the full command, with credentials, in the build logs. 27 | # set -x 28 | # Code under repo is checked out to ${KOKORO_ARTIFACTS_DIR}/git. 29 | # The final directory name in this path is determined by the scm name specified 30 | # in the job configuration. 31 | 32 | function setup() { 33 | # Resize partition from 100 GB to max 34 | sudo apt -y install cloud-guest-utils 35 | sudo growpart /dev/sda 1 36 | sudo resize2fs /dev/sda1 37 | 38 | # Install Docker-Compose v2 39 | sudo curl -L "https://github.com/docker/compose/releases/download/v2.5.0/docker-compose-$(uname -s)-$(uname -m)" \ 40 | -o /usr/local/bin/docker-compose 41 | sudo chmod +x /usr/local/bin/docker-compose 42 | 43 | # Update gcloud components 44 | gcloud components update 45 | } 46 | 47 | setup 48 | cd "${KOKORO_ARTIFACTS_DIR}/github/fhir-gateway" 49 | ./build.sh 50 | -------------------------------------------------------------------------------- /kokoro/gcp_ubuntu/presubmit.cfg: -------------------------------------------------------------------------------- 1 | # -*- protobuffer -*- 2 | # proto-file: google3/devtools/kokoro/config/proto/build.proto 3 | # proto-message: BuildConfig 4 | 5 | # Location of the bash script. Should have value /. 6 | # github_scm.name is specified in the job configuration. 7 | build_file: "fhir-gateway/kokoro/gcp_ubuntu/kokoro_build.sh" 8 | -------------------------------------------------------------------------------- /license-header.txt: -------------------------------------------------------------------------------- 1 | Copyright ${license.git.copyrightYears} Google LLC 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /plugins/README.md: -------------------------------------------------------------------------------- 1 | # AccessChecker plugins 2 | 3 | To implement an access-checker plugin, the 4 | [AccessCheckerFactory interface](../server/src/main/java/com/google/fhir/gateway/interfaces/AccessCheckerFactory.java) 5 | must be implemented, and it must be annotated by a `@Named(value = "KEY")` 6 | annotation. `KEY` is the name of the access-checker that can be used when 7 | running the proxy server (by setting `ACCESS_CHECKER` environment variable). 8 | 9 | Example access-checker plugins in this module are 10 | [ListAccessChecker](src/main/java/com/google/fhir/gateway/plugin/ListAccessChecker.java) 11 | and 12 | [PatientAccessChecker](src/main/java/com/google/fhir/gateway/plugin/PatientAccessChecker.java). 13 | 14 | Beside doing basic validation of the access-token, the server also provides some 15 | query parameters and resource parsing functionality which are wrapped inside 16 | [PatientFinder](../server/src/main/java/com/google/fhir/gateway/interfaces/PatientFinder.java). 17 | 18 | 19 | -------------------------------------------------------------------------------- /plugins/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 19 | 20 | 4.0.0 21 | 22 | 24 | com.google.fhir.gateway 25 | fhir-gateway 26 | 0.4.1-SNAPSHOT 27 | 28 | 29 | plugins 30 | 31 | 32 | ${project.parent.basedir} 33 | 34 | 35 | 36 | 37 | ${project.groupId} 38 | server 39 | ${project.version} 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /plugins/src/main/java/com/google/fhir/gateway/plugin/SmartScopeChecker.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021-2023 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.google.fhir.gateway.plugin; 17 | 18 | import com.google.fhir.gateway.plugin.SmartFhirScope.Permission; 19 | import com.google.fhir.gateway.plugin.SmartFhirScope.Principal; 20 | import java.util.Collections; 21 | import java.util.List; 22 | import java.util.Map; 23 | import java.util.Set; 24 | import java.util.stream.Collectors; 25 | 26 | public class SmartScopeChecker { 27 | 28 | private final Map> permissionsByResourceType; 29 | 30 | SmartScopeChecker(List scopes, Principal permissionContext) { 31 | this.permissionsByResourceType = 32 | scopes.stream() 33 | .filter(smartFhirScope -> smartFhirScope.getPrincipal() == permissionContext) 34 | .collect( 35 | Collectors.groupingBy( 36 | SmartFhirScope::getResourceType, 37 | Collectors.flatMapping( 38 | resourceScopes -> resourceScopes.getPermissions().stream(), 39 | Collectors.toSet()))); 40 | } 41 | 42 | boolean hasPermission(String resourceType, Permission permission) { 43 | return this.permissionsByResourceType 44 | .getOrDefault(resourceType, Collections.emptySet()) 45 | .contains(permission) 46 | || this.permissionsByResourceType 47 | .getOrDefault(SmartFhirScope.ALL_RESOURCE_TYPES_WILDCARD, Collections.emptySet()) 48 | .contains(permission); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /plugins/src/test/java/com/google/fhir/gateway/plugin/AccessGrantedAndUpdateListTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021-2023 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.google.fhir.gateway.plugin; 17 | 18 | import ca.uhn.fhir.context.FhirContext; 19 | import com.google.common.io.Resources; 20 | import com.google.fhir.gateway.HttpFhirClient; 21 | import com.google.fhir.gateway.interfaces.RequestDetailsReader; 22 | import java.io.IOException; 23 | import java.net.URL; 24 | import java.nio.charset.StandardCharsets; 25 | import org.apache.http.HttpResponse; 26 | import org.junit.Before; 27 | import org.junit.Test; 28 | import org.junit.runner.RunWith; 29 | import org.mockito.Answers; 30 | import org.mockito.Mock; 31 | import org.mockito.junit.MockitoJUnitRunner; 32 | 33 | @RunWith(MockitoJUnitRunner.class) 34 | public class AccessGrantedAndUpdateListTest { 35 | 36 | private static final String TEST_LIST_ID = "test-list"; 37 | 38 | @Mock private HttpFhirClient httpFhirClientMock; 39 | 40 | @Mock(answer = Answers.RETURNS_DEEP_STUBS) 41 | private HttpResponse responseMock; 42 | 43 | @Mock(answer = Answers.RETURNS_DEEP_STUBS) 44 | private RequestDetailsReader requestDetailsReader; 45 | 46 | private static final FhirContext fhirContext = FhirContext.forR4(); 47 | 48 | private AccessGrantedAndUpdateList testInstance; 49 | 50 | @Before 51 | public void setUp() throws IOException { 52 | URL url = Resources.getResource("test_patient.json"); 53 | String testJson = Resources.toString(url, StandardCharsets.UTF_8); 54 | TestUtil.setUpFhirResponseMock(responseMock, testJson); 55 | } 56 | 57 | @Test 58 | public void postProcessNewPatientPut() throws IOException { 59 | testInstance = 60 | AccessGrantedAndUpdateList.forPatientResource( 61 | TEST_LIST_ID, httpFhirClientMock, fhirContext); 62 | testInstance.postProcess(requestDetailsReader, responseMock); 63 | } 64 | 65 | @Test 66 | public void postProcessNewPatientPost() throws IOException { 67 | testInstance = 68 | AccessGrantedAndUpdateList.forPatientResource( 69 | TEST_LIST_ID, httpFhirClientMock, fhirContext); 70 | testInstance.postProcess(requestDetailsReader, responseMock); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /plugins/src/test/java/com/google/fhir/gateway/plugin/SmartScopeCheckerTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021-2023 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.google.fhir.gateway.plugin; 17 | 18 | import static org.hamcrest.MatcherAssert.assertThat; 19 | import static org.hamcrest.Matchers.equalTo; 20 | 21 | import com.google.fhir.gateway.plugin.SmartFhirScope.Permission; 22 | import com.google.fhir.gateway.plugin.SmartFhirScope.Principal; 23 | import java.util.List; 24 | import org.hl7.fhir.r4.model.ResourceType; 25 | import org.junit.Test; 26 | import org.junit.runner.RunWith; 27 | import org.mockito.junit.MockitoJUnitRunner; 28 | 29 | @RunWith(MockitoJUnitRunner.class) 30 | public class SmartScopeCheckerTest { 31 | 32 | @Test 33 | public void hasPermissionCreateObservationPatientPrincipal() { 34 | SmartScopeChecker scopeChecker = 35 | new SmartScopeChecker( 36 | SmartFhirScope.extractSmartFhirScopesFromTokens( 37 | List.of( 38 | "user/Encounter.read", 39 | "patient/Observation.read", 40 | "patient/Observation.write")), 41 | Principal.PATIENT); 42 | assertThat( 43 | scopeChecker.hasPermission(ResourceType.Observation.name(), Permission.CREATE), 44 | equalTo(true)); 45 | } 46 | 47 | @Test 48 | public void hasPermissionCreateObservationPatientPrincipalNoValidScope() { 49 | SmartScopeChecker scopeChecker = 50 | new SmartScopeChecker( 51 | SmartFhirScope.extractSmartFhirScopesFromTokens( 52 | List.of("user/Observation.create", "patient/Observation.read")), 53 | Principal.PATIENT); 54 | assertThat( 55 | scopeChecker.hasPermission(ResourceType.Observation.name(), Permission.CREATE), 56 | equalTo(false)); 57 | } 58 | 59 | @Test 60 | public void hasPermissionReadObservationPatientPrincipalAllResources() { 61 | SmartScopeChecker scopeChecker = 62 | new SmartScopeChecker( 63 | SmartFhirScope.extractSmartFhirScopesFromTokens( 64 | List.of("user/*.read", "patient/Observation.write")), 65 | Principal.PATIENT); 66 | assertThat( 67 | scopeChecker.hasPermission(ResourceType.Observation.name(), Permission.READ), 68 | equalTo(false)); 69 | } 70 | 71 | @Test 72 | public void hasPermissionDeleteObservationPatientPrincipalAllResources() { 73 | SmartScopeChecker scopeChecker = 74 | new SmartScopeChecker( 75 | SmartFhirScope.extractSmartFhirScopesFromTokens( 76 | List.of("patient/*.read", "patient/Observation.write")), 77 | Principal.PATIENT); 78 | assertThat( 79 | scopeChecker.hasPermission(ResourceType.Observation.name(), Permission.DELETE), 80 | equalTo(true)); 81 | } 82 | 83 | @Test 84 | public void hasPermissionCreateObservationV2Scopes() { 85 | SmartScopeChecker scopeChecker = 86 | new SmartScopeChecker( 87 | SmartFhirScope.extractSmartFhirScopesFromTokens( 88 | List.of("patient/*.rs", "patient/Observation.u")), 89 | Principal.PATIENT); 90 | assertThat( 91 | scopeChecker.hasPermission(ResourceType.Observation.name(), Permission.CREATE), 92 | equalTo(false)); 93 | } 94 | 95 | @Test 96 | public void hasPermissionDeleteObservationV2Scopes() { 97 | SmartScopeChecker scopeChecker = 98 | new SmartScopeChecker( 99 | SmartFhirScope.extractSmartFhirScopesFromTokens( 100 | List.of("user/*.rs", "user/Observation.cr")), 101 | Principal.PATIENT); 102 | assertThat( 103 | scopeChecker.hasPermission(ResourceType.Observation.name(), Permission.DELETE), 104 | equalTo(false)); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /plugins/src/test/java/com/google/fhir/gateway/plugin/TestUtil.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021-2023 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.google.fhir.gateway.plugin; 17 | 18 | import static org.mockito.Mockito.when; 19 | 20 | import com.google.common.base.Preconditions; 21 | import java.nio.charset.StandardCharsets; 22 | import org.apache.http.HttpResponse; 23 | import org.apache.http.HttpStatus; 24 | import org.apache.http.StatusLine; 25 | import org.apache.http.entity.StringEntity; 26 | import org.mockito.Mockito; 27 | 28 | class TestUtil { 29 | 30 | public static void setUpFhirResponseMock(HttpResponse fhirResponseMock, String responseJson) { 31 | Preconditions.checkNotNull(responseJson); 32 | StatusLine statusLineMock = Mockito.mock(StatusLine.class); 33 | StringEntity testEntity = new StringEntity(responseJson, StandardCharsets.UTF_8); 34 | when(fhirResponseMock.getStatusLine()).thenReturn(statusLineMock); 35 | when(statusLineMock.getStatusCode()).thenReturn(HttpStatus.SC_OK); 36 | when(fhirResponseMock.getEntity()).thenReturn(testEntity); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /plugins/src/test/resources/bundle_empty.json: -------------------------------------------------------------------------------- 1 | { 2 | "link": [ 3 | { 4 | "relation": "search", 5 | "url": "https://healthcare.googleapis.com/v1/projects/fhir-sdk/locations/us/datasets/synthea-sample-data/fhirStores/gcs-data/fhir/List/?_elements=id&_id=patient-list-example&item=Patient%2Fbe92a43f-de46-affa-b131-bbf9eea51140TTTT" 6 | }, 7 | { 8 | "relation": "first", 9 | "url": "https://healthcare.googleapis.com/v1/projects/fhir-sdk/locations/us/datasets/synthea-sample-data/fhirStores/gcs-data/fhir/List/?_elements=id&_id=patient-list-example&item=Patient%2Fbe92a43f-de46-affa-b131-bbf9eea51140TTTT" 10 | }, 11 | { 12 | "relation": "self", 13 | "url": "https://healthcare.googleapis.com/v1/projects/fhir-sdk/locations/us/datasets/synthea-sample-data/fhirStores/gcs-data/fhir/List/?_elements=id&_id=patient-list-example&item=Patient%2Fbe92a43f-de46-affa-b131-bbf9eea51140TTTT" 14 | } 15 | ], 16 | "resourceType": "Bundle", 17 | "total": 0, 18 | "type": "searchset" 19 | } 20 | -------------------------------------------------------------------------------- /plugins/src/test/resources/bundle_list_patient_item.json: -------------------------------------------------------------------------------- 1 | { 2 | "entry": [ 3 | { 4 | "fullUrl": "https://healthcare.googleapis.com/v1/projects/fhir-sdk/locations/us/datasets/synthea-sample-data/fhirStores/gcs-data/fhir/List/patient-list-example", 5 | "resource": { 6 | "id": "patient-list-example", 7 | "meta": { 8 | "lastUpdated": "2021-11-25T18:54:23.389580+00:00", 9 | "tag": [ 10 | { 11 | "code": "SUBSETTED", 12 | "system": "http://hl7.org/fhir/v3/ObservationValue" 13 | } 14 | ], 15 | "versionId": "MTYzNzg2NjQ2MzM4OTU4MDAwMA" 16 | }, 17 | "resourceType": "List" 18 | }, 19 | "search": { 20 | "mode": "match" 21 | } 22 | } 23 | ], 24 | "link": [ 25 | { 26 | "relation": "search", 27 | "url": "https://healthcare.googleapis.com/v1/projects/fhir-sdk/locations/us/datasets/synthea-sample-data/fhirStores/gcs-data/fhir/List/?_elements=id&_id=patient-list-example&item=Patient%2Fbe92a43f-de46-affa-b131-bbf9eea51140" 28 | }, 29 | { 30 | "relation": "first", 31 | "url": "https://healthcare.googleapis.com/v1/projects/fhir-sdk/locations/us/datasets/synthea-sample-data/fhirStores/gcs-data/fhir/List/?_elements=id&_id=patient-list-example&item=Patient%2Fbe92a43f-de46-affa-b131-bbf9eea51140" 32 | }, 33 | { 34 | "relation": "self", 35 | "url": "https://healthcare.googleapis.com/v1/projects/fhir-sdk/locations/us/datasets/synthea-sample-data/fhirStores/gcs-data/fhir/List/?_elements=id&_id=patient-list-example&item=Patient%2Fbe92a43f-de46-affa-b131-bbf9eea51140" 36 | } 37 | ], 38 | "resourceType": "Bundle", 39 | "total": 1, 40 | "type": "searchset" 41 | } 42 | -------------------------------------------------------------------------------- /plugins/src/test/resources/bundle_transaction_delete_multiple_patient.json: -------------------------------------------------------------------------------- 1 | { 2 | "resourceType": "Bundle", 3 | "type": "transaction", 4 | "entry": [ 5 | { 6 | "request": { 7 | "method": "DELETE", 8 | "url": "Patient?_id=be92a43f-de46-affa-b131-bbf9eea51140,420e791b-e419-c19b-3144-29e101c2c12f" 9 | } 10 | } 11 | ] 12 | } -------------------------------------------------------------------------------- /plugins/src/test/resources/bundle_transaction_delete_non_patient.json: -------------------------------------------------------------------------------- 1 | { 2 | "resourceType": "Bundle", 3 | "type": "transaction", 4 | "entry": [ 5 | { 6 | "request": { 7 | "method": "DELETE", 8 | "url": "Observation?patient=be92a43f-de46-affa-b131-bbf9eea51140&_id=4047" 9 | } 10 | } 11 | ] 12 | } -------------------------------------------------------------------------------- /plugins/src/test/resources/bundle_transaction_delete_patient.json: -------------------------------------------------------------------------------- 1 | { 2 | "resourceType": "Bundle", 3 | "type": "transaction", 4 | "entry": [ 5 | { 6 | "resource": { 7 | "resourceType": "Patient", 8 | "name": [ 9 | { 10 | "family": "Smith", 11 | "given": [ 12 | "Darcy" 13 | ] 14 | } 15 | ], 16 | "gender": "female", 17 | "address": [ 18 | { 19 | "line": [ 20 | "123 Main St." 21 | ], 22 | "city": "Anycity", 23 | "state": "CA", 24 | "postalCode": "12345" 25 | } 26 | ] 27 | }, 28 | "request": { 29 | "method": "DELETE", 30 | "url": "Patient/be92a43f-de46-affa-b131-bbf9eea51140" 31 | } 32 | } 33 | ] 34 | } -------------------------------------------------------------------------------- /plugins/src/test/resources/bundle_transaction_delete_patient_unauthorized.json: -------------------------------------------------------------------------------- 1 | { 2 | "resourceType": "Bundle", 3 | "type": "transaction", 4 | "entry": [ 5 | { 6 | "resource": { 7 | "resourceType": "Patient", 8 | "name": [ 9 | { 10 | "family": "Smith", 11 | "given": [ 12 | "Darcy" 13 | ] 14 | } 15 | ], 16 | "gender": "female", 17 | "address": [ 18 | { 19 | "line": [ 20 | "123 Main St." 21 | ], 22 | "city": "Anycity", 23 | "state": "CA", 24 | "postalCode": "12345" 25 | } 26 | ] 27 | }, 28 | "request": { 29 | "method": "DELETE", 30 | "url": "Patient/patient-non-authorized" 31 | } 32 | } 33 | ] 34 | } -------------------------------------------------------------------------------- /plugins/src/test/resources/bundle_transaction_get_multiple_with_null_patient.json: -------------------------------------------------------------------------------- 1 | { 2 | "resourceType": "Bundle", 3 | "type": "transaction", 4 | "entry": [ 5 | { 6 | "request": { 7 | "method": "GET", 8 | "url": "Encounter?patient=be92a43f-de46-affa-b131-bbf9eea51140" 9 | } 10 | }, 11 | { 12 | "request": { 13 | "method": "GET", 14 | "url": "Encounter" 15 | } 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /plugins/src/test/resources/bundle_transaction_get_non_patient_authorized.json: -------------------------------------------------------------------------------- 1 | { 2 | "resourceType": "Bundle", 3 | "type": "transaction", 4 | "entry": [ 5 | { 6 | "request": { 7 | "method": "GET", 8 | "url": "Encounter?patient=be92a43f-de46-affa-b131-bbf9eea51140" 9 | } 10 | } 11 | ] 12 | } -------------------------------------------------------------------------------- /plugins/src/test/resources/bundle_transaction_get_non_patient_multiple_authorized.json: -------------------------------------------------------------------------------- 1 | { 2 | "resourceType": "Bundle", 3 | "type": "transaction", 4 | "entry": [ 5 | { 6 | "request": { 7 | "method": "GET", 8 | "url": "Encounter?patient=be92a43f-de46-affa-b131-bbf9eea51140,420e791b-e419-c19b-3144-29e101c2c12f" 9 | } 10 | } 11 | ] 12 | } -------------------------------------------------------------------------------- /plugins/src/test/resources/bundle_transaction_get_non_patient_unauthorized.json: -------------------------------------------------------------------------------- 1 | { 2 | "resourceType": "Bundle", 3 | "type": "transaction", 4 | "entry": [ 5 | { 6 | "request": { 7 | "method": "GET", 8 | "url": "Encounter?patient=db6e42c7-04fc-4d9d-b394-9ff33a41e178" 9 | } 10 | } 11 | ] 12 | } -------------------------------------------------------------------------------- /plugins/src/test/resources/bundle_transaction_get_patient_unauthorized.json: -------------------------------------------------------------------------------- 1 | { 2 | "resourceType": "Bundle", 3 | "type": "transaction", 4 | "entry": [ 5 | { 6 | "request": { 7 | "method": "GET", 8 | "url": "" 9 | } 10 | } 11 | ] 12 | } -------------------------------------------------------------------------------- /plugins/src/test/resources/bundle_transaction_no_patient_in_url.json: -------------------------------------------------------------------------------- 1 | { 2 | "resourceType": "Bundle", 3 | "type": "transaction", 4 | "entry": [ 5 | { 6 | "fullUrl": "Observation/observation-jamess-bond-id-1", 7 | "resource": { 8 | "category": [ 9 | { 10 | "coding": [ 11 | { 12 | "code": "laboratory", 13 | "display": "laboratory", 14 | "system": "http://terminology.hl7.org/CodeSystem/observation-category" 15 | } 16 | ] 17 | } 18 | ], 19 | "code": { 20 | "coding": [ 21 | { 22 | "code": "33914-3", 23 | "display": "Estimated Glomerular Filtration Rate", 24 | "system": "http://loinc.org" 25 | } 26 | ], 27 | "text": "Estimated Glomerular Filtration Rate" 28 | }, 29 | "effectiveDateTime": "2020-10-01T18:56:10-04:00", 30 | "encounter": { 31 | "reference": "Encounter/encounter-jamess-bond-id-1" 32 | }, 33 | "id": "observation-jamess-bond-id-1", 34 | "issued": "2020-10-01T18:56:10.396-04:00", 35 | "resourceType": "Observation", 36 | "status": "final", 37 | "subject": { 38 | "reference": "Patient/420e791b-e419-c19b-3144-29e101c2c12f" 39 | }, 40 | "performer":[{ 41 | "reference": "Patient/be92a43f-de46-affa-b131-bbf9eea51140" 42 | }], 43 | "valueQuantity": { 44 | "code": "mL/min/{1.73_m2}", 45 | "system": "http://unitsofmeasure.org", 46 | "unit": "mL/min/{1.73_m2}", 47 | "value": 76.02971496321274 48 | } 49 | }, 50 | "request": { 51 | "method": "PUT", 52 | "url": "Observation?_id=observation-jamess-bond-id-1&subject=Patient/420e791b-e419-c19b-3144-29e101c2c12f" 53 | } 54 | }, 55 | { 56 | "fullUrl": "Encounter/encounter-jamess-bond-id-1", 57 | "resource": { 58 | "class": { 59 | "code": "AMB", 60 | "system": "http://terminology.hl7.org/CodeSystem/v3-ActCode" 61 | }, 62 | "id": "encounter-jamess-bond-id-1", 63 | "period": { 64 | "end": "2015-07-01T19:11:10-04:00", 65 | "start": "2015-07-01T18:56:10-04:00" 66 | }, 67 | "reasonCode": [ 68 | { 69 | "coding": [ 70 | { 71 | "code": "444814009", 72 | "display": "Viral sinusitis (disorder)", 73 | "system": "http://snomed.info/sct" 74 | } 75 | ] 76 | } 77 | ], 78 | "resourceType": "Encounter", 79 | "status": "finished", 80 | "subject": { 81 | "reference": "Patient/420e791b-e419-c19b-3144-29e101c2c12f" 82 | }, 83 | "type": [ 84 | { 85 | "coding": [ 86 | { 87 | "code": "185345009", 88 | "display": "Encounter for symptom", 89 | "system": "http://snomed.info/sct" 90 | } 91 | ], 92 | "text": "Encounter for symptom" 93 | } 94 | ] 95 | }, 96 | "request": { 97 | "method": "PUT", 98 | "url": "Encounter?_id=encounter-jamess-bond-id-1" 99 | } 100 | } 101 | ] 102 | } -------------------------------------------------------------------------------- /plugins/src/test/resources/bundle_transaction_no_patient_ref.json: -------------------------------------------------------------------------------- 1 | { 2 | "resourceType": "Bundle", 3 | "type": "transaction", 4 | "entry": [ 5 | { 6 | "fullUrl": "Observation/observation-jamess-bond-id-1", 7 | "resource": { 8 | "category": [ 9 | { 10 | "coding": [ 11 | { 12 | "code": "laboratory", 13 | "display": "laboratory", 14 | "system": "http://terminology.hl7.org/CodeSystem/observation-category" 15 | } 16 | ] 17 | } 18 | ], 19 | "code": { 20 | "coding": [ 21 | { 22 | "code": "33914-3", 23 | "display": "Estimated Glomerular Filtration Rate", 24 | "system": "http://loinc.org" 25 | } 26 | ], 27 | "text": "Estimated Glomerular Filtration Rate" 28 | }, 29 | "effectiveDateTime": "2020-10-01T18:56:10-04:00", 30 | "encounter": { 31 | "reference": "Encounter/encounter-jamess-bond-id-1" 32 | }, 33 | "id": "observation-jamess-bond-id-1", 34 | "issued": "2020-10-01T18:56:10.396-04:00", 35 | "resourceType": "Observation", 36 | "status": "final", 37 | "subject": { 38 | "reference": "Patient/420e791b-e419-c19b-3144-29e101c2c12f" 39 | }, 40 | "valueQuantity": { 41 | "code": "mL/min/{1.73_m2}", 42 | "system": "http://unitsofmeasure.org", 43 | "unit": "mL/min/{1.73_m2}", 44 | "value": 76.02971496321274 45 | } 46 | }, 47 | "request": { 48 | "method": "PUT", 49 | "url": "Observation/observation-jamess-bond-id-1" 50 | } 51 | }, 52 | { 53 | "fullUrl": "Encounter/encounter-jamess-bond-id-1", 54 | "resource": { 55 | "class": { 56 | "code": "AMB", 57 | "system": "http://terminology.hl7.org/CodeSystem/v3-ActCode" 58 | }, 59 | "id": "encounter-jamess-bond-id-1", 60 | "period": { 61 | "end": "2015-07-01T19:11:10-04:00", 62 | "start": "2015-07-01T18:56:10-04:00" 63 | }, 64 | "reasonCode": [ 65 | { 66 | "coding": [ 67 | { 68 | "code": "444814009", 69 | "display": "Viral sinusitis (disorder)", 70 | "system": "http://snomed.info/sct" 71 | } 72 | ] 73 | } 74 | ], 75 | "resourceType": "Encounter", 76 | "status": "finished", 77 | "type": [ 78 | { 79 | "coding": [ 80 | { 81 | "code": "185345009", 82 | "display": "Encounter for symptom", 83 | "system": "http://snomed.info/sct" 84 | } 85 | ], 86 | "text": "Encounter for symptom" 87 | } 88 | ] 89 | }, 90 | "request": { 91 | "method": "PUT", 92 | "url": "Encounter/encounter-jamess-bond-id-1" 93 | } 94 | } 95 | ] 96 | } -------------------------------------------------------------------------------- /plugins/src/test/resources/bundle_transaction_no_resource_field.json: -------------------------------------------------------------------------------- 1 | { 2 | "resourceType": "Bundle", 3 | "type": "transaction", 4 | "entry": [ 5 | { 6 | "fullUrl": "Observation/observation-jamess-bond-id-1", 7 | "resource": { 8 | "category": [ 9 | { 10 | "coding": [ 11 | { 12 | "code": "laboratory", 13 | "display": "laboratory", 14 | "system": "http://terminology.hl7.org/CodeSystem/observation-category" 15 | } 16 | ] 17 | } 18 | ], 19 | "code": { 20 | "coding": [ 21 | { 22 | "code": "33914-3", 23 | "display": "Estimated Glomerular Filtration Rate", 24 | "system": "http://loinc.org" 25 | } 26 | ], 27 | "text": "Estimated Glomerular Filtration Rate" 28 | }, 29 | "effectiveDateTime": "2020-10-01T18:56:10-04:00", 30 | "encounter": { 31 | "reference": "Encounter/encounter-jamess-bond-id-1" 32 | }, 33 | "id": "observation-jamess-bond-id-1", 34 | "issued": "2020-10-01T18:56:10.396-04:00", 35 | "resourceType": "Observation", 36 | "status": "final", 37 | "subject": { 38 | "reference": "Patient/420e791b-e419-c19b-3144-29e101c2c12f" 39 | }, 40 | "performer":[{ 41 | "reference": "Patient/be92a43f-de46-affa-b131-bbf9eea51140" 42 | }], 43 | "valueQuantity": { 44 | "code": "mL/min/{1.73_m2}", 45 | "system": "http://unitsofmeasure.org", 46 | "unit": "mL/min/{1.73_m2}", 47 | "value": 76.02971496321274 48 | } 49 | }, 50 | "request": { 51 | "method": "PUT", 52 | "url": "Observation?_id=observation-jamess-bond-id-1&subject=Patient/420e791b-e419-c19b-3144-29e101c2c12f" 53 | } 54 | }, 55 | { 56 | "fullUrl": "Encounter/encounter-jamess-bond-id-1", 57 | "request": { 58 | "method": "PUT", 59 | "url": "Encounter?_id=encounter-jamess-bond-id-1&patient=Patient/420e791b-e419-c19b-3144-29e101c2c12f" 60 | } 61 | }, 62 | { 63 | "resource": { 64 | "resourceType": "Patient", 65 | "name": [ 66 | { 67 | "family": "Smith", 68 | "given": [ 69 | "Darcy" 70 | ] 71 | } 72 | ], 73 | "gender": "female", 74 | "address": [ 75 | { 76 | "line": [ 77 | "123 Main St." 78 | ], 79 | "city": "Anycity", 80 | "state": "CA", 81 | "postalCode": "12345" 82 | } 83 | ] 84 | }, 85 | "request": { 86 | "method": "PUT", 87 | "url": "Patient/db6e42c7-04fc-4d9d-b394-9ff33a41e178" 88 | } 89 | } 90 | ] 91 | } -------------------------------------------------------------------------------- /plugins/src/test/resources/bundle_transaction_non_patients.json: -------------------------------------------------------------------------------- 1 | { 2 | "resourceType": "Bundle", 3 | "type": "transaction", 4 | "entry": [ 5 | { 6 | "fullUrl": "Observation/observation-jamess-bond-id-1", 7 | "resource": { 8 | "category": [ 9 | { 10 | "coding": [ 11 | { 12 | "code": "laboratory", 13 | "display": "laboratory", 14 | "system": "http://terminology.hl7.org/CodeSystem/observation-category" 15 | } 16 | ] 17 | } 18 | ], 19 | "code": { 20 | "coding": [ 21 | { 22 | "code": "33914-3", 23 | "display": "Estimated Glomerular Filtration Rate", 24 | "system": "http://loinc.org" 25 | } 26 | ], 27 | "text": "Estimated Glomerular Filtration Rate" 28 | }, 29 | "effectiveDateTime": "2020-10-01T18:56:10-04:00", 30 | "encounter": { 31 | "reference": "Encounter/encounter-jamess-bond-id-1" 32 | }, 33 | "id": "observation-jamess-bond-id-1", 34 | "issued": "2020-10-01T18:56:10.396-04:00", 35 | "resourceType": "Observation", 36 | "status": "final", 37 | "subject": { 38 | "reference": "Patient/420e791b-e419-c19b-3144-29e101c2c12f" 39 | }, 40 | "performer":[{ 41 | "reference": "Patient/be92a43f-de46-affa-b131-bbf9eea51140" 42 | }], 43 | "valueQuantity": { 44 | "code": "mL/min/{1.73_m2}", 45 | "system": "http://unitsofmeasure.org", 46 | "unit": "mL/min/{1.73_m2}", 47 | "value": 76.02971496321274 48 | } 49 | }, 50 | "request": { 51 | "method": "PUT", 52 | "url": "Observation?_id=observation-jamess-bond-id-1&subject=Patient/420e791b-e419-c19b-3144-29e101c2c12f" 53 | } 54 | }, 55 | { 56 | "fullUrl": "Encounter/encounter-jamess-bond-id-1", 57 | "resource": { 58 | "class": { 59 | "code": "AMB", 60 | "system": "http://terminology.hl7.org/CodeSystem/v3-ActCode" 61 | }, 62 | "id": "encounter-jamess-bond-id-1", 63 | "period": { 64 | "end": "2015-07-01T19:11:10-04:00", 65 | "start": "2015-07-01T18:56:10-04:00" 66 | }, 67 | "reasonCode": [ 68 | { 69 | "coding": [ 70 | { 71 | "code": "444814009", 72 | "display": "Viral sinusitis (disorder)", 73 | "system": "http://snomed.info/sct" 74 | } 75 | ] 76 | } 77 | ], 78 | "resourceType": "Encounter", 79 | "status": "finished", 80 | "subject": { 81 | "reference": "Patient/420e791b-e419-c19b-3144-29e101c2c12f" 82 | }, 83 | "type": [ 84 | { 85 | "coding": [ 86 | { 87 | "code": "185345009", 88 | "display": "Encounter for symptom", 89 | "system": "http://snomed.info/sct" 90 | } 91 | ], 92 | "text": "Encounter for symptom" 93 | } 94 | ] 95 | }, 96 | "request": { 97 | "method": "PUT", 98 | "url": "Encounter?_id=encounter-jamess-bond-id-1" 99 | } 100 | } 101 | ] 102 | } -------------------------------------------------------------------------------- /plugins/src/test/resources/bundle_transaction_patch_authorized.json: -------------------------------------------------------------------------------- 1 | { 2 | "resourceType": "Bundle", 3 | "type": "transaction", 4 | "entry": [ 5 | { 6 | "fullUrl": "Patient/be92a43f-de46-affa-b131-bbf9eea51140", 7 | "resource": { 8 | "resourceType": "Binary", 9 | "contentType": "application/json-patch+json", 10 | "data": "WyB7ICJvcCI6InJlcGxhY2UiLCAicGF0aCI6Ii9hY3RpdmUiLCAidmFsdWUiOnRydWUgfSBd" 11 | }, 12 | "request": { 13 | "method": "PATCH", 14 | "url": "Patient/be92a43f-de46-affa-b131-bbf9eea51140" 15 | } 16 | }, 17 | { 18 | "fullUrl": "Observation/observation-jamess-bond-id-1", 19 | "resource": { 20 | "resourceType": "Binary", 21 | "contentType": "application/json-patch+json", 22 | "data": "WwogIHsKICAgICJvcCI6ICJyZXBsYWNlIiwKICAgICJwYXRoIjogIi9zdWJqZWN0L3JlZmVyZW5jZSIsCiAgICAidmFsdWUiOiAiUGF0aWVudC9iZTkyYTQzZi1kZTQ2LWFmZmEtYjEzMS1iYmY5ZWVhNTExNDAiCiAgfSx7CiAgIm9wIjogImFkZCIsCiAgInBhdGgiOiAiL3BlcmZvcm1lciIsCiAgInZhbHVlIjpbXQp9LAogIHsKICAgICJvcCI6ICJhZGQiLAogICAgInBhdGgiIDogIi9wZXJmb3JtZXIvMCIsCiAgICAidmFsdWUiIDogewogICAgICAicmVmZXJlbmNlIjogIlBhdGllbnQvbWljaGFlbCIKICAgIH0KICB9Cl0=" 23 | }, 24 | "request": { 25 | "method": "PATCH", 26 | "url": "Observation?_id=observation-jamess-bond-id-1&subject=Patient/be92a43f-de46-affa-b131-bbf9eea51140" 27 | } 28 | } 29 | ] 30 | } -------------------------------------------------------------------------------- /plugins/src/test/resources/bundle_transaction_patch_not_binary.json: -------------------------------------------------------------------------------- 1 | { 2 | "resourceType": "Bundle", 3 | "type": "transaction", 4 | "entry": [ 5 | { 6 | "fullUrl": "Patient/be92a43f-de46-affa-b131-bbf9eea51140", 7 | "resource": { 8 | "resourceType": "Parameters", 9 | "parameter": [ { 10 | "name": "operation", 11 | "part": [ { 12 | "name": "type", 13 | "valueCode": "replace" 14 | }, { 15 | "name": "path", 16 | "valueString": "Patient.birthDate" 17 | }, { 18 | "name": "value", 19 | "valueDate": "1930-01-01" 20 | } ] 21 | } ] 22 | }, 23 | "request": { 24 | "method": "PATCH", 25 | "url": "Patient/be92a43f-de46-affa-b131-bbf9eea51140" 26 | } 27 | }, 28 | { 29 | "fullUrl": "Observation/observation-jamess-bond-id-1", 30 | "resource": { 31 | "resourceType": "Binary", 32 | "contentType": "application/json-patch+json", 33 | "data": "WwogIHsKICAgICJvcCI6ICJyZXBsYWNlIiwKICAgICJwYXRoIjogIi9zdWJqZWN0L3JlZmVyZW5jZSIsCiAgICAidmFsdWUiOiAiUGF0aWVudC9ib2IiCiAgfSx7CiAgIm9wIjogImFkZCIsCiAgInBhdGgiOiAiL3BlcmZvcm1lciIsCiAgInZhbHVlIjpbXQp9LAogIHsKICAgICJvcCI6ICJhZGQiLAogICAgInBhdGgiIDogIi9wZXJmb3JtZXIvMCIsCiAgICAidmFsdWUiIDogewogICAgICAicmVmZXJlbmNlIjogIlBhdGllbnQvbWljaGFlbCIKICAgIH0KICB9Cl0=" 34 | }, 35 | "request": { 36 | "method": "PATCH", 37 | "url": "Observation?_id=observation-jamess-bond-id-1&subject=Patient/420e791b-e419-c19b-3144-29e101c2c12f" 38 | } 39 | } 40 | ] 41 | } -------------------------------------------------------------------------------- /plugins/src/test/resources/bundle_transaction_patch_unauthorized.json: -------------------------------------------------------------------------------- 1 | { 2 | "resourceType": "Bundle", 3 | "type": "transaction", 4 | "entry": [ 5 | { 6 | "fullUrl": "Patient/be92a43f-de46-affa-b131-bbf9eea51140", 7 | "resource": { 8 | "resourceType": "Binary", 9 | "contentType": "application/json-patch+json", 10 | "data": "WyB7ICJvcCI6InJlcGxhY2UiLCAicGF0aCI6Ii9hY3RpdmUiLCAidmFsdWUiOnRydWUgfSBd" 11 | }, 12 | "request": { 13 | "method": "PATCH", 14 | "url": "Patient/be92a43f-de46-affa-b131-bbf9eea51140" 15 | } 16 | }, 17 | { 18 | "fullUrl": "Observation/observation-jamess-bond-id-1", 19 | "resource": { 20 | "resourceType": "Binary", 21 | "contentType": "application/json-patch+json", 22 | "data": "WwogIHsKICAgICJvcCI6ICJyZXBsYWNlIiwKICAgICJwYXRoIjogIi9zdWJqZWN0L3JlZmVyZW5jZSIsCiAgICAidmFsdWUiOiAiUGF0aWVudC9ib2IiCiAgfSx7CiAgIm9wIjogImFkZCIsCiAgInBhdGgiOiAiL3BlcmZvcm1lciIsCiAgInZhbHVlIjpbXQp9LAogIHsKICAgICJvcCI6ICJhZGQiLAogICAgInBhdGgiIDogIi9wZXJmb3JtZXIvMCIsCiAgICAidmFsdWUiIDogewogICAgICAicmVmZXJlbmNlIjogIlBhdGllbnQvbWljaGFlbCIKICAgIH0KICB9Cl0=" 23 | }, 24 | "request": { 25 | "method": "PATCH", 26 | "url": "Observation?_id=observation-jamess-bond-id-1&subject=Patient/420e791b-e419-c19b-3144-29e101c2c12f" 27 | } 28 | } 29 | ] 30 | } -------------------------------------------------------------------------------- /plugins/src/test/resources/bundle_transaction_patient_and_non_patients.json: -------------------------------------------------------------------------------- 1 | { 2 | "resourceType": "Bundle", 3 | "type": "transaction", 4 | "entry": [ 5 | { 6 | "fullUrl": "Observation/observation-jamess-bond-id-1", 7 | "resource": { 8 | "category": [ 9 | { 10 | "coding": [ 11 | { 12 | "code": "laboratory", 13 | "display": "laboratory", 14 | "system": "http://terminology.hl7.org/CodeSystem/observation-category" 15 | } 16 | ] 17 | } 18 | ], 19 | "code": { 20 | "coding": [ 21 | { 22 | "code": "33914-3", 23 | "display": "Estimated Glomerular Filtration Rate", 24 | "system": "http://loinc.org" 25 | } 26 | ], 27 | "text": "Estimated Glomerular Filtration Rate" 28 | }, 29 | "effectiveDateTime": "2020-10-01T18:56:10-04:00", 30 | "encounter": { 31 | "reference": "Encounter/encounter-jamess-bond-id-1" 32 | }, 33 | "id": "observation-jamess-bond-id-1", 34 | "issued": "2020-10-01T18:56:10.396-04:00", 35 | "resourceType": "Observation", 36 | "status": "final", 37 | "subject": { 38 | "reference": "Patient/420e791b-e419-c19b-3144-29e101c2c12f" 39 | }, 40 | "performer":[{ 41 | "reference": "Patient/be92a43f-de46-affa-b131-bbf9eea51140" 42 | }], 43 | "valueQuantity": { 44 | "code": "mL/min/{1.73_m2}", 45 | "system": "http://unitsofmeasure.org", 46 | "unit": "mL/min/{1.73_m2}", 47 | "value": 76.02971496321274 48 | } 49 | }, 50 | "request": { 51 | "method": "PUT", 52 | "url": "Observation?_id=observation-jamess-bond-id-1&subject=Patient/420e791b-e419-c19b-3144-29e101c2c12f" 53 | } 54 | }, 55 | { 56 | "fullUrl": "Encounter/encounter-jamess-bond-id-1", 57 | "resource": { 58 | "class": { 59 | "code": "AMB", 60 | "system": "http://terminology.hl7.org/CodeSystem/v3-ActCode" 61 | }, 62 | "id": "encounter-jamess-bond-id-1", 63 | "period": { 64 | "end": "2015-07-01T19:11:10-04:00", 65 | "start": "2015-07-01T18:56:10-04:00" 66 | }, 67 | "reasonCode": [ 68 | { 69 | "coding": [ 70 | { 71 | "code": "444814009", 72 | "display": "Viral sinusitis (disorder)", 73 | "system": "http://snomed.info/sct" 74 | } 75 | ] 76 | } 77 | ], 78 | "resourceType": "Encounter", 79 | "status": "finished", 80 | "subject": { 81 | "reference": "Patient/420e791b-e419-c19b-3144-29e101c2c12f" 82 | }, 83 | "type": [ 84 | { 85 | "coding": [ 86 | { 87 | "code": "185345009", 88 | "display": "Encounter for symptom", 89 | "system": "http://snomed.info/sct" 90 | } 91 | ], 92 | "text": "Encounter for symptom" 93 | } 94 | ] 95 | }, 96 | "request": { 97 | "method": "PUT", 98 | "url": "Encounter?_id=encounter-jamess-bond-id-1&patient=Patient/420e791b-e419-c19b-3144-29e101c2c12f" 99 | } 100 | }, 101 | { 102 | "resource": { 103 | "resourceType": "Patient", 104 | "name": [ 105 | { 106 | "family": "Smith", 107 | "given": [ 108 | "Darcy" 109 | ] 110 | } 111 | ], 112 | "gender": "female", 113 | "address": [ 114 | { 115 | "line": [ 116 | "123 Main St." 117 | ], 118 | "city": "Anycity", 119 | "state": "CA", 120 | "postalCode": "12345" 121 | } 122 | ] 123 | }, 124 | "request": { 125 | "method": "PUT", 126 | "url": "Patient/db6e42c7-04fc-4d9d-b394-9ff33a41e178" 127 | } 128 | } 129 | ] 130 | } -------------------------------------------------------------------------------- /plugins/src/test/resources/bundle_transaction_post_patient.json: -------------------------------------------------------------------------------- 1 | { 2 | "resourceType": "Bundle", 3 | "type": "transaction", 4 | "entry": [ 5 | { 6 | "resource": { 7 | "resourceType": "Patient", 8 | "name": [ 9 | { 10 | "family": "Smith", 11 | "given": [ 12 | "Darcy" 13 | ] 14 | } 15 | ], 16 | "gender": "female", 17 | "address": [ 18 | { 19 | "line": [ 20 | "123 Main St." 21 | ], 22 | "city": "Anycity", 23 | "state": "CA", 24 | "postalCode": "12345" 25 | } 26 | ] 27 | }, 28 | "request": { 29 | "method": "POST", 30 | "url": "Patient" 31 | } 32 | } 33 | ] 34 | } -------------------------------------------------------------------------------- /plugins/src/test/resources/bundle_transaction_put_authorized_patient.json: -------------------------------------------------------------------------------- 1 | { 2 | "resourceType": "Bundle", 3 | "type": "transaction", 4 | "entry": [ 5 | { 6 | "resource": { 7 | "resourceType": "Patient", 8 | "name": [ 9 | { 10 | "family": "Smith", 11 | "given": [ 12 | "Darcy" 13 | ] 14 | } 15 | ], 16 | "gender": "female", 17 | "address": [ 18 | { 19 | "line": [ 20 | "123 Main St." 21 | ], 22 | "city": "Anycity", 23 | "state": "CA", 24 | "postalCode": "12345" 25 | } 26 | ] 27 | }, 28 | "request": { 29 | "method": "PUT", 30 | "url": "Patient/be92a43f-de46-affa-b131-bbf9eea51140" 31 | } 32 | } 33 | ] 34 | } -------------------------------------------------------------------------------- /plugins/src/test/resources/bundle_transaction_put_patient.json: -------------------------------------------------------------------------------- 1 | { 2 | "resourceType": "Bundle", 3 | "type": "transaction", 4 | "entry": [ 5 | { 6 | "resource": { 7 | "resourceType": "Patient", 8 | "name": [ 9 | { 10 | "family": "Smith", 11 | "given": [ 12 | "Darcy" 13 | ] 14 | } 15 | ], 16 | "gender": "female", 17 | "address": [ 18 | { 19 | "line": [ 20 | "123 Main St." 21 | ], 22 | "city": "Anycity", 23 | "state": "CA", 24 | "postalCode": "12345" 25 | } 26 | ] 27 | }, 28 | "request": { 29 | "method": "PUT", 30 | "url": "Patient/be92a43f-de46-affa-b131-bbf9eea51140" 31 | } 32 | }, 33 | { 34 | "resource": { 35 | "resourceType": "Patient", 36 | "name": [ 37 | { 38 | "family": "Smith", 39 | "given": [ 40 | "Darcy" 41 | ] 42 | } 43 | ], 44 | "gender": "female", 45 | "address": [ 46 | { 47 | "line": [ 48 | "123 Main St." 49 | ], 50 | "city": "Anycity", 51 | "state": "CA", 52 | "postalCode": "12345" 53 | } 54 | ] 55 | }, 56 | "request": { 57 | "method": "PUT", 58 | "url": "Patient/420e791b-e419-c19b-3144-29e101c2c12f" 59 | } 60 | } 61 | ] 62 | } -------------------------------------------------------------------------------- /plugins/src/test/resources/bundle_transaction_put_unauthorized.json: -------------------------------------------------------------------------------- 1 | { 2 | "resourceType": "Bundle", 3 | "type": "transaction", 4 | "entry": [ 5 | { 6 | "resource": { 7 | "resourceType": "Patient", 8 | "name": [ 9 | { 10 | "family": "Smith", 11 | "given": [ 12 | "Darcy" 13 | ] 14 | } 15 | ], 16 | "gender": "female", 17 | "address": [ 18 | { 19 | "line": [ 20 | "123 Main St." 21 | ], 22 | "city": "Anycity", 23 | "state": "CA", 24 | "postalCode": "12345" 25 | } 26 | ] 27 | }, 28 | "request": { 29 | "method": "PUT", 30 | "url": "Patient/patient-non-authorized" 31 | } 32 | } 33 | ] 34 | } -------------------------------------------------------------------------------- /plugins/src/test/resources/capability.json: -------------------------------------------------------------------------------- 1 | { 2 | "resourceType": "CapabilityStatement", 3 | "id": "test", 4 | "name": "modified from sample instance /metadata", 5 | "status": "draft", 6 | "date": "2022-02-18", 7 | "publisher": "Google", 8 | "description": "A CapabilityStatement for testing proxy functionality", 9 | "kind": "instance", 10 | "fhirVersion": "4.0.1", 11 | "format": [ 12 | "json" 13 | ], 14 | "rest": [ 15 | { 16 | "documentation": "Main FHIR endpoint for FHIR store fhir_r4_1M_a", 17 | "interaction": [ 18 | { 19 | "code": "batch" 20 | }, 21 | { 22 | "code": "transaction" 23 | }, 24 | { 25 | "code": "search-system" 26 | } 27 | ], 28 | "mode": "server", 29 | "operation": [ 30 | { 31 | "definition": "OperationDefinition/Patient-everything", 32 | "name": "everything" 33 | }, 34 | { 35 | "definition": "OperationDefinition/Observation-lastn", 36 | "name": "lastn" 37 | }, 38 | { 39 | "definition": "OperationDefinition/ConceptMap-translate", 40 | "name": "translate" 41 | } 42 | ], 43 | "resource": [ 44 | { 45 | "type": "Patient", 46 | "conditionalCreate": false, 47 | "conditionalDelete": "not-supported", 48 | "conditionalRead": "full-support", 49 | "conditionalUpdate": false, 50 | "documentation": "Creating identities is not allowed if allow_create_update of the FHIR store is set to false.", 51 | "interaction": [ 52 | { 53 | "code": "read" 54 | }, 55 | { 56 | "code": "update" 57 | }, 58 | { 59 | "code": "delete" 60 | }, 61 | { 62 | "code": "create" 63 | } 64 | ], 65 | "readHistory": true, 66 | "searchInclude": [ 67 | "Patient.general-practitioner", 68 | "Patient.link", 69 | "Patient.organization" 70 | ], 71 | "searchParam": [ 72 | { 73 | "definition": "http://hl7.org/fhir/SearchParameter/Patient-active", 74 | "name": "active", 75 | "type": "token" 76 | } 77 | ] 78 | } 79 | ] 80 | } 81 | ] 82 | } 83 | -------------------------------------------------------------------------------- /plugins/src/test/resources/patient-list-example.json: -------------------------------------------------------------------------------- 1 | { 2 | "entry": [ 3 | { 4 | "item": { 5 | "reference": "Patient/be92a43f-de46-affa-b131-bbf9eea51140" 6 | } 7 | }, 8 | { 9 | "item": { 10 | "reference": "Patient/420e791b-e419-c19b-3144-29e101c2c12f" 11 | } 12 | } 13 | ], 14 | "id": "patient-list-example", 15 | "meta": { 16 | "lastUpdated": "2021-11-25T18:54:23.389580+00:00", 17 | "versionId": "MTYzNzg2NjQ2MzM4OTU4MDAwMA" 18 | }, 19 | "mode": "working", 20 | "resourceType": "List", 21 | "status": "current" 22 | } -------------------------------------------------------------------------------- /plugins/src/test/resources/patient_id_search_single.json: -------------------------------------------------------------------------------- 1 | { 2 | "entry": [ 3 | { 4 | "fullUrl": "https://healthcare.googleapis.com/v1/projects/fhir-sdk/locations/us/datasets/synthea-sample-data/fhirStores/gcs-data/fhir/Patient/be92a43f-de46-affa-b131-bbf9eea51140", 5 | "resource": { 6 | "id": "be92a43f-de46-affa-b131-bbf9eea51140", 7 | "meta": { 8 | "lastUpdated": "2021-11-25T19:03:28.085054+00:00", 9 | "tag": [ 10 | { 11 | "code": "SUBSETTED", 12 | "system": "http://hl7.org/fhir/v3/ObservationValue" 13 | } 14 | ], 15 | "versionId": "MTYzNzg2NzAwODA4NTA1NDAwMA" 16 | }, 17 | "resourceType": "Patient" 18 | }, 19 | "search": { 20 | "mode": "match" 21 | } 22 | } 23 | ], 24 | "link": [ 25 | { 26 | "relation": "search", 27 | "url": "https://healthcare.googleapis.com/v1/projects/fhir-sdk/locations/us/datasets/synthea-sample-data/fhirStores/gcs-data/fhir/Patient/?_count=10&_elements=id" 28 | }, 29 | { 30 | "relation": "first", 31 | "url": "https://healthcare.googleapis.com/v1/projects/fhir-sdk/locations/us/datasets/synthea-sample-data/fhirStores/gcs-data/fhir/Patient/?_count=10&_elements=id" 32 | }, 33 | { 34 | "relation": "self", 35 | "url": "https://healthcare.googleapis.com/v1/projects/fhir-sdk/locations/us/datasets/synthea-sample-data/fhirStores/gcs-data/fhir/Patient/?_count=10&_elements=id" 36 | } 37 | ], 38 | "resourceType": "Bundle", 39 | "total": 1, 40 | "type": "searchset" 41 | } 42 | -------------------------------------------------------------------------------- /plugins/src/test/resources/test_obs.json: -------------------------------------------------------------------------------- 1 | { 2 | "category": [ 3 | { 4 | "coding": [ 5 | { 6 | "code": "survey", 7 | "display": "survey", 8 | "system": "http://terminology.hl7.org/CodeSystem/observation-category" 9 | } 10 | ] 11 | } 12 | ], 13 | "code": { 14 | "coding": [ 15 | { 16 | "code": "1250AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", 17 | "display": "ANTIRETROVIRALS STARTED", 18 | "system": "http://loinc.org" 19 | } 20 | ], 21 | "text": "ANTIRETROVIRALS STARTED" 22 | }, 23 | "effectiveDateTime": "2004-03-26T01:52:09+00:00", 24 | "encounter": { 25 | "reference": "Encounter/0ed72aff-0275-af43-4a59-c2695f9859d7" 26 | }, 27 | "id": "8d82f8ba-e045-4e43-833e-c8aa4de303a5", 28 | "issued": "2004-03-26T02:52:09.437+00:00", 29 | "meta": { 30 | "lastUpdated": "2021-12-04T07:57:08.423056+00:00", 31 | "versionId": "MTYzODYwNDYyODQyMzA1NjAwMA" 32 | }, 33 | "resourceType": "Observation", 34 | "status": "final", 35 | "subject": { 36 | "reference": "Patient/be92a43f-de46-affa-b131-bbf9eea51140" 37 | }, 38 | "valueCodeableConcept": { 39 | "coding": [ 40 | { 41 | "code": "86663AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", 42 | "display": "Zidovudine", 43 | "system": "http://snomed.info/sct" 44 | } 45 | ], 46 | "text": "Zidovudine" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /plugins/src/test/resources/test_obs_no_subject.json: -------------------------------------------------------------------------------- 1 | { 2 | "category": [ 3 | { 4 | "coding": [ 5 | { 6 | "code": "survey", 7 | "display": "survey", 8 | "system": "http://terminology.hl7.org/CodeSystem/observation-category" 9 | } 10 | ] 11 | } 12 | ], 13 | "code": { 14 | "coding": [ 15 | { 16 | "code": "1250AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", 17 | "display": "ANTIRETROVIRALS STARTED", 18 | "system": "http://loinc.org" 19 | } 20 | ], 21 | "text": "ANTIRETROVIRALS STARTED" 22 | }, 23 | "effectiveDateTime": "2004-03-26T01:52:09+00:00", 24 | "encounter": { 25 | "reference": "Encounter/0ed72aff-0275-af43-4a59-c2695f9859d7" 26 | }, 27 | "id": "8d82f8ba-e045-4e43-833e-c8aa4de303a5", 28 | "issued": "2004-03-26T02:52:09.437+00:00", 29 | "meta": { 30 | "lastUpdated": "2021-12-04T07:57:08.423056+00:00", 31 | "versionId": "MTYzODYwNDYyODQyMzA1NjAwMA" 32 | }, 33 | "resourceType": "Observation", 34 | "status": "final", 35 | "valueCodeableConcept": { 36 | "coding": [ 37 | { 38 | "code": "86663AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", 39 | "display": "Zidovudine", 40 | "system": "http://snomed.info/sct" 41 | } 42 | ], 43 | "text": "Zidovudine" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /plugins/src/test/resources/test_obs_patch.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "op": "replace", 4 | "path": "/subject/reference", 5 | "value": "Patient/be92a43f-de46-affa-b131-bbf9eea51140" 6 | }, 7 | { 8 | "op": "replace", 9 | "path": "/subject/display", 10 | "value": "MyName" 11 | }, 12 | { 13 | "op": "add", 14 | "path": "/performer", 15 | "value":[] 16 | }, 17 | { 18 | "op": "replace", 19 | "path": "/valueQuantity/value", 20 | "value": 30 21 | } 22 | ] -------------------------------------------------------------------------------- /plugins/src/test/resources/test_obs_patch_no_reference.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "op": "replace", 4 | "path": "/subject", 5 | "value": { 6 | "type": "Patient" 7 | } 8 | }, 9 | { 10 | "op": "replace", 11 | "path": "/status", 12 | "value": "final" 13 | } 14 | ] -------------------------------------------------------------------------------- /plugins/src/test/resources/test_obs_patch_unauthorized_no_patient_id.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "op": "replace", 4 | "path": "/subject", 5 | "value": { 6 | "reference": "Patient/" 7 | } 8 | } 9 | ] -------------------------------------------------------------------------------- /plugins/src/test/resources/test_obs_patch_unauthorized_patient.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "op": "replace", 4 | "path": "/subject/reference", 5 | "value": "Patient/bob" 6 | },{ 7 | "op": "add", 8 | "path": "/performer", 9 | "value":[] 10 | }, 11 | { 12 | "op": "add", 13 | "path" : "/performer/0", 14 | "value" : { 15 | "reference": "Patient/michael" 16 | } 17 | } 18 | ] -------------------------------------------------------------------------------- /plugins/src/test/resources/test_obs_patch_unauthorized_remove.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "op": "replace", 4 | "path": "/subject/reference", 5 | "value": "Patient/be92a43f-de46-affa-b131-bbf9eea51140" 6 | }, 7 | { 8 | "op": "add", 9 | "path": "/performer", 10 | "value":[] 11 | }, 12 | { 13 | "op": "remove", 14 | "path": "/patient" 15 | } 16 | ] -------------------------------------------------------------------------------- /plugins/src/test/resources/test_obs_performers.json: -------------------------------------------------------------------------------- 1 | { 2 | "category": [ 3 | { 4 | "coding": [ 5 | { 6 | "code": "survey", 7 | "display": "survey", 8 | "system": "http://terminology.hl7.org/CodeSystem/observation-category" 9 | } 10 | ] 11 | } 12 | ], 13 | "code": { 14 | "coding": [ 15 | { 16 | "code": "1250AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", 17 | "display": "ANTIRETROVIRALS STARTED", 18 | "system": "http://loinc.org" 19 | } 20 | ], 21 | "text": "ANTIRETROVIRALS STARTED" 22 | }, 23 | "effectiveDateTime": "2004-03-26T01:52:09+00:00", 24 | "encounter": { 25 | "reference": "Encounter/0ed72aff-0275-af43-4a59-c2695f9859d7" 26 | }, 27 | "id": "8d82f8ba-e045-4e43-833e-c8aa4de303a5", 28 | "issued": "2004-03-26T02:52:09.437+00:00", 29 | "meta": { 30 | "lastUpdated": "2021-12-04T07:57:08.423056+00:00", 31 | "versionId": "MTYzODYwNDYyODQyMzA1NjAwMA" 32 | }, 33 | "resourceType": "Observation", 34 | "status": "final", 35 | "subject": { 36 | "reference": "Patient/test-patient-1" 37 | }, 38 | "performer": [ 39 | { 40 | "reference": "Practitioner/test-practitioner-1" 41 | }, 42 | { 43 | "reference": "Patient/be92a43f-de46-affa-b131-bbf9eea51140" 44 | }, 45 | { 46 | "reference": "Patient/test-patient-2" 47 | } 48 | ], 49 | "valueCodeableConcept": { 50 | "coding": [ 51 | { 52 | "code": "86663AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", 53 | "display": "Zidovudine", 54 | "system": "http://snomed.info/sct" 55 | } 56 | ], 57 | "text": "Zidovudine" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /plugins/src/test/resources/test_obs_unauthorized.json: -------------------------------------------------------------------------------- 1 | { 2 | "category": [ 3 | { 4 | "coding": [ 5 | { 6 | "code": "survey", 7 | "display": "survey", 8 | "system": "http://terminology.hl7.org/CodeSystem/observation-category" 9 | } 10 | ] 11 | } 12 | ], 13 | "code": { 14 | "coding": [ 15 | { 16 | "code": "1250AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", 17 | "display": "ANTIRETROVIRALS STARTED", 18 | "system": "http://loinc.org" 19 | } 20 | ], 21 | "text": "ANTIRETROVIRALS STARTED" 22 | }, 23 | "effectiveDateTime": "2004-03-26T01:52:09+00:00", 24 | "encounter": { 25 | "reference": "Encounter/0ed72aff-0275-af43-4a59-c2695f9859d7" 26 | }, 27 | "id": "8d82f8ba-e045-4e43-833e-c8aa4de303a5", 28 | "issued": "2004-03-26T02:52:09.437+00:00", 29 | "meta": { 30 | "lastUpdated": "2021-12-04T07:57:08.423056+00:00", 31 | "versionId": "MTYzODYwNDYyODQyMzA1NjAwMA" 32 | }, 33 | "resourceType": "Observation", 34 | "status": "final", 35 | "subject": { 36 | "reference": "Patient/patient-non-authorized" 37 | }, 38 | "valueCodeableConcept": { 39 | "coding": [ 40 | { 41 | "code": "86663AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", 42 | "display": "Zidovudine", 43 | "system": "http://snomed.info/sct" 44 | } 45 | ], 46 | "text": "Zidovudine" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /plugins/src/test/resources/test_patient.json: -------------------------------------------------------------------------------- 1 | { 2 | "address": [ 3 | { 4 | "city": "Braintree", 5 | "country": "US", 6 | "extension": [ 7 | { 8 | "extension": [ 9 | { 10 | "url": "latitude", 11 | "valueDecimal": 42.21111261554208 12 | }, 13 | { 14 | "url": "longitude", 15 | "valueDecimal": -70.96074217780242 16 | } 17 | ], 18 | "url": "http://hl7.org/fhir/StructureDefinition/geolocation" 19 | } 20 | ], 21 | "line": [ 22 | "864 Schuster Rue" 23 | ], 24 | "postalCode": "02184", 25 | "state": "MA" 26 | } 27 | ], 28 | "birthDate": "1971-01-13", 29 | "gender": "male", 30 | "id": "be92a43f-de46-affa-b131-bbf9eea51140", 31 | "identifier": [ 32 | { 33 | "system": "https://github.com/synthetichealth/synthea", 34 | "value": "be92a43f-de46-affa-b131-bbf9eea51140" 35 | }, 36 | { 37 | "system": "http://hospital.smarthealthit.org", 38 | "type": { 39 | "coding": [ 40 | { 41 | "code": "MR", 42 | "display": "Medical Record Number", 43 | "system": "http://terminology.hl7.org/CodeSystem/v2-0203" 44 | } 45 | ], 46 | "text": "Medical Record Number" 47 | }, 48 | "value": "be92a43f-de46-affa-b131-bbf9eea51140" 49 | }, 50 | { 51 | "system": "http://hl7.org/fhir/sid/us-ssn", 52 | "type": { 53 | "coding": [ 54 | { 55 | "code": "SS", 56 | "display": "Social Security Number", 57 | "system": "http://terminology.hl7.org/CodeSystem/v2-0203" 58 | } 59 | ], 60 | "text": "Social Security Number" 61 | }, 62 | "value": "999-17-8182" 63 | } 64 | ], 65 | "maritalStatus": { 66 | "coding": [ 67 | { 68 | "code": "S", 69 | "display": "S", 70 | "system": "http://terminology.hl7.org/CodeSystem/v3-MaritalStatus" 71 | } 72 | ], 73 | "text": "S" 74 | }, 75 | "meta": { 76 | "lastUpdated": "2021-11-25T19:03:28.085054+00:00", 77 | "profile": [ 78 | "http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient" 79 | ], 80 | "versionId": "MTYzNzg2NzAwODA4NTA1NDAwMA" 81 | }, 82 | "multipleBirthBoolean": false, 83 | "name": [ 84 | { 85 | "family": "Anderson154", 86 | "given": [ 87 | "Micheal721", 88 | "NEW_NAME" 89 | ], 90 | "prefix": [ 91 | "Mr." 92 | ], 93 | "use": "official" 94 | } 95 | ], 96 | "resourceType": "Patient", 97 | "telecom": [ 98 | { 99 | "system": "phone", 100 | "use": "home", 101 | "value": "555-435-4405" 102 | } 103 | ], 104 | "text": { 105 | "div": "
Generated by Synthea.Version identifier: a3482c8\n . Person seed: -3805195760489406909 Population seed: 1632186774891
", 106 | "status": "generated" 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /resources/README.md: -------------------------------------------------------------------------------- 1 | # Files description 2 | 3 | - `patient-list-example.json`: This is a sample list of patient IDs that can be 4 | uploaded to a FHIR store and used as authorization list: 5 | ```shell 6 | $ curl --request PUT \ 7 | -H "Authorization: Bearer $(gcloud auth print-access-token)" \ 8 | -H "Content-Type: application/fhir+json; charset=utf-8" \ 9 | "https://healthcare.googleapis.com/v1/projects/fhir-sdk/locations/us/datasets/synthea-sample-data/fhirStores/gcs-data/fhir/List/patient-list-example" \ 10 | -d @patient-list-example.json 11 | ``` 12 | The test user that is configured on the default test Keycloak IDP has the ID 13 | of this list as its `patient_list` claim. 14 | -------------------------------------------------------------------------------- /resources/fhir_access_proxy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/fhir-gateway/bfd3cb7b2aa4376bda9ddd8cc4175b295e671e54/resources/fhir_access_proxy.png -------------------------------------------------------------------------------- /resources/hapi_page_url_allowed_queries.json: -------------------------------------------------------------------------------- 1 | { 2 | "entries": [ 3 | { 4 | "path": "", 5 | "queryParams": { 6 | "_getpages": "ANY_VALUE" 7 | }, 8 | "allowExtraParams": true, 9 | "allParamsRequired": true 10 | } 11 | ] 12 | } -------------------------------------------------------------------------------- /resources/patient-list-example.json: -------------------------------------------------------------------------------- 1 | { 2 | "resourceType": "List", 3 | "id": "patient-list-example", 4 | "status": "current", 5 | "mode": "working", 6 | "entry": [ 7 | { 8 | "item": { 9 | "reference": "Patient/be92a43f-de46-affa-b131-bbf9eea51140" 10 | } 11 | }, 12 | { 13 | "item": { 14 | "reference": "Patient/420e791b-e419-c19b-3144-29e101c2c12f" 15 | } 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /server/src/main/java/com/google/fhir/gateway/AllowedQueriesConfig.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021-2023 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.google.fhir.gateway; 17 | 18 | import java.util.Arrays; 19 | import java.util.List; 20 | import java.util.Map; 21 | import lombok.Getter; 22 | 23 | /** 24 | * This is a minimal implementation of a configuration for a query allow-list. Each allowed query is 25 | * defined by its expected path and set of expected query parameters. 26 | */ 27 | class AllowedQueriesConfig { 28 | public static final String MATCHES_ANY_VALUE = "ANY_VALUE"; 29 | 30 | // Note this is a very simplistic config for an allow-listed query template; we should expand this 31 | // with information from the access token once needed. 32 | @Getter 33 | public static class AllowedQueryEntry { 34 | // Supports exact path match and special path ending with /ANY_VALUE 35 | // Eg - Composition/ANY_VALUE will match Composition and paths matching Composition/.* regex. 36 | private String path; 37 | 38 | // Case in-sensitive Http request type allowed by the config. 39 | private String requestType; 40 | private Map queryParams; 41 | // If true, this means other parameters not listed in `queryParams` are allowed too. 42 | private boolean allowExtraParams; 43 | // If true, this means all parameters in `queryParams` are required, i.e., none are optional. 44 | private boolean allParamsRequired; 45 | 46 | private boolean allowUnauthenticatedRequests; 47 | 48 | @Override 49 | public String toString() { 50 | String builder = 51 | "path=" 52 | + path 53 | + " queryParams=" 54 | + Arrays.toString(queryParams.entrySet().toArray()) 55 | + " allowExtraParams=" 56 | + allowExtraParams 57 | + " allParamsRequired=" 58 | + allParamsRequired 59 | + " allowUnauthenticatedRequests=" 60 | + allowUnauthenticatedRequests 61 | + " requestType=" 62 | + requestType; 63 | return builder; 64 | } 65 | } 66 | 67 | @Getter List entries; 68 | } 69 | -------------------------------------------------------------------------------- /server/src/main/java/com/google/fhir/gateway/BundlePatients.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021-2023 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.google.fhir.gateway; 17 | 18 | import com.google.api.client.util.Lists; 19 | import com.google.common.collect.ImmutableList; 20 | import com.google.common.collect.ImmutableSet; 21 | import com.google.common.collect.Sets; 22 | import java.util.List; 23 | import java.util.Set; 24 | import lombok.Getter; 25 | 26 | @Getter 27 | public class BundlePatients { 28 | 29 | private final ImmutableList> referencedPatients; 30 | private final ImmutableSet updatedPatients; 31 | private final boolean patientsToCreate; 32 | 33 | private final ImmutableSet deletedPatients; 34 | 35 | private BundlePatients( 36 | List> referencedPatients, 37 | Set updatedPatients, 38 | Set deletedPatients, 39 | boolean patientIdsToCreate) { 40 | this.referencedPatients = ImmutableList.copyOf(referencedPatients); 41 | this.updatedPatients = ImmutableSet.copyOf(updatedPatients); 42 | this.deletedPatients = ImmutableSet.copyOf(deletedPatients); 43 | this.patientsToCreate = patientIdsToCreate; 44 | } 45 | 46 | public boolean areTherePatientToCreate() { 47 | return patientsToCreate; 48 | } 49 | 50 | public static class BundlePatientsBuilder { 51 | private final Set updatedPatients = Sets.newHashSet(); 52 | private final Set deletedPatients = Sets.newHashSet(); 53 | private final List> referencedPatients = Lists.newArrayList(); 54 | private boolean patientsToCreate = false; 55 | 56 | public BundlePatientsBuilder addUpdatePatients(Set patientsToUpdate) { 57 | updatedPatients.addAll(patientsToUpdate); 58 | return this; 59 | } 60 | 61 | public BundlePatientsBuilder addDeletedPatients(Set patientsToDelete) { 62 | deletedPatients.addAll(patientsToDelete); 63 | addReferencedPatients(patientsToDelete); 64 | return this; 65 | } 66 | 67 | enum PatientOp { 68 | UPDATE, 69 | READ 70 | } 71 | 72 | public void addReferencedPatients(Set patientIds) { 73 | referencedPatients.add(ImmutableSet.copyOf(patientIds)); 74 | } 75 | 76 | public void addPatient(PatientOp operation, String patientId) { 77 | if (operation == PatientOp.READ) { 78 | referencedPatients.add(ImmutableSet.of(patientId)); 79 | } 80 | 81 | if (operation == PatientOp.UPDATE) { 82 | updatedPatients.add(patientId); 83 | } 84 | } 85 | 86 | public void setPatientCreationFlag(boolean createPatient) { 87 | patientsToCreate = createPatient; 88 | } 89 | 90 | public BundlePatients build() { 91 | return new BundlePatients( 92 | referencedPatients, updatedPatients, deletedPatients, patientsToCreate); 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /server/src/main/java/com/google/fhir/gateway/CapabilityPostProcessor.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021-2023 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.google.fhir.gateway; 17 | 18 | import ca.uhn.fhir.context.FhirContext; 19 | import ca.uhn.fhir.parser.IParser; 20 | import com.google.common.base.Preconditions; 21 | import com.google.common.io.CharStreams; 22 | import com.google.fhir.gateway.interfaces.AccessDecision; 23 | import com.google.fhir.gateway.interfaces.RequestDetailsReader; 24 | import com.google.fhir.gateway.interfaces.RequestMutation; 25 | import java.io.IOException; 26 | import org.apache.http.HttpResponse; 27 | import org.hl7.fhir.instance.model.api.IBaseResource; 28 | import org.hl7.fhir.r4.model.CapabilityStatement; 29 | import org.hl7.fhir.r4.model.CapabilityStatement.CapabilityStatementRestComponent; 30 | import org.hl7.fhir.r4.model.CapabilityStatement.CapabilityStatementRestSecurityComponent; 31 | import org.hl7.fhir.r4.model.ResourceType; 32 | import org.hl7.fhir.r4.model.codesystems.RestfulSecurityService; 33 | import org.slf4j.Logger; 34 | import org.slf4j.LoggerFactory; 35 | 36 | public class CapabilityPostProcessor implements AccessDecision { 37 | private static final Logger logger = LoggerFactory.getLogger(AccessDecision.class); 38 | private static final String SECURITY_DESCRIPTION = 39 | "To access FHIR resources behind this proxy, each request needs a Bearer Authorization " 40 | + "header containing a JWT access token. This token must have been issued by the " 41 | + "authorization server defined by the configured TOKEN_ISSUER."; 42 | private static CapabilityPostProcessor instance = null; 43 | 44 | private final FhirContext fhirContext; 45 | 46 | private CapabilityPostProcessor(FhirContext fhirContext) { 47 | this.fhirContext = fhirContext; 48 | } 49 | 50 | static synchronized CapabilityPostProcessor getInstance(FhirContext fhirContext) { 51 | if (instance == null) { 52 | instance = new CapabilityPostProcessor(fhirContext); 53 | } 54 | return instance; 55 | } 56 | 57 | @Override 58 | public RequestMutation getRequestMutation(RequestDetailsReader requestDetailsReader) { 59 | return null; 60 | } 61 | 62 | @Override 63 | public boolean canAccess() { 64 | return true; 65 | } 66 | 67 | @Override 68 | public String postProcess(RequestDetailsReader requestDetailsReader, HttpResponse response) 69 | throws IOException { 70 | Preconditions.checkState(HttpUtil.isResponseValid(response)); 71 | String content = CharStreams.toString(HttpUtil.readerFromEntity(response.getEntity())); 72 | IParser parser = fhirContext.newJsonParser(); 73 | IBaseResource resource = parser.parseResource(content); 74 | 75 | if (!FhirUtil.isSameResourceType(resource.fhirType(), ResourceType.CapabilityStatement)) { 76 | String errorMessage = 77 | String.format( 78 | "Expected to get a %s resource; got: %s ", 79 | ResourceType.CapabilityStatement, resource.fhirType()); 80 | logger.error(errorMessage); 81 | return content; 82 | } 83 | CapabilityStatement capability = (CapabilityStatement) resource; 84 | if (!capability.hasRest()) { 85 | return content; 86 | } 87 | for (CapabilityStatementRestComponent rest : capability.getRest()) { 88 | addCors(rest.getSecurity()); 89 | } 90 | return parser.encodeResourceToString(capability); 91 | } 92 | 93 | private void addCors(CapabilityStatementRestSecurityComponent security) { 94 | // See FhirProxyServer.registerCorsInterceptor for our default CORS support. 95 | security.setCors(true); 96 | security 97 | .addService() 98 | .addCoding() 99 | .setSystem(RestfulSecurityService.OAUTH.getSystem()) 100 | .setCode(RestfulSecurityService.OAUTH.toCode()); 101 | security.setDescription(SECURITY_DESCRIPTION); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /server/src/main/java/com/google/fhir/gateway/ExceptionUtil.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021-2023 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.google.fhir.gateway; 17 | 18 | import com.google.common.base.Preconditions; 19 | import java.lang.reflect.InvocationTargetException; 20 | import javax.annotation.Nullable; 21 | import org.slf4j.Logger; 22 | 23 | public class ExceptionUtil { 24 | 25 | static void throwRuntimeExceptionAndLog( 26 | Logger logger, 27 | String errorMessage, 28 | @Nullable Exception origException, 29 | Class runTimeExceptionClass) { 30 | Preconditions.checkNotNull(runTimeExceptionClass); 31 | // logging the error message followed by the stack trace. 32 | if (origException == null) { 33 | logger.error(errorMessage, new Exception("stack-trace")); 34 | } else { 35 | logger.error(errorMessage, origException); 36 | } 37 | try { 38 | throw runTimeExceptionClass.getDeclaredConstructor(String.class).newInstance(errorMessage); 39 | } catch (InstantiationException 40 | | IllegalAccessException 41 | | NoSuchMethodException 42 | | InvocationTargetException e) { 43 | throw new RuntimeException(errorMessage); 44 | } 45 | } 46 | 47 | static void throwRuntimeExceptionAndLog( 48 | Logger logger, String errorMessage, Class runTimeExceptionClass) { 49 | throwRuntimeExceptionAndLog(logger, errorMessage, null, runTimeExceptionClass); 50 | } 51 | 52 | static void throwRuntimeExceptionAndLog(Logger logger, String errorMessage) { 53 | throwRuntimeExceptionAndLog(logger, errorMessage, null, RuntimeException.class); 54 | } 55 | 56 | public static void throwRuntimeExceptionAndLog(Logger logger, String errorMessage, Exception e) { 57 | throwRuntimeExceptionAndLog(logger, errorMessage, e, RuntimeException.class); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /server/src/main/java/com/google/fhir/gateway/FhirClientFactory.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021-2023 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.google.fhir.gateway; 17 | 18 | import com.google.fhir.gateway.GenericFhirClient.GenericFhirClientBuilder; 19 | import java.io.IOException; 20 | 21 | /** 22 | * This is a helper class to create appropriate FHIR clients to talk to the configured FHIR server. 23 | */ 24 | public class FhirClientFactory { 25 | private static final String PROXY_TO_ENV = "PROXY_TO"; 26 | private static final String BACKEND_TYPE_ENV = "BACKEND_TYPE"; 27 | 28 | public static HttpFhirClient createFhirClientFromEnvVars() throws IOException { 29 | String backendType = System.getenv(BACKEND_TYPE_ENV); 30 | if (backendType == null) { 31 | throw new IllegalArgumentException( 32 | String.format("The environment variable %s is not set!", BACKEND_TYPE_ENV)); 33 | } 34 | String fhirStore = System.getenv(PROXY_TO_ENV); 35 | if (fhirStore == null) { 36 | throw new IllegalArgumentException( 37 | String.format("The environment variable %s is not set!", PROXY_TO_ENV)); 38 | } 39 | return chooseHttpFhirClient(backendType, fhirStore); 40 | } 41 | 42 | private static HttpFhirClient chooseHttpFhirClient(String backendType, String fhirStore) 43 | throws IOException { 44 | // TODO add an enum if the list of special FHIR servers grow and rename HAPI to GENERIC. 45 | if (backendType.equals("GCP")) { 46 | return new GcpFhirClient(fhirStore, GcpFhirClient.createCredentials()); 47 | } 48 | 49 | if (backendType.equals("HAPI")) { 50 | return new GenericFhirClientBuilder().setFhirStore(fhirStore).build(); 51 | } 52 | throw new IllegalArgumentException( 53 | String.format( 54 | "The environment variable %s is not set to either GCP or HAPI!", BACKEND_TYPE_ENV)); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /server/src/main/java/com/google/fhir/gateway/FhirUtil.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021-2023 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.google.fhir.gateway; 17 | 18 | import ca.uhn.fhir.context.FhirContext; 19 | import ca.uhn.fhir.parser.IParser; 20 | import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; 21 | import com.google.common.base.Preconditions; 22 | import com.google.fhir.gateway.interfaces.RequestDetailsReader; 23 | import java.io.IOException; 24 | import java.nio.charset.Charset; 25 | import java.nio.charset.StandardCharsets; 26 | import java.util.regex.Pattern; 27 | import javax.annotation.Nullable; 28 | import org.apache.http.HttpResponse; 29 | import org.hl7.fhir.exceptions.FHIRException; 30 | import org.hl7.fhir.instance.model.api.IBaseResource; 31 | import org.hl7.fhir.r4.model.Bundle; 32 | import org.hl7.fhir.r4.model.ResourceType; 33 | import org.slf4j.Logger; 34 | import org.slf4j.LoggerFactory; 35 | 36 | public class FhirUtil { 37 | 38 | private static final Logger logger = LoggerFactory.getLogger(FhirUtil.class); 39 | 40 | // This is based on https://www.hl7.org/fhir/datatypes.html#id 41 | private static final Pattern ID_PATTERN = Pattern.compile("[A-Za-z0-9\\-.]{1,64}"); 42 | 43 | public static boolean isSameResourceType(@Nullable String resourceType, ResourceType type) { 44 | return type.name().equals(resourceType); 45 | } 46 | 47 | public static String getIdOrNull(RequestDetailsReader requestDetails) { 48 | if (requestDetails.getId() == null) { 49 | return null; 50 | } 51 | return requestDetails.getId().getIdPart(); 52 | } 53 | 54 | public static Bundle parseResponseToBundle(FhirContext fhirContext, HttpResponse httpResponse) 55 | throws IOException { 56 | Preconditions.checkState(HttpUtil.isResponseEntityValid(httpResponse)); 57 | IParser jsonParser = fhirContext.newJsonParser(); 58 | IBaseResource resource = jsonParser.parseResource(httpResponse.getEntity().getContent()); 59 | Preconditions.checkArgument( 60 | FhirUtil.isSameResourceType(resource.fhirType(), ResourceType.Bundle)); 61 | return (Bundle) resource; 62 | } 63 | 64 | public static IBaseResource createResourceFromRequest( 65 | FhirContext fhirContext, RequestDetailsReader request) { 66 | byte[] requestContentBytes = request.loadRequestContents(); 67 | Charset charset = request.getCharset(); 68 | if (charset == null) { 69 | charset = StandardCharsets.UTF_8; 70 | } 71 | String requestContent = new String(requestContentBytes, charset); 72 | IParser jsonParser = fhirContext.newJsonParser(); 73 | return jsonParser.parseResource(requestContent); 74 | } 75 | 76 | public static Bundle parseRequestToBundle(FhirContext fhirContext, RequestDetailsReader request) { 77 | IBaseResource resource = createResourceFromRequest(fhirContext, request); 78 | Preconditions.checkArgument( 79 | FhirUtil.isSameResourceType(resource.fhirType(), ResourceType.Bundle)); 80 | return (Bundle) resource; 81 | } 82 | 83 | public static boolean isValidId(String id) { 84 | return ID_PATTERN.matcher(id).matches(); 85 | } 86 | 87 | public static boolean isValidFhirResourceType(String resourceType) { 88 | try { 89 | ResourceType.fromCode(resourceType); 90 | return true; 91 | } catch (FHIRException fe) { 92 | return false; 93 | } 94 | } 95 | 96 | public static String checkIdOrFail(String idPart) { 97 | if (!isValidId(idPart)) { 98 | ExceptionUtil.throwRuntimeExceptionAndLog( 99 | logger, String.format("ID %s is invalid!", idPart), InvalidRequestException.class); 100 | } 101 | return idPart; // This is for convenience. 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /server/src/main/java/com/google/fhir/gateway/GcpFhirClient.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021-2023 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.google.fhir.gateway; 17 | 18 | import com.google.auth.oauth2.AccessToken; 19 | import com.google.auth.oauth2.GoogleCredentials; 20 | import java.io.IOException; 21 | import java.net.URI; 22 | import java.net.URISyntaxException; 23 | import java.util.Collections; 24 | import org.apache.http.Header; 25 | import org.apache.http.client.utils.URIBuilder; 26 | import org.apache.http.message.BasicHeader; 27 | import org.slf4j.Logger; 28 | import org.slf4j.LoggerFactory; 29 | 30 | /** This class adds customizations needed for talking to a GCP FHIR store. */ 31 | public class GcpFhirClient extends HttpFhirClient { 32 | 33 | private static final Logger logger = LoggerFactory.getLogger(GcpFhirClient.class); 34 | 35 | private static final String CLOUD_PLATFORM_SCOPE = 36 | "https://www.googleapis.com/auth/cloud-platform"; 37 | 38 | private final GoogleCredentials credentials; 39 | private final String gcpFhirStore; 40 | 41 | public GcpFhirClient(String gcpFhirStore, GoogleCredentials credentials) throws IOException { 42 | // Remove trailing '/'s since proxy's base URL has no trailing '/'. 43 | this.gcpFhirStore = gcpFhirStore.replaceAll("/+$", ""); 44 | this.credentials = credentials; 45 | 46 | logger.info("Initialized a client for GCP FHIR store: " + gcpFhirStore); 47 | } 48 | 49 | @Override 50 | protected String getBaseUrl() { 51 | return gcpFhirStore; 52 | } 53 | 54 | private String getAccessToken() { 55 | // TODO add support for refresh token expiration. 56 | try { 57 | credentials.refreshIfExpired(); 58 | } catch (IOException e) { 59 | ExceptionUtil.throwRuntimeExceptionAndLog( 60 | logger, "Cannot refresh access token due to: " + e.getMessage(), e); 61 | } 62 | AccessToken accessToken = credentials.getAccessToken(); 63 | if (accessToken == null) { 64 | ExceptionUtil.throwRuntimeExceptionAndLog(logger, "Cannot get an access token!"); 65 | } 66 | return accessToken.getTokenValue(); 67 | } 68 | 69 | @Override 70 | protected URI getUriForResource(String resourcePath) throws URISyntaxException { 71 | String uri = String.format("%s/%s", gcpFhirStore, resourcePath); 72 | URIBuilder uriBuilder = new URIBuilder(uri); 73 | return uriBuilder.build(); 74 | } 75 | 76 | @Override 77 | protected Header getAuthHeader() { 78 | String authToken = String.format("Bearer %s", getAccessToken()); 79 | return new BasicHeader("Authorization", authToken); 80 | } 81 | 82 | public static GoogleCredentials createCredentials() throws IOException { 83 | return GoogleCredentials.getApplicationDefault() 84 | .createScoped(Collections.singleton(GcpFhirClient.CLOUD_PLATFORM_SCOPE)); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /server/src/main/java/com/google/fhir/gateway/GenericFhirClient.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021-2023 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.google.fhir.gateway; 17 | 18 | import java.net.URI; 19 | import java.net.URISyntaxException; 20 | import org.apache.http.Header; 21 | import org.apache.http.client.utils.URIBuilder; 22 | import org.apache.http.message.BasicHeader; 23 | import org.slf4j.Logger; 24 | import org.slf4j.LoggerFactory; 25 | 26 | /** This class adds customizations needed for talking to a Generic FHIR server. */ 27 | public final class GenericFhirClient extends HttpFhirClient { 28 | 29 | private static final Logger logger = LoggerFactory.getLogger(GenericFhirClient.class); 30 | 31 | private final String genericFhirStore; 32 | 33 | private GenericFhirClient(String genericFhirStore) { 34 | this.genericFhirStore = genericFhirStore; 35 | logger.info("Initialized client for generic FHIR server: " + genericFhirStore); 36 | } 37 | 38 | @Override 39 | protected String getBaseUrl() { 40 | return genericFhirStore; 41 | } 42 | 43 | @Override 44 | protected URI getUriForResource(String resourcePath) throws URISyntaxException { 45 | String uri = String.format("%s/%s", genericFhirStore, resourcePath); 46 | URIBuilder uriBuilder = new URIBuilder(uri); 47 | return uriBuilder.build(); 48 | } 49 | 50 | @Override 51 | protected Header getAuthHeader() { 52 | return new BasicHeader("Authorization", ""); 53 | } 54 | 55 | public static class GenericFhirClientBuilder { 56 | private String fhirStore; 57 | 58 | public GenericFhirClientBuilder setFhirStore(String fhirStore) { 59 | this.fhirStore = fhirStore; 60 | return this; 61 | } 62 | 63 | public GenericFhirClient build() { 64 | if (fhirStore == null || fhirStore.isBlank()) { 65 | throw new IllegalArgumentException("FhirStore not set!"); 66 | } 67 | return new GenericFhirClient(fhirStore); 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /server/src/main/java/com/google/fhir/gateway/JwtUtil.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021-2023 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.google.fhir.gateway; 17 | 18 | import ca.uhn.fhir.rest.server.exceptions.AuthenticationException; 19 | import com.auth0.jwt.interfaces.Claim; 20 | import com.auth0.jwt.interfaces.DecodedJWT; 21 | 22 | public class JwtUtil { 23 | public static String getClaimOrDie(DecodedJWT jwt, String claimName) { 24 | Claim claim = jwt.getClaim(claimName); 25 | if (claim.asString() == null) { 26 | throw new AuthenticationException( 27 | String.format("The provided token has no %s claim!", claimName)); 28 | } 29 | return claim.asString(); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /server/src/main/java/com/google/fhir/gateway/PermissiveAccessChecker.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021-2023 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.google.fhir.gateway; 17 | 18 | import ca.uhn.fhir.context.FhirContext; 19 | import com.auth0.jwt.interfaces.DecodedJWT; 20 | import com.google.fhir.gateway.interfaces.AccessChecker; 21 | import com.google.fhir.gateway.interfaces.AccessCheckerFactory; 22 | import com.google.fhir.gateway.interfaces.AccessDecision; 23 | import com.google.fhir.gateway.interfaces.NoOpAccessDecision; 24 | import com.google.fhir.gateway.interfaces.PatientFinder; 25 | import com.google.fhir.gateway.interfaces.RequestDetailsReader; 26 | 27 | /** This is the default no-op access-checker which lets all requests to go through. */ 28 | public class PermissiveAccessChecker implements AccessChecker { 29 | @Override 30 | public AccessDecision checkAccess(RequestDetailsReader requestDetails) { 31 | return new NoOpAccessDecision(true); 32 | } 33 | 34 | // Note this is a special case access-checker and should not have @Named. 35 | public static class Factory implements AccessCheckerFactory { 36 | @Override 37 | public AccessChecker create( 38 | DecodedJWT jwt, 39 | HttpFhirClient httpFhirClient, 40 | FhirContext fhirContext, 41 | PatientFinder patientFinder) { 42 | return new PermissiveAccessChecker(); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /server/src/main/java/com/google/fhir/gateway/ProxyConstants.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021-2023 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.google.fhir.gateway; 17 | 18 | import ca.uhn.fhir.rest.api.Constants; 19 | import org.apache.http.entity.ContentType; 20 | 21 | public class ProxyConstants { 22 | // Note we should not set charset here; otherwise GCP FHIR store complains about Content-Type. 23 | static final ContentType JSON_PATCH_CONTENT = ContentType.create(Constants.CT_JSON_PATCH); 24 | public static final String HTTP_URL_SEPARATOR = "/"; 25 | } 26 | -------------------------------------------------------------------------------- /server/src/main/java/com/google/fhir/gateway/RequestDetailsToReader.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021-2023 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.google.fhir.gateway; 17 | 18 | import ca.uhn.fhir.context.FhirContext; 19 | import ca.uhn.fhir.rest.api.RequestTypeEnum; 20 | import ca.uhn.fhir.rest.api.RestOperationTypeEnum; 21 | import ca.uhn.fhir.rest.api.server.RequestDetails; 22 | import com.google.fhir.gateway.interfaces.RequestDetailsReader; 23 | import java.nio.charset.Charset; 24 | import java.util.List; 25 | import java.util.Map; 26 | import org.hl7.fhir.instance.model.api.IIdType; 27 | 28 | // Note instances of this class are expected to be one per thread and this class is not thread-safe 29 | // the same way the underlying `requestDetails` is not. 30 | public class RequestDetailsToReader implements RequestDetailsReader { 31 | private final RequestDetails requestDetails; 32 | 33 | RequestDetailsToReader(RequestDetails requestDetails) { 34 | this.requestDetails = requestDetails; 35 | } 36 | 37 | public String getRequestId() { 38 | return requestDetails.getRequestId(); 39 | } 40 | 41 | public Charset getCharset() { 42 | return requestDetails.getCharset(); 43 | } 44 | 45 | public String getCompleteUrl() { 46 | return requestDetails.getCompleteUrl(); 47 | } 48 | 49 | public FhirContext getFhirContext() { 50 | // TODO: There might be a race condition in the underlying `getFhirContext`; check if this is 51 | // true. Note the `myServer` object is shared between threads. 52 | return requestDetails.getFhirContext(); 53 | } 54 | 55 | public String getFhirServerBase() { 56 | return requestDetails.getFhirServerBase(); 57 | } 58 | 59 | public String getHeader(String name) { 60 | return requestDetails.getHeader(name); 61 | } 62 | 63 | public List getHeaders(String name) { 64 | return requestDetails.getHeaders(name); 65 | } 66 | 67 | public IIdType getId() { 68 | return requestDetails.getId(); 69 | } 70 | 71 | public String getOperation() { 72 | return requestDetails.getOperation(); 73 | } 74 | 75 | public Map getParameters() { 76 | return requestDetails.getParameters(); 77 | } 78 | 79 | public String getRequestPath() { 80 | return requestDetails.getRequestPath(); 81 | } 82 | 83 | public RequestTypeEnum getRequestType() { 84 | return requestDetails.getRequestType(); 85 | } 86 | 87 | public String getResourceName() { 88 | return requestDetails.getResourceName(); 89 | } 90 | 91 | public RestOperationTypeEnum getRestOperationType() { 92 | return requestDetails.getRestOperationType(); 93 | } 94 | 95 | public String getSecondaryOperation() { 96 | return requestDetails.getSecondaryOperation(); 97 | } 98 | 99 | public boolean isRespondGzip() { 100 | return requestDetails.isRespondGzip(); 101 | } 102 | 103 | public byte[] loadRequestContents() { 104 | return requestDetails.loadRequestContents(); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /server/src/main/java/com/google/fhir/gateway/interfaces/AccessChecker.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021-2023 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.google.fhir.gateway.interfaces; 17 | 18 | /** 19 | * The main interface for deciding whether to grant access to a request or not. Implementations of 20 | * this do not have to be thread-safe as it is guaranteed by the server code not to call {@code 21 | * checkAccess} concurrently. 22 | */ 23 | public interface AccessChecker { 24 | 25 | /** 26 | * Checks whether the current user has access to requested resources. 27 | * 28 | * @param requestDetails details about the resource and operation requested 29 | * @return the outcome of access checking 30 | */ 31 | AccessDecision checkAccess(RequestDetailsReader requestDetails); 32 | } 33 | -------------------------------------------------------------------------------- /server/src/main/java/com/google/fhir/gateway/interfaces/AccessCheckerFactory.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021-2023 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.google.fhir.gateway.interfaces; 17 | 18 | import ca.uhn.fhir.context.FhirContext; 19 | import ca.uhn.fhir.rest.server.exceptions.AuthenticationException; 20 | import com.auth0.jwt.interfaces.DecodedJWT; 21 | import com.google.fhir.gateway.HttpFhirClient; 22 | 23 | /** 24 | * The factory for creating {@link AccessChecker} instances. A single instance of this might be used 25 | * for multiple queries; this is expected to be thread-safe. 26 | */ 27 | public interface AccessCheckerFactory { 28 | 29 | /** 30 | * Creates an AccessChecker for a given FHIR store and JWT. Note the scope of this is for a single 31 | * access token, i.e., one instance is created for each request. 32 | * 33 | * @param jwt the access token in the JWT format; after being validated and decoded. 34 | * @param httpFhirClient the client to use for accessing the FHIR store. 35 | * @param fhirContext the FhirContext object that can be used for creating other HAPI FHIR 36 | * objects. This is an expensive object and should not be recreated for each access check. 37 | * @param patientFinder the utility class for finding patient IDs in query parameters/resources. 38 | * @return an AccessChecker; should never be {@code null}. 39 | * @throws AuthenticationException if an AccessChecker cannot be created for the given token; this 40 | * is where AccessChecker specific errors can be communicated to the user. 41 | */ 42 | AccessChecker create( 43 | DecodedJWT jwt, 44 | HttpFhirClient httpFhirClient, 45 | FhirContext fhirContext, 46 | PatientFinder patientFinder) 47 | throws AuthenticationException; 48 | } 49 | -------------------------------------------------------------------------------- /server/src/main/java/com/google/fhir/gateway/interfaces/AccessDecision.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021-2023 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.google.fhir.gateway.interfaces; 17 | 18 | import java.io.IOException; 19 | import javax.annotation.Nullable; 20 | import org.apache.http.HttpResponse; 21 | 22 | public interface AccessDecision { 23 | 24 | /** 25 | * @return true iff access was granted. 26 | */ 27 | boolean canAccess(); 28 | 29 | /** 30 | * Allows the incoming request mutation based on the access decision. 31 | * 32 | *

Response is used to mutate the incoming request before executing the FHIR operation. We 33 | * currently only support query parameters update for GET Http method. This is expected to be 34 | * called after checking the access using @canAccess method. Mutating the request before checking 35 | * access can have side effect of wrong access check. 36 | * 37 | * @param requestDetailsReader details about the resource and operation requested 38 | * @return mutation to be applied on the incoming request or null if no mutation required 39 | */ 40 | @Nullable 41 | RequestMutation getRequestMutation(RequestDetailsReader requestDetailsReader); 42 | 43 | /** 44 | * Depending on the outcome of the FHIR operations, this does any post-processing operations that 45 | * are related to access policies. This is expected to be called only if the actual FHIR operation 46 | * is finished successfully. 47 | * 48 | *

An example of this is when a new patient is created as the result of the query and that 49 | * patient ID should be added to some access lists. 50 | * 51 | * @param request the client to server request details 52 | * @param response the response returned from the FHIR store 53 | * @return the response entity content (with any post-processing modifications needed) if this 54 | * reads the response; otherwise null. Note that we should try to avoid reading the whole 55 | * content in memory whenever it is not needed for post-processing. 56 | */ 57 | String postProcess(RequestDetailsReader request, HttpResponse response) throws IOException; 58 | } 59 | -------------------------------------------------------------------------------- /server/src/main/java/com/google/fhir/gateway/interfaces/NoOpAccessDecision.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021-2023 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.google.fhir.gateway.interfaces; 17 | 18 | import org.apache.http.HttpResponse; 19 | 20 | public final class NoOpAccessDecision implements AccessDecision { 21 | 22 | private final boolean accessGranted; 23 | 24 | public NoOpAccessDecision(boolean accessGranted) { 25 | this.accessGranted = accessGranted; 26 | } 27 | 28 | @Override 29 | public RequestMutation getRequestMutation(RequestDetailsReader requestDetailsReader) { 30 | return null; 31 | } 32 | 33 | @Override 34 | public boolean canAccess() { 35 | return accessGranted; 36 | } 37 | 38 | @Override 39 | public String postProcess(RequestDetailsReader requestDetailsReader, HttpResponse response) { 40 | return null; 41 | } 42 | 43 | public static AccessDecision accessGranted() { 44 | return new NoOpAccessDecision(true); 45 | } 46 | 47 | public static AccessDecision accessDenied() { 48 | return new NoOpAccessDecision(false); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /server/src/main/java/com/google/fhir/gateway/interfaces/PatientFinder.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021-2024 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.google.fhir.gateway.interfaces; 17 | 18 | import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; 19 | import com.google.fhir.gateway.BundlePatients; 20 | import java.util.Set; 21 | import org.hl7.fhir.r4.model.Bundle; 22 | 23 | public interface PatientFinder { 24 | /** 25 | * Finds the patient ID from the query if it is a direct Patient fetch (i.e., /Patient/PID) or the 26 | * patient can be inferred from query parameters. 27 | * 28 | * @param requestDetails the request 29 | * @return the ids of the patients that this query belongs to or an empty set if it cannot be 30 | * inferred (never null). 31 | * @throws InvalidRequestException for various reasons when unexpected parameters or content are 32 | * encountered. Callers are expected to deny access when this happens. 33 | */ 34 | // TODO add @NotNull once we decide on null-check tooling. 35 | Set findPatientsFromParams(RequestDetailsReader requestDetails); 36 | 37 | /** 38 | * Find all patients referenced or updated in a Bundle. 39 | * 40 | * @param bundle bundle request to find patient references in. 41 | * @return the {@link BundlePatients} that wraps all found patients. 42 | * @throws InvalidRequestException for various reasons when unexpected content is encountered. 43 | * Callers are expected to deny access when this happens. 44 | */ 45 | BundlePatients findPatientsInBundle(Bundle bundle); 46 | 47 | /** 48 | * Finds all patients in the content of a request. 49 | * 50 | * @param request that is expected to have a Bundle content. 51 | * @return the {@link BundlePatients} that wraps all found patients. 52 | * @throws InvalidRequestException for various reasons when unexpected content is encountered. 53 | * Callers are expected to deny access when this happens. 54 | */ 55 | Set findPatientsInResource(RequestDetailsReader request); 56 | 57 | /** 58 | * Finds all patients in the body of a patch request 59 | * 60 | * @param request that is expected to have a body with a patch 61 | * @param resourceName the FHIR resource being patched 62 | * @return the set of patient ids in the patch 63 | * @throws InvalidRequestException for various reasons when unexpected content is encountered. 64 | * Callers are expected to deny access when this happens. 65 | */ 66 | Set findPatientsInPatch(RequestDetailsReader request, String resourceName); 67 | } 68 | -------------------------------------------------------------------------------- /server/src/main/java/com/google/fhir/gateway/interfaces/RequestDetailsReader.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021-2023 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.google.fhir.gateway.interfaces; 17 | 18 | import ca.uhn.fhir.context.FhirContext; 19 | import ca.uhn.fhir.rest.api.RequestTypeEnum; 20 | import ca.uhn.fhir.rest.api.RestOperationTypeEnum; 21 | import java.nio.charset.Charset; 22 | import java.util.List; 23 | import java.util.Map; 24 | import org.hl7.fhir.instance.model.api.IIdType; 25 | 26 | /** 27 | * This is mostly a wrapper for {@link ca.uhn.fhir.rest.api.server.RequestDetails} exposing an 28 | * immutable subset of its API; there are minor exceptions like {@code loadRequestContents}. The 29 | * method names are preserved; for documentation see {@code RequestDetails}. 30 | */ 31 | public interface RequestDetailsReader { 32 | 33 | String getRequestId(); 34 | 35 | Charset getCharset(); 36 | 37 | String getCompleteUrl(); 38 | 39 | FhirContext getFhirContext(); 40 | 41 | String getFhirServerBase(); 42 | 43 | String getHeader(String name); 44 | 45 | List getHeaders(String name); 46 | 47 | IIdType getId(); 48 | 49 | String getOperation(); 50 | 51 | Map getParameters(); 52 | 53 | String getRequestPath(); 54 | 55 | RequestTypeEnum getRequestType(); 56 | 57 | String getResourceName(); 58 | 59 | RestOperationTypeEnum getRestOperationType(); 60 | 61 | String getSecondaryOperation(); 62 | 63 | boolean isRespondGzip(); 64 | 65 | byte[] loadRequestContents(); 66 | } 67 | -------------------------------------------------------------------------------- /server/src/main/java/com/google/fhir/gateway/interfaces/RequestMutation.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021-2024 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.google.fhir.gateway.interfaces; 17 | 18 | import java.util.ArrayList; 19 | import java.util.HashMap; 20 | import java.util.List; 21 | import java.util.Map; 22 | import lombok.Builder; 23 | import lombok.Getter; 24 | 25 | /** Defines mutations that can be applied to the incoming request by an {@link AccessChecker}. */ 26 | @Builder 27 | @Getter 28 | public class RequestMutation { 29 | 30 | // Additional query parameters and list of values for a parameter that should be added to the 31 | // outgoing FHIR request. 32 | // New values overwrites the old one if there is a conflict for a request param (i.e. a returned 33 | // parameter in RequestMutation is already present in the original request). 34 | // Old parameter values should be explicitly retained while mutating values for that parameter. 35 | @Builder.Default Map> additionalQueryParams = new HashMap<>(); 36 | 37 | // Query parameters that are no longer needed when forwarding the request to the upstream server 38 | // Parameters with the keys in this list will be removed 39 | @Builder.Default List discardQueryParams = new ArrayList<>(); 40 | } 41 | -------------------------------------------------------------------------------- /server/src/main/resources/README.md: -------------------------------------------------------------------------------- 1 | # Description of resource files 2 | 3 | - `CompartmentDefinition-patient.json`: This is from the FHIR specification. It 4 | can be fetched on-the-fly when the proxy runs. However, given the importance 5 | of this resource and to make things simpler, it is downloaded and made 6 | available statically: 7 | 8 | ```shell 9 | $ curl -X GET -L -H "Accept: application/fhir+json" \ 10 | http://hl7.org/fhir/CompartmentDefinition/patient \ 11 | -o CompartmentDefinition-patient.json 12 | ``` 13 | 14 | **NOTE**: We have also made changes to this file due to 15 | [b/215051963](b/215051963). 16 | 17 | - `patient_paths.json`: For each FHIR resource, this file has the corresponding 18 | mapping of the list of FHIR paths that should be searched for finding patients 19 | in that resource. This is used for access control of POST and PUT requests 20 | where a resource is provided by the client (see [b/209207333](b/209207333)). 21 | 22 | - `logback.xml`: The Logback configuration 23 | -------------------------------------------------------------------------------- /server/src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 18 | 19 | 20 | 21 | 22 | INFO 23 | 24 | 25 | %d{HH:mm:ss.SSS} [%thread] %-5level %-40logger{40} [%file:%line] %msg%n 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /server/src/main/resources/patient_paths.json: -------------------------------------------------------------------------------- 1 | { 2 | "Account": [ 3 | "subject" 4 | ], 5 | "AdverseEvent": [ 6 | "subject" 7 | ], 8 | "AllergyIntolerance": [ 9 | "patient", 10 | "recorder", 11 | "asserter" 12 | ], 13 | "Appointment": [ 14 | "participant.actor" 15 | ], 16 | "AppointmentResponse": [ 17 | "actor" 18 | ], 19 | "AuditEvent": [ 20 | "agent.who" 21 | ], 22 | "Basic": [ 23 | "subject", 24 | "author" 25 | ], 26 | "BodyStructure": [ 27 | "patient" 28 | ], 29 | "CarePlan": [ 30 | "subject", 31 | "activity.detail.performer" 32 | ], 33 | "CareTeam": [ 34 | "subject", 35 | "participant.member" 36 | ], 37 | "ChargeItem": [ 38 | "subject" 39 | ], 40 | "Claim": [ 41 | "patient", 42 | "payee.party" 43 | ], 44 | "ClaimResponse": [ 45 | "patient" 46 | ], 47 | "ClinicalImpression": [ 48 | "subject" 49 | ], 50 | "Communication": [ 51 | "subject", 52 | "sender", 53 | "recipient" 54 | ], 55 | "CommunicationRequest": [ 56 | "subject", 57 | "sender", 58 | "recipient", 59 | "requester" 60 | ], 61 | "Composition": [ 62 | "subject", 63 | "author", 64 | "attester.party" 65 | ], 66 | "Condition": [ 67 | "subject", 68 | "asserter" 69 | ], 70 | "Consent": [ 71 | "patient" 72 | ], 73 | "Coverage": [ 74 | "policyHolder", 75 | "subscriber", 76 | "beneficiary", 77 | "payor" 78 | ], 79 | "CoverageEligibilityRequest": [ 80 | "patient" 81 | ], 82 | "CoverageEligibilityResponse": [ 83 | "patient" 84 | ], 85 | "DetectedIssue": [ 86 | "patient" 87 | ], 88 | "DeviceRequest": [ 89 | "subject", 90 | "performer" 91 | ], 92 | "DeviceUseStatement": [ 93 | "subject" 94 | ], 95 | "DiagnosticReport": [ 96 | "subject" 97 | ], 98 | "DocumentManifest": [ 99 | "subject", 100 | "author", 101 | "recipient" 102 | ], 103 | "DocumentReference": [ 104 | "subject", 105 | "author" 106 | ], 107 | "Encounter": [ 108 | "subject" 109 | ], 110 | "EnrollmentRequest": [ 111 | "candidate" 112 | ], 113 | "EpisodeOfCare": [ 114 | "patient" 115 | ], 116 | "ExplanationOfBenefit": [ 117 | "patient", 118 | "payee.party" 119 | ], 120 | "FamilyMemberHistory": [ 121 | "patient" 122 | ], 123 | "Flag": [ 124 | "subject" 125 | ], 126 | "Goal": [ 127 | "subject" 128 | ], 129 | "Group": [ 130 | "member.entity" 131 | ], 132 | "ImagingStudy": [ 133 | "subject" 134 | ], 135 | "Immunization": [ 136 | "patient" 137 | ], 138 | "ImmunizationEvaluation": [ 139 | "patient" 140 | ], 141 | "ImmunizationRecommendation": [ 142 | "patient" 143 | ], 144 | "Invoice": [ 145 | "subject", 146 | "recipient" 147 | ], 148 | "List": [ 149 | "subject", 150 | "source" 151 | ], 152 | "MeasureReport": [ 153 | "subject" 154 | ], 155 | "Media": [ 156 | "subject" 157 | ], 158 | "MedicationAdministration": [ 159 | "performer.actor", 160 | "subject" 161 | ], 162 | "MedicationDispense": [ 163 | "subject", 164 | "receiver" 165 | ], 166 | "MedicationRequest": [ 167 | "subject" 168 | ], 169 | "MedicationStatement": [ 170 | "subject" 171 | ], 172 | "MolecularSequence": [ 173 | "patient" 174 | ], 175 | "NutritionOrder": [ 176 | "patient" 177 | ], 178 | "Observation": [ 179 | "subject", 180 | "patient", 181 | "performer" 182 | ], 183 | "Person": [ 184 | "link.target" 185 | ], 186 | "Procedure": [ 187 | "subject", 188 | "performer.actor" 189 | ], 190 | "Provenance": [ 191 | "target" 192 | ], 193 | "QuestionnaireResponse": [ 194 | "subject", 195 | "author" 196 | ], 197 | "RelatedPerson": [ 198 | "patient" 199 | ], 200 | "RequestGroup": [ 201 | "subject", 202 | "action.participant" 203 | ], 204 | "ResearchSubject": [ 205 | "individual" 206 | ], 207 | "RiskAssessment": [ 208 | "subject" 209 | ], 210 | "Schedule": [ 211 | "actor" 212 | ], 213 | "ServiceRequest": [ 214 | "subject", 215 | "performer" 216 | ], 217 | "Specimen": [ 218 | "subject" 219 | ], 220 | "SupplyDelivery": [ 221 | "patient" 222 | ], 223 | "SupplyRequest": [ 224 | "deliverTo" 225 | ], 226 | "VisionPrescription": [ 227 | "patient" 228 | ] 229 | } 230 | -------------------------------------------------------------------------------- /server/src/main/webapp/WEB-INF/web.xml: -------------------------------------------------------------------------------- 1 | 18 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /server/src/test/java/com/google/fhir/gateway/FhirUtilTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021-2023 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.google.fhir.gateway; 17 | 18 | import static org.hamcrest.MatcherAssert.assertThat; 19 | import static org.hamcrest.Matchers.equalTo; 20 | import static org.mockito.Mockito.mock; 21 | import static org.mockito.Mockito.when; 22 | 23 | import ca.uhn.fhir.context.FhirContext; 24 | import com.google.common.io.Resources; 25 | import com.google.fhir.gateway.interfaces.RequestDetailsReader; 26 | import java.io.IOException; 27 | import java.net.URL; 28 | import org.junit.Test; 29 | import org.junit.runner.RunWith; 30 | import org.mockito.junit.MockitoJUnitRunner; 31 | 32 | @RunWith(MockitoJUnitRunner.class) 33 | public class FhirUtilTest { 34 | 35 | private static final FhirContext fhirContext = FhirContext.forR4(); 36 | 37 | @Test 38 | public void isValidIdPass() { 39 | assertThat(FhirUtil.isValidId("simple-id"), equalTo(true)); 40 | } 41 | 42 | @Test 43 | public void isValidIdDotPass() { 44 | assertThat(FhirUtil.isValidId("id.with.dots"), equalTo(true)); 45 | } 46 | 47 | @Test 48 | public void isValidIdUnderscoreFails() { 49 | assertThat(FhirUtil.isValidId("id_with_underscore"), equalTo(false)); 50 | } 51 | 52 | @Test 53 | public void isValidIdPercentFails() { 54 | assertThat(FhirUtil.isValidId("id-with-%"), equalTo(false)); 55 | } 56 | 57 | @Test 58 | public void isValidIdWithNumbersPass() { 59 | assertThat(FhirUtil.isValidId("id-with-numbers-892-12"), equalTo(true)); 60 | } 61 | 62 | @Test 63 | public void isValidIdSlashFails() { 64 | assertThat(FhirUtil.isValidId("id-with-//"), equalTo(false)); 65 | } 66 | 67 | @Test 68 | public void isValidIdTooShort() { 69 | assertThat(FhirUtil.isValidId(""), equalTo(false)); 70 | } 71 | 72 | @Test 73 | public void isValidIdTooLong() { 74 | assertThat( 75 | FhirUtil.isValidId( 76 | "too-long-id-0123456789012345678901234567890123456789012345678901234567890123456789"), 77 | equalTo(false)); 78 | } 79 | 80 | @Test 81 | public void isValidIdNull() { 82 | assertThat(FhirUtil.isValidId(""), equalTo(false)); 83 | } 84 | 85 | @Test 86 | public void canParseValidBundle() throws IOException { 87 | URL bundleUrl = Resources.getResource("patient_id_search.json"); 88 | byte[] bundleBytes = Resources.toByteArray(bundleUrl); 89 | RequestDetailsReader requestMock = mock(RequestDetailsReader.class); 90 | when(requestMock.loadRequestContents()).thenReturn(bundleBytes); 91 | assertThat( 92 | FhirUtil.parseRequestToBundle(fhirContext, requestMock).getEntry().size(), equalTo(10)); 93 | } 94 | 95 | @Test(expected = IllegalArgumentException.class) 96 | public void throwExceptionOnNonBundleResource() throws IOException { 97 | URL bundleUrl = Resources.getResource("test_patient.json"); 98 | byte[] bundleBytes = Resources.toByteArray(bundleUrl); 99 | RequestDetailsReader requestMock = mock(RequestDetailsReader.class); 100 | when(requestMock.loadRequestContents()).thenReturn(bundleBytes); 101 | FhirUtil.parseRequestToBundle(fhirContext, requestMock).getEntry().size(); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /server/src/test/java/com/google/fhir/gateway/GcpFhirClientTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021-2023 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.google.fhir.gateway; 17 | 18 | import static org.hamcrest.MatcherAssert.assertThat; 19 | import static org.hamcrest.Matchers.equalTo; 20 | 21 | import com.google.auth.oauth2.AccessToken; 22 | import com.google.auth.oauth2.GoogleCredentials; 23 | import java.io.IOException; 24 | import java.net.URI; 25 | import java.net.URISyntaxException; 26 | import java.util.List; 27 | import org.apache.http.Header; 28 | import org.apache.http.HttpResponse; 29 | import org.apache.http.message.BasicHeader; 30 | import org.junit.Before; 31 | import org.junit.Test; 32 | import org.junit.runner.RunWith; 33 | import org.mockito.Mock; 34 | import org.mockito.Mockito; 35 | import org.mockito.junit.MockitoJUnitRunner; 36 | 37 | @RunWith(MockitoJUnitRunner.class) 38 | public class GcpFhirClientTest { 39 | 40 | private GcpFhirClient gcpFhirClient; 41 | 42 | private final GoogleCredentials mockCredential = 43 | GoogleCredentials.create(new AccessToken("complicatedCode", null)); 44 | 45 | @Mock private HttpResponse fhirResponseMock = Mockito.mock(HttpResponse.class); 46 | 47 | @Before 48 | public void setUp() throws IOException { 49 | gcpFhirClient = new GcpFhirClient("test", mockCredential); 50 | Header[] mockHeader = { 51 | new BasicHeader("LAST-MODIFIED", "today"), 52 | new BasicHeader("date", "yesterday"), 53 | new BasicHeader("keep", "no") 54 | }; 55 | Mockito.when(fhirResponseMock.getAllHeaders()).thenReturn(mockHeader); 56 | } 57 | 58 | @Test 59 | public void getHeaderTest() { 60 | Header header = gcpFhirClient.getAuthHeader(); 61 | assertThat(header.getElements().length, equalTo(1)); 62 | assertThat(header.getElements()[0].getName(), equalTo("Bearer complicatedCode")); 63 | } 64 | 65 | @Test 66 | public void getUriForResourceTest() throws URISyntaxException { 67 | URI uri = gcpFhirClient.getUriForResource("hello/world"); 68 | assertThat(uri.toString(), equalTo("test/hello/world")); 69 | } 70 | 71 | @Test 72 | public void responseHeadersToKeepTest() { 73 | List

headersToKeep = gcpFhirClient.responseHeadersToKeep(fhirResponseMock); 74 | assertThat(headersToKeep.size(), equalTo(2)); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /server/src/test/java/com/google/fhir/gateway/GenericFhirClientTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021-2023 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.google.fhir.gateway; 17 | 18 | import static org.hamcrest.MatcherAssert.assertThat; 19 | import static org.hamcrest.Matchers.equalTo; 20 | 21 | import com.google.fhir.gateway.GenericFhirClient.GenericFhirClientBuilder; 22 | import java.net.URI; 23 | import java.net.URISyntaxException; 24 | import org.apache.http.Header; 25 | import org.junit.Test; 26 | import org.junit.runner.RunWith; 27 | import org.mockito.junit.MockitoJUnitRunner; 28 | 29 | @RunWith(MockitoJUnitRunner.class) 30 | public class GenericFhirClientTest { 31 | 32 | @Test(expected = IllegalArgumentException.class) 33 | public void buildGenericFhirClientFhirStoreNotSetTest() { 34 | new GenericFhirClientBuilder().build(); 35 | } 36 | 37 | @Test(expected = IllegalArgumentException.class) 38 | public void buildGenericFhirClientNoFhirStoreBlankTest() { 39 | new GenericFhirClientBuilder().setFhirStore(" ").build(); 40 | } 41 | 42 | @Test 43 | public void getAuthHeaderNoUsernamePasswordTest() { 44 | GenericFhirClient genericFhirClient = 45 | new GenericFhirClientBuilder().setFhirStore("random.fhir").build(); 46 | Header header = genericFhirClient.getAuthHeader(); 47 | assertThat(header.getName(), equalTo("Authorization")); 48 | assertThat(header.getValue(), equalTo("")); 49 | } 50 | 51 | @Test 52 | public void getUriForResourceTest() throws URISyntaxException { 53 | GenericFhirClient genericFhirClient = 54 | new GenericFhirClientBuilder().setFhirStore("random.fhir").build(); 55 | URI uri = genericFhirClient.getUriForResource("hello/world"); 56 | assertThat(uri.toString(), equalTo("random.fhir/hello/world")); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /server/src/test/java/com/google/fhir/gateway/HttpFhirClientTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021-2023 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.google.fhir.gateway; 17 | 18 | import static org.hamcrest.MatcherAssert.assertThat; 19 | import static org.hamcrest.Matchers.arrayContainingInAnyOrder; 20 | import static org.hamcrest.Matchers.containsInAnyOrder; 21 | import static org.hamcrest.Matchers.empty; 22 | import static org.hamcrest.Matchers.nullValue; 23 | import static org.mockito.Mockito.when; 24 | 25 | import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; 26 | import java.util.Arrays; 27 | import java.util.HashMap; 28 | import java.util.List; 29 | import java.util.Map; 30 | import org.apache.http.Header; 31 | import org.apache.http.HttpResponse; 32 | import org.apache.http.client.methods.RequestBuilder; 33 | import org.apache.http.message.BasicHeader; 34 | import org.junit.Test; 35 | import org.junit.runner.RunWith; 36 | import org.mockito.Mock; 37 | import org.mockito.Spy; 38 | import org.mockito.junit.MockitoJUnitRunner; 39 | 40 | @RunWith(MockitoJUnitRunner.class) 41 | public class HttpFhirClientTest { 42 | 43 | @Spy private HttpFhirClient fhirClient; 44 | 45 | @Mock private ServletRequestDetails requestMock; 46 | 47 | @Mock private HttpResponse httpResponse; 48 | 49 | @Test 50 | public void copyRequiredHeaders_passAllowedHeaders_addsToRequest() { 51 | Map> headers = new HashMap<>(); 52 | String allowedRequestHeader = "etag"; 53 | List headerValues = List.of("test-val-1", "test-val-2"); 54 | headers.put(allowedRequestHeader, headerValues); 55 | when(requestMock.getHeaders()).thenReturn(headers); 56 | RequestBuilder requestBuilder = RequestBuilder.create("POST"); 57 | 58 | fhirClient.copyRequiredHeaders(requestMock, requestBuilder); 59 | 60 | assertThat( 61 | Arrays.stream(requestBuilder.getHeaders(allowedRequestHeader)) 62 | .map(header -> (BasicHeader) header) 63 | .map(BasicHeader::getValue) 64 | .toArray(), 65 | arrayContainingInAnyOrder("test-val-1", "test-val-2")); 66 | } 67 | 68 | @Test 69 | public void copyRequiredHeaders_passNotAllowedHeaders_emptyRequestHeaders() { 70 | Map> headers = new HashMap<>(); 71 | String unsupportedHeader = "unsupported-header"; 72 | headers.put(unsupportedHeader, List.of("test-val-1", "test-val-2")); 73 | when(requestMock.getHeaders()).thenReturn(headers); 74 | RequestBuilder requestBuilder = RequestBuilder.create("POST"); 75 | 76 | fhirClient.copyRequiredHeaders(requestMock, requestBuilder); 77 | 78 | assertThat(requestBuilder.getHeaders(unsupportedHeader), nullValue()); 79 | } 80 | 81 | @Test 82 | public void responseHeadersToKeep_addAllowedHeaders() { 83 | String allowedRequestHeader = "etag"; 84 | Header[] headers = { 85 | new BasicHeader(allowedRequestHeader, "test-val-1"), 86 | new BasicHeader(allowedRequestHeader, "test-val-2") 87 | }; 88 | when(httpResponse.getAllHeaders()).thenReturn(headers); 89 | 90 | List
responseHeaders = fhirClient.responseHeadersToKeep(httpResponse); 91 | 92 | assertThat(responseHeaders, containsInAnyOrder(headers)); 93 | } 94 | 95 | @Test 96 | public void responseHeadersToKeep_passNotAllowedHeaders_emptyRequestHeaders() { 97 | String unsupportedHeader = "unsupportedHeader"; 98 | Header[] headers = { 99 | new BasicHeader(unsupportedHeader, "test-val-1"), 100 | new BasicHeader(unsupportedHeader, "test-val-2") 101 | }; 102 | when(httpResponse.getAllHeaders()).thenReturn(headers); 103 | 104 | List
responseHeaders = fhirClient.responseHeadersToKeep(httpResponse); 105 | 106 | assertThat(responseHeaders, empty()); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /server/src/test/java/com/google/fhir/gateway/TestUtil.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021-2023 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.google.fhir.gateway; 17 | 18 | import static org.mockito.Mockito.when; 19 | 20 | import com.google.common.base.Preconditions; 21 | import java.nio.charset.StandardCharsets; 22 | import org.apache.http.HttpResponse; 23 | import org.apache.http.HttpStatus; 24 | import org.apache.http.entity.StringEntity; 25 | 26 | class TestUtil { 27 | 28 | public static void setUpFhirResponseMock(HttpResponse fhirResponseMock, String responseJson) { 29 | Preconditions.checkNotNull(responseJson); 30 | StringEntity testEntity = new StringEntity(responseJson, StandardCharsets.UTF_8); 31 | when(fhirResponseMock.getStatusLine().getStatusCode()).thenReturn(HttpStatus.SC_OK); 32 | when(fhirResponseMock.getEntity()).thenReturn(testEntity); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /server/src/test/resources/allowed_queries_with_no_extra_params.json: -------------------------------------------------------------------------------- 1 | { 2 | "entries": [ 3 | { 4 | "path": "", 5 | "queryParams": { 6 | "_getpages": "ANY_VALUE" 7 | }, 8 | "allowExtraParams": false, 9 | "allParamsRequired": true 10 | } 11 | ] 12 | } -------------------------------------------------------------------------------- /server/src/test/resources/allowed_queries_with_path_type.json: -------------------------------------------------------------------------------- 1 | { 2 | "entries": [ 3 | { 4 | "path": "Observation", 5 | "queryParams": { 6 | } 7 | }, 8 | { 9 | "path": "Composition/ANY_VALUE", 10 | "queryParams": { 11 | } 12 | }, 13 | { 14 | "path": "Encounter", 15 | "queryParams": { 16 | }, 17 | "requestType" : "Get" 18 | } 19 | ] 20 | } -------------------------------------------------------------------------------- /server/src/test/resources/allowed_unauthenticated_queries.json: -------------------------------------------------------------------------------- 1 | { 2 | "entries": [ 3 | { 4 | "path": "Composition", 5 | "allowUnauthenticatedRequests": true, 6 | "queryParams": { 7 | "_getpages": "ANY_VALUE" 8 | } 9 | } 10 | ] 11 | } -------------------------------------------------------------------------------- /server/src/test/resources/capability.json: -------------------------------------------------------------------------------- 1 | { 2 | "resourceType": "CapabilityStatement", 3 | "id": "test", 4 | "name": "modified from sample instance /metadata", 5 | "status": "draft", 6 | "date": "2022-02-18", 7 | "publisher": "Google", 8 | "description": "A CapabilityStatement for testing proxy functionality", 9 | "kind": "instance", 10 | "fhirVersion": "4.0.1", 11 | "format": [ 12 | "json" 13 | ], 14 | "rest": [ 15 | { 16 | "documentation": "Main FHIR endpoint for FHIR store fhir_r4_1M_a", 17 | "interaction": [ 18 | { 19 | "code": "batch" 20 | }, 21 | { 22 | "code": "transaction" 23 | }, 24 | { 25 | "code": "search-system" 26 | } 27 | ], 28 | "mode": "server", 29 | "operation": [ 30 | { 31 | "definition": "OperationDefinition/Patient-everything", 32 | "name": "everything" 33 | }, 34 | { 35 | "definition": "OperationDefinition/Observation-lastn", 36 | "name": "lastn" 37 | }, 38 | { 39 | "definition": "OperationDefinition/ConceptMap-translate", 40 | "name": "translate" 41 | } 42 | ], 43 | "resource": [ 44 | { 45 | "type": "Patient", 46 | "conditionalCreate": false, 47 | "conditionalDelete": "not-supported", 48 | "conditionalRead": "full-support", 49 | "conditionalUpdate": false, 50 | "documentation": "Creating identities is not allowed if allow_create_update of the FHIR store is set to false.", 51 | "interaction": [ 52 | { 53 | "code": "read" 54 | }, 55 | { 56 | "code": "update" 57 | }, 58 | { 59 | "code": "delete" 60 | }, 61 | { 62 | "code": "create" 63 | } 64 | ], 65 | "readHistory": true, 66 | "searchInclude": [ 67 | "Patient.general-practitioner", 68 | "Patient.link", 69 | "Patient.organization" 70 | ], 71 | "searchParam": [ 72 | { 73 | "definition": "http://hl7.org/fhir/SearchParameter/Patient-active", 74 | "name": "active", 75 | "type": "token" 76 | } 77 | ] 78 | } 79 | ] 80 | } 81 | ] 82 | } 83 | -------------------------------------------------------------------------------- /server/src/test/resources/error_operation_outcome.json: -------------------------------------------------------------------------------- 1 | { 2 | "resourceType": "OperationOutcome", 3 | "meta": { 4 | "profile": ["https://healthcare.googleapis.com/v1/projects/fhir-sdk/locations/us/datasets/synthea-sample-data/fhirStores/gcs-data/fhir/StructureDefinition/Spine-OperationOutcome-1"] 5 | }, 6 | "issue": [{ 7 | "severity": "error", 8 | "code": "exception", 9 | "details": { 10 | "coding": [{ 11 | "system": "https://healthcare.googleapis.com/v1/projects/fhir-sdk/locations/us/datasets/synthea-sample-data/fhirStores/gcs-data/fhir/ValueSet/Spine-ErrorOrWarningCode-1", 12 | "code": "INTERNAL_SERVER_ERROR", 13 | "display": "Internal server error" 14 | }] 15 | }, 16 | "diagnostics": "Any further internal debug details i.e. stack trace details etc." 17 | }] 18 | } -------------------------------------------------------------------------------- /server/src/test/resources/hapi_page_url_allowed_queries.json: -------------------------------------------------------------------------------- 1 | { 2 | "entries": [ 3 | { 4 | "path": "", 5 | "queryParams": { 6 | "_getpages": "ANY_VALUE" 7 | }, 8 | "allowExtraParams": true, 9 | "allParamsRequired": true 10 | } 11 | ] 12 | } -------------------------------------------------------------------------------- /server/src/test/resources/idp_keycloak_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "issuer": "https://token.issuer/realms/test", 3 | "authorization_endpoint": "https://token.issuer/protocol/openid-connect/auth", 4 | "token_endpoint": "https://token.issuer/protocol/openid-connect/token", 5 | "jwks_uri": "https://token.issuer/protocol/openid-connect/certs", 6 | "grant_types_supported": ["authorization_code"], 7 | "response_types_supported": [ 8 | "code", 9 | "none", 10 | "id_token", 11 | "token", 12 | "id_token token", 13 | "code id_token", 14 | "code token", 15 | "code id_token token" 16 | ], 17 | "subject_types_supported": [ 18 | "public", 19 | "pairwise" 20 | ], 21 | "id_token_signing_alg_values_supported": [ 22 | "PS384", 23 | "ES384", 24 | "RS384", 25 | "HS256", 26 | "HS512", 27 | "ES256", 28 | "RS256", 29 | "HS384", 30 | "ES512", 31 | "PS256", 32 | "PS512", 33 | "RS512" 34 | ], 35 | "code_challenge_methods_supported": ["S256"] 36 | } -------------------------------------------------------------------------------- /server/src/test/resources/malformed_allowed_queries.json: -------------------------------------------------------------------------------- 1 | { 2 | "entriesTT": [ 3 | { 4 | "path": "", 5 | "queryParams": { 6 | "_getpages": "ANY_VALUE" 7 | }, 8 | "allowExtraParams": true, 9 | "allParamsRequired": true 10 | } 11 | ] 12 | } -------------------------------------------------------------------------------- /server/src/test/resources/no_path_allowed_queries.json: -------------------------------------------------------------------------------- 1 | { 2 | "entries": [ 3 | { 4 | "queryParams": { 5 | "_getpages": "ANY_VALUE" 6 | }, 7 | "allowExtraParams": true, 8 | "allParamsRequired": true 9 | } 10 | ] 11 | } -------------------------------------------------------------------------------- /server/src/test/resources/patient-list-example.json: -------------------------------------------------------------------------------- 1 | { 2 | "entry": [ 3 | { 4 | "item": { 5 | "reference": "Patient/be92a43f-de46-affa-b131-bbf9eea51140" 6 | } 7 | }, 8 | { 9 | "item": { 10 | "reference": "Patient/420e791b-e419-c19b-3144-29e101c2c12f" 11 | } 12 | } 13 | ], 14 | "id": "patient-list-example", 15 | "meta": { 16 | "lastUpdated": "2021-11-25T18:54:23.389580+00:00", 17 | "versionId": "MTYzNzg2NjQ2MzM4OTU4MDAwMA" 18 | }, 19 | "mode": "working", 20 | "resourceType": "List", 21 | "status": "current" 22 | } -------------------------------------------------------------------------------- /server/src/test/resources/test_patient.json: -------------------------------------------------------------------------------- 1 | { 2 | "address": [ 3 | { 4 | "city": "Braintree", 5 | "country": "US", 6 | "extension": [ 7 | { 8 | "extension": [ 9 | { 10 | "url": "latitude", 11 | "valueDecimal": 42.21111261554208 12 | }, 13 | { 14 | "url": "longitude", 15 | "valueDecimal": -70.96074217780242 16 | } 17 | ], 18 | "url": "http://hl7.org/fhir/StructureDefinition/geolocation" 19 | } 20 | ], 21 | "line": [ 22 | "864 Schuster Rue" 23 | ], 24 | "postalCode": "02184", 25 | "state": "MA" 26 | } 27 | ], 28 | "birthDate": "1971-01-13", 29 | "gender": "male", 30 | "id": "be92a43f-de46-affa-b131-bbf9eea51140", 31 | "identifier": [ 32 | { 33 | "system": "https://github.com/synthetichealth/synthea", 34 | "value": "be92a43f-de46-affa-b131-bbf9eea51140" 35 | }, 36 | { 37 | "system": "http://hospital.smarthealthit.org", 38 | "type": { 39 | "coding": [ 40 | { 41 | "code": "MR", 42 | "display": "Medical Record Number", 43 | "system": "http://terminology.hl7.org/CodeSystem/v2-0203" 44 | } 45 | ], 46 | "text": "Medical Record Number" 47 | }, 48 | "value": "be92a43f-de46-affa-b131-bbf9eea51140" 49 | }, 50 | { 51 | "system": "http://hl7.org/fhir/sid/us-ssn", 52 | "type": { 53 | "coding": [ 54 | { 55 | "code": "SS", 56 | "display": "Social Security Number", 57 | "system": "http://terminology.hl7.org/CodeSystem/v2-0203" 58 | } 59 | ], 60 | "text": "Social Security Number" 61 | }, 62 | "value": "999-17-8182" 63 | } 64 | ], 65 | "maritalStatus": { 66 | "coding": [ 67 | { 68 | "code": "S", 69 | "display": "S", 70 | "system": "http://terminology.hl7.org/CodeSystem/v3-MaritalStatus" 71 | } 72 | ], 73 | "text": "S" 74 | }, 75 | "meta": { 76 | "lastUpdated": "2021-11-25T19:03:28.085054+00:00", 77 | "profile": [ 78 | "http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient" 79 | ], 80 | "versionId": "MTYzNzg2NzAwODA4NTA1NDAwMA" 81 | }, 82 | "multipleBirthBoolean": false, 83 | "name": [ 84 | { 85 | "family": "Anderson154", 86 | "given": [ 87 | "Micheal721", 88 | "NEW_NAME" 89 | ], 90 | "prefix": [ 91 | "Mr." 92 | ], 93 | "use": "official" 94 | } 95 | ], 96 | "resourceType": "Patient", 97 | "telecom": [ 98 | { 99 | "system": "phone", 100 | "use": "home", 101 | "value": "555-435-4405" 102 | } 103 | ], 104 | "text": { 105 | "div": "
Generated by Synthea.Version identifier: a3482c8\n . Person seed: -3805195760489406909 Population seed: 1632186774891
", 106 | "status": "generated" 107 | } 108 | } 109 | --------------------------------------------------------------------------------