-
Notifications
You must be signed in to change notification settings - Fork 29
Understanding access checker plugins
FHIR Information 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. In this guide you'll learn about access checker plugins by looking at the List
access checker plugin example.
To create your own access checker plugin, create an implementation of the AccessCheckerFactory
interface annotated with a @Named(value = "name")
annotation defining the name of the plugin. The most important parts are to implement a custom AccessChecker
to be returned by the factory and its checkAccess
function which specifies if access is granted or not by returning an AccessDecision
.
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.
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.
@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);
}
}
}
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
, then run the gateway using e.g. java -jar exec/target/exec-0.1.0.jar --server.port=8080
.
The ListAccessChecker
plugin is a simple example of list-based access control. It works by assigning each user a FHIR List resource 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 the request contains or references at least one Patient
on that 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 decoded JWT of the access token used to authorize requests to the FHIR Information Gateway server. For example, following the test Docker deployment you may get a decoded access token like the following:
{
"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",
"realm_access":
{
"roles":
[
"default-roles-test",
"offline_access",
"uma_authorization"
]
},
"resource_access":
{
"account":
{
"roles":
[
"manage-account",
"manage-account-links",
"view-profile"
]
}
},
"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 client's patient access list resource is http://localhost:8099/fhir/List/patient-list-example
.
The decoded JWT is passed to your AccessCheckerFactory
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
function splits access logic according to the HTTP method. Simple yes/no access decisions like processGet()
use the NoOpAccessDecision
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()
. In this case, implement your own version of AccessDecision
. 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
.