Skip to content

Understanding access checker plugins

williamito edited this page Mar 6, 2023 · 4 revisions

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.

Create an access checker plugin

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.

Explore the List access checker plugin

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.