Skip to content

Commit

Permalink
#450: Consider changes in the CH:PPQm profile
Browse files Browse the repository at this point in the history
  • Loading branch information
unixoid committed Jul 9, 2024
1 parent 9e535c0 commit f1619cf
Show file tree
Hide file tree
Showing 14 changed files with 164 additions and 27 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ class FhirToXacmlTranslator {
def templateId = ChPpqmUtils.extractConsentId(consent, ChPpqmUtils.ConsentIdTypes.TEMPLATE_ID)
switch (templateId) {
case '301':
case '304':
substitutions.put('gln', consent.provision.actor[0].reference.identifier.value)
break
case '302':
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,9 @@ class XacmlToFhirTranslator {
}

String gln = extractAttributeValue(subjectMatches, 'urn:oasis:names:tc:xacml:1.0:subject:subject-id')
return create301Consent(id, eprSpid, gln, policyIdReference, startDate, endDate)
return policyIdReference.contains('delegation')
? create304Consent(id, eprSpid, gln, policyIdReference, startDate, endDate)
: create301Consent(id, eprSpid, gln, policyIdReference, startDate, endDate)
}

private static Date parseDate(GPathResult xacml, String matchId) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ private static Consent createConsent(
.collect(Collectors.toList())));

consent.setId(consentId);
consent.getMeta().addProfile(ChPpqmUtils.Profiles.CONSENT);
consent.getMeta().addProfile(ChPpqmUtils.getTemplateProfileUri(templateId));
return consent;
}

Expand Down Expand Up @@ -110,14 +110,16 @@ private static Consent.provisionActorComponent createActor(SubjectRole role) {
private static Consent.provisionActorComponent createInstanceActor(
SubjectRole role,
String idQualifier,
String id)
String id,
String idSystem)
{
return createActor(role)
.setReference(new Reference()
.setIdentifier(new Identifier()
.setType(new CodeableConcept(new Coding()
.setSystem(Constants.URN_IETF_RFC_3986)
.setCode(idQualifier)))
.setSystem(idSystem)
.setValue(id)));
}

Expand All @@ -137,7 +139,7 @@ public static Consent create201Consent(String id, String eprSpid) {
"201",
eprSpid,
"urn:e-health-suisse:2015:policies:access-level:full",
createInstanceActor(SubjectRole.PATIENT, "urn:e-health-suisse:2015:epr-spid", eprSpid),
createInstanceActor(SubjectRole.PATIENT, "urn:e-health-suisse:2015:epr-spid", eprSpid, "urn:oid:" + PpqConstants.CodingSystemIds.SWISS_PATIENT_ID),
null,
null,
Collections.emptyList());
Expand Down Expand Up @@ -169,7 +171,7 @@ public static Consent create203Consent(String id, String eprSpid, String policyI
List.of(PurposeOfUse.NORMAL, PurposeOfUse.AUTO, PurposeOfUse.DICOM_AUTO));
}

// template 301 -- read access for a particular HCP
// template 301 -- read access for a particular HCP without the possibility to delegate
public static Consent create301Consent(
String id,
String eprSpid,
Expand All @@ -178,16 +180,12 @@ public static Consent create301Consent(
Date startDate,
Date endDate)
{
if (policyIdReference.contains("delegation") && (endDate == null)) {
// TODO: In Swiss EPR spec revision 2024, delegation will be moved to template 304.
throw new IllegalArgumentException("In delegation policies, the end date shall be provided");
}
return createConsent(
id,
"301",
eprSpid,
policyIdReference,
createInstanceActor(SubjectRole.PROFESSIONAL, "urn:gs1:gln", gln),
createInstanceActor(SubjectRole.PROFESSIONAL, "urn:gs1:gln", gln, ChPpqmUtils.CodingSystems.GLN),
startDate,
endDate,
List.of(PurposeOfUse.NORMAL));
Expand All @@ -210,7 +208,7 @@ public static Consent create302Consent(
"302",
eprSpid,
policyIdReference,
createInstanceActor(SubjectRole.PROFESSIONAL, "urn:oasis:names:tc:xspa:1.0:subject:organization-id", groupOid),
createInstanceActor(SubjectRole.PROFESSIONAL, "urn:oasis:names:tc:xspa:1.0:subject:organization-id", groupOid, null),
startDate,
endDate,
List.of(PurposeOfUse.NORMAL));
Expand All @@ -229,10 +227,33 @@ public static Consent create303Consent(
"303",
eprSpid,
"urn:e-health-suisse:2015:policies:access-level:full",
createInstanceActor(SubjectRole.REPRESENTATIVE, "urn:e-health-suisse:representative-id", representativeId),
createInstanceActor(SubjectRole.REPRESENTATIVE, "urn:e-health-suisse:representative-id", representativeId, null),
startDate,
endDate,
Collections.emptyList());
}

// template 304 -- read access for a particular HCP, with the possibility to delegate
public static Consent create304Consent(
String id,
String eprSpid,
String gln,
String policyIdReference,
Date startDate,
Date endDate)
{
if (policyIdReference.contains("delegation") && (endDate == null)) {
throw new IllegalArgumentException("In delegation policies, the end date shall be provided");
}
return createConsent(
id,
"304",
eprSpid,
policyIdReference,
createInstanceActor(SubjectRole.PROFESSIONAL, "urn:gs1:gln", gln, ChPpqmUtils.CodingSystems.GLN),
startDate,
endDate,
List.of(PurposeOfUse.NORMAL));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,9 @@ public class ChPpqmUtils {
try {
FHIR_CONTEXT = IgBasedFhirContextSupplier.getContext(
FhirContext.forR4(),
"classpath:/igs/ch-ppqm-2.0.0.tgz",
"classpath:/igs/ch-epr-term-2.0.9.tgz");
"classpath:/igs/ch-epr-fhir-4.0.1-ballot.tgz",
"classpath:/igs/ch-epr-term-2.0.9.tgz",
"classpath:/igs/ch-core-4.0.1.tgz");
} catch (IOException e) {
throw new ExceptionInInitializerError(e);
}
Expand All @@ -55,14 +56,25 @@ public static FhirContext getFhirContext() {
return FHIR_CONTEXT;
}

public static final Set<String> TEMPLATE_IDS = Set.of("201", "202", "203", "301", "302", "303", "304");

public static String getTemplateProfileUri(String templateId) {
return Profiles.BASE_URL + "/PpqmConsentTemplate" + templateId;
}

public static final Set<String> TEMPLATE_PROFILE_URIS = TEMPLATE_IDS.stream()
.map(ChPpqmUtils::getTemplateProfileUri)
.collect(Collectors.toSet());

public static class Profiles {
public static final String CONSENT = "http://fhir.ch/ig/ch-epr-ppqm/StructureDefinition/PpqmConsent";
public static final String FEED_REQUEST_BUNDLE = "http://fhir.ch/ig/ch-epr-ppqm/StructureDefinition/PpqmFeedRequestBundle";
public static final String RETRIEVE_RESPONSE_BUNDLE = "http://fhir.ch/ig/ch-epr-ppqm/StructureDefinition/PpqmRetrieveResponseBundle";
public static final String BASE_URL = "http://fhir.ch/ig/ch-epr-fhir/StructureDefinition";
public static final String FEED_REQUEST_BUNDLE = BASE_URL + "/PpqmFeedRequestBundle";
public static final String RETRIEVE_RESPONSE_BUNDLE = BASE_URL + "/PpqmRetrieveResponseBundle";
}

public static class CodingSystems {
public static String CONSENT_IDENTIFIER_TYPE = "http://fhir.ch/ig/ch-epr-ppqm/CodeSystem/PpqmConsentIdentifierType";
public static String CONSENT_IDENTIFIER_TYPE = "http://fhir.ch/ig/ch-epr-fhir/CodeSystem/PpqmConsentIdentifierType";
public static String GLN = "urn:oid:2.51.1.3";
}

public static class ConsentIdTypes {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ private OperationOutcome doValidateRequest(Object payload, Map<String, Object> p
switch (method) {
case "POST":
case "PUT":
return validateProfileConformance((Resource) payload, ChPpqmUtils.Profiles.CONSENT);
return validateProfileConformance((Resource) payload, ChPpqmUtils.TEMPLATE_PROFILE_URIS);

case "DELETE":
String resourceId = (String) payload;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ public void validateRequest(Object payload, Map<String, Object> parameters) {

@Override
public void validateResponse(Object payload, Map<String, Object> parameters) {
handleOperationOutcome(validateProfileConformance((Resource) payload, ChPpqmUtils.Profiles.CONSENT));
handleOperationOutcome(validateProfileConformance((Resource) payload, ChPpqmUtils.TEMPLATE_PROFILE_URIS));
}

}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ public void testConsent203Creation1() throws Exception {
@Test
public void testConsent301Creation1() throws Exception {
Consent consent = create301Consent(createUuid(), "123456789012345678", "3210987654321",
"urn:e-health-suisse:2015:policies:access-level:delegation-and-normal", null, new Date());
"urn:e-health-suisse:2015:policies:access-level:normal", null, new Date());
doTest("301", consent, "POST", null);
}

Expand All @@ -144,6 +144,13 @@ public void testConsent303Creation1() throws Exception {
doTest("303", consent, "POST", null);
}

@Test
public void testConsent304Creation1() throws Exception {
Consent consent = create304Consent(createUuid(), "123456789012345678", "3210987654321",
"urn:e-health-suisse:2015:policies:access-level:delegation-and-normal", null, new Date());
doTest("304", consent, "POST", null);
}

@Test
public void testPpq3To1RequestTranslation() {
Consent consent = create303Consent(createUuid(), "123456789012345678", "rep123", null, null);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,15 @@
import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
import ca.uhn.fhir.validation.FhirValidator;
import ca.uhn.fhir.validation.ValidationResult;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Validate;
import org.hl7.fhir.r4.model.CanonicalType;
import org.hl7.fhir.r4.model.OperationOutcome;
import org.hl7.fhir.r4.model.Resource;
import org.openehealth.ipf.commons.ihe.fhir.FhirTransactionValidator;

import java.util.Comparator;
import java.util.Set;

/**
* Validator which uses Implementation Guides to validate FHIR resources.
Expand All @@ -46,8 +48,8 @@ protected IgBasedInstanceValidator(FhirContext fhirContext) {
* @param resource FHIR resource to validate.
* @return {@link OperationOutcome} containing or not containing validation errors (never <code>null</code>).
*/
protected OperationOutcome validateProfileConformance(Resource resource, String profileUri) {

protected OperationOutcome validateProfileConformance(Resource resource, Set<String> allowedProfileUris) {
String profileUri = allowedProfileUris.iterator().next();
if (profileUri.startsWith(STANDARD_PREFIX)) {
String expectedResourceType = profileUri.substring(STANDARD_PREFIX.length());
if (resource.fhirType().equals(expectedResourceType)) {
Expand All @@ -61,7 +63,7 @@ protected OperationOutcome validateProfileConformance(Resource resource, String
}
} else {
for (CanonicalType profile : resource.getMeta().getProfile()) {
if (profile.equals(profileUri)) {
if (allowedProfileUris.contains(profile.asStringValue())) {
return doValidate(resource);
}
}
Expand All @@ -71,7 +73,11 @@ protected OperationOutcome validateProfileConformance(Resource resource, String
.addIssue(new OperationOutcome.OperationOutcomeIssueComponent()
.setSeverity(OperationOutcome.IssueSeverity.ERROR)
.setCode(OperationOutcome.IssueType.REQUIRED)
.setDiagnostics("Resource shall declare profile " + profileUri));
.setDiagnostics("Resource shall declare one of the profiles: " + StringUtils.join(allowedProfileUris, ", ")));
}

protected OperationOutcome validateProfileConformance(Resource resource, String allowedProfileUri) {
return validateProfileConformance(resource, Set.of(allowedProfileUri));
}

private OperationOutcome doValidate(Resource resource) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ class ChPpqPolicySetCreator {
velocityProperties.setProperty('resource.loader.classpath.class', ClasspathResourceLoader.class.getName());
VELOCITY = new VelocityEngine(velocityProperties)
VELOCITY.init()
POLICY_SET_TEMPLATES = ['201', '202', '203', '301', '302', '303'].collectEntries { templateId ->
POLICY_SET_TEMPLATES = ['201', '202', '203', '301', '302', '303', '304'].collectEntries { templateId ->
[templateId, VELOCITY.getTemplate("templates/policy-set-template-${templateId}.xml")]
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@
#if ($startDate || $endDate)
<Environments>
<Environment>
<!-- Valid to / optional for exclusion list assignments and HCP assignments but NOT optional for HCP assignments with delegation-->
#if ($startDate)
<EnvironmentMatch
MatchId="urn:oasis:names:tc:xacml:1.0:function:date-less-than-or-equal">
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
<!-- Template 304 -->
<PolicySet
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:hl7="urn:hl7-org:v3"
xmlns="urn:oasis:names:tc:xacml:2.0:policy:schema:os"
PolicySetId="${id}"
PolicyCombiningAlgId="urn:oasis:names:tc:xacml:1.0:policy-combining-algorithm:deny-overrides">
<Description>
Patient specific PolicySet for User Assignment 304 - allowing a user (health professional) to access the patient's EPD according to the scope of the referenced access level (PolicySetIdReference below)
with the possibility of delegation
</Description>
<Target>
<Subjects>
<Subject>
<SubjectMatch MatchId="urn:oasis:names:tc:xacml:1.0:function:string-equal">
<AttributeValue DataType="http://www.w3.org/2001/XMLSchema#string">${gln}</AttributeValue>
<SubjectAttributeDesignator
AttributeId="urn:oasis:names:tc:xacml:1.0:subject:subject-id"
DataType="http://www.w3.org/2001/XMLSchema#string" />
</SubjectMatch>
<SubjectMatch MatchId="urn:oasis:names:tc:xacml:1.0:function:string-equal">
<AttributeValue DataType="http://www.w3.org/2001/XMLSchema#string">urn:gs1:gln</AttributeValue>
<SubjectAttributeDesignator
AttributeId="urn:oasis:names:tc:xacml:1.0:subject:subject-id-qualifier"
DataType="http://www.w3.org/2001/XMLSchema#string" />
</SubjectMatch>
<SubjectMatch MatchId="urn:hl7-org:v3:function:CV-equal">
<AttributeValue DataType="urn:hl7-org:v3#CV">
<hl7:CodedValue code="HCP" codeSystem="2.16.756.5.30.1.127.3.10.6" />
</AttributeValue>
<SubjectAttributeDesignator
DataType="urn:hl7-org:v3#CV"
AttributeId="urn:oasis:names:tc:xacml:2.0:subject:role" />
</SubjectMatch>
</Subject>
</Subjects>
<Resources>
<Resource>
<ResourceMatch MatchId="urn:hl7-org:v3:function:II-equal">
<AttributeValue DataType="urn:hl7-org:v3#II">
<hl7:InstanceIdentifier root="2.16.756.5.30.1.127.3.10.3" extension="${eprSpid}"/>
</AttributeValue>
<ResourceAttributeDesignator
DataType="urn:hl7-org:v3#II"
AttributeId="urn:e-health-suisse:2015:epr-spid"/>
</ResourceMatch>
#if ($startDate)
<ResourceMatch
MatchId="urn:oasis:names:tc:xacml:1.0:function:date-less-than-or-equal">
<AttributeValue DataType="http://www.w3.org/2001/XMLSchema#date">${startDate}</AttributeValue>
<ResourceAttributeDesignator
AttributeId="urn:e-health-suisse:2023:policy-attributes:start-date"
DataType="http://www.w3.org/2001/XMLSchema#date"/>
</ResourceMatch>
#end
<ResourceMatch
MatchId="urn:oasis:names:tc:xacml:1.0:function:date-greater-than-or-equal">
<AttributeValue DataType="http://www.w3.org/2001/XMLSchema#date">${endDate}</AttributeValue>
<ResourceAttributeDesignator
AttributeId="urn:e-health-suisse:2023:policy-attributes:end-date"
DataType="http://www.w3.org/2001/XMLSchema#date"/>
</ResourceMatch>
</Resource>
</Resources>
<Environments>
<Environment>
#if ($startDate)
<EnvironmentMatch
MatchId="urn:oasis:names:tc:xacml:1.0:function:date-less-than-or-equal">
<AttributeValue DataType="http://www.w3.org/2001/XMLSchema#date">${startDate}</AttributeValue>
<EnvironmentAttributeDesignator
AttributeId="urn:oasis:names:tc:xacml:1.0:environment:current-date"
DataType="http://www.w3.org/2001/XMLSchema#date"/>
</EnvironmentMatch>
#end
<EnvironmentMatch
MatchId="urn:oasis:names:tc:xacml:1.0:function:date-greater-than-or-equal">
<AttributeValue DataType="http://www.w3.org/2001/XMLSchema#date">${endDate}</AttributeValue>
<EnvironmentAttributeDesignator
AttributeId="urn:oasis:names:tc:xacml:1.0:environment:current-date"
DataType="http://www.w3.org/2001/XMLSchema#date"/>
</EnvironmentMatch>
</Environment>
</Environments>
</Target>
<PolicySetIdReference>
${policyIdReference}
</PolicySetIdReference>
</PolicySet>

0 comments on commit f1619cf

Please sign in to comment.