From 07dd74daa9aba063648b96deee5d953a57f81572 Mon Sep 17 00:00:00 2001 From: bstewartlg <114590619+bstewartlg@users.noreply.github.com> Date: Wed, 13 Nov 2024 20:13:04 -0500 Subject: [PATCH] Added parallel support for $match and $idi-match operations. Added validation options for match operation for incoming Patient resource --- .../uhn/fhir/jpa/starter/AppProperties.java | 10 + .../ca/uhn/fhir/jpa/starter/Application.java | 8 +- .../starter/operations/IdentityMatching.java | 325 ++++++++++++++---- .../models/IdentityMatchParams.java | 21 ++ .../models/IdentityMatchValidationLevel.java | 28 ++ .../IdentityMatchingAuthInterceptor.java | 14 +- .../security/models/SecurityConfig.java | 3 + src/main/resources/application.yaml | 4 + 8 files changed, 334 insertions(+), 79 deletions(-) create mode 100644 src/main/java/ca/uhn/fhir/jpa/starter/operations/models/IdentityMatchParams.java create mode 100644 src/main/java/ca/uhn/fhir/jpa/starter/operations/models/IdentityMatchValidationLevel.java diff --git a/src/main/java/ca/uhn/fhir/jpa/starter/AppProperties.java b/src/main/java/ca/uhn/fhir/jpa/starter/AppProperties.java index df41e9e6..99c43ad1 100644 --- a/src/main/java/ca/uhn/fhir/jpa/starter/AppProperties.java +++ b/src/main/java/ca/uhn/fhir/jpa/starter/AppProperties.java @@ -6,6 +6,7 @@ import ca.uhn.fhir.jpa.api.config.JpaStorageSettings.IdStrategyEnum; import ca.uhn.fhir.jpa.model.entity.NormalizedQuantitySearchLevel; import ca.uhn.fhir.jpa.packages.PackageInstallationSpec; +import ca.uhn.fhir.jpa.starter.operations.models.IdentityMatchValidationLevel; import ca.uhn.fhir.rest.api.EncodingEnum; import lombok.Getter; import lombok.Setter; @@ -970,4 +971,13 @@ public void setResource_dbhistory_enabled(Boolean resource_dbhistory_enabled) { this.resource_dbhistory_enabled = resource_dbhistory_enabled; } + + // custom properties + + @Getter @Setter + private String matchValidationHeader = "X-Match-Validation"; + + @Getter @Setter + private IdentityMatchValidationLevel matchValidationLevel = IdentityMatchValidationLevel.DEFAULT; + } diff --git a/src/main/java/ca/uhn/fhir/jpa/starter/Application.java b/src/main/java/ca/uhn/fhir/jpa/starter/Application.java index 6f6cfc9d..466e4f4e 100644 --- a/src/main/java/ca/uhn/fhir/jpa/starter/Application.java +++ b/src/main/java/ca/uhn/fhir/jpa/starter/Application.java @@ -113,7 +113,7 @@ public ServletRegistrationBean hapiServletRegistration(RestfulSer restfulServer.registerInterceptor(securityDiscoveryInterceptor); IdentityMatchingAuthInterceptor authInterceptor = new IdentityMatchingAuthInterceptor( - securityConfig.getEnableAuthentication(), //securityConfig.getBypassHeader(), + securityConfig.getEnableAuthentication(), securityConfig.getBypassHeader(), securityConfig.getIssuer(), securityConfig.getPublicKey(), securityConfig.getIntrospectionUrl(), securityConfig.getClientId(), securityConfig.getClientSecret(), securityConfig.getProtectedEndpoints(), securityConfig.getPublicEndpoints()); @@ -130,7 +130,8 @@ public ServletRegistrationBean hapiServletRegistration(RestfulSer CorsInterceptor corsInterceptor = (CorsInterceptor) existingCorsInterceptor; // Add custom header to the existing CORS configuration - // corsInterceptor.getConfig().addAllowedHeader(securityConfig.getBypassHeader()); + corsInterceptor.getConfig().addAllowedHeader(securityConfig.getBypassHeader()); + corsInterceptor.getConfig().addAllowedHeader(appProperties.getMatchValidationHeader()); } else { // Define your CORS configuration @@ -140,7 +141,8 @@ public ServletRegistrationBean hapiServletRegistration(RestfulSer config.addAllowedHeader("Accept"); config.addAllowedHeader("X-Requested-With"); config.addAllowedHeader("Content-Type"); - // config.addAllowedHeader(securityConfig.getBypassHeader()); + config.addAllowedHeader(securityConfig.getBypassHeader()); + config.addAllowedHeader(appProperties.getMatchValidationHeader()); config.addAllowedOrigin("*"); diff --git a/src/main/java/ca/uhn/fhir/jpa/starter/operations/IdentityMatching.java b/src/main/java/ca/uhn/fhir/jpa/starter/operations/IdentityMatching.java index 328d7511..b4d11953 100644 --- a/src/main/java/ca/uhn/fhir/jpa/starter/operations/IdentityMatching.java +++ b/src/main/java/ca/uhn/fhir/jpa/starter/operations/IdentityMatching.java @@ -1,14 +1,16 @@ package ca.uhn.fhir.jpa.starter.operations; import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.fhirpath.IFhirPath; import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao; import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.jpa.starter.AppProperties; import ca.uhn.fhir.jpa.starter.common.FhirContextProvider; import ca.uhn.fhir.jpa.starter.operations.models.IdentifierQueryParams; +import ca.uhn.fhir.jpa.starter.operations.models.IdentityMatchParams; +import ca.uhn.fhir.jpa.starter.operations.models.IdentityMatchValidationLevel; import ca.uhn.fhir.jpa.starter.operations.models.IdentityMatchingScorer; import ca.uhn.fhir.model.base.composite.BaseIdentifierDt; import ca.uhn.fhir.model.dstu2.composite.IdentifierDt; -import ca.uhn.fhir.parser.DataFormatException; import ca.uhn.fhir.rest.annotation.Operation; import ca.uhn.fhir.rest.annotation.ResourceParam; import ca.uhn.fhir.rest.api.server.IBundleProvider; @@ -17,6 +19,8 @@ import ca.uhn.fhir.rest.gclient.IQuery; import ca.uhn.fhir.rest.gclient.StringClientParam; import ca.uhn.fhir.rest.param.*; +import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; +import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import org.apache.commons.lang3.StringUtils; import org.hl7.fhir.r4.model.*; import org.springframework.core.io.ResourceLoader; @@ -24,8 +28,10 @@ import java.io.IOException; import java.time.Instant; import java.util.ArrayList; +import java.util.EnumSet; import java.util.HashSet; import java.util.List; +import java.util.stream.Collectors; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; @@ -36,84 +42,151 @@ public class IdentityMatching { private final String IDI_Patient_Profile = "http://hl7.org/fhir/us/identity-matching/StructureDefinition/IDI-Patient"; private final String IDI_Patient_L0_Profile = "http://hl7.org/fhir/us/identity-matching/StructureDefinition/IDI-Patient-L0"; private final String IDI_Patient_L1_Profile = "http://hl7.org/fhir/us/identity-matching/StructureDefinition/IDI-Patient-L1"; - private boolean assertIDIPatientProfile = false; - private boolean assertIDIPatientL0Profile = false; - private boolean assertIDIPatientL1Profile = false; + + private final String IDI_Patient_FhirPath = "identifier.exists() or telecom.exists() or (name.family.exists() and name.given.exists()) or (address.line.exists() and address.city.exists()) or birthDate.exists()"; + private final String IDI_Patient_L0_FhirPath = "((identifier.type.coding.exists(code = 'PPN' or code = 'DL' or code = 'STID') or identifier.exists(system='http://hl7.org/fhir/us/identity-matching/ns/HL7Identifier')) and identifier.value.exists()).toInteger()*10 + iif(((address.exists(use = 'home') and address.line.exists() and (address.postalCode.exists() or (address.state.exists() and address.city.exists()))).toInteger() + (identifier.type.coding.exists(code != 'PPN' and code != 'DL' and code != 'STID') and identifier.value.exists()).toInteger() + (telecom.exists(system = 'email') and telecom.value.exists()).toInteger() + (telecom.exists(system = 'phone') and telecom.value.exists()).toInteger() + (photo.exists()).toInteger()) =1,4,iif(((address.exists(use = 'home') and address.line.exists() and (address.postalCode.exists() or (address.state.exists() and address.city.exists()))).toInteger() + (identifier.type.coding.exists(code != 'PPN' and code != 'DL' and code != 'STID') and identifier.value.exists()).toInteger() + (telecom.exists(system = 'email') and telecom.value.exists()).toInteger() + (telecom.exists(system = 'phone') and telecom.value.exists()).toInteger() + (photo.exists()).toInteger()) >1,5,0)) + (name.family.exists() and name.given.exists()).toInteger()*3 + (birthDate.exists().toInteger()*2) >= 9"; + private final String IDI_Patient_L1_FhirPath = "((identifier.type.coding.exists(code = 'PPN' or code = 'DL' or code = 'STID') or identifier.exists(system='http://hl7.org/fhir/us/identity-matching/ns/HL7Identifier')) and identifier.value.exists()).toInteger()*10 + iif(((address.exists(use = 'home') and address.line.exists() and (address.postalCode.exists() or (address.state.exists() and address.city.exists()))).toInteger() + (identifier.type.coding.exists(code != 'PPN' and code != 'DL' and code != 'STID') and identifier.value.exists()).toInteger() + (telecom.exists(system = 'email') and telecom.value.exists()).toInteger() + (telecom.exists(system = 'phone') and telecom.value.exists()).toInteger() + (photo.exists()).toInteger()) =1,4,iif(((address.exists(use = 'home') and address.line.exists() and (address.postalCode.exists() or (address.state.exists() and address.city.exists()))).toInteger() + (identifier.type.coding.exists(code != 'PPN' and code != 'DL' and code != 'STID') and identifier.value.exists()).toInteger() + (telecom.exists(system = 'email') and telecom.value.exists()).toInteger() + (telecom.exists(system = 'phone') and telecom.value.exists()).toInteger() + (photo.exists()).toInteger()) >1,5,0)) + (name.family.exists() and name.given.exists()).toInteger()*3 + (birthDate.exists().toInteger()*2) >= 10"; + + + private AppProperties appProperties; private String serverAddress; private IFhirResourceDao patientDao; private ResourceLoader resourceLoader; public IdentityMatching(AppProperties appProperties, IFhirResourceDao patientDao, ResourceLoader resourceLoader) { + this.appProperties = appProperties; this.serverAddress = appProperties.getServer_address(); this.patientDao = patientDao; this.resourceLoader = resourceLoader; } - - @Operation(name = "$idi-match", typeName = "Patient", manualResponse = true) - public void patientMatchOperation( + + /** + * $match operation defined in Identity Matching IG STU1 + * Extends the HL7 FHIR patient $match operation: http://hl7.org/fhir/R4/patient-operation-match.html + */ + @Operation(name = "$match", typeName = "Patient") + public Bundle patientMatchOperation( @ResourceParam Parameters params, HttpServletRequest theServletRequest, HttpServletResponse theServletResponse - ) throws DataFormatException, IOException + ) throws Exception { - assertIDIPatientProfile = false; - assertIDIPatientL0Profile = false; - assertIDIPatientL1Profile = false; - - FhirContext ctx = FhirContextProvider.getFhirContext(); - IGenericClient client = ctx.newRestfulGenericClient(serverAddress); - - Patient patient = null; - boolean onlyCertainMatches = false; - IntegerType count; - Bundle outputBundle = new Bundle(); + IdentityMatchParams identityMatchParams = new IdentityMatchParams(); for(var param : params.getParameter()) { //if a patient resource, set as patient to search against - if(param.getName().equals("patient") && param.getResource().getClass().equals(Patient.class)) + if(param.getName().equals("resource") && param.getResource() != null && param.getResource().getClass().equals(Patient.class)) { - patient = (Patient)param.getResource(); + identityMatchParams.setPatient((Patient)param.getResource()); } //check for onlyCertainMatches if(param.getName().equals("onlyCertainMatches")) { - onlyCertainMatches = ((BooleanType) param.getValue()).booleanValue(); + identityMatchParams.setOnlyCertainMatches((BooleanType) param.getValue()); } //check for count if(param.getName().equals("count")) { - count = (IntegerType)param.getValue(); + identityMatchParams.setCount((IntegerType)param.getValue()); } - } // Patient resource must be provided and the parameter must be named "patient" - if (patient == null) { + if (identityMatchParams.getPatient() == null) { + + String message = "A parameter named 'resource' must be provided with a valid Patient resource."; OperationOutcome outcome = new OperationOutcome(); outcome.addIssue().setCode(OperationOutcome.IssueType.INVALID).setSeverity(OperationOutcome.IssueSeverity.ERROR) - .setDiagnostics("A parameter named 'patient' must be provided with a valid Patient resource."); + .setDiagnostics(message); - writeResponse(theServletRequest, theServletResponse, outcome, HttpServletResponse.SC_BAD_REQUEST); - return; + throw new InvalidRequestException(message, outcome); } + return doMatch(identityMatchParams, theServletRequest, theServletResponse); - //check profile assertions - List metaProfiles = patient.getMeta().getProfile(); - for(CanonicalType profile : metaProfiles) { - switch(profile.getValue()) { - case(IDI_Patient_Profile): { assertIDIPatientProfile = true; } break; - case(IDI_Patient_L0_Profile): { assertIDIPatientL0Profile = true; } break; - case(IDI_Patient_L1_Profile): { assertIDIPatientL1Profile = true; } break; + } + + + /** + * $idi-match operation defined in Identity Matching IG STU2 + */ + @Operation(name = "$idi-match", typeName = "Patient") + public Bundle idiMatchOperation( + @ResourceParam Parameters params, + HttpServletRequest theServletRequest, + HttpServletResponse theServletResponse + ) throws IOException + { + + IdentityMatchParams identityMatchParams = new IdentityMatchParams(); + + for(var param : params.getParameter()) { + //if a patient resource, set as patient to search against + if(param.getName().equals("patient") && param.getResource() != null && param.getResource().getClass().equals(Patient.class)) + { + identityMatchParams.setPatient((Patient)param.getResource()); + } + + //check for onlyCertainMatches + if(param.getName().equals("onlyCertainMatches")) + { + identityMatchParams.setOnlyCertainMatches((BooleanType) param.getValue()); + } + + //check for count + if(param.getName().equals("count")) + { + identityMatchParams.setCount((IntegerType)param.getValue()); } } + + // Patient resource must be provided and the parameter must be named "patient" + if (identityMatchParams.getPatient() == null) { + + String message = "A parameter named 'patient' must be provided with a valid Patient resource."; + OperationOutcome outcome = new OperationOutcome(); + outcome.addIssue().setCode(OperationOutcome.IssueType.INVALID).setSeverity(OperationOutcome.IssueSeverity.ERROR) + .setDiagnostics(message); + + throw new InvalidRequestException(message, outcome); + } + + return doMatch(identityMatchParams, theServletRequest, theServletResponse); + + } + + + protected Bundle doMatch( + IdentityMatchParams params, + HttpServletRequest theServletRequest, + HttpServletResponse theServletResponse + ) throws IOException + { + + // assertIDIPatientProfile = false; + // assertIDIPatientL0Profile = false; + // assertIDIPatientL1Profile = false; + + FhirContext ctx = FhirContextProvider.getFhirContext(); + IGenericClient client = ctx.newRestfulGenericClient(serverAddress); + + Patient patient = params.getPatient(); + BooleanType onlyCertainMatches = params.getOnlyCertainMatches(); + IntegerType count = params.getCount(); + Bundle outputBundle = new Bundle(); + + + // ensure we have a valid Patient resource + assertPatientIsValid(patient, theServletRequest); + + //build out identifier search params and base identifier params by traversing the identifiers List identifierParams = new ArrayList<>(); List baseIdentifierParams = new ArrayList<>(); @@ -274,18 +347,126 @@ else if(com.hasSystem() && com.getSystem().toCode().equals(ContactPoint.ContactP outputBundle.getEntry().add(0, createBundleEntry(exampleOrg)); } else { + String message = "Organization-OrgExample.json file not found."; OperationOutcome outcome = new OperationOutcome(); outcome.addIssue().setCode(OperationOutcome.IssueType.EXCEPTION).setSeverity(OperationOutcome.IssueSeverity.ERROR) - .setDiagnostics("Organization-OrgExample.json file not found."); + .setDiagnostics(message); - writeResponse(theServletRequest, theServletResponse, outcome, HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + throw new InternalErrorException(message, outcome); } // add organization to identifier property Reference orgRef = new Reference("http://example.org/Organization/" + exampleOrg.getIdPart()); outputBundle.setIdentifier(new Identifier().setAssigner(orgRef)); - writeResponse(theServletRequest, theServletResponse, outputBundle, HttpServletResponse.SC_OK); + return outputBundle; + + } + + private void assertPatientIsValid(Patient patient, HttpServletRequest theServletRequest) { + + // Patient resource must be provided + if (patient == null) { + String message = "A valid Patient resource must be provided."; + OperationOutcome outcome = new OperationOutcome(); + outcome.addIssue().setCode(OperationOutcome.IssueType.INVALID).setSeverity(OperationOutcome.IssueSeverity.ERROR) + .setDiagnostics(message); + + throw new InvalidRequestException(message, outcome); + } + + // Validate the Patient resource if configured + + IdentityMatchValidationLevel validationLevel = appProperties.getMatchValidationLevel(); + + // override validationLevel to the value of the header if it is a valid value of the enum + String matchHeader = theServletRequest.getHeader(appProperties.getMatchValidationHeader()); + if (matchHeader != null) { + try { + validationLevel = IdentityMatchValidationLevel.valueOf(matchHeader.toUpperCase()); + } catch (IllegalArgumentException e) { + throw new InvalidRequestException("Invalid value for " + appProperties.getMatchValidationHeader() + " header: \"" + matchHeader + + "\". Valid values are: " + + EnumSet.allOf(IdentityMatchValidationLevel.class).stream().map(Enum::toString).collect(Collectors.joining(", ")) + ); + } + } + + // no validation required + if (validationLevel == IdentityMatchValidationLevel.NONE) { + return; + } + + // check if the Patient resource declares conformance to an expected profile + boolean assertIDIPatientProfile = false; + boolean assertIDIPatientL0Profile = false; + boolean assertIDIPatientL1Profile = false; + List metaProfiles = patient.getMeta().getProfile(); + for(CanonicalType profile : metaProfiles) { + switch(profile.getValue()) { + case(IDI_Patient_L1_Profile): { assertIDIPatientL1Profile = true; } break; + case(IDI_Patient_L0_Profile): { assertIDIPatientL0Profile = true; } break; + case(IDI_Patient_Profile): { assertIDIPatientProfile = true; } break; + } + } + + // if the server is configured to require a profile in the meta.profile, + // throw an error if the Patient resource does not declare conformance to an expected profile + if (validationLevel == IdentityMatchValidationLevel.META_PROFILE) { + if (!(assertIDIPatientProfile || assertIDIPatientL0Profile || assertIDIPatientL1Profile)) { + String message = "The Patient resource must declare conformance to an appropriate IDI-Patient profile in the meta.profile field."; + OperationOutcome outcome = new OperationOutcome(); + outcome.addIssue().setCode(OperationOutcome.IssueType.INVALID).setSeverity(OperationOutcome.IssueSeverity.ERROR) + .setDiagnostics(message); + + throw new InvalidRequestException(message, outcome); + } + } + + + // default validation level starts here + // validate the resource against the most restrictive profile declared or the base IDI-Patient profile + + // ValidationResult result = validator.validateWithResult(patient); + IFhirPath fhirPath = FhirContextProvider.getFhirContext().newFhirPath(); //.evaluateFirst(patient, IDI_Patient_FhirPath, BooleanType.class); + + String message = null; + + + + // IDI-Patient L1 profile validation + if (assertIDIPatientL1Profile) { + System.out.println("Evaluating IDI-Patient-L1 profile"); + var result = fhirPath.evaluateFirst(patient, IDI_Patient_L1_FhirPath, BooleanType.class); + if (result == null || !result.isPresent() || !result.get().booleanValue()) { + message = "The Patient resource does not meet the requirements of the IDI-Patient-L1 profile."; + } + } + + // IDI-Patient L0 profile validation + else if (assertIDIPatientL0Profile) { + System.out.println("Evaluating IDI-Patient-L0 profile"); + var result = fhirPath.evaluateFirst(patient, IDI_Patient_L0_FhirPath, BooleanType.class); + if (result == null || !result.isPresent() || !result.get().booleanValue()) { + message = "The Patient resource does not meet the requirements of the IDI-Patient-L0 profile."; + } + } + + // base IDI-Patient profile validation + else { + System.out.println("Evaluating IDI-Patient profile"); + var result = fhirPath.evaluateFirst(patient, IDI_Patient_FhirPath, BooleanType.class); + if (result == null || !result.isPresent() || !result.get().booleanValue()) { + message = "The Patient resource does not meet the requirements of the IDI-Patient profile."; + } + } + + + if (message != null) { + OperationOutcome outcome = new OperationOutcome(); + outcome.addIssue().setCode(OperationOutcome.IssueType.INVALID).setSeverity(OperationOutcome.IssueSeverity.ERROR).setDiagnostics(message); + throw new InvalidRequestException(message, outcome); + } } @@ -474,19 +655,19 @@ private IQuery buildMatchQuery(Patient patient, IGenericClient client, L } - private String getProfileAssertion() { - if(assertIDIPatientProfile) { - return IDI_Patient_Profile; - } - else if(assertIDIPatientL0Profile) { - return IDI_Patient_L0_Profile; - } - else if(assertIDIPatientL1Profile) { - return IDI_Patient_L1_Profile; - } + // private String getProfileAssertion() { + // if(assertIDIPatientProfile) { + // return IDI_Patient_Profile; + // } + // else if(assertIDIPatientL0Profile) { + // return IDI_Patient_L0_Profile; + // } + // else if(assertIDIPatientL1Profile) { + // return IDI_Patient_L1_Profile; + // } - return "No profile provided"; - } + // return "No profile provided"; + // } private IdentityMatchingScorer gradePatientReference(Patient referencePatient) { IdentityMatchingScorer refScorer = new IdentityMatchingScorer(); @@ -566,22 +747,22 @@ else if(com.getSystem().toCode().equals(ContactPoint.ContactPointSystem.EMAIL.to } - private boolean passesProfileAssertion(IdentityMatchingScorer refScorer) { - Integer scoredWeight = refScorer.getMatchWeight(); + // private boolean passesProfileAssertion(IdentityMatchingScorer refScorer) { + // Integer scoredWeight = refScorer.getMatchWeight(); - if(assertIDIPatientProfile) { - return true; - } - else if(assertIDIPatientL0Profile) { - return scoredWeight >= 9; - } - else if(assertIDIPatientL1Profile) { - return scoredWeight >= 10; - } - else { - return false; - } - } + // if(assertIDIPatientProfile) { + // return true; + // } + // else if(assertIDIPatientL0Profile) { + // return scoredWeight >= 9; + // } + // else if(assertIDIPatientL1Profile) { + // return scoredWeight >= 10; + // } + // else { + // return false; + // } + // } //TESTING DOA @@ -623,7 +804,7 @@ private Bundle getPatientMatch(Patient refPatient) { //check for birthdate if present if(refPatient.hasBirthDate()) { - searchMap.add(Patient.BIRTHDATE.getParamName(), new DateParam(refPatient.getBirthDateElement().getValue().toString())); + searchMap.add(Patient.BIRTHDATE.getParamName(), new DateParam(ParamPrefixEnum.EQUAL, refPatient.getBirthDateElement().getValue())); } //check gender if present @@ -717,12 +898,12 @@ private Bundle.BundleEntryComponent createBundleEntry(DomainResource resource) { } - private void writeResponse(HttpServletRequest theServletRequest, HttpServletResponse theServletResponse, Resource resource, int reponseStatus) throws IOException { - theServletResponse.setStatus(reponseStatus); - theServletResponse.setContentType("application/fhir+json"); + // private void writeResponse(HttpServletRequest theServletRequest, HttpServletResponse theServletResponse, Resource resource, int reponseStatus) throws IOException { + // theServletResponse.setStatus(reponseStatus); + // theServletResponse.setContentType("application/fhir+json"); - FhirContext ctx = FhirContextProvider.getFhirContext(); - ctx.newJsonParser().encodeResourceToWriter(resource, theServletResponse.getWriter()); - } + // FhirContext ctx = FhirContextProvider.getFhirContext(); + // ctx.newJsonParser().encodeResourceToWriter(resource, theServletResponse.getWriter()); + // } } diff --git a/src/main/java/ca/uhn/fhir/jpa/starter/operations/models/IdentityMatchParams.java b/src/main/java/ca/uhn/fhir/jpa/starter/operations/models/IdentityMatchParams.java new file mode 100644 index 00000000..b622689a --- /dev/null +++ b/src/main/java/ca/uhn/fhir/jpa/starter/operations/models/IdentityMatchParams.java @@ -0,0 +1,21 @@ +package ca.uhn.fhir.jpa.starter.operations.models; + +import org.hl7.fhir.r4.model.BooleanType; +import org.hl7.fhir.r4.model.IntegerType; +import org.hl7.fhir.r4.model.Patient; + +import lombok.Getter; +import lombok.Setter; + +public class IdentityMatchParams { + + @Getter @Setter + Patient patient; + + @Getter @Setter + BooleanType onlyCertainMatches = new BooleanType(false); + + @Getter @Setter + IntegerType count; + +} diff --git a/src/main/java/ca/uhn/fhir/jpa/starter/operations/models/IdentityMatchValidationLevel.java b/src/main/java/ca/uhn/fhir/jpa/starter/operations/models/IdentityMatchValidationLevel.java new file mode 100644 index 00000000..46aa59bc --- /dev/null +++ b/src/main/java/ca/uhn/fhir/jpa/starter/operations/models/IdentityMatchValidationLevel.java @@ -0,0 +1,28 @@ +package ca.uhn.fhir.jpa.starter.operations.models; + + +/** + * The level of validation to perform on the Patient resource parameter when performing an identity match operation + */ +public enum IdentityMatchValidationLevel { + + /** + * Requires that the Patient validates against an IDI-Patient profile specified in the meta.profile field. + * If no profile is provided, the Patient will be validated against the base IDI-Patient profile: + * http://hl7.org/fhir/us/identity-matching/StructureDefinition/IDI-Patient + */ + DEFAULT, + + /** + * Validate the Patient resource against the most restrictive IDI-Patient profile specified in the meta.profile field. + * If an expected IDI-Patient profile is not found, the validation will fail. + */ + META_PROFILE, + + /** + * Do not perform any validation of the Patient resource + */ + NONE + + +} diff --git a/src/main/java/ca/uhn/fhir/jpa/starter/security/IdentityMatchingAuthInterceptor.java b/src/main/java/ca/uhn/fhir/jpa/starter/security/IdentityMatchingAuthInterceptor.java index 69742f51..efd11bf7 100644 --- a/src/main/java/ca/uhn/fhir/jpa/starter/security/IdentityMatchingAuthInterceptor.java +++ b/src/main/java/ca/uhn/fhir/jpa/starter/security/IdentityMatchingAuthInterceptor.java @@ -47,6 +47,7 @@ @Interceptor public class IdentityMatchingAuthInterceptor { private boolean enableAuthentication = false; + private String bypassHeader; private String issuer; @@ -59,10 +60,10 @@ public class IdentityMatchingAuthInterceptor { private List publicEndpoints; private final Logger _logger = LoggerFactory.getLogger(IdentityMatchingAuthInterceptor.class); - private final String allowPublicAccessHeader = "X-Allow-Public-Access"; - public IdentityMatchingAuthInterceptor(boolean enableAuthentication, String issuer, String publicKey, String introspectUrl, String clientId, String clientSecret, List protectedEndpoints, List publicEndpoints) { + public IdentityMatchingAuthInterceptor(boolean enableAuthentication, String bypassHeader, String issuer, String publicKey, String introspectUrl, String clientId, String clientSecret, List protectedEndpoints, List publicEndpoints) { this.enableAuthentication = enableAuthentication; + this.bypassHeader = bypassHeader; this.issuer = issuer; this.publicKey = publicKey; this.introspectUrl = introspectUrl; @@ -74,13 +75,18 @@ public IdentityMatchingAuthInterceptor(boolean enableAuthentication, String issu @Hook(Pointcut.SERVER_INCOMING_REQUEST_POST_PROCESSED) public boolean incomingRequestPostProcessed(RequestDetails details, HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException { + + if (!enableAuthentication) { + return true; + } + boolean authenticated = false; //check for public access header, if not detected then proceed with authentication checks // if public access header is present, circumvent authentication and allow public access to all endpoints // //*** THIS IS JUST FOR RI TESTING, THIS SHOULD NOT BE INCLUDED IN A PRODUCTION SYSTEM *** - String publicAccessHeader = request.getHeader(allowPublicAccessHeader); + String publicAccessHeader = request.getHeader(bypassHeader); if(publicAccessHeader == null) { // check if request path is an endpoint that needs validation @@ -115,7 +121,7 @@ public boolean incomingRequestPostProcessed(RequestDetails details, HttpServletR } else { //public access header detected or a public access point was requested - allow request authenticated = true; - _logger.info("The 'X-Allow-Public-Access' header was detected, ignoring security configuration."); + _logger.info("The '" + bypassHeader + "' header was detected, ignoring security configuration."); } if(!authenticated) { diff --git a/src/main/java/ca/uhn/fhir/jpa/starter/security/models/SecurityConfig.java b/src/main/java/ca/uhn/fhir/jpa/starter/security/models/SecurityConfig.java index 2909c424..ed231c92 100644 --- a/src/main/java/ca/uhn/fhir/jpa/starter/security/models/SecurityConfig.java +++ b/src/main/java/ca/uhn/fhir/jpa/starter/security/models/SecurityConfig.java @@ -39,6 +39,9 @@ public class SecurityConfig { @Getter @Setter boolean fetchCert = true; + @Getter @Setter + String bypassHeader = "X-Allow-Public-Access"; + public List getProtectedEndpoints() { if(this.protectedEndpoints.size() > 0) { return List.of(this.protectedEndpoints.get(0).split("[;]")); diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index ceaf6b7b..810d97aa 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -47,6 +47,7 @@ security: # cert-file: classpath:client-cert.pfx # cert-password: udap-test fetch-cert: true + bypass-header: X-Allow-Public-Access spring: main: allow-circular-references: true @@ -94,6 +95,9 @@ spring: # hibernate.search.backend.analysis.configurer: ca.uhn.fhir.jpa.search.HapiHSearchAnalysisConfigurers$HapiElasticAnalysisConfigurer hapi: fhir: + match-validation-header: X-Match-Validation + match-validation-level: DEFAULT + ### This flag when enabled to true, will avail evaluate measure operations from CR Module. ### Flag is false by default, can be passed as command line argument to override. cr: