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.
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:
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 reachedThe 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"
}
}
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.
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());
}
}
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).
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
.
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:
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"} ]}
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" } ] }
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.
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.
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());
}
}
}
The diagram below shows what a request could look like using the sample JSON configuration:
The resources used to configure the Consent Service for a single request. These are the search results for the Consent Fetch Queries.
A FHIR repository that contains clinical data (e.g. Observation, Condition, etc.) to be protected by Consent resources.
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.
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.
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.
The individual responsible for creating and managing Consent
resources.
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 |
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:
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.Consent?status=active&actor=Organization/1&patient:missing=true
will find Consent resources that apply to that actor regardless of the subject 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).
The FHIR repository that contains Consent resources which mediate access to the Clinical Repository. This may be the same as the Clinical Repository.
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.
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.
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
).