From 0b8476d49850a6e93c2d1ce7c3ab6eb112ed03f9 Mon Sep 17 00:00:00 2001 From: Fred Hersch Date: Fri, 29 Nov 2024 10:37:04 +0800 Subject: [PATCH] Add Mkdocs site (#299) --- README.md | 6 +- doc/.gitignore | 1 + doc/README.md | 2 + doc/docs/concepts.md | 201 +++++++++++++++++++++ doc/docs/contribute.md | 31 ++++ doc/{ => docs}/design.md | 10 +- doc/docs/getting_started.md | 124 +++++++++++++ doc/docs/images/Info_Gateway_Overview.png | Bin 0 -> 26422 bytes doc/docs/images/Info_Gateway_Use_Cases.png | Bin 0 -> 52000 bytes doc/{ => docs/images}/flow.png | Bin doc/{ => docs/images}/integrated.png | Bin doc/{ => docs/images}/separate.png | Bin doc/{ => docs/images}/summary.png | Bin doc/docs/index.md | 55 ++++++ doc/docs/release_process.md | 44 +++++ doc/docs/support.md | 46 +++++ doc/docs/tutorial_docker.md | 148 +++++++++++++++ doc/docs/tutorial_first_access_checker.md | 99 ++++++++++ doc/docs/tutorials.md | 2 + doc/mkdocs.yml | 81 +++++++++ doc/requirements.txt | 2 + 21 files changed, 843 insertions(+), 9 deletions(-) create mode 100644 doc/.gitignore create mode 100644 doc/README.md create mode 100644 doc/docs/concepts.md create mode 100644 doc/docs/contribute.md rename doc/{ => docs}/design.md (99%) create mode 100644 doc/docs/getting_started.md create mode 100644 doc/docs/images/Info_Gateway_Overview.png create mode 100644 doc/docs/images/Info_Gateway_Use_Cases.png rename doc/{ => docs/images}/flow.png (100%) rename doc/{ => docs/images}/integrated.png (100%) rename doc/{ => docs/images}/separate.png (100%) rename doc/{ => docs/images}/summary.png (100%) create mode 100644 doc/docs/index.md create mode 100644 doc/docs/release_process.md create mode 100644 doc/docs/support.md create mode 100644 doc/docs/tutorial_docker.md create mode 100644 doc/docs/tutorial_first_access_checker.md create mode 100644 doc/docs/tutorials.md create mode 100644 doc/mkdocs.yml create mode 100644 doc/requirements.txt diff --git a/README.md b/README.md index dccaa7c0..b887f4a3 100644 --- a/README.md +++ b/README.md @@ -22,10 +22,10 @@ The requests to the access proxy should have the access token as a Bearer Authorization header. Based on that, the proxy decides whether to grant access for a FHIR query. -Modules involved in FHIR authorization/access-control +Modules involved in FHIR authorization/access-control For more information on the technical design, -[see the design doc](doc/design.md). +[see the design doc](doc/docs/design.md). # Modules @@ -177,7 +177,7 @@ accessing the proxy from an Android emulator, which runs on a separate network. GCP note: if this is not on a VM with proper service account (e.g., on a local host), you need to pass GCP credentials to it, for example by mapping the `.config/gcloud` volume (i.e., add `-v ~/.config/gcloud:/root/.config/gcloud` to -the above command). +the docker command). # How to use this proxy diff --git a/doc/.gitignore b/doc/.gitignore new file mode 100644 index 00000000..a7ca9005 --- /dev/null +++ b/doc/.gitignore @@ -0,0 +1 @@ +site/** diff --git a/doc/README.md b/doc/README.md new file mode 100644 index 00000000..0e264637 --- /dev/null +++ b/doc/README.md @@ -0,0 +1,2 @@ +To generate the documentation web-site content from `docs/` see instructions +[here](https://github.com/google/fhir-data-pipes/tree/master/doc#documentation-site). \ No newline at end of file diff --git a/doc/docs/concepts.md b/doc/docs/concepts.md new file mode 100644 index 00000000..41848328 --- /dev/null +++ b/doc/docs/concepts.md @@ -0,0 +1,201 @@ +# Concepts + +## Common Terminologies + +These are some common terminologies that are important when dealing with access +control in general: + +| Term | Description | +|---------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------| +| Authentication and Identity Provider (IDP) | Who the user is. Establishing the identity can be done through a shared service (e.g., Google, GitHub) or a special one. | +| Authorization (AuthZ) | Given the identity, what can a user access? Has context specific pieces (e.g., scopes) | +| Access-control | How to make sure users access authorized resources only. This is the **core focus of the info gateway**. | +| Client app | An app which needs to access FHIR resources on behalf of a user. | +| User | The user that is using the app; this is the identity being "authenticated". | +| Access Token | A JWT that is provided as a Bearer token when accessing FHIR resources. | +| OAuth2.0 | A standard to grant access to an application on behalf of a user. | +| [SMART-on-FHIR](https://hl7.org/fhir/smart-app-launch/) | Defines workflows that an application can use to securely request and access FHIR data. | + +The following picture helps to visualise the relationship of these concepts. +A client app (e.g., a SMART-on-FHIR app) should first use a process to fetch an +_"access token"_ from the IDP+AuthZ service. For example, this process might +be OAuth's +[Authorization Code Flow](https://auth0.com/docs/get-started/authentication-and-authorization-flow/authorization-code-flow). +This token is then provided on each request to the access-control gateway when +accessing FHIR resources. + +![FHIR Info Gateway](images/Info_Gateway_Overview.png) + +## Info Gateway Modules + +The Info Gateway consists of a core, which is in +the [`server`](https://github.com/google/fhir-gateway/tree/main/server) module, +and a set of _access-checker_ plugins, which can be implemented by third parties +and added to the proxy server. Two sample plugins are implemented in the +[`plugins`](https://github.com/google/fhir-gateway/tree/main/plugins) module. + +There is also a sample +[`exec`](https://github.com/google/fhir-gateway/tree/main/exec) module which +shows how all pieces can be woven together into a single Spring Boot app. It +also has examples for implementing custom end-points. + +**Notes:** + +* [1] Spring Boot is not a requirement for using FHIR Info Gateway; we just use + it to simplify the + [MainApp](https://github.com/google/fhir-gateway/tree/main/exec/src/main/java/com/google/fhir/gateway/MainApp.java). +* [2] The only Spring-related requirement is to do a + [@ComponentScan](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/context/annotation/ComponentScan.html) + to find all access-checker plugins in the classpath. + +## Configuration parameters + +The configuration parameters are provided through environment variables: + +- `PROXY_TO`: The base url of the FHIR server e.g.: + ```shell + export PROXY_TO=https://example.com/fhir + ``` + +- `TOKEN_ISSUER`: The URL of the access token issuer, e.g.: + ```shell + export TOKEN_ISSUER=http://localhost:9080/auth/realms/test + ``` + The above example is based on the default config of a test IDP+AuthZ + [Keycloak](https://www.keycloak.org/) server. To see how this server is + configured, check the + [docker/keycloak](docker/https://github.com/google/fhir-gateway/tree/main/docker/keycloak) + directory. If you want to use a SMART-on-FHIR (SoF) app use this realm instead + which is based on Keycloak's + [SoF extension](https://github.com/Alvearie/keycloak-extensions-for-fhir): + ```shell + export TOKEN_ISSUER=http://localhost:9080/auth/realms/test-smart + ``` + +- `ACCESS_CHECKER`: The access-checker to use. Each access-checker has a name + (see [plugins](https://github.com/google/fhir-gateway/tree/main/plugins) for + details) and this variable should be set to the name of the plugin to use. For + example, to use one of the sample plugins use one of: + ```shell + export ACCESS_CHECKER=list + export ACCESS_CHECKER=patient + ``` + For more information on how access-checkers work and building your own, + see [section on access checkers](#access-checkers). + +- `ALLOWED_QUERIES_FILE`: A list of URL requests that should bypass the access + checker and always be allowed. + [`AllowedQueriesChecker`](https://github.com/google/fhir-gateway/blob/main/server/src/main/java/com/google/fhir/gateway/AllowedQueriesChecker.java) + compares the incoming request with a configured set of allowed-queries. The + intended use of this checker is to override all other access-checkers for + certain user-defined criteria. The user defines their criteria in a config + file and if the URL query matches an entry in the config file, access is + granted. [`AllowedQueriesConfig`](https://github.com/google/fhir-gateway/blob/main/server/src/main/java/com/google/fhir/gateway/AllowedQueriesConfig.java) + shows all the supported configurations. An example config file is + [`hapi_page_url_allowed_queries.json`](https://github.com/google/fhir-gateway/blob/main/resources/hapi_page_url_allowed_queries.json). + To use this file with `ALLOWED_QUERIES_FILE`: + ```shell + export ALLOWED_QUERIES_FILE="resources/hapi_page_url_allowed_queries.json" + ``` + +- `BACKEND_TYPE`: The type of backend, either `HAPI` or `GCP`. `HAPI` should be + used for most FHIR servers, while `GCP` should be used for GCP FHIR stores. + +## Access Checkers + +FHIR Info Gateway uses _access checker plugins_ to define the logic it uses to +make decisions for access requests. Most users should create an access checker +plugin to implement the access control logic for a specific use case. You can +learn about access checker plugins by looking at the sample access checker +[plugins](https://github.com/google/fhir-gateway/tree/main/plugins/src/main/java/com/google/fhir/gateway/plugin). + +See tutorial on +[creating an access checker plugin](tutorial_first_access_checker.md). The core +of FHIR Info Gateway, provides libraries that make it easier to create +access-checkers. For example, +[PatientFinder](https://github.com/google/fhir-gateway/blob/main/server/src/main/java/com/google/fhir/gateway/interfaces/PatientFinder.java) +can be used to limit access to a certain set of patients. + +### Patient access checker plugin +The [`PatientAccessChecker` plugin](https://github.com/google/fhir-gateway/blob/main/plugins/src/main/java/com/google/fhir/gateway/plugin/PatientAccessChecker.java) +can be used if the client is a SMART-on-FHIR app that uses the +[standalone app launch flow](https://www.hl7.org/fhir/smart-app-launch/app-launch.html#launch-app-standalone-launch). +It expects a `patient_id` claim in the access-token and limits access to FHIR +resources that belong to that patient. It supports +[SoF scopes](https://www.hl7.org/fhir/smart-app-launch/scopes-and-launch-context.html#scopes-for-requesting-clinical-data) +(both v1 and v2). + +### Explore the List access checker plugin + +The [`ListAccessChecker` plugin](https://github.com/google/fhir-gateway/blob/main/plugins/src/main/java/com/google/fhir/gateway/plugin/ListAccessChecker.java) +is a simple example of list-based access control. It works by assigning each +user a [FHIR `List` resource](https://www.hl7.org/fhir/list.html) which contains +a list of references of `Patient` resources that the user should have access to. +When a client makes a request to FHIR Information Gateway, +the `ListAccessChecker` grants access if all patients that are referenced in +the query are on the user's patient access list. + +The plugin expects the patient `List` resource's ID to be included as the value +to a claim named `patient_list` in the access token used to authorize requests +to the FHIR Information Gateway server. For example, following the +[test Docker deployment](https://github.com/google/fhir-access-proxy/wiki/Try-out-FHIR-Information-Gateway) +you may get a decoded JWT like the following (note if you use the default +settings you will get more claims that are not relevant to the access-checker +logic; so they are removed in this example): + +```json +{ + "header": { + "alg": "RS256", + "typ": "JWT", + "kid": "MnXk25Vp_W6X_UMi4sA3_iEMwuumZkwhOuE8eMY8LFo" + }, + "payload": { + "exp": 1673990497, + "iat": 1673990197, + "jti": "5bb2b1a0-e9c6-442f-abfd-a22f1798fd11", + "iss": "http://localhost:9080/auth/realms/test", + "aud": "account", + "sub": "76315cd1-9681-4a4e-b733-e6d811058e40", + "typ": "Bearer", + "azp": "my-fhir-client", + "session_state": "967e82a2-0188-4774-abbc-6bb4ce26536f", + "acr": "1", + "scope": "email profile", + "sid": "967e82a2-0188-4774-abbc-6bb4ce26536f", + "email_verified": false, + "patient_list": "patient-list-example", + "preferred_username": "testuser", + "group": [ + "fhirUser" + ] + } +} +``` + +Here `patient_list` equals `patient-list-example`, so if your FHIR server is +at `http://localhost:8099/fhir/` then this user's patient access list resource +is `http://localhost:8099/fhir/List/patient-list-example`. + +The decoded JWT is passed to the +[`AccessCheckerFactory`](https://github.com/google/fhir-gateway/blob/main/server/src/main/java/com/google/fhir/gateway/interfaces/AccessCheckerFactory.java) +implementation's `create()` function. The `ListAccessChecker` implementation +extracts the patient list ID from the JWT and saves it internally. Custom JWT +claims in the access token can be a good way to pass additional information to +your access checker beyond what your authentication server provides. + +`ListAccessChecker`'s +[`checkAccess`](https://github.com/google/fhir-gateway/blob/19447d7152804d2b790f22cc44ad3b1ca21c7040/plugins/src/main/java/com/google/fhir/gateway/plugin/ListAccessChecker.java#L157) +function splits access logic according to the HTTP method. Simple yes/no access +decisions like `processGet()` use the +[`NoOpAccessDecision`](https://github.com/google/fhir-gateway/blob/main/server/src/main/java/com/google/fhir/gateway/interfaces/NoOpAccessDecision.java) +class which you may also use in your own implementations. Alternatively, you may +have more complex decision needs, such as doing additional processing after the +data access like +[`processPost()`](https://github.com/google/fhir-gateway/blob/19447d7152804d2b790f22cc44ad3b1ca21c7040/plugins/src/main/java/com/google/fhir/gateway/plugin/ListAccessChecker.java#L202). +In this case, implement your own version of +[`AccessDecision`](https://github.com/google/fhir-access-proxy/blob/main/server/src/main/java/com/google/fhir/gateway/interfaces/AccessDecision.java). +The `ListAccessChecker` allows clients to create new `Patient` resources without +restriction (always allow access), and then as a post-processing step adds the +new Patient id to the client's patient access list. You can see this implemented +in [`AccessGrantedAndUpdateList`](https://github.com/google/fhir-access-proxy/blob/main/plugins/src/main/java/com/google/fhir/gateway/plugin/AccessGrantedAndUpdateList.java). diff --git a/doc/docs/contribute.md b/doc/docs/contribute.md new file mode 100644 index 00000000..d3aa0674 --- /dev/null +++ b/doc/docs/contribute.md @@ -0,0 +1,31 @@ +# How to Contribute + +We'd love to accept your patches and contributions to this project. There are +just a few small guidelines you need to follow. + +## Contributor License Agreement + +Contributions to this project must be accompanied by a Contributor License +Agreement (CLA). You (or your employer) retain the copyright to your +contribution; this simply gives us permission to use and redistribute your +contributions as part of the project. Head over +to to see your current agreements on file +or to sign a new one. + +You generally only need to submit a CLA once, so if you've already submitted +one (even if it was for a different project), you probably don't need to do it +again. + +## Code reviews + +All code changes require review. We use GitHub pull-requests for this purpose. + +* Consult [GitHub Help](https://help.github.com/articles/about-pull-requests/) + for more information on using pull requests. + +* We use GitHub for issue tracking. + +## Community Guidelines + +This project +follows [Google's Open Source Community Guidelines](https://opensource.google/conduct/). \ No newline at end of file diff --git a/doc/design.md b/doc/docs/design.md similarity index 99% rename from doc/design.md rename to doc/docs/design.md index 7f47e551..80f43bae 100644 --- a/doc/design.md +++ b/doc/docs/design.md @@ -21,7 +21,7 @@ For more context, please see the [Introduction](#introduction) section. Here is a summary of proposals and decisions in this design document, for the first version (V1) of the FHIR gateway: - +![High-level Architecture](images/summary.png) - There are three main components responsible for access decisions: an Identity Provider (IDP) which authenticates the user, an Authorization server (AuthZ) @@ -198,8 +198,6 @@ sometimes referred to as the Identity and Access Management (IAM) or simply We also have an "access gateway" which is responsible for processing any access request for the FHIR server and is the main focus of this design doc. - - ### Authentication and authorization flow Before the user can access data, their identity should be established and based @@ -226,7 +224,7 @@ to use [PKCE](https://auth0.com/docs/authorization/flows/authorization-code-flow-with-proof-key-for-code-exchange-pkce) which is not shown in this diagram)[^1]: - +![flow](images/flow.png) Here is a brief description of each step: @@ -265,7 +263,7 @@ to AuthZ which in turn maps the user identity to some access rules to FHIR resources. Popular IAM servers like [Keycloak](https://www.keycloak.org/), support separate/3rd party IDPs. - +![separate](images/separate.png) **Integrated AuthZ+Gateway** @@ -277,7 +275,7 @@ simplifies the deployment architecture significantly, however we note that the elimination of the access token might be incompatible with some use-cases like SoF apps. - +![integrated](images/integrated.png) ## Access gateway diff --git a/doc/docs/getting_started.md b/doc/docs/getting_started.md new file mode 100644 index 00000000..de113993 --- /dev/null +++ b/doc/docs/getting_started.md @@ -0,0 +1,124 @@ +# Getting Started + +!!! tip "Quick Start Guide" + + The easiest way to get started is to follow the ["Run the Info Gateway in Docker" guide](tutorial_docker.md). + +## Building from source + +To build all modules, from the root run (if you are a developer, do _not_ use +`spotless.apply.skip=true`): + +```shell +mvn package -Dspotless.apply.skip=true +``` + +The `server` and `plugins` modules can be run together through this executable +jar (`--server.port` is just one of the many default Spring Boot flags): + +```shell +java -jar exec/target/exec-0.1.0.jar --server.port=8081 +``` + +Note that extra access-checker plugins can be added through the `loader.path` +property (although it is probably easier to build them into your server): + +```shell +java -Dloader.path="PATH-TO-ADDITIONAL-PLUGINGS/custom-plugins.jar" \ + -jar exec/target/exec-0.1.0.jar --server.port=8081 +``` + +The plugin library can be swapped with any third party access-checker as +described in +the [plugins](https://github.com/google/fhir-gateway/tree/main/plugins) +directory. Learn more about [AccessCheckers](concepts.md#access-checkers). + +## Gateway to server access + +The proxy must be able to send FHIR queries to the FHIR server. The FHIR server +must be configured to accept connections from the proxy while rejecting most +other requests. + +If you use +a [GCP FHIR store](https://cloud.google.com/healthcare-api/docs/concepts/fhir) +you have the following options: + +- If you have access to the FHIR store, you can use your own credentials by + doing [application-default login](https://cloud.google.com/sdk/gcloud/reference/auth/application-default/login). + This is useful when testing the proxy on your local machine, and you have + access to the FHIR server through your credentials. + +- Use a service account with required access (e.g., "Healthcare FHIR Resource + Reader", "Healthcare Dataset Viewer", "Healthcare FHIR Store Viewer"). You can + then run the proxy in the same GCP project on a VM with this service account. + +- [not-recommended] You can create and download a key file for the above service + account, then use it with: + ```shell + export GOOGLE_APPLICATION_CREDENTIALS="PATH_TO_THE_JSON_KEY_FILE" + ``` + +## Running the gateway + +!!! tip "Configuration Parameters" + + Take a moment to review the [configuration parameters](concepts.md#configuration-parameters). + +Once you have set all the above, you can run the proxy server. The sample `exec` +module uses [Apache Tomcat](https://tomcat.apache.org/) +through [Spring Boot](https://spring.io/projects/spring-boot) and the usual +configuration parameters apply, e.g., to run on port 8081: + +```shell +java -jar exec/target/exec-0.1.0.jar --server.port=8081 +``` + +!!! tip "Android Emulator" + + If the `TOKEN_ISSUER` is on the `localhost` and you are accessing the gateway from an Android emulator (which runs on a separate network), you may need to bypass proxy's token issuer check by setting `RUN_MODE=DEV` environment variable. + +## Using the Info Gateway + +In this section we assume that a Keycloak instance is set up using the sample +docker setup provided +[here](https://github.com/google/fhir-gateway/blob/main/docker/keycloak/config-compose.yaml). +If you have a different IDP+AuthZ setup, you need to adjust the parameters +below accordingly. + +Once the gateway is running, we first need to fetch an access token from +the `TOKEN_ISSUER`; you need the test server's `username` and `password` plus +the `client_id`: + +```shell +$ curl -X POST -d 'client_id=CLIENT_ID' -d 'username=testuser' \ + -d 'password=testpass' -d 'grant_type=password' \ +"http://localhost:9080/auth/realms/test/protocol/openid-connect/token" +``` + +We need the `access_token` of the returned JSON to be able to convince the +gateway to authorize our FHIR requests (there is also a `refresh_token` in the +above response). Assuming this is stored in the `ACCESS_TOKEN` environment +variable, we can access the FHIR store: + +```shell +$ curl -X GET -H "Authorization: Bearer ${ACCESS_TOKEN}" \ +-H "Content-Type: application/json; charset=utf-8" \ +'http://localhost:8081/Patient/f16b5191-af47-4c5a-b9ca-71e0a4365824' +``` + +```shell +$ curl -X PUT -H "Authorization: Bearer ${ACCESS_TOKEN}" \ +-H "Content-Type: application/json; charset=utf-8" \ +'http://localhost:8081/Patient/f16b5191-af47-4c5a-b9ca-71e0a4365824' \ +-d @Patient_f16b5191-af47-4c5a-b9ca-71e0a4365824_modified.json +``` + +Of course, whether a query is accepted or denied, depends on the access-checker +used and the `ACCESS_TOKEN` claims. + +For example: + +* For `ACCESS_CHECKER=list` there should be a `patient_list` claim which is the + ID of a `List` FHIR resource with all the patients that this user has access + to. +* For `ACCESS_CHECKER=patient`, there should be a `patient_id` claim with a valid Patient resource ID. \ No newline at end of file diff --git a/doc/docs/images/Info_Gateway_Overview.png b/doc/docs/images/Info_Gateway_Overview.png new file mode 100644 index 0000000000000000000000000000000000000000..5cb081af955344765ee254cbcaaa023fb0a243a7 GIT binary patch literal 26422 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 Ns!CdrQU%L5{|^TNX1@Rc literal 0 HcmV?d00001 diff --git a/doc/docs/images/Info_Gateway_Use_Cases.png b/doc/docs/images/Info_Gateway_Use_Cases.png new file mode 100644 index 0000000000000000000000000000000000000000..d0be0d7e8f8506193854e2411a5514a65239fa12 GIT binary patch literal 52000 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 bGpA>AZqxXXoamPkm{6A2kSmmV`uhI?A}k_f literal 0 HcmV?d00001 diff --git a/doc/flow.png b/doc/docs/images/flow.png similarity index 100% rename from doc/flow.png rename to doc/docs/images/flow.png diff --git a/doc/integrated.png b/doc/docs/images/integrated.png similarity index 100% rename from doc/integrated.png rename to doc/docs/images/integrated.png diff --git a/doc/separate.png b/doc/docs/images/separate.png similarity index 100% rename from doc/separate.png rename to doc/docs/images/separate.png diff --git a/doc/summary.png b/doc/docs/images/summary.png similarity index 100% rename from doc/summary.png rename to doc/docs/images/summary.png diff --git a/doc/docs/index.md b/doc/docs/index.md new file mode 100644 index 00000000..cebbaf8f --- /dev/null +++ b/doc/docs/index.md @@ -0,0 +1,55 @@ +# FHIR Info Gateway + +The Info Gateway is a reverse proxy which controls client access to FHIR +resources on a server. It works by inspecting FHIR requests and verifying that +the client is authorized to access the requested resources. + +It makes it easier for developers to enforce various forms of authorization +policies including organizational role based access control (RBAC) policies +when working with FHIR data. + +* To enable authorization and access-control (ACL) policy enforcement between a + client application and a FHIR server, the Info Gateway is used along with an + Identity Provider (IDP) and Authorization server (AuthZ). +* The IDP can be a generic OpenID Connect (OIDC) compliant service or a special + purpose one. +* The IDP+AuthZ should provide a JSON Web Token (JWT) to the client. The client + uses this as a Bearer access-token (AT) when sending FHIR requests. +* A sample end-to-end implementation with Keycloak as the IDP+AuthZ service is + provided and has been tested with HAPI FHIR and Google Cloud Healthcare + FHIR-store as the FHIR server. + +![FHIR Info Gateway](images/Info_Gateway_Overview.png) + +## Key Features +Key features of the Info Gateway include: + +* A stand-alone service that can work with any FHIR compliant servers. +* A pluggable architecture for defining an access-checker to allow for + implementation configurability. +* Query filtering to block/allow specific queries. +* Post-processing of the results returned by the FHIR-server, for example to + remove sensitive information. +* A generic interface for implementing custom endpoints, e.g., a sync endpoint + to return updates for all patients assigned to a health-worker. + +## Common use cases + +The Info Gateway is designed to solve for a generic problem, that is, access +control for **any client** and **any FHIR server**. + +Common access-check use-cases include: + +1. For a mobile app used by community based front-line health workers possibly + with offline support +2. Web based dashboard used by program admins +3. For a personal health record app used by patients or caregivers +4. To enable SMART-on-FHIR apps for patient or system level scopes + +FHIR Info Gateway is implemented as a "FHIR facade", i.e., it is a FHIR server +itself which is implemented using the +[HAPI FHIR Plain Server](https://hapifhir.io/hapi-fhir/docs/server_plain/introduction.html) +library: + +![FHIR Info Gateway](images/Info_Gateway_Use_Cases.png) + diff --git a/doc/docs/release_process.md b/doc/docs/release_process.md new file mode 100644 index 00000000..32b97669 --- /dev/null +++ b/doc/docs/release_process.md @@ -0,0 +1,44 @@ +# Semantic versioning + +FHIR Info Gateway artifacts are released on +[Maven](https://central.sonatype.com/namespace/com.google.fhir.gateway). A +docker image is also published on +[GCP Artifact Registry](https://console.cloud.google.com/artifacts/docker/fhir-proxy-build/us/stable/fhir-gateway?project=fhir-proxy-build). + +Versioning across all Open Health Stack components is based on the +major.minor.patch scheme and respects Semantic Versioning. + +Respecting Semantic Versioning is important for multiple reasons: + +* It guarantees simple minor version upgrades, as long as you only use the + public APIs. +* A new major version is an opportunity to thoroughly document breaking changes. +* A new major/minor version is an opportunity to communicate new features + through a blog post. + +## Major versions + +The major version number is incremented on every breaking change. + +Whenever a new major version is released, we publish: + +* a blog post with feature highlights, major bug fixes, breaking changes, and + upgrade instructions. +* an exhaustive changelog entry via the release notes + +## Minor versions + +The minor version number is incremented on every significant retro-compatible +change. + +Whenever a new minor version is released, we publish: + +* an exhaustive changelog entry via the release notes. + +## Patch versions + +The patch version number is incremented on bugfixes releases. + +Whenever a new patch version is released, we publish: + +* an exhaustive changelog entry. \ No newline at end of file diff --git a/doc/docs/support.md b/doc/docs/support.md new file mode 100644 index 00000000..fe6e8ea3 --- /dev/null +++ b/doc/docs/support.md @@ -0,0 +1,46 @@ +# Support + +On this page we've listed some ways you can get technical support along with +Open Health Stack communities and forums that you can be a part of. + +Before participating please read +our [code of conduct](https://opensource.google/conduct) that we expect all +community members to adhere too. + +## Developer calls + +There are weekly Open Health Stack developer calls that you are welcome to join. + +* Calls are on Thursdays and **alternate** between Android FHIR SDK and OHS + server side components (FHIR Data Pipes and Info Gateway). +* See the schedule below for more details. +* To be added to the calls, please email: `hello-ohs[at]google.com`. + +**Developer call schedule** + +| OHS Developers Call | GMT | East Africa | India | +| :------------ | :-: | :---------: | :---: | +| Android FHIR SDK | 10:00 UK | 12:00 Nairobi | 14:30 Delhi | +| Analytics and Info Gateway | 13:00 UK | 15:00 Nairobi | 17:30 Delhi | + +## Discussion forums + +We are in the process of setting up a dedicated discussion forum for Open Health +Stack. In the meantime, you can reach out to `hello-ohs[at]google.com`. + +## Stack Overflow + +Stack Overflow is a popular forum to ask code-level questions or if you're stuck +with a specific error. It would be nice to tag your question with +`open-health-stack`! + +## Bugs or Feature requests + +Before submitting a bug or filing a feature reqeust, please review the open +issues on +our [github repository](https://github.com/google/fhir-data-pipes/issues). + +If your issue is there, please add a comment. Otherwise, create a new issue to +file a bug or submit a new feature request. + +Please review the [contributing section](contribute.md). \ No newline at end of file diff --git a/doc/docs/tutorial_docker.md b/doc/docs/tutorial_docker.md new file mode 100644 index 00000000..0a10e03f --- /dev/null +++ b/doc/docs/tutorial_docker.md @@ -0,0 +1,148 @@ +# Run the Info Gateway in Docker + +In this guide, you will learn how to run FHIR Info Gateway in a Docker +image, and see it work in concert with a sample Keycloak and HAPI FHIR server +running on your local machine. We assume that +[Docker](https://docs.docker.com/get-docker/) and +[Docker Compose](https://docs.docker.com/compose/) are installed. The sample +commands are shown on a Linux/shell environment and may need to be adjusted for +your environment. + +!!! tip "Important" + + The setup used in this guide **should not be used in a production environment**. It is designed to get things up and running quickly for demonstration or testing purposes only. The FHIR Information Gateway Docker image might be used in a production environment if deployed appropriately, however the example access-checker plugins may not satisfy real-world use cases. + +## Start the Docker images + +1. Clone + the [FHIR Info Gateway repo from GitHub](https://github.com/google/fhir-gateway). +2. Open a terminal window and `cd` to the directory where you cloned the repo. +3. Bring up the sample Keycloak service using `docker compose`. + ```shell + docker compose -f docker/keycloak/config-compose.yaml up + ``` + This runs an instance of [Keycloak](https://www.keycloak.org/) with + [SoF extension](https://github.com/Alvearie/keycloak-extensions-for-fhir), + preloaded with a test configuration. It is accessible at + `http://localhost:9080`. + +4. Run the sample HAPI FHIR server Docker image. + ```shell + docker run -p 8099:8080 us-docker.pkg.dev/fhir-proxy-build/stable/hapi-synthea:latest + ``` + The server is preloaded with synthetic patient data and a FHIR + `List/patient-list-example` resource. + +5. Run the FHIR Information Gateway Docker image with the `list` access + checker. + ```shell + docker run \ + -e TOKEN_ISSUER=http://localhost:9080/auth/realms/test \ + -e PROXY_TO=http://localhost:8099/fhir \ + -e BACKEND_TYPE=HAPI \ + -e RUN_MODE=PROD \ + -e ACCESS_CHECKER=list \ + --network=host \ + us-docker.pkg.dev/fhir-proxy-build/stable/fhir-gateway:latest + ``` + +Several environment variables are used to configure FHIR Information Gateway: + +* `TOKEN_ISSUER`: The URL of the token issuer. For Keycloak this is typically + `http://{keycloak-host}:{keycloak-port}/auth/realms/{realm-name}`. +* `PROXY_TO`: The [Service Base URL](https://build.fhir.org/http.html#root) of + the FHIR server that FHIR Access Proxy communicates with. +* `BACKEND_TYPE`: One of `HAPI` for a HAPI FHIR Server or `GCP` for a Cloud + Healthcare FHIR-store. +* `RUN_MODE`: One of `PROD` or `DEV`. DEV removes validation of the issuer URL, + which is useful when using the docker image with an Android emulator as the + emulator runs on its own virtual network and sees a different address than + the host. +* `ACCESS_CHECKER`: The access-checker plugin to use. The Docker image includes + the [`list`](https://github.com/google/fhir-gateway/blob/main/plugins/src/main/java/com/google/fhir/gateway/plugin/ListAccessChecker.java) + and [`patient`](https://github.com/google/fhir-gateway/blob/main/plugins/src/main/java/com/google/fhir/gateway/plugin/PatientAccessChecker.java) + example access-checkers. + +!!! tip "GCP Note" + + If the FHIR server is GCP FHIR-store and the gateway is not run on a VM with proper service account (e.g., running on a localhost), you need to pass GCP credentials to it, for example by mapping the `.config/gcloud` volume (i.e., add `-v ~/.config/gcloud:/root/.config/gcloud` to the above command). + +## Examine the sample Keycloak configuration + +In this section you will review the Keycloak settings relevant to the FHIR +Information Gateway with the sample `list` access checker plugin. + +1. Open a web browser and navigate to `http://localhost:9080/auth/admin/`. +2. Login using user `admin` and password `adminpass`. +3. Select the `test` realm. +4. From the left menu, find the **Manage** section and click **Users**. Click + **View all users**, then click the **ID** of the only result to view the + user `Testuser`. +5. Select the **Attributes** tab. Note the attribute `patient_list` with value + `patient-list-example`. The client `my-fhir-client` has a corresponding + [User Attribute + mapper](https://www.keycloak.org/docs/latest/server_admin/#_protocol-mappers) + to add this as a claim to the access token JWT, which you can see under + **Clients > my-fhir-client > Mappers > list-mapper**. +6. `patient-list-example` is the ID of a FHIR List resource which lists all the + Patient resources the user can access. Open + `http://localhost:8099/fhir/List/patient-list-example` to see the list + referencing two Patients: + + ```json + ... + "entry": [ { + "item": { + "reference": "Patient/75270" + } + }, { + "item": { + "reference": "Patient/3810" + } + } ] + ... + ``` + +## Get a FHIR resource using FHIR Information Gateway + +1. Get an access token for the test user. This command uses + [jq](https://stedolan.github.io/jq/) to parse the access token from the JSON + response. + + ```shell + ACCESS_TOKEN="$( \ + curl -X POST \ + -d 'client_id=my-fhir-client' \ + -d 'username=testuser' \ + -d 'password=testpass' \ + -d 'grant_type=password' \ + "http://localhost:9080/auth/realms/test/protocol/openid-connect/token" \ + | jq .access_token \ + | tr -d '"' \ + )" + ``` + + You will need to rerun this command when the access token expires after 5 + minutes. In a real application, implement your Identity Provider's + authentication flow, including refresh tokens. + +2. Send a request to FHIR Information Gateway using the access token. + + ```shell + curl -X GET -H "Authorization: Bearer ${ACCESS_TOKEN}" \ + -H "Content-Type: application/json; charset=utf-8" \ + 'http://localhost:8080/fhir/Patient/75270' + ``` + + You should get a response containing the Patient resource. + +3. Send a second request for a patient the user does not have access to. + + ```shell + curl -X GET -H "Authorization: Bearer ${ACCESS_TOKEN}" \ + -H "Content-Type: application/json; charset=utf-8" \ + 'http://localhost:8080/fhir/Patient/3' + ``` + + You should get a response of `User is not authorized to GET + http://localhost:8080/fhir/Patient/3`. \ No newline at end of file diff --git a/doc/docs/tutorial_first_access_checker.md b/doc/docs/tutorial_first_access_checker.md new file mode 100644 index 00000000..0aa539dc --- /dev/null +++ b/doc/docs/tutorial_first_access_checker.md @@ -0,0 +1,99 @@ +# Create an access checker plugin + +In this guide you will create your own access checker plugin. + +## Implement the `AccessCheckerFactory` interface + +To create your own access checker plugin, create an implementation of +the [`AccessCheckerFactory` interface](https://github.com/google/fhir-gateway/blob/main/server/src/main/java/com/google/fhir/gateway/interfaces/AccessCheckerFactory.java) +annotated with a `@Named(value = "name")` annotation defining the name of the +plugin. + +The most important parts are to implement a +custom [`AccessChecker`](https://github.com/google/fhir-gateway/blob/main/server/src/main/java/com/google/fhir/gateway/interfaces/AccessChecker.java) +to be returned by the factory and its `checkAccess` function which specifies if +access is granted or not by returning +an [`AccessDecision`](https://github.com/google/fhir-gateway/blob/main/server/src/main/java/com/google/fhir/gateway/interfaces/AccessDecision.java). + +## Create a new class + +The simplest way to create your own access checker is to make a new class file +in the `plugins/src/main/java/com/google/fhir/gateway/plugin` directory, next to +the existing sample plugins. The following code can be used as a starting +template for a minimal access checker: + +```java +package com.google.fhir.gateway.plugin; + +import ca.uhn.fhir.context.FhirContext; +import com.auth0.jwt.interfaces.DecodedJWT; +import com.google.fhir.gateway.FhirUtil; +import com.google.fhir.gateway.HttpFhirClient; +import com.google.fhir.gateway.JwtUtil; +import com.google.fhir.gateway.interfaces.AccessChecker; +import com.google.fhir.gateway.interfaces.AccessCheckerFactory; +import com.google.fhir.gateway.interfaces.AccessDecision; +import com.google.fhir.gateway.interfaces.NoOpAccessDecision; +import com.google.fhir.gateway.interfaces.PatientFinder; +import com.google.fhir.gateway.interfaces.RequestDetailsReader; +import javax.inject.Named; + +public class MyAccessChecker implements AccessChecker { + + private final FhirContext fhirContext; + private final HttpFhirClient httpFhirClient; + private final String claim; + private final PatientFinder patientFinder; + + // We're not using any of the parameters here, but real access checkers + // would likely use some/all. + private MyAccessChecker( + HttpFhirClient httpFhirClient, + String claim, + FhirContext fhirContext, + PatientFinder patientFinder) { + this.fhirContext = fhirContext; + this.claim = claim; + this.httpFhirClient = httpFhirClient; + this.patientFinder = patientFinder; + } + + @Override + public AccessDecision checkAccess(RequestDetailsReader requestDetails) { + // Implement your access logic here. + return NoOpAccessDecision.accessGranted(); + } + + // The factory must be thread-safe but the AccessChecker instances it returns + // do not need to be thread-safe. + @Named(value = "sample") + public static class Factory implements AccessCheckerFactory { + + static final String CLAIM = "sub"; + + private String getClaim(DecodedJWT jwt) { + return FhirUtil.checkIdOrFail(JwtUtil.getClaimOrDie(jwt, CLAIM)); + } + + @Override + public AccessChecker create( + DecodedJWT jwt, + HttpFhirClient httpFhirClient, + FhirContext fhirContext, + PatientFinder patientFinder) { + String claim = getClaim(jwt); + return new MyAccessChecker(httpFhirClient, claim, fhirContext, patientFinder); + } + } +} + +``` + +## Rebuild to include the plugin +Once you're done implementing your access checker plugin, rebuild using +`mvn package` from the root of the project to include the plugin, set the +access-checker using e.g. `export ACCESS_CHECKER=sample` + +## Run the gateway +Run the gateway using e.g. +`java -jar exec/target/exec-0.1.0.jar --server.port=8080`. \ No newline at end of file diff --git a/doc/docs/tutorials.md b/doc/docs/tutorials.md new file mode 100644 index 00000000..6a087c64 --- /dev/null +++ b/doc/docs/tutorials.md @@ -0,0 +1,2 @@ +# Tutorials +Explore the developer resources to learn how to get started with the Info Gateway \ No newline at end of file diff --git a/doc/mkdocs.yml b/doc/mkdocs.yml new file mode 100644 index 00000000..d37a3704 --- /dev/null +++ b/doc/mkdocs.yml @@ -0,0 +1,81 @@ +site_name: FHIR Info Gateway Docs +theme: + name: material + features: +# - navigation.tabs + - navigation.tabs.sticky + - navigation.section + - toc.follow +# - toc.integrate + - navigation.top + - navigation.path + - search.suggest + - search.highlight + - content.tabs.link + - content.code.annotation + - content.code.copy + - navigation.footer + language: en + palette: + - scheme: default + toggle: + icon: material/toggle-switch-off-outline + name: Switch to dark mode + primary: indigo + accent: purple + - scheme: slate + toggle: + icon: material/toggle-switch + name: Switch to light mode + primary: indigo + accent: lime + icon: + repo: fontawesome/brands/github + +font: + text: inter + + +plugins: + - search + +site_url: https://example.com/info-gateway/ + +repo_url: https://github.com/google/fhir-gateway + +repo_name: FHIR Info Gateway + +nav: + - Home: 'index.md' + - Concepts: 'concepts.md' + - Getting Started: 'getting_started.md' + - Tutorials: + - 'Run the Info Gateway in Docker' : 'tutorial_docker.md' + - 'Create an access checker' : 'tutorial_first_access_checker.md' + - Design: 'design.md' + - Community: + - 'Support' : 'support.md' + - 'Contributing': 'contribute.md' + - 'Release process': 'release_process.md' + +markdown_extensions: + - pymdownx.highlight: + anchor_linenums: true + - pymdownx.inlinehilite + - pymdownx.snippets + - admonition + - pymdownx.arithmatex: + generic: true + - footnotes + - pymdownx.details + - pymdownx.superfences + - pymdownx.tabbed: + alternate_style: true + - pymdownx.mark + - attr_list + - pymdownx.emoji: + emoji_index: !!python/name:materialx.emoji.twemoji + emoji_generator: !!python/name:materialx.emoji.to_svg + +copyright: | + © 2024 Google Health Open Health Stack \ No newline at end of file diff --git a/doc/requirements.txt b/doc/requirements.txt new file mode 100644 index 00000000..9a8a4ca4 --- /dev/null +++ b/doc/requirements.txt @@ -0,0 +1,2 @@ +mkdocs +mkdocs-material