From 42651b9fbedddda1e05ea91b82047840ce56041e Mon Sep 17 00:00:00 2001 From: Stephen Nielson Date: Tue, 18 Oct 2022 10:03:14 -0400 Subject: [PATCH] Openemr inferno fixes for #5827 #5828 #5829 #5830 #5831 #5833 #5834 (#5836) * OpenEMR FHIR Api bug/feature fixes Fixes #5831 - capabilities statement add passthrough statement Fixes #5830 - add document title to DocumentReference endpoint Fixes #5829 - enable patients to download their own documents. Fixes #5828 - OperationDefinition endpoint, $bulkdata-status updates Fixes #5827 - Migrate FHIR doc binary download to root Binary endpoint Fixes #5833 - Patient portal scopes independent of fhir scopes * Fix scope permission on insert/create/delete Fixes #5834 - Add delete interaction in capability statement to handle write scopes correctly. We were using the wrong interaction name for the capability statement and I renamed it in the prior commit. However, I wasn't handling it in our ScopeRepository correctly. Changed the interaction reference to be 'create' instead of 'insert' and put in a documentation url of where to go to get the reference. * Fix styles and unit tests * Fix missing definition URL Found the definition url was missing for the operation. Not sure how I ended up removing it. --- API_README.md | 6 +- FHIR_README.md | 16 +-- _rest_config.php | 10 +- _rest_routes.inc.php | 125 +++++++++++++++--- docker/library/api-scope-listing | 2 +- library/classes/Document.class.php | 10 ++ .../Repositories/ScopeRepository.php | 29 ++-- ...reateClientCredentialsAssertionCommand.php | 2 +- src/Common/Http/HttpRestRouteHandler.php | 13 +- src/FHIR/SMART/Capability.php | 17 ++- .../FHIR/FhirDocumentRestController.php | 13 +- .../FHIR/FhirMetaDataRestController.php | 23 +++- .../FhirOperationDefinitionRestController.php | 87 ++++++++++++ .../FhirOperationExportRestController.php | 2 +- src/RestControllers/RestControllerHelper.php | 73 ++++++++-- .../FhirPatientDocumentReferenceService.php | 3 +- src/Services/PatientService.php | 5 + swagger/openemr-api.yaml | 46 ++++++- tests/Tests/Api/CapabilityFhirTest.php | 2 +- .../Common/Http/HttpRestParsedRouteTest.php | 6 +- 20 files changed, 419 insertions(+), 71 deletions(-) create mode 100644 src/RestControllers/FHIR/Operations/FhirOperationDefinitionRestController.php diff --git a/API_README.md b/API_README.md index 35a3af2d74e..097960292ea 100644 --- a/API_README.md +++ b/API_README.md @@ -67,13 +67,13 @@ This is a listing of scopes: - `api:fhir` (fhir which are the /fhir/ endpoints) - `patient/AllergyIntolerance.read` - `patient/Appointment.read` + - `patient/Binary.read` - `patient/CarePlan.read` - `patient/CareTeam.read` - `patient/Condition.read` - `patient/Coverage.read` - `patient/Device.read` - `patient/DiagnosticReport.read` - - `patient/Document.read` - `patient/DocumentReference.read` - `patient/DocumentReference.$docref` - `patient/Encounter.read` @@ -90,13 +90,13 @@ This is a listing of scopes: - `patient/Procedure.read` - `patient/Provenance.read` - `system/AllergyIntolerance.read` + - `system/Binary.read` - `system/CarePlan.read` - `system/CareTeam.read` - `system/Condition.read` - `system/Coverage.read` - `system/Device.read` - `system/DiagnosticReport.read` - - `system/Document.read` - `system/DocumentReference.read` - `system/DocumentReference.$docref` - `system/Encounter.read` @@ -119,13 +119,13 @@ This is a listing of scopes: - `system/*.$bulkdata-status` - `system/*.$export` - `user/AllergyIntolerance.read` + - `user/Binary.read` - `user/CarePlan.read` - `user/CareTeam.read` - `user/Condition.read` - `user/Coverage.read` - `user/Device.read` - `user/DiagnosticReport.read` - - `user/Document.read` - `user/DocumentReference.read` - `user/DocumentReference.$docref` - `user/Encounter.read` diff --git a/FHIR_README.md b/FHIR_README.md index bcaa2fedc76..a2ccc2e82f6 100644 --- a/FHIR_README.md +++ b/FHIR_README.md @@ -131,11 +131,11 @@ A status Query will return a result like the following: "requiresAccessToken": true, "output": [ { - "url": "https:\/\/localhost:9300\/apis\/default\/fhir\/Document\/97552\/Binary", + "url": "https:\/\/localhost:9300\/apis\/default\/fhir\/Binary\/97552", "type": "Patient" }, { - "url": "https:\/\/localhost:9300\/apis\/default\/fhir\/Document\/105232\/Binary", + "url": "https:\/\/localhost:9300\/apis\/default\/fhir\/Binary\/105232", "type": "Encounter" } ], @@ -145,15 +145,15 @@ A status Query will return a result like the following: You can download the exported documents which are formatted in Newline Delimited JSON (NDJSON) by making a call to: ```sh - curl -X GET 'https://localhost:9300/apis/default/fhir/Document/105232/Binary' + curl -X GET 'https://localhost:9300/apis/default/fhir/Binary/105232' ``` -In order to download the documents you will need the **system/Document.read** scope. +In order to download the documents you will need the **system/Binary.read** scope. #### Bulk FHIR Scope Reference -- All System export - **system/\*.$export system\*.$bulkdata-status system/Document.read** -- Group System export - **system/Group.$export system\*.$bulkdata-status system/Document.read** -- Patient System export - **system/Patient.$export system\*.$bulkdata-status system/Document.read** +- All System export - **system/\*.$export system\*.$bulkdata-status system/Binary.read** +- Group System export - **system/Group.$export system\*.$bulkdata-status system/Binary.read** +- Patient System export - **system/Patient.$export system\*.$bulkdata-status system/Binary.read** #### ## 3rd Party SMART Apps @@ -205,7 +205,7 @@ It is recommended that native applications follow best practices for native clie ### Generate CCDA - [Tutorial to Generate CCDA (with Screenshots)](https://github.com/openemr/openemr/issues/5284#issuecomment-1155678620) ### Details Docref -- Requires /DocumentReference.$docref, /DocumentReference.read, and /Document.read scopes +- Requires /DocumentReference.$docref, /DocumentReference.read, and /Binary.read scopes - Start and end date filter encounter related events for the following sections: - History of Procedures - Relevant DX Tests / LAB Data diff --git a/_rest_config.php b/_rest_config.php index c06bc10a274..01abcb9ca61 100644 --- a/_rest_config.php +++ b/_rest_config.php @@ -373,9 +373,15 @@ public static function skipApiAuth($resource): bool exit(); } // let the capability statement for FHIR or the SMART-on-FHIR through + $resource = str_replace('/' . self::$SITE, '', $resource); if ( - $resource === ("/" . self::$SITE . "/fhir/metadata") || - $resource === ("/" . self::$SITE . "/fhir/.well-known/smart-configuration") + // TODO: @adunsulag we need to centralize our auth skipping logic... as we have this duplicated in HttpRestRouteHandler + // however, at the point of this method we don't have the resource identified and haven't gone through our parsing + // routine to handle that logic... + $resource === ("/fhir/metadata") || + $resource === ("/fhir/.well-known/smart-configuration") || + // skip list and single instance routes + 0 === strpos("/fhir/OperationDefinition", $resource) ) { return true; } else { diff --git a/_rest_routes.inc.php b/_rest_routes.inc.php index 5acf5190570..3ad7216101f 100644 --- a/_rest_routes.inc.php +++ b/_rest_routes.inc.php @@ -35,13 +35,13 @@ * "api:fhir": "FHIR R4 API", * "patient/AllergyIntolerance.read": "Read allergy intolerance resources for the current patient (api:fhir)", * "patient/Appointment.read": "Read appointment resources for the current patient (api:fhir)", + * "patient/Binary.read": "Read binary document resources for the current patient (api:fhir)", * "patient/CarePlan.read": "Read care plan resources for the current patient (api:fhir)", * "patient/CareTeam.read": "Read care team resources for the current patient (api:fhir)", * "patient/Condition.read": "Read condition resources for the current patient (api:fhir)", * "patient/Coverage.read": "Read coverage resources for the current patient (api:fhir)", * "patient/Device.read": "Read device resources for the current patient (api:fhir)", * "patient/DiagnosticReport.read": "Read diagnostic report resources for the current patient (api:fhir)", - * "patient/Document.read": "Read document resources for the current patient (api:fhir)", * "patient/DocumentReference.read": "Read document reference resources for the current patient (api:fhir)", * "patient/DocumentReference.$docref" : "Generate a document for the current patient or returns the most current Clinical Summary of Care Document (CCD)", * "patient/Encounter.read": "Read encounter resources for the current patient (api:fhir)", @@ -58,13 +58,13 @@ * "patient/Procedure.read": "Read procedure resources for the current patient (api:fhir)", * "patient/Provenance.read": "Read provenance resources for the current patient (api:fhir)", * "system/AllergyIntolerance.read": "Read all allergy intolerance resources in the system (api:fhir)", + * "system/Binary.read": "Read all binary document resources in the system (api:fhir)", * "system/CarePlan.read": "Read all care plan resources in the system (api:fhir)", * "system/CareTeam.read": "Read all care team resources in the system (api:fhir)", * "system/Condition.read": "Read all condition resources in the system (api:fhir)", * "system/Coverage.read": "Read all coverage resources in the system (api:fhir)", * "system/Device.read": "Read all device resources in the system (api:fhir)", * "system/DiagnosticReport.read": "Read all diagnostic report resources in the system (api:fhir)", - * "system/Document.read": "Read all document resources in the system (api:fhir)", * "system/DocumentReference.read": "Read all document reference resources in the system (api:fhir)", * "system/DocumentReference.$docref" : "Generate a document for any patient in the system or returns the most current Clinical Summary of Care Document (CCD)", * "system/Encounter.read": "Read all encounter resources in the system (api:fhir)", @@ -83,13 +83,13 @@ * "system/Procedure.read": "Read all procedure resources in the system (api:fhir)", * "system/Provenance.read": "Read all provenance resources in the system (api:fhir)", * "user/AllergyIntolerance.read": "Read all allergy intolerance resources the user has access to (api:fhir)", + * "user/Binary.read" : "Read all binary documents the user has access to (api:fhir)", * "user/CarePlan.read": "Read all care plan resources the user has access to (api:fhir)", * "user/CareTeam.read": "Read all care team resources the user has access to (api:fhir)", * "user/Condition.read": "Read all condition resources the user has access to (api:fhir)", * "user/Coverage.read": "Read all coverage resources the user has access to (api:fhir)", * "user/Device.read": "Read all device resources the user has access to (api:fhir)", * "user/DiagnosticReport.read": "Read all diagnostic report resources the user has access to (api:fhir)", - * "user/Document.read" : "Read all documents the user has access to (api:fhir)", * "user/DocumentReference.read": "Read all document reference resources the user has access to (api:fhir)", * "user/DocumentReference.$docref" : "Generate a document for any patient the user has access to or returns the most current Clinical Summary of Care Document (CCD) (api:fhir)", * "user/Encounter.read": "Read all encounter resources the user has access to (api:fhir)", @@ -7094,6 +7094,7 @@ use OpenEMR\RestControllers\FHIR\FhirMetaDataRestController; use OpenEMR\RestControllers\FHIR\Operations\FhirOperationExportRestController; use OpenEMR\RestControllers\FHIR\Operations\FhirOperationDocRefRestController; +use OpenEMR\RestControllers\FHIR\Operations\FhirOperationDefinitionRestController; // Note that the fhir route includes both user role and patient role // (there is a mechanism in place to ensure patient role is binded @@ -8732,7 +8733,7 @@ * { * "attachment": { * "contentType": "image/gif", - * "url": "https://localhost:9300/apis/default/fhir/Document/7/Binary" + * "url": "https://localhost:9300/apis/default/fhir/Binary/7" * }, * "format": { * "system": "http://ihe.net/fhir/ValueSet/IHE.FormatCode.codesystem", @@ -8775,7 +8776,7 @@ /** * @OA\Get( - * path="/fhir/Document/{id}/Binary", + * path="/fhir/Binary/{id}", * description="Used for downloading binary documents generated either with BULK FHIR Export or with the $docref CCD export operation. Documentation can be found at https://www.open-emr.org/wiki/index.php/OpenEMR_Wiki_Home_Page#API", * tags={"fhir"}, * @OA\Parameter( @@ -8802,15 +8803,16 @@ * security={{"openemr_auth":{}}} * ) */ - 'GET /fhir/Document/:id/Binary' => function ($documentId, HttpRestRequest $request) { - // TODO: @adunsulag we need to be able to retrieve our CCDA documents this way... - // currently only allow users with the same permissions as export to take a file out - // this could be relaxed to allow other types of files ie such as patient access etc. - RestConfig::authorization_check("admin", "users"); - - // Grab the document id + 'GET /fhir/Binary/:id' => function ($documentId, HttpRestRequest $request) { $docController = new \OpenEMR\RestControllers\FHIR\FhirDocumentRestController($request); - $response = $docController->downloadDocument($documentId); + + if ($request->isPatientRequest()) { + $response = $docController->downloadDocument($documentId, $request->getPatientUUIDString()); + } else { + RestConfig::authorization_check("admin", "users"); + $response = $docController->downloadDocument($documentId); + } + return $response; }, @@ -11584,8 +11586,20 @@ * ) */ "GET /fhir/Person/:uuid" => function ($uuid, HttpRestRequest $request) { - RestConfig::authorization_check("admin", "users"); - $return = (new FhirPersonRestController())->getOne($uuid); + // if the api user is requesting their own user we need to let it through + // this is because the /Person endpoint needs to be responsive to the fhirUser return value + // for the currently logged in user + if ($request->getRequestUserUUIDString() == $uuid) { + $return = (new FhirPersonRestController())->getOne($uuid); + } else if (!$request->isPatientRequest()) { + // not a patient ,make sure we have access to the users ACL + RestConfig::authorization_check("admin", "users"); + $return = (new FhirPersonRestController())->getOne($uuid); + } else { + // if we are a patient bound request we need to make sure we are only bound to the patient + $return = (new FhirPersonRestController())->getOne($uuid, $request->getPatientUUIDString()); + } + RestConfig::apiLog($return); return $return; }, @@ -12541,6 +12555,87 @@ return $return; }, + /** + * @OA\Get( + * path="/fhir/OperationDefinition", + * description="Returns a list of the OperationDefinition resources that are specific to this OpenEMR installation", + * tags={"fhir"}, + * @OA\Response( + * response="200", + * description="Return list of OperationDefinition resources" + * ) + * ) + */ + "GET /fhir/OperationDefinition" => function (HttpRestRequest $request) { + // for now we will just hard code the custom resources + $operationDefinitionController = new FhirOperationDefinitionRestController(); + $return = $operationDefinitionController->getAll($request->getQueryParams()); + RestConfig::apiLog($return); + return $return; + }, + + /** + * @OA\Get( + * path="/fhir/OperationDefinition/{operation}", + * description="Returns a single OperationDefinition resource that is specific to this OpenEMR installation", + * tags={"fhir"}, + * @OA\Parameter( + * name="operation", + * in="path", + * description="The name of the operation to query. For example $bulkdata-status", + * required=true, + * @OA\Schema( + * type="string" + * ) + * ), + * @OA\Response( + * response="200", + * description="Standard Response", + * @OA\MediaType( + * mediaType="application/json", + * @OA\Schema( + * @OA\Property( + * property="json object", + * description="FHIR Json object.", + * type="object" + * ), + * example={ + * "resourceType": "OperationDefinition", + * "name": "$bulkdata-status", + * "status": "active", + * "kind": "operation", + * "parameter": { + * { + * "name": "job", + * "use": "in", + * "min": 1, + * "max": 1, + * "type": { + * "system": "http://hl7.org/fhir/data-types", + * "code": "string", + * "display": "string" + * }, + * "searchType": { + * "system": "http://hl7.org/fhir/ValueSet/search-param-type", + * "code": "string", + * "display": "string" + * } + * } + * } + * } + * ) + * ) + * ), + * ) + */ + "GET /fhir/OperationDefinition/:operation" => function ($operation, HttpRestRequest $request) { + // for now we will just hard code the custom resources + $operationDefinitionController = new FhirOperationDefinitionRestController(); + $return = $operationDefinitionController->getOne($operation); + RestConfig::apiLog($return); + return $return; + }, + // FHIR root level operations /** diff --git a/docker/library/api-scope-listing b/docker/library/api-scope-listing index d7e7228bbf1..ebc975b36e7 100644 --- a/docker/library/api-scope-listing +++ b/docker/library/api-scope-listing @@ -1 +1 @@ -openid offline_access launch/patient api:fhir api:oemr api:port patient/AllergyIntolerance.read patient/Appointment.read patient/CarePlan.read patient/CareTeam.read patient/Condition.read patient/Coverage.read patient/Device.read patient/DiagnosticReport.read patient/Document.read patient/DocumentReference.read patient/DocumentReference.$docref patient/Encounter.read patient/Goal.read patient/Immunization.read patient/Location.read patient/Medication.read patient/MedicationRequest.read patient/Observation.read patient/Organization.read patient/Patient.read patient/Person.read patient/Practitioner.read patient/Procedure.read patient/Provenance.read user/AllergyIntolerance.read user/CarePlan.read user/CareTeam.read user/Condition.read user/Coverage.read user/Device.read user/DiagnosticReport.read user/Document.read user/DocumentReference.read user/DocumentReference.$docref user/Encounter.read user/Goal.read user/Immunization.read user/Location.read user/Medication.read user/MedicationRequest.read user/Observation.read user/Organization.read user/Organization.write user/Patient.read user/Patient.write user/Person.read user/Practitioner.read user/Practitioner.write user/PractitionerRole.read user/Procedure.read user/Provenance.read user/allergy.read user/allergy.write user/appointment.read user/appointment.write user/dental_issue.read user/dental_issue.write user/document.read user/document.write user/drug.read user/encounter.read user/encounter.write user/facility.read user/facility.write user/immunization.read user/insurance.read user/insurance.write user/insurance_company.read user/insurance_company.write user/insurance_type.read user/list.read user/medical_problem.read user/medical_problem.write user/medication.read user/medication.write user/message.write user/patient.read user/patient.write user/practitioner.read user/practitioner.write user/prescription.read user/procedure.read user/soap_note.read user/soap_note.write user/surgery.read user/surgery.write user/transaction.read user/transaction.write user/vital.read user/vital.write patient/encounter.read patient/patient.read patient/appointment.read +openid offline_access launch/patient api:fhir api:oemr api:port patient/AllergyIntolerance.read patient/Appointment.read patient/CarePlan.read patient/CareTeam.read patient/Condition.read patient/Coverage.read patient/Device.read patient/DiagnosticReport.read patient/Binary.read patient/DocumentReference.read patient/DocumentReference.$docref patient/Encounter.read patient/Goal.read patient/Immunization.read patient/Location.read patient/Medication.read patient/MedicationRequest.read patient/Observation.read patient/Organization.read patient/Patient.read patient/Person.read patient/Practitioner.read patient/Procedure.read patient/Provenance.read user/AllergyIntolerance.read user/CarePlan.read user/CareTeam.read user/Condition.read user/Coverage.read user/Device.read user/DiagnosticReport.read user/Document.read user/DocumentReference.read user/DocumentReference.$docref user/Encounter.read user/Goal.read user/Immunization.read user/Location.read user/Medication.read user/MedicationRequest.read user/Observation.read user/Organization.read user/Organization.write user/Patient.read user/Patient.write user/Person.read user/Practitioner.read user/Practitioner.write user/PractitionerRole.read user/Procedure.read user/Provenance.read user/allergy.read user/allergy.write user/appointment.read user/appointment.write user/dental_issue.read user/dental_issue.write user/document.read user/document.write user/drug.read user/encounter.read user/encounter.write user/facility.read user/facility.write user/immunization.read user/insurance.read user/insurance.write user/insurance_company.read user/insurance_company.write user/insurance_type.read user/list.read user/medical_problem.read user/medical_problem.write user/medication.read user/medication.write user/message.write user/patient.read user/patient.write user/practitioner.read user/practitioner.write user/prescription.read user/procedure.read user/soap_note.read user/soap_note.write user/surgery.read user/surgery.write user/transaction.read user/transaction.write user/vital.read user/vital.write patient/encounter.read patient/patient.read patient/appointment.read diff --git a/library/classes/Document.class.php b/library/classes/Document.class.php index ef7d953c93e..2244d0ac6f6 100644 --- a/library/classes/Document.class.php +++ b/library/classes/Document.class.php @@ -283,6 +283,16 @@ public function has_expired() return false; } + public function can_patient_access($pid) + { + $foreignId = $this->get_foreign_id(); + // TODO: if any information blocking rule checks were to be applied, they can be done here + if (!empty($foreignId) && $foreignId == $pid) { + return true; + } + return false; + } + /** * Checks whether the passed in $user can access the document or not. It checks against all of the access * permissions for the categories the document is in. If there are any categories that the document is tied to diff --git a/src/Common/Auth/OpenIDConnect/Repositories/ScopeRepository.php b/src/Common/Auth/OpenIDConnect/Repositories/ScopeRepository.php index a59a23cf4dd..ad7d6f15ea1 100644 --- a/src/Common/Auth/OpenIDConnect/Repositories/ScopeRepository.php +++ b/src/Common/Auth/OpenIDConnect/Repositories/ScopeRepository.php @@ -333,6 +333,7 @@ public function fhirScopes(): array "patient/AllergyIntolerance.read", // "patient/AllergyIntolerance.write", "patient/Appointment.read", + "patient/Binary.read", // "patient/Appointment.write", "patient/CarePlan.read", "patient/CareTeam.read", @@ -343,7 +344,6 @@ public function fhirScopes(): array // "patient/Coverage.write", "patient/DiagnosticReport.read", "patient/Device.read", - "patient/Document.read", "patient/DocumentReference.read", 'patient/DocumentReference.$docref', // generate or view most recent CCD for the selected patient // "patient/DocumentReference.write", @@ -381,6 +381,7 @@ public function fhirScopes(): array "user/AllergyIntolerance.write", "user/Appointment.read", "user/Appointment.write", + "user/Binary.read", "user/CarePlan.read", "user/CareTeam.read", "user/Condition.read", @@ -390,7 +391,6 @@ public function fhirScopes(): array "user/Coverage.write", "user/Device.read", "user/DiagnosticReport.read", - "user/Document.read", "user/DocumentReference.read", "user/DocumentReference.write", 'user/DocumentReference.$docref', // export CCD for any patient user has access to @@ -438,6 +438,7 @@ public function systemScopes(): array "system/AllergyIntolerance.read", // "system/AllergyIntolerance.write", "system/Appointment.read", + "system/Binary.read", // used for Bulk FHIR export downloads // "system/Appointment.write", "system/CarePlan.read", "system/CareTeam.read", @@ -447,7 +448,6 @@ public function systemScopes(): array "system/Coverage.read", // "system/Coverage.write", "system/Device.read", - "system/Document.read", // used for Bulk FHIR export downloads "system/DocumentReference.read", 'system/DocumentReference.$docref', // generate / view CCD for any patient in the system "system/DiagnosticReport.read", @@ -705,7 +705,8 @@ public function getCurrentSmartScopes(): array $scopesEvent = new RestApiScopeEvent(); $scopesEvent->setApiType(RestApiScopeEvent::API_TYPE_FHIR); - $scopesEvent->setScopes($scopesSupported); + $scopesSupportedList = $scopesSupported; + $scopesEvent->setScopes($scopesSupportedList); $scopesEvent = $GLOBALS["kernel"]->getEventDispatcher()->dispatch(RestApiScopeEvent::EVENT_TYPE_GET_SUPPORTED_SCOPES, $scopesEvent, 10); @@ -731,6 +732,8 @@ public function getCurrentStandardScopes(): array $scopeRead = $resourceType . ".read"; $scopeWrite = $resourceType . ".write"; $interactionCode = $interaction->getCode()->getValue(); + // these values come from this valuset http://hl7.org/fhir/2021Mar/valueset-type-restful-interaction.html + // for SMART on FHIR 2.0 we will have more granular permissions than *.read and *.write switch ($interactionCode) { case 'read': $scopes_api['user/' . $scopeRead] = 'user/' . $scopeRead; @@ -741,8 +744,9 @@ public function getCurrentStandardScopes(): array $scopes_api['system/' . $scopeRead] = 'system/' . $scopeRead; break; case 'put': - case 'insert': + case 'create': case 'update': + case 'delete': $scopes_api['user/' . $scopeWrite] = 'user/' . $scopeWrite; $scopes_api['system/' . $scopeWrite] = 'system/' . $scopeWrite; break; @@ -760,6 +764,8 @@ public function getCurrentStandardScopes(): array $scopeRead = $resourceType . ".read"; $scopeWrite = $resourceType . ".write"; $interactionCode = $interaction->getCode()->getValue(); + // these values come from this valuset http://hl7.org/fhir/2021Mar/valueset-type-restful-interaction.html + // for SMART on FHIR 2.0 we will have more granular permissions than *.read and *.write switch ($interactionCode) { case 'read': $scopes_api_portal['patient/' . $scopeRead] = 'patient/' . $scopeRead; @@ -768,19 +774,21 @@ public function getCurrentStandardScopes(): array $scopes_api_portal['patient/' . $scopeRead] = 'patient/' . $scopeRead; break; case 'put': - case 'insert': + case 'create': case 'update': + case 'delete': $scopes_api_portal['patient/' . $scopeWrite] = 'patient/' . $scopeWrite; break; } } } } + $oidc = array_combine($this->oidcScopes(), $this->oidcScopes()); $scopes_api = array_merge($scopes_api, $scopes_api_portal); $scopesSupported = $this->apiScopes(); $scopes_dict = array_combine($scopesSupported, $scopesSupported); - $scopesSupported = null; + $scopesSupported = null; // this is odd, why do we have this? // verify scope permissions are allowed for role being used. foreach ($scopes_api as $key => $scope) { if (empty($scopes_dict[$key])) { @@ -789,10 +797,12 @@ public function getCurrentStandardScopes(): array $scopesSupported[$key] = $scope; } asort($scopesSupported); + $serverScopes = $this->getServerScopes(); + $scopesSupported = array_keys(array_merge($oidc, $serverScopes, $scopesSupported)); $scopesEvent = new RestApiScopeEvent(); $scopesEvent->setApiType(RestApiScopeEvent::API_TYPE_STANDARD); - $scopesSupportedList = array_keys($scopesSupported); + $scopesSupportedList = $scopesSupported; $scopesEvent->setScopes($scopesSupportedList); $scopesEvent = $GLOBALS["kernel"]->getEventDispatcher()->dispatch(RestApiScopeEvent::EVENT_TYPE_GET_SUPPORTED_SCOPES, $scopesEvent, 10); @@ -860,6 +870,9 @@ public function buildScopeValidatorArray(): array $scopes['nonce'] = ['description' => 'Nonce value used to detect replay attacks by third parties']; foreach ($mergedScopes as $scope) { + // TODO: @adunsulag look at adding the actual scope description here and what the ramifications are. + // Looks like this line could be + // $scopes[$scope] = ['description' => $this->lookupDescriptionForScope($scope, false)]; $scopes[$scope] = ['description' => 'OpenId Connect']; } diff --git a/src/Common/Command/CreateClientCredentialsAssertionCommand.php b/src/Common/Command/CreateClientCredentialsAssertionCommand.php index 11c9b8fc846..67ce35c6b49 100644 --- a/src/Common/Command/CreateClientCredentialsAssertionCommand.php +++ b/src/Common/Command/CreateClientCredentialsAssertionCommand.php @@ -110,7 +110,7 @@ public function execute(CommandContext $context) echo "\n\nSample CURL request using assertion: \n"; $assertionType = CustomClientCredentialsGrant::OAUTH_JWT_CLIENT_ASSERTION_TYPE; $scope = 'system/*.\$export system/*.\$bulkdata-status system/Group.\$export system/Patient.\$export ' - . 'system/Encounter.read system/Document.read'; + . 'system/Encounter.read system/Binary.read'; echo "--> curl -k -X POST --data-urlencode \"client_assertion_type=$assertionType\" \\\n" . " --data-urlencode \"client_assertion=$assertion\" \\\n" . " --data-urlencode \"grant_type=client_credentials\" \\\n" diff --git a/src/Common/Http/HttpRestRouteHandler.php b/src/Common/Http/HttpRestRouteHandler.php index 56b323d1f03..9a0bf354323 100644 --- a/src/Common/Http/HttpRestRouteHandler.php +++ b/src/Common/Http/HttpRestRouteHandler.php @@ -258,10 +258,7 @@ private static function checkSecurity(HttpRestRequest $restRequest) if ($restRequest->isFhir()) { // don't do any checks on our open fhir resources - if ( - $restRequest->getResource() == 'metadata' - || $restRequest->getResource() == '.well-known' - ) { + if (self::fhirRestRequestSkipSecurityCheck($restRequest)) { return; } // we do NOT want logged in patients writing data at this point so we fail @@ -305,4 +302,12 @@ private static function checkSecurity(HttpRestRequest $restRequest) // handle our scope checks $config::scope_check($scopeType, $resource, $permission); } + + public static function fhirRestRequestSkipSecurityCheck(HttpRestRequest $restRequest): bool + { + $resource = $restRequest->getResource(); + // capability statement, smart well knowns, and operation definitions are skipped. + $skippedChecks = ['metadata', '.well-known', 'OperationDefinition']; + return array_search($resource, $skippedChecks) !== false; + } } diff --git a/src/FHIR/SMART/Capability.php b/src/FHIR/SMART/Capability.php index 1bae09590ce..efea36d6d49 100644 --- a/src/FHIR/SMART/Capability.php +++ b/src/FHIR/SMART/Capability.php @@ -27,7 +27,14 @@ class Capability const SUPPORTED_CAPABILITIES = [self::LAUNCH_EHR, self::CONTEXT_BANNER, self::CONTEXT_EHR_PATIENT , self::CONTEXT_STYLE, self::SSO_OPENID_CONNECTION, self::CLIENT_CONFIDENTIAL_SYMMETRIC, self::PERMISSION_USER , self::CONTEXT_STANDALONE_PATIENT, self::LAUNCH_STANDALONE, self::PERMISSION_PATIENT - , self::PERMISSION_OFFLINE, self::CLIENT_PUBLIC, self::PERMISSION_V1]; + , self::PERMISSION_OFFLINE, self::CLIENT_PUBLIC]; + + const FHIR_SUPPORTED_CAPABILITIES = [ + self::LAUNCH_EHR, self::CONTEXT_BANNER_PASSTHROUGH, self::CONTEXT_EHR_PATIENT + , self::CONTEXT_STYLE_PASSTHROUGH, self::SSO_OPENID_CONNECTION, self::CLIENT_CONFIDENTIAL_SYMMETRIC, self::PERMISSION_USER + , self::CONTEXT_STANDALONE_PATIENT, self::LAUNCH_STANDALONE, self::PERMISSION_PATIENT + , self::PERMISSION_OFFLINE, self::CLIENT_PUBLIC + ]; // support for SMART’s EHR Launch mode const LAUNCH_EHR = 'launch-ehr'; @@ -47,6 +54,9 @@ class Capability // support for “need patient banner” launch context (conveyed via need_patient_banner token parameter) const CONTEXT_BANNER = "context-banner"; + // FHIR capability statement for some reason requires this capability which is the same as context-banner... + const CONTEXT_BANNER_PASSTHROUGH = "context-passthrough-banner"; + // support for “SMART style URL” launch context (conveyed via smart_style_url token parameter) // NOTE: context-style is marked in HL7 SMART as EXPERIMENTAL, so expect this to change in time // HL7/SMART chat forum was a bit confused by ONC's decision to include this, so again expect @@ -54,6 +64,9 @@ class Capability // @see SMARTConfigurationController->getStyles() const CONTEXT_STYLE = "context-style"; + // FHIR capability statement for some reason requires this capability which is the same as context-style... + const CONTEXT_STYLE_PASSTHROUGH = "context-passthrough-style"; + // support for patient-level launch context (requested by launch scope, conveyed via patient token parameter) const CONTEXT_EHR_PATIENT = "context-ehr-patient"; @@ -77,7 +90,7 @@ class Capability const PERMISSION_USER = "permission-user"; /** - * Support for SMART v1 scopes (e.g. patient/Observation.read) + * Support for SMART v1 scopes (e.g. patient/Observation.read) - This is a SMART 2.0 capability identifier in R5 */ const PERMISSION_V1 = "permission-v1"; diff --git a/src/RestControllers/FHIR/FhirDocumentRestController.php b/src/RestControllers/FHIR/FhirDocumentRestController.php index bba5ac0a6ec..95df9c3cee3 100644 --- a/src/RestControllers/FHIR/FhirDocumentRestController.php +++ b/src/RestControllers/FHIR/FhirDocumentRestController.php @@ -20,9 +20,11 @@ use OpenEMR\Common\Http\Psr17Factory; use OpenEMR\Common\Http\StatusCode; use OpenEMR\Common\Logging\SystemLogger; +use OpenEMR\Common\Uuid\UuidRegistry; use OpenEMR\Services\CDADocumentService; use OpenEMR\Services\FHIR\Document\BaseDocumentDownloader; use OpenEMR\Services\FHIR\Document\IDocumentDownloader; +use OpenEMR\Services\PatientService; use OpenEMR\Services\Search\ReferenceSearchField; use Psr\Http\Message\ResponseInterface; use Ramsey\Uuid\Uuid; @@ -51,7 +53,7 @@ public function __construct(HttpRestRequest $request) * expiration are checked against the document. * @param $documentId The document we are requesting to access */ - public function downloadDocument($documentId): ResponseInterface + public function downloadDocument($documentId, $patientUuid): ResponseInterface { $document = $this->findDocumentForDocumentId($documentId); if (empty($document)) { @@ -66,6 +68,15 @@ public function downloadDocument($documentId): ResponseInterface return (new Psr17Factory())->createResponse(StatusCode::NOT_FOUND); } + // patients need to be able to access their own documents, we expose that here if we have a patientUuid + if (!empty($patientUuid)) { + $pid = (new PatientService())->getPidByUuid(UuidRegistry::uuidToBytes($patientUuid)); + // allows for both checking the patient id, and any Information Blocking / Access rules to the document + if (!$document->can_patient_access($pid)) { + return (new Psr17Factory())->createResponse(StatusCode::UNAUTHORIZED); + } + } + if (!$document->can_access()) { return (new Psr17Factory())->createResponse(StatusCode::UNAUTHORIZED); } diff --git a/src/RestControllers/FHIR/FhirMetaDataRestController.php b/src/RestControllers/FHIR/FhirMetaDataRestController.php index 66fba2a2a68..83f36697eb2 100644 --- a/src/RestControllers/FHIR/FhirMetaDataRestController.php +++ b/src/RestControllers/FHIR/FhirMetaDataRestController.php @@ -11,9 +11,11 @@ namespace OpenEMR\RestControllers\FHIR; use OpenEMR\FHIR\R4\FHIRElement\FHIRCanonical; +use OpenEMR\FHIR\R4\FHIRElement\FHIRCapabilityStatementKind; use OpenEMR\FHIR\R4\FHIRElement\FHIRCodeableConcept; use OpenEMR\FHIR\R4\FHIRElement\FHIRCoding; use OpenEMR\FHIR\R4\FHIRElement\FHIRExtension; +use OpenEMR\FHIR\R4\FHIRElement\FHIRPublicationStatus; use OpenEMR\FHIR\R4\FHIRResource\FHIRCapabilityStatement\FHIRCapabilityStatementSecurity; use OpenEMR\FHIR\SMART\Capability; use OpenEMR\RestControllers\AuthorizationController; @@ -40,26 +42,35 @@ class FhirMetaDataRestController private $fhirService; private $fhirValidate; private $restHelper; + /** + * @var \RestConfig + */ + private $restConfig; public function __construct() { $this->fhirService = new FhirResourcesService(); $this->fhirValidate = new FhirValidationService(); - $this->restHelper = new RestControllerHelper(); + $gbl = \RestConfig::GetInstance(); + $this->restHelper = new RestControllerHelper($gbl::$apisBaseFullUrl . "/fhir"); + $this->restConfig = $gbl; } protected function buildCapabilityStatement(): FHIRCapabilityStatement { - $gbl = \RestConfig::GetInstance(); + $gbl = $this->restConfig; $routes = $gbl::$FHIR_ROUTE_MAP; $serverRoot = $gbl::$webserver_root; $capabilityStatement = new FHIRCapabilityStatement(); - $capabilityStatement->setStatus("active"); + $pubStatus = new FHIRPublicationStatus(); + $pubStatus->setValue("active"); + $capabilityStatement->setStatus($pubStatus); $fhirVersion = new FHIRFHIRVersion(); $fhirVersion->setValue("4.0.1"); $capabilityStatement->setFhirVersion($fhirVersion); - $capabilityStatement->setKind("instance"); - $capabilityStatement->setStatus("Not provided"); + $kind = new FHIRCapabilityStatementKind(); + $kind->setValue("instance"); + $capabilityStatement->setKind($kind); $capabilityStatement->addFormat(new FHIRCode("application/json")); $resturl = new FHIRUrl(); $resturl->setValue($gbl::$apisBaseFullUrl . "/fhir"); @@ -143,7 +154,7 @@ private function addOauthSecurityExtensions(FHIRCapabilityStatementSecurity $sta $statement->addExtension($oauthExtension); // now add our SMART capabilities - foreach (Capability::SUPPORTED_CAPABILITIES as $smartCapability) { + foreach (Capability::FHIR_SUPPORTED_CAPABILITIES as $smartCapability) { $extension = new FHIRExtension(); $fhirCode = new FHIRCode($smartCapability); $extension->setUrl("http://fhir-registry.smarthealthit.org/StructureDefinition/capabilities"); diff --git a/src/RestControllers/FHIR/Operations/FhirOperationDefinitionRestController.php b/src/RestControllers/FHIR/Operations/FhirOperationDefinitionRestController.php new file mode 100644 index 00000000000..b7250870ace --- /dev/null +++ b/src/RestControllers/FHIR/Operations/FhirOperationDefinitionRestController.php @@ -0,0 +1,87 @@ +fhirService = new FhirResourcesService(); + } + + /** + * Queries for FHIR OperationDefinition resources using various search parameters. + * @param @searchParams + * @return FHIR bundle with query results, if found + */ + public function getAll($searchParams) + { + // the only resource we have right now is the Export operation + $resources = [ + $this->getBulkDataStatusDefinition() + ]; + + $bundleSearchResult = $this->fhirService->createBundle('OperationDefinition', $resources, false); + $response = $this->createResponseForCode(StatusCode::OK); + $response->getBody()->write(json_encode($bundleSearchResult)); + return $response; + } + + public function getOne($operationId) + { + $processingResult = new ProcessingResult(); + $statusCode = 200; + if ($operationId == '$bulkdata-status') { + $processingResult->addData($this->getBulkDataStatusDefinition()); + } + return RestControllerHelper::handleFhirProcessingResult($processingResult, $statusCode); + } + /** + * Create a response object for the given status code with our default set of headers. + * @param $statusCode + * @return ResponseInterface + */ + private function createResponseForCode($statusCode) + { + $response = (new Psr17Factory())->createResponse($statusCode); + return $response->withAddedHeader('Content-Type', 'application/json'); + } + + private function getBulkDataStatusDefinition() + { + $opDef = new FHIROperationDefinition(); + $opDef->setName('$bulkdata-status'); + $opDef->setStatus("active"); + + $opDefKind = new FHIROperationKind(); + $opDefParameter = new FHIROperationDefinitionParameter(); + $opDefParameter->setName("job"); + $opDefParameterUse = new FHIROperationParameterUse(); + $opDefParameterUse->setValue('in'); + $opDefParameter->setUse($opDefParameterUse); + $opDefParameter->setMin(1); + $opDefParameter->setMax(1); + $opDefParameter->setType(UtilsService::createCoding("string", "string", "http://hl7.org/fhir/data-types")); + $opDefKind->setValue("operation"); + $opDefParameter->setSearchType(UtilsService::createCoding("string", "string", "http://hl7.org/fhir/ValueSet/search-param-type")); + $opDef->setKind($opDefKind); + $opDef->addParameter($opDefParameter); + return $opDef; + } +} diff --git a/src/RestControllers/FHIR/Operations/FhirOperationExportRestController.php b/src/RestControllers/FHIR/Operations/FhirOperationExportRestController.php index 8f83d6b790d..f1377855296 100644 --- a/src/RestControllers/FHIR/Operations/FhirOperationExportRestController.php +++ b/src/RestControllers/FHIR/Operations/FhirOperationExportRestController.php @@ -395,7 +395,7 @@ private function createOutputResultForData(ExportJob $job, $resource, &$data) private function getResultForResourceDocument($resource, \Document $document) { return [ - 'url' => $this->request->getApiBaseFullUrl() . '/fhir/Document/' . $document->get_id() . '/Binary' + 'url' => $this->request->getApiBaseFullUrl() . '/fhir/Binary/' . $document->get_id() , "type" => $resource ]; } diff --git a/src/RestControllers/RestControllerHelper.php b/src/RestControllers/RestControllerHelper.php index 292f259296d..43d364b0d37 100644 --- a/src/RestControllers/RestControllerHelper.php +++ b/src/RestControllers/RestControllerHelper.php @@ -16,7 +16,11 @@ use OpenEMR\Events\RestApiExtend\RestApiCreateEvent; use OpenEMR\Events\RestApiExtend\RestApiResourceServiceEvent; use OpenEMR\Events\RestApiExtend\RestApiScopeEvent; +use OpenEMR\FHIR\R4\FHIRDomainResource\FHIROperationDefinition; +use OpenEMR\FHIR\R4\FHIRElement\FHIROperationKind; +use OpenEMR\FHIR\R4\FHIRElement\FHIROperationParameterUse; use OpenEMR\Services\FHIR\IResourceSearchableService; +use OpenEMR\Services\FHIR\UtilsService; use OpenEMR\Services\Search\FhirSearchParameterDefinition; use OpenEMR\Services\Search\SearchFieldType; use OpenEMR\FHIR\R4\FHIRDomainResource\FHIRPatient; @@ -47,9 +51,18 @@ class RestControllerHelper */ const FHIR_SERVICES_NAMESPACE = "OpenEMR\\Services\\FHIR\\Fhir"; + const DEFAULT_STRUCTURE_DEFINITION = "http://hl7.org/fhir/StructureDefinition/"; + // @see https://www.hl7.org/fhir/search.html#table const FHIR_SEARCH_CONTROL_PARAM_REV_INCLUDE_PROVENANCE = "Provenance:target"; + private $restURL = ""; + + public function __construct($restAPIUrl = "") + { + $this->restURL = $restAPIUrl; + } + /** * Configures the HTTP status code and payload returned within a response. * @@ -197,6 +210,9 @@ public function setSearchParams($resource, FHIRCapabilityStatementResource $capR $paramExists = false; $type = $searchDefinition->getType(); + if ($type == SearchFieldType::DATETIME) { + $type = 'date'; // fhir merges date and datetime into a single date for capability statement purposes. + } foreach ($capResource->getSearchParam() as $searchParam) { if (strcmp($searchParam->getName(), $fhirSearchField) == 0) { @@ -231,9 +247,23 @@ public function getFullyQualifiedServiceClassForResource($resource, $serviceClas public function addOperations($resource, $items, FHIRCapabilityStatementResource $capResource) { + // TODO: @adunsulag we need to architect a more generic way of adding operations like we do with resources $operation = end($items); // we want to skip over anything that's not a resource $operation + + // first check to make sure the operation is not already defined + // such as $bulkdata-status when we have both a POST and a DELETE rest route to the same operation + if (!empty($capResource->getOperation())) { + foreach ($capResource->getOperation() as $existingOperation) { + // this doesn't handle the $export operations + // TODO: is there a better way to handle all operations and not just things such as $bulkdata-status? + if ($existingOperation->getName() == $operation) { + return; // already exists so let's skip adding this operation + } + } + } + if ($operation == '$export') { if ($resource != '$export') { $operationName = strtolower($resource) . '-export'; @@ -246,6 +276,10 @@ public function addOperations($resource, $items, FHIRCapabilityStatementResource $fhirOperation->setDefinition(new FHIRCanonical('http://hl7.org/fhir/uv/bulkdata/OperationDefinition/' . $operationName)); $capResource->addOperation($fhirOperation); } else if ($operation === '$bulkdata-status') { + $fhirOperation = new FHIRCapabilityStatementOperation(); + $fhirOperation->setName($operation); + $fhirOperation->setDefinition($this->restURL . '/OperationDefinition/$bulkdata-status'); + $capResource->addOperation($fhirOperation); // TODO: @adunsulag we should document in our capability statement how to use the bulkdata-status operation } else if ($operation === '$docref') { $fhirOperation = new FHIRCapabilityStatementOperation(); @@ -275,9 +309,11 @@ public function addRequestMethods($items, FHIRCapabilityStatementResource $capRe $code = "search-type"; } } elseif (strcmp($reqMethod, "POST") == 0) { - $code = "insert"; + $code = "create"; } elseif (strcmp($reqMethod, "PUT") == 0) { $code = "update"; + } elseif (strcmp($reqMethod, "DELETE") == 0) { + $code = "delete"; } if (!empty($code)) { @@ -290,7 +326,7 @@ public function addRequestMethods($items, FHIRCapabilityStatementResource $capRe } - public function getCapabilityRESTObject($routes, $serviceClassNameSpace = self::FHIR_SERVICES_NAMESPACE, $structureDefinition = "http://hl7.org/fhir/StructureDefinition/"): FHIRCapabilityStatementRest + public function getCapabilityRESTObject($routes, $serviceClassNameSpace = self::FHIR_SERVICES_NAMESPACE, $structureDefinition = self::DEFAULT_STRUCTURE_DEFINITION): FHIRCapabilityStatementRest { $restItem = new FHIRCapabilityStatementRest(); $mode = new FHIRRestfulCapabilityMode(); @@ -325,11 +361,14 @@ public function getCapabilityRESTObject($routes, $serviceClassNameSpace = self:: if (!empty($serviceClass)) { $service = new $serviceClass(); } + // typically the type is the same as the resource, but for operations it will be our OperationDefinition + $type = self::getResourceTypeForResource($resource); + $capResource = $resourcesHash[$type] ?? null; - if (!array_key_exists($resource, $resourcesHash)) { + if (empty($capResource)) { $capResource = new FHIRCapabilityStatementResource(); - $capResource->setType(new FHIRCode($resource)); - $capResource->setProfile(new FHIRCanonical($structureDefinition . $resource)); + $capResource->setType(new FHIRCode($type)); + $capResource->setProfile(new FHIRCanonical($structureDefinition . $type)); if ($service instanceof IResourceUSCIGProfileService) { $profileUris = $service->getProfileURIs(); @@ -337,11 +376,12 @@ public function getCapabilityRESTObject($routes, $serviceClassNameSpace = self:: $capResource->addSupportedProfile(new FHIRCanonical($uri)); } } - $resourcesHash[$resource] = $capResource; + // per the specification type must be unique in the capability statement + $resourcesHash[$type] = $capResource; } - $this->setSearchParams($resource, $resourcesHash[$resource], $service); - $this->addRequestMethods($items, $resourcesHash[$resource]); - $this->addOperations($resource, $items, $resourcesHash[$resource]); + $this->setSearchParams($resource, $capResource, $service); + $this->addRequestMethods($items, $capResource); + $this->addOperations($resource, $items, $capResource); } } @@ -351,6 +391,21 @@ public function getCapabilityRESTObject($routes, $serviceClassNameSpace = self:: return $restItem; } + /** + * Given a resource we've pulled from our rest route definitions figure out the type from our valueset + * for the resource type: http://hl7.org/fhir/2021Mar/valueset-resource-types.html + * @param string $resource + * @return string + */ + private static function getResourceTypeForResource(string $resource) + { + $firstChar = $resource[0] ?? ''; + if ($firstChar == '$') { + return 'OperationDefinition'; + } + return $resource; + } + /** * Fires off a system event for the given API resource to filter the serviceClass. This gives module writers * the opportunity to extend the api, add / remove Implementation Guide profiles and declare different API conformance diff --git a/src/Services/FHIR/DocumentReference/FhirPatientDocumentReferenceService.php b/src/Services/FHIR/DocumentReference/FhirPatientDocumentReferenceService.php index 64f51391189..ccb3758888e 100644 --- a/src/Services/FHIR/DocumentReference/FhirPatientDocumentReferenceService.php +++ b/src/Services/FHIR/DocumentReference/FhirPatientDocumentReferenceService.php @@ -134,11 +134,12 @@ public function parseOpenEMRRecord($dataRecord = array(), $encode = false) // populate the link to download the patient document if (!empty($dataRecord['uuid'])) { - $url = $this->getFhirApiURL() . '/fhir/Document/' . $dataRecord['uuid'] . '/Binary'; + $url = $this->getFhirApiURL() . '/fhir/Binary/' . $dataRecord['uuid']; $content = new FHIRDocumentReferenceContent(); $attachment = new FHIRAttachment(); $attachment->setContentType($dataRecord['mimetype']); $attachment->setUrl(new FHIRUrl($url)); + $attachment->setTitle($dataRecord['name'] ?? ''); $content->setAttachment($attachment); // TODO: if we support tagging a specific document with a reference code we can put that here. // since it's plain text we have no other interpretation so we just use the mime type sufficient IHE Format code diff --git a/src/Services/PatientService.php b/src/Services/PatientService.php index d50c80f359b..f683299bf32 100644 --- a/src/Services/PatientService.php +++ b/src/Services/PatientService.php @@ -605,6 +605,11 @@ public function getUuid($pid) return self::getUuidById($pid, self::TABLE_NAME, 'pid'); } + public function getPidByUuid($uuid) + { + return self::getIdByUuid($uuid, self::TABLE_NAME, 'pid'); + } + private function saveCareTeamHistory($patientData, $oldProviders, $oldFacilities) { $careTeamService = new CareTeamService(); diff --git a/swagger/openemr-api.yaml b/swagger/openemr-api.yaml index e201a97b5d9..f42151f38fc 100644 --- a/swagger/openemr-api.yaml +++ b/swagger/openemr-api.yaml @@ -4360,7 +4360,7 @@ paths: subject: { reference: Patient/946da619-c631-431a-a282-487cd6fb7802, type: Patient } date: '2021-09-19T03:15:56+00:00' author: [null] - content: [{ attachment: { contentType: image/gif, url: 'https://localhost:9300/apis/default/fhir/Document/7/Binary' }, format: { system: 'http://ihe.net/fhir/ValueSet/IHE.FormatCode.codesystem', code: 'urn:ihe:iti:xds:2017:mimeTypeSufficient', display: 'mimeType Sufficient' } }] + content: [{ attachment: { contentType: image/gif, url: 'https://localhost:9300/apis/default/fhir/Binary/7' }, format: { system: 'http://ihe.net/fhir/ValueSet/IHE.FormatCode.codesystem', code: 'urn:ihe:iti:xds:2017:mimeTypeSufficient', display: 'mimeType Sufficient' } }] '400': $ref: '#/components/responses/badrequest' '401': @@ -4370,7 +4370,7 @@ paths: security: - openemr_auth: [] - '/fhir/Document/{id}/Binary': + '/fhir/Binary/{id}': get: tags: - fhir @@ -6318,6 +6318,42 @@ paths: responses: '200': description: 'Return smart configuration of the fhir server' + /fhir/OperationDefinition: + get: + tags: + - fhir + description: 'Returns a list of the OperationDefinition resources that are specific to this OpenEMR installation' + responses: + '200': + description: 'Return list of OperationDefinition resources' + '/fhir/OperationDefinition/{operation}': + get: + tags: + - fhir + description: 'Returns a single OperationDefinition resource that is specific to this OpenEMR installation' + parameters: + - + name: operation + in: path + description: 'The name of the operation to query. For example $bulkdata-status' + required: true + schema: + type: string + responses: + '200': + description: 'Standard Response' + content: + application/json: + schema: + properties: + 'json object': { description: 'FHIR Json object.', type: object } + type: object + example: + resourceType: OperationDefinition + name: $bulkdata-status + status: active + kind: operation + parameter: [{ name: job, use: in, min: 1, max: 1, type: { system: 'http://hl7.org/fhir/data-types', code: string, display: string }, searchType: { system: 'http://hl7.org/fhir/ValueSet/search-param-type', code: string, display: string } }] /fhir/$export: get: tags: @@ -7409,13 +7445,13 @@ components: 'api:fhir': 'FHIR R4 API' patient/AllergyIntolerance.read: 'Read allergy intolerance resources for the current patient (api:fhir)' patient/Appointment.read: 'Read appointment resources for the current patient (api:fhir)' + patient/Binary.read: 'Read binary document resources for the current patient (api:fhir)' patient/CarePlan.read: 'Read care plan resources for the current patient (api:fhir)' patient/CareTeam.read: 'Read care team resources for the current patient (api:fhir)' patient/Condition.read: 'Read condition resources for the current patient (api:fhir)' patient/Coverage.read: 'Read coverage resources for the current patient (api:fhir)' patient/Device.read: 'Read device resources for the current patient (api:fhir)' patient/DiagnosticReport.read: 'Read diagnostic report resources for the current patient (api:fhir)' - patient/Document.read: 'Read document resources for the current patient (api:fhir)' patient/DocumentReference.read: 'Read document reference resources for the current patient (api:fhir)' patient/DocumentReference.$docref: 'Generate a document for the current patient or returns the most current Clinical Summary of Care Document (CCD)' patient/Encounter.read: 'Read encounter resources for the current patient (api:fhir)' @@ -7432,13 +7468,13 @@ components: patient/Procedure.read: 'Read procedure resources for the current patient (api:fhir)' patient/Provenance.read: 'Read provenance resources for the current patient (api:fhir)' system/AllergyIntolerance.read: 'Read all allergy intolerance resources in the system (api:fhir)' + system/Binary.read: 'Read all binary document resources in the system (api:fhir)' system/CarePlan.read: 'Read all care plan resources in the system (api:fhir)' system/CareTeam.read: 'Read all care team resources in the system (api:fhir)' system/Condition.read: 'Read all condition resources in the system (api:fhir)' system/Coverage.read: 'Read all coverage resources in the system (api:fhir)' system/Device.read: 'Read all device resources in the system (api:fhir)' system/DiagnosticReport.read: 'Read all diagnostic report resources in the system (api:fhir)' - system/Document.read: 'Read all document resources in the system (api:fhir)' system/DocumentReference.read: 'Read all document reference resources in the system (api:fhir)' system/DocumentReference.$docref: 'Generate a document for any patient in the system or returns the most current Clinical Summary of Care Document (CCD)' system/Encounter.read: 'Read all encounter resources in the system (api:fhir)' @@ -7457,13 +7493,13 @@ components: system/Procedure.read: 'Read all procedure resources in the system (api:fhir)' system/Provenance.read: 'Read all provenance resources in the system (api:fhir)' user/AllergyIntolerance.read: 'Read all allergy intolerance resources the user has access to (api:fhir)' + user/Binary.read: 'Read all binary documents the user has access to (api:fhir)' user/CarePlan.read: 'Read all care plan resources the user has access to (api:fhir)' user/CareTeam.read: 'Read all care team resources the user has access to (api:fhir)' user/Condition.read: 'Read all condition resources the user has access to (api:fhir)' user/Coverage.read: 'Read all coverage resources the user has access to (api:fhir)' user/Device.read: 'Read all device resources the user has access to (api:fhir)' user/DiagnosticReport.read: 'Read all diagnostic report resources the user has access to (api:fhir)' - user/Document.read: 'Read all documents the user has access to (api:fhir)' user/DocumentReference.read: 'Read all document reference resources the user has access to (api:fhir)' user/DocumentReference.$docref: 'Generate a document for any patient the user has access to or returns the most current Clinical Summary of Care Document (CCD) (api:fhir)' user/Encounter.read: 'Read all encounter resources the user has access to (api:fhir)' diff --git a/tests/Tests/Api/CapabilityFhirTest.php b/tests/Tests/Api/CapabilityFhirTest.php index 450373e85e5..da609a0f40a 100644 --- a/tests/Tests/Api/CapabilityFhirTest.php +++ b/tests/Tests/Api/CapabilityFhirTest.php @@ -130,7 +130,7 @@ private function assertCapabilityHasSMARTRequirements($statement) } // the capabilities the server currently has - $expectedCapabilities = Capability::SUPPORTED_CAPABILITIES; + $expectedCapabilities = Capability::FHIR_SUPPORTED_CAPABILITIES; $missing_capabilities = array_diff($expectedCapabilities, $enabledCapabilities); $this->assertEquals([], $missing_capabilities, "Capabilities statement is missing expected SMART extensions of " . implode(",", $missing_capabilities)); } diff --git a/tests/Tests/Unit/Common/Http/HttpRestParsedRouteTest.php b/tests/Tests/Unit/Common/Http/HttpRestParsedRouteTest.php index aa0ff1938b7..10a17a087f3 100644 --- a/tests/Tests/Unit/Common/Http/HttpRestParsedRouteTest.php +++ b/tests/Tests/Unit/Common/Http/HttpRestParsedRouteTest.php @@ -55,11 +55,11 @@ public function testGetResourceWithRootOperation() public function testGetResourceWithDocumentBinaryFormat() { - $request = '/fhir/Document/15/Binary'; - $definition = 'GET /fhir/Document/:id/Binary'; + $request = '/fhir/Binary/15'; + $definition = 'GET /fhir/Binary/:id'; $parsedRoute = new HttpRestParsedRoute("GET", $request, $definition); - $this->assertEquals("Document", $parsedRoute->getResource()); + $this->assertEquals("Binary", $parsedRoute->getResource()); } public function testIsOperation()