15.15.1Consent Module: Overview
Experimental

 

15.15.1.1Summary

Please contact us if you would like to try out this experimental new feature.

The Consent Module contributes a Consent Service to FHIR Endpoints. A Consent Service can remove or alter the resource(s) in a response to enforce privacy rules, or to remove proprietary data during a data transfer between organizations. The Consent Service configured by a Consent Module is configured dynamically for each request based on the relevant Consent resources in a FHIR repository. Each relevant Consent resource is matched to ordered rules, and translated into a partial Consent Service. These partial Consent Services are then combined using the ordered rules to determine the verdict from the startOperation, canSeeResource, and willSeeResource consent hooks.

15.15.1.2Overview

A Consent Service can use the startOperation to block requests, and thecanSeeResource, and willSeeResource hooks remove or alter the resource(s) in a response. For background on the Consent Service, and the capabilities of startOperation, canSeeResource, and willSeeResource, see the introduction and api documentation for here:

Each method in this interface represents a consent method. Consent logic can be applied to resources in a clinical repository however, there is no integration with Consent resources in a consent repository.

The Consent Module has the same consent methods, but instead of code, implementers use JSON configuration to define rules. These rules are:

  1. Driven by Consent resources 2. Evaluated in the order they are defined 3. Protected by a fallback consent rule which is used if a decisive Consent verdict is not reached

15.15.1.3Consent Module JSON Configuration

The sample JSON below will be used to show how the Consent Module is configured:

{
	"willSeeResource": {
		"consentServiceFactory": "myConsentService",
		"consentRules": [
			{ "name": "BREAK_THE_GLASS_RULE", "matching": [  { "matchUrl": "Consent?purpose=BTG"} ]},
			{ "name": "PATIENT_GRANT_RULE", "matching": [  { "matchUrl": "Consent?category=patient-grant-code" } ] }
		],
		"fallbackConsentRule": "myFallbackConsentService"
	}
}

15.15.1.3.1Step 1: Determine the Applicable Consent Method(s)

The first thing implementers must decide is which consent method the consent rules should apply to.

The Consent Module supports the startOperation, canSeeResource, and willSeeResource consent methods.

The sample JSON configuration above uses the willSeeResource consent method.

15.15.1.3.2Step 2: Define the Consent Fetch Queries

Implementers are required to provide a list of Consent fetch queries by creating and registering an interceptor that has the CONSENT_FETCH_QUERIES pointcut.

A fetch query will typically define a Consent patient and a Consent actor. The fetch query callback for the resource-specific methods (i.e. canSeeResource, and the willSeeResource) will include the Patient id for resources in a Patient Compartment. Implementors should extract the Consent Actor from the request. Common locations are custom headers, custom OIDC/SMART claims, user information, and others.

For example, if a request was being made by Organization/1 to view Observation/1 with Observation.subject = Patient/1, a sensible fetch query might be:

Consent?status=active&actor=Organization/1&patient=Patient/1

Other common queries are:

  • Consent?status=active&actor:missing=true&patient=Patient/patient-1 for patient blanket Consent resources not related to a specific actor.
  • Consent?status=active&actor=Organization/organization-1&patient:missing=true for blanket Consent scoped to an Organization for any Patient. The code snippet below shows a sample interceptor that implements the CONSENT_FETCH_QUERIES pointcut.
/*-
 * #%L
 * Smile CDR - CDR
 * %%
 * Copyright (C) 2016 - 2024 Smile CDR, Inc.
 * %%
 * All rights reserved.
 * #L%
 */
package com.smilecdr.demo.consentmodule;

import ca.cdr.api.consent.ConsentLookupContext;
import ca.cdr.api.fhir.interceptor.CdrHook;
import ca.cdr.api.fhir.interceptor.CdrPointcut;
import ca.cdr.api.model.json.UserSessionDetailsJson;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.springframework.web.util.UriComponentsBuilder;

import java.util.HashSet;
import java.util.Optional;

/**
 * A sample interceptor that demonstrates how to provide a list of Consent fetch queries by
 * implementing the CONSENT_FETCH_QUERIES pointcut.
 */
@SuppressWarnings({"unused"})
public class ConsentFetchQueriesInterceptor {

	private static final FhirContext myFhirContext = FhirContext.forR4Cached();

	/**
	 * Uses the CONSENT_FETCH_QUERIES pointcut to provide a list of fetch queries
	 * @param theRequestDetails - the details of the request
	 * @param theConsentLookupContext - the consent lookup context
	 * @param theResource - the resource that consent rules should apply to
	 * @param theUserSessionDetails - the user session details
	 * @return a list of queries that will be used to fetch Consent resources
	 */
	@CdrHook(CdrPointcut.CONSENT_FETCH_QUERIES)
	public String[] getConsentFetchQueries(
		RequestDetails theRequestDetails, ConsentLookupContext theConsentLookupContext, IBaseResource theResource, UserSessionDetailsJson theUserSessionDetails) {

		IdDt consentActor = getConsentActor(theUserSessionDetails);
		IdDt consentPatient = getConsentPatient(theResource);

		UriComponentsBuilder query = UriComponentsBuilder.fromPath("Consent")
			.queryParam("status", "active")
			.queryParam("actor", consentActor.getValue())
			.queryParam("patient", consentPatient.getValue());

		return new String[]{query.toUriString()};
	}

	/**
	 * Determines the Organization of the requester that will be used as <code>Consent.actor</code>
	 * @param theUserSessionDetails - the user session details
	 * @return IdDt of the Organization that will be used as the <code>Consent.actor</code>
	 */
	private IdDt getConsentActor(UserSessionDetailsJson theUserSessionDetails) {
		// UserSessionDetailsJson contains all the user session and OIDC information for the incoming request
		// so implementers can determine the consent actor.
		String username = theUserSessionDetails.getUsername();
		if ("ADMIN".equals(theUserSessionDetails.getUsername())){
			return new IdDt("Organization", "1");
		} else {
			throw new IllegalArgumentException("Could not determine actor from UserSessionDetailsJson");
		}
	}


	/**
	 * Finds the Patient compartment of the provided resource which is used to determine the <code>Consent.patient</code>
	 * @param theResource - the resource that consent rules should apply to
	 * @return IdDt of the Patient that will be used as <code>Consent.patient</code>
	 */
	private IdDt getConsentPatient(IBaseResource theResource) {
		Optional<IdDt> consentPatient = myFhirContext.newTerser().getCompartmentOwnersForResource("Patient", theResource, new HashSet<>()).stream()
			.map(id -> new IdDt(id.getValue()))
			.findFirst();

		if (consentPatient.isPresent()){
			return consentPatient.get();
		}

		throw new IllegalArgumentException("Could not determine consent patient from: " + theResource.getIdElement().getIdPart());
	}
}

15.15.1.3.3Step 3: Define the `consentRules`

The consentRules are used create a list of Consent buckets that Consent resources will be put into. These are defined by a name and matchUrl. After Consent resources are obtained from Step 2 they will be placed into the "bucket" with the specified name if they meet the matchUrl criteria.

A new instance of an IConsentService will be created for each Consent resource in a bucket. The actual implementation of the IConsentService will be mapped using the Consent service factory (consentServiceFactory property).

When clinical resources are accessed, the consent buckets will be evaluated in the order that they are defined. The clinical resource will be evaluated by each IConsentService instance that was created for the Consent resources in that bucket.

Note: Implementers may define buckets for different Consent actors (i.e. Patient vs Organization).

15.15.1.3.3.1Evaluation Within Consent Buckets

If any of the IConsentService instances in the bucket produce a REJECT verdict, the verdict for the entire bucket will be REJECT. If the bucket does not produce any REJECT verdicts and an AUTHORIZED verdict, the verdict for the entire bucket will be AUTHORIZED. If the bucket only produces PROCEED verdicts, the verdict for the entire bucket will be PROCEED.

15.15.1.3.3.2Evaluation Across Consent Buckets

As soon as there is a decisive verdict (i.e. AUTHORIZED or REJECT) from a Consent bucket, it will be used and none of the remaining Consent buckets will be evaluated. However, if the verdict from a bucket is indecisive (i.e. PROCEED) the subsequent buckets(s) will be evaluated. Finally, if there are no decisive outcomes from any of the buckets, the fallback Consent rule will be applied.

The consentRules in the sample JSON configuration above are used to define two Consent buckets:

  1. The first Consent bucket is called BREAK_THE_GLASS_RULE and will contain all Consent resources from Step 2 that have a purpose of BTG.

    	{ "name": "BREAK_THE_GLASS_RULE", "matching": [  { "matchUrl": "Consent?purpose=BTG"} ]}
    
  2. The second Consent bucket is called PATIENT_GRANT_RULE and will contain all Consent resources from Step 2 that have category of patient-grant-code.

    	{ "name": "PATIENT_GRANT_RULE", "matching": [  { "matchUrl": "Consent?category=patient-grant-code" } ] }
    

15.15.1.3.4Step 4: Define the Consent Service Factory

The Consent Service Factory (consentServiceFactory property) is the name of the IConsentService that will be created for each Consent resource in a Consent Bucket. Clinical resources being accessed will be evaluated against each IConsentService instance to determine a verdict based on the rules outlined in the Step 3.

The consentServiceFactory can be a Smile provided IConsentService or a custom IConsentService.

If implementers want to use a custom IConsentService, they must implement the IConsentService interface and map the value of the consentServiceFactory property to a new instance of their IConsentService. This is done by creating and registering an interceptor that has the CONSENT_BUILD_CONSENT_SERVICE pointcut.

The sample CONSENT_BUILD_CONSENT_SERVICE Interceptor shows how a custom consentServiceFactory is created.

15.15.1.3.5Step 5: Define the Fallback Consent Rule

The Fallback Consent Rule (fallbackConsentRule property) is the name of the IConsentService that is used if none of the Consent Buckets produces a decisive verdict (i.e. all buckets produce a PROCEED verdict).

Implementers can specify a ConsentOperationStatusEnum if the fallback consent rule is constant (i.e. REJECT, AUTHORIZED, or PROCEED).

Implementers can also specify a custom IConsentService if more complex fallback logic is required. This should be done in a similar manner as the Consent Service Factory where a custom IConsentService is defined and mapped using the CONSENT_BUILD_CONSENT_SERVICE pointcut.

The sample CONSENT_BUILD_CONSENT_SERVICE Interceptor shows how a custom fallbackConsentRule is created.

15.15.1.3.5.1Sample CONSENT_BUILD_CONSENT_SERVICE Interceptor

The code snippet below shows a sample interceptor that implements the CONSENT_BUILD_CONSENT_SERVICE pointcut to provide the custom consentServiceFactory and custom fallbackConsentRule defined in the sample JSON configuration above.

/*-
 * #%L
 * Smile CDR - CDR
 * %%
 * Copyright (C) 2016 - 2024 Smile CDR, Inc.
 * %%
 * All rights reserved.
 * #L%
 */
package com.smilecdr.demo.consentmodule;

import ca.cdr.api.fhir.interceptor.CdrHook;
import ca.cdr.api.fhir.interceptor.CdrPointcut;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.server.interceptor.consent.ConsentOutcome;
import ca.uhn.fhir.rest.server.interceptor.consent.IConsentContextServices;
import ca.uhn.fhir.rest.server.interceptor.consent.IConsentService;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.r4.model.Consent;

/**
 * A sample interceptor that demonstrates how to use the CONSENT_BUILD_CONSENT_SERVICE pointcut
 */
@SuppressWarnings({"unused"})
public class BuildConsentServiceInterceptor {

	public static final String CONSENT_SERVICE_NAME = "myConsentService";
	public static final String FALLBACK_CONSENT_SERVICE_NAME = "myFallbackConsentService";

	/**
	 * Uses the CONSENT_BUILD_CONSENT_SERVICE pointcut to map the <code>consentServiceFactory</code> and <code>fallbackConsentRule</code>
	 * properties to custom <code>IConsentService</code> implementations.
	 * @param theConsentServiceName - the name of the consent service that should be mapped (from <code>consentServiceFactory</code> or <code>fallbackConsentRule</code>)
	 * @param theOptionalConsent - the Consent resource. This will be present for the <code>consentServiceFactory</code> and absent for the <code>fallbackConsentRule</code>.
	 * @return an IConsentService that will be used to supply the <code>consentServiceFactory</code> or <code>fallbackConsentRule</code>
	 */
	@CdrHook(CdrPointcut.CONSENT_BUILD_CONSENT_SERVICE)
	public IConsentService buildConsentService(String theConsentServiceName, IBaseResource theOptionalConsent) {
		if (CONSENT_SERVICE_NAME.equals(theConsentServiceName)) {
			return new MyConsentService(theOptionalConsent);
		} else if (FALLBACK_CONSENT_SERVICE_NAME.equals(theConsentServiceName)) {
			return new MyFallbackConsentService();
		}
		throw new IllegalArgumentException("No IConsentService registered for: " + theConsentServiceName);
	}

	/**
	 * The implementation of IConsentService that will be used for the <code>consentServiceFactory</code>
	 */
	static class MyConsentService implements IConsentService {
		private final Consent myConsent;

		public MyConsentService(IBaseResource theConsent) {
			if (!(theConsent instanceof Consent)){
				throw new IllegalArgumentException("Invalid Consent resource: " + theConsent.getIdElement().getIdPart());
			}
			myConsent = (Consent) theConsent;
		}

		@Override
		public ConsentOutcome canSeeResource(RequestDetails theRequestDetails, IBaseResource theResource, IConsentContextServices theContextServices) {
			if (isSpecialConsent(myConsent) && isSpecialResource(theResource)){
				return ConsentOutcome.AUTHORIZED;
			}
			return ConsentOutcome.PROCEED;
		}

		private boolean isSpecialConsent(Consent theConsent){
			return "some-special-consent".equals(myConsent.getId());
		}

		private boolean isSpecialResource(IBaseResource theResource){
			return "some-special-resource".equals(theResource.getIdElement().getIdPart());
		}

	}

	/**
	 * The implementation of IConsentService that will be used for the <code>fallbackConsentRule</code>
	 */
	static class MyFallbackConsentService implements IConsentService {
		@Override
		public ConsentOutcome canSeeResource(RequestDetails theRequestDetails, IBaseResource theResource, IConsentContextServices theContextServices) {
			if (isSpecialResource(theResource)){
				return ConsentOutcome.AUTHORIZED;
			}
			return ConsentOutcome.REJECT;
		}

		private boolean isSpecialResource(IBaseResource theResource){
			return "some-special-resource".equals(theResource.getIdElement().getIdPart());
		}
	}
}

15.15.1.4Consent Module Configuration (Diagram)

Consent Module Configuration

15.15.1.5Consent Module Request Flow

The diagram below shows what a request could look like using the sample JSON configuration:

Consent Module Request Flow

15.15.1.6Glossary

15.15.1.6.1Active Consent Resources

The resources used to configure the Consent Service for a single request. These are the search results for the Consent Fetch Queries.

15.15.1.6.2Clinical Repository

A FHIR repository that contains clinical data (e.g. Observation, Condition, etc.) to be protected by Consent resources.

15.15.1.6.3Clinical Patient Compartment(s)

The Patient compartment(s) implicated by the query or Resource. For example, the query Observation?patient=Patient/1 would implicate the Patient/1 compartment.

Note: Chained queries may implicate many compartments. For example, if patient-1-mdm-id is a golden resource, then the query Observation?patient.identifier:mdm=patient-1-mdm-id may match several patients. In this case, each matched Patient resource will have its' own compartment.

15.15.1.6.4Consent Actor

The id of a resource in the Clinical Repository that will identify the actor of a request for purposes of Consent resolution. Each request should be mapped to an effective Consent.actor (i.e. Organization, Patient, Practitioner, etc.).

Implementers can use the UserSessionDetailsJson object provided by the CONSENT_FETCH_QUERIES pointcut to determine the Consent Actor. For example, a custom claim could be added to an OIDC client Access Token (i.e. Organization/1) which is mapped to the Consent Actor. Alternatively, the username of the Local Inbound Security User could be used to determine an effective Consent Actor.

15.15.1.6.5Consent Buckets

Consent buckets are defined by the consentRules property. They consist of a name and matchUrl. After Consent resources are obtained from the Consent fetch query they will be placed into the "bucket" with the specified name if they meet the matchUrl criteria.

15.15.1.6.6Consent Custodian

The individual responsible for creating and managing Consent resources.

15.15.1.6.7Consent Methods

The methods subject to Consent evaluation. The following Consent methods are supported by the Consent Module:

Consent Module JavaScript API Equivalent Java API Equivalent
startOperation consentStartOperation startOperation
canSeeResource consentCanSeeResource canSeeResource
willSeeResource consentWillSeeResource willSeeResource

15.15.1.6.8Consent Fetch Queries

The queries used to search for active Consent resources in the Consent Repository for a single request.

For example, this query would find Consent resources for a request on behalf of actor Organization/1 that apply to Patient/1, (i.e. Consent?status=active&actor=Organization/1&patient=Patient/1). Depending on your use of Consent resources, you may need to combine queries:

  • The query Consent?status=active&actor:missing=true&patient=Patient/1 will find Consent resources for to patient-1 that are not specific to a single actor. E.g. a blanket withdrawal of Consent.
  • The query Consent?status=active&actor=Organization/1&patient:missing=true will find Consent resources that apply to that actor regardless of the subject Patient.

15.15.1.6.9Consent Patient

The patient id (e.g. Patient/1) used to search for Consent resources in the Consent Repository (i.e. Consent.patient). This may be the Clinical Patient compartment of the resource which Consent rules should apply, or might come from a header, or via some MDM Service. Actual Patient compartments may have a many-to-one relationship to a Consent Patient compartment.

For example, in the query Observation?category=imaging&patient=Patient/1 if Patient/1 is a golden resource, then there may be multiple Consent Patient compartments (i.e. for each matching Patient).

15.15.1.6.10Consent Repository

The FHIR repository that contains Consent resources which mediate access to the Clinical Repository. This may be the same as the Clinical Repository.

15.15.1.6.11Consent Service Factory

Specified by the consentServiceFactory property of the JSON configuration. It is the name of the IConsentService instance that will be created for each of the Consent resources in a Consent Bucket which will be used to determine a Consent Verdict of a request.

15.15.1.6.12Consent Verdict

Consent adjudications which may be one of three results: AUTHORIZED (i.e. allow), PROCEED (i.e. abstain), and REJECT (i.e. deny). These verdicts are defined by the ConsentOperationStatusEnum.

Of the three verdicts, AUTHORIZED and REJECT are considered decisive. A PROCEED verdict is considered indecisive.

15.15.1.6.13Fallback Consent Rule

Specified by the fallbackConsentRule property. This rule defines how a Consent verdict should be reached if all the Consent buckets produce an indecisive verdict (i.e. PROCEED).