4 * Authorization Server Member
7 * @link http://www.open-emr.org
8 * @author Jerry Padgett <sjpadgett@gmail.com>
9 * @copyright Copyright (c) 2020 Jerry Padgett <sjpadgett@gmail.com>
10 * @license https://github.com/openemr/openemr/blob/master/LICENSE GNU General Public License 3
13 namespace OpenEMR\Common\Auth\OpenIDConnect\Repositories
;
15 use League\OAuth2\Server\Entities\ClientEntityInterface
;
16 use League\OAuth2\Server\Exception\OAuthServerException
;
17 use League\OAuth2\Server\Repositories\ScopeRepositoryInterface
;
18 use OpenEMR\Common\Auth\OpenIDConnect\Entities\ClientEntity
;
19 use OpenEMR\Common\Auth\OpenIDConnect\Entities\ScopeEntity
;
20 use OpenEMR\Common\Auth\UuidUserAccount
;
21 use OpenEMR\Common\Logging\SystemLogger
;
22 use OpenEMR\Common\System\System
;
23 use OpenEMR\Events\RestApiExtend\RestApiCreateEvent
;
24 use OpenEMR\Events\RestApiExtend\RestApiScopeEvent
;
25 use OpenEMR\RestControllers\RestControllerHelper
;
26 use Psr\Log\LoggerInterface
;
28 class ScopeRepository
implements ScopeRepositoryInterface
31 * @var LoggerInterface
35 private $validationScopes;
40 private $requestScopes;
43 * Session string containing the scopes populated in the current session.
46 private $sessionScopes;
54 * Array(string => callback) Where the string is the route and the callback is the route handler
57 private $fhirRouteMap = [];
60 * Array(string => callback) Where the string is the route and the callback is the route handler
63 private $routeMap = [];
66 * Array(string => callback) Where the string is the route and the callback is the route handler
69 private $portalRouteMap = [];
72 * ScopeRepository constructor.
73 * @param $restConfig \RestConfig normally we would typesafe this, but RestConfig isn't in the autoloader so we leave it out so we can unit test this class better
75 public function __construct($restConfig = null)
77 $this->logger
= new SystemLogger();
78 $this->requestScopes
= isset($_REQUEST['scope']) ?
$_REQUEST['scope'] : null;
79 $this->sessionScopes
= $_SESSION['scopes'] ??
'';
80 $this->restConfig
= $restConfig;
81 if (!empty($restConfig)) {
82 $this->fhirRouteMap
= $restConfig::$FHIR_ROUTE_MAP ??
[];
83 $this->routeMap
= $restConfig::$ROUTE_MAP ??
[];
84 $this->portalRouteMap
= $restConfig::$PORTAL_ROUTE_MAP ??
[];
91 public function getFhirRouteMap(): array
93 return $this->fhirRouteMap
;
97 * @param array $fhirRouteMap
99 public function setFhirRouteMap(array $fhirRouteMap): void
101 $this->fhirRouteMap
= $fhirRouteMap;
107 public function getStandardRouteMap(): array
109 return $this->routeMap
;
113 * @param array $routeMap
115 public function setStandardRouteMap(array $routeMap): void
117 $this->routeMap
= $routeMap;
123 public function getPortalRouteMap(): array
125 return $this->portalRouteMap
;
129 * @param array $portalRouteMap
131 public function setPortalRouteMap(array $portalRouteMap): void
133 $this->portalRouteMap
= $portalRouteMap;
137 * @param string $identifier
138 * @return ScopeEntity|null
140 public function getScopeEntityByIdentifier($identifier): ?ScopeEntity
142 if (empty($this->validationScopes
)) {
143 $this->logger
->debug("ScopeRepository->getScopeEntityByIdentifier() attempting to build validation scopes");
144 $this->validationScopes
= $this->buildScopeValidatorArray();
147 if (array_key_exists($identifier, $this->validationScopes
) === false && stripos($identifier, 'site:') === false) {
148 $this->logger
->error("ScopeRepository->getScopeEntityByIdentifier() request access to invalid scope", [
149 "scope" => $identifier
150 , 'validationScopes' => $this->validationScopes
]);
153 $this->logger
->debug("ScopeRepository->getScopeEntityByIdentifier() scope requested exists in system", ["identifier" => $identifier]);
155 $scope = new ScopeEntity();
156 $scope->setIdentifier($identifier);
161 public function finalizeScopes(
164 ClientEntityInterface
$clientEntity,
165 $userIdentifier = null
167 $finalizedScopes = [];
168 $scopeListNames = [];
169 $finalizedScopeNames = [];
171 // we only let scopes that the client initially registered with through instead of whatever they request in
173 if ($clientEntity instanceof ClientEntity
) {
174 $clientScopes = $clientEntity->getScopes();
175 foreach ($scopes as $scope) {
176 $scopeListNames[] = $scope->getIdentifier();
177 if (\
in_array($scope->getIdentifier(), $clientScopes)) {
178 $finalizedScopes[] = $scope;
179 $finalizedScopeNames[] = $scope->getIdentifier();
183 $this->logger
->error("client entity was not an instance of ClientEntity and scopes could not be retrieved");
186 // If a nonce is passed in, add a nonce scope for id token nonce claim
187 if (!empty($_SESSION['nonce'])) {
188 $scope = new ScopeEntity();
189 $scope->setIdentifier('nonce');
190 $finalizedScopes[] = $scope;
191 $finalizedScopeNames[] = "nonce";
194 // Need a site id for our apis
195 $siteScope = $this->getSiteScope();
196 $finalizedScopeNames[] = $siteScope->getIdentifier();
197 $finalizedScopes[] = $siteScope;
199 $this->logger
->debug(
200 "ScopeRepository->finalizeScopes() scopes finalized ",
201 ['finalizedScopes' => $finalizedScopeNames, 'clientScopes' => $clientScopes
203 'initialScopes' => $scopeListNames]
205 return $finalizedScopes;
208 public function getSiteScope(): ScopeEntity
210 // TODO: adunsulag, sjpadget Won't refresh token validation fail on multi-site since we won't
211 // have the id in the session?
212 if ($_SESSION['site_id']) {
213 $siteScope = "site:" . $_SESSION['site_id'];
215 $siteScope = "site:default";
217 $scope = new ScopeEntity();
218 $scope->setIdentifier($siteScope);
222 public function setRequestScopes($scopes)
224 if (!is_string($scopes)) {
225 (new SystemLogger())->error("Attempted to set request scopes to something other than a string", ['scopes' => $scopes]);
226 throw new \
InvalidArgumentException("Invalid scope parameter set");
229 $this->requestScopes
= $scopes;
232 public function getRequestScopes()
234 return $this->requestScopes
;
237 public function getSessionScopes()
239 return $this->sessionScopes
;
242 // nonce claim and nonce scope is handled by server logic.
243 // current locale is enUS need to set from openemr locale.
244 public function getSupportedClaims(): array
260 "iat", // token create time
261 "iss", // token issuer(https://domain)
262 "exp", // token expiry time.
263 "sub" // the subject of token. usually patient UUID.
267 public function oidcScopes(): array
288 public function fhirRequiredSmartScopes(): array
301 // we define our Bulk FHIR here
302 // There really is no defined standard on how to handle SMART scopes for operations ($operation)
303 // hopefully its defined in V2, but for now we are going to implement using the following scopes
304 // @see https://chat.fhir.org/#narrow/stream/179170-smart/topic/SMART.20scopes.20and.20custom.20operations/near/156832330
305 if (isset($this->restConfig
) && $this->restConfig
->areSystemScopesEnabled()) {
306 $requiredSmart[] = 'system/Patient.$export';
307 $requiredSmart[] = 'system/Group.$export';
308 $requiredSmart[] = 'system/*.$bulkdata-status';
309 $requiredSmart[] = 'system/*.$export';
311 return $requiredSmart;
317 * Returns all fhir permitted scopes and permissions.
318 * These will be qualified against existing and future routes
319 * with role(patient, user or system facing applications).
323 public function fhirScopes(): array
325 // Note: we only allow patient/read access for FHIR apps right now, if someone wants write access they have to
326 // have a user/<resource>.write permission as the security context for patient only rights has too much risk
329 // we've restricted our patient scopes just to what we have in portal. We will slowly open them up as we
330 // verify the access rights on them.
332 // "patient/Account.read",
333 "patient/AllergyIntolerance.read",
334 // "patient/AllergyIntolerance.write",
335 "patient/Appointment.read",
336 "patient/Binary.read",
337 // "patient/Appointment.write",
338 "patient/CarePlan.read",
339 "patient/CareTeam.read",
340 "patient/Condition.read",
341 // "patient/Condition.write",
342 // "patient/Consent.read",
343 "patient/Coverage.read",
344 // "patient/Coverage.write",
345 "patient/DiagnosticReport.read",
346 "patient/Device.read",
347 "patient/DocumentReference.read",
348 'patient/DocumentReference.$docref', // generate or view most recent CCD for the selected patient
349 // "patient/DocumentReference.write",
350 "patient/Encounter.read",
351 // "patient/Encounter.write",
353 "patient/Immunization.read",
354 // "patient/Immunization.write",
355 "patient/Location.read",
356 "patient/MedicationRequest.read",
357 "patient/Medication.read",
358 // "patient/MedicationRequest.write",
359 // "patient/NutritionOrder.read",
360 "patient/Observation.read",
361 // "patient/Observation.write",
362 "patient/Organization.read",
363 // "patient/Organization.write",
364 "patient/Patient.read",
365 // "patient/Patient.write",
366 "patient/Person.read",
367 "patient/Practitioner.read",
368 // "patient/Practitioner.write",
369 // "patient/PractitionerRole.read",
370 // "patient/PractitionerRole.write",
371 "patient/Procedure.read",
372 // "patient/Procedure.write",
373 "patient/Provenance.read",
374 // "patient/Provenance.write",
375 // "patient/RelatedPerson.read",
376 // "patient/RelatedPerson.write",
377 // "patient/Schedule.read",
378 // "patient/ServiceRequest.read",
380 "user/AllergyIntolerance.read",
381 "user/AllergyIntolerance.write",
382 "user/Appointment.read",
383 "user/Appointment.write",
385 "user/CarePlan.read",
386 "user/CareTeam.read",
387 "user/Condition.read",
388 "user/Condition.write",
390 "user/Coverage.read",
391 "user/Coverage.write",
393 "user/DiagnosticReport.read",
394 "user/DocumentReference.read",
395 "user/DocumentReference.write",
396 'user/DocumentReference.$docref', // export CCD for any patient user has access to
397 "user/Encounter.read",
398 "user/Encounter.write",
400 "user/Immunization.read",
401 "user/Immunization.write",
402 "user/Location.read",
403 "user/MedicationRequest.read",
404 "user/MedicationRequest.write",
405 "user/Medication.read",
406 "user/NutritionOrder.read",
407 "user/Observation.read",
408 "user/Observation.write",
409 "user/Organization.read",
410 "user/Organization.write",
412 "user/Patient.write",
414 "user/Practitioner.read",
415 "user/Practitioner.write",
416 "user/PractitionerRole.read",
417 "user/PractitionerRole.write",
418 "user/Procedure.read",
419 "user/Procedure.write",
420 "user/Provenance.read",
421 "user/Provenance.write",
422 "user/RelatedPerson.read",
423 "user/RelatedPerson.write",
424 "user/Schedule.read",
425 "user/ServiceRequest.read",
426 "user/ValueSet.read",
429 if ($this->restConfig
->areSystemScopesEnabled()) {
430 return array_merge($permitted, $this->systemScopes());
435 public function systemScopes(): array
438 "system/Account.read",
439 "system/AllergyIntolerance.read",
440 // "system/AllergyIntolerance.write",
441 "system/Appointment.read",
442 "system/Binary.read", // used for Bulk FHIR export downloads
443 // "system/Appointment.write",
444 "system/CarePlan.read",
445 "system/CareTeam.read",
446 "system/Condition.read",
447 // "system/Condition.write",
448 "system/Consent.read",
449 "system/Coverage.read",
450 // "system/Coverage.write",
451 "system/Device.read",
452 "system/DocumentReference.read",
453 'system/DocumentReference.$docref', // generate / view CCD for any patient in the system
454 "system/DiagnosticReport.read",
455 // "system/DocumentReference.write",
456 "system/Encounter.read",
457 // "system/Encounter.write",
460 "system/Immunization.read",
461 // "system/Immunization.write",
462 "system/Location.read",
463 "system/MedicationRequest.read",
464 "system/Medication.read",
465 // "system/MedicationRequest.write",
466 "system/NutritionOrder.read",
467 "system/Observation.read",
468 // "system/Observation.write",
469 "system/Organization.read",
470 // "system/Organization.write",
471 "system/Patient.read",
472 // "system/Patient.write",
473 "system/Person.read",
474 "system/Practitioner.read",
475 // "system/Practitioner.write",
476 "system/PractitionerRole.read",
477 // "system/PractitionerRole.write",
478 "system/Procedure.read",
479 // "system/Procedure.write",
480 "system/Provenance.read",
481 // "system/Provenance.write",
482 "system/RelatedPerson.read",
483 // "system/RelatedPerson.write",
484 "system/Schedule.read",
485 "system/ServiceRequest.read",
486 "system/ValueSet.read",
490 public function apiScopes(): array
493 "patient/allergy.read",
494 "patient/allergy.write",
495 "patient/appointment.read",
496 "patient/appointment.write",
497 "patient/dental_issue.read",
498 "patient/dental_issue.write",
499 "patient/document.read",
500 "patient/document.write",
502 "patient/encounter.read",
503 "patient/encounter.write",
504 "patient/facility.read",
505 "patient/facility.write",
506 "patient/immunization.read",
507 "patient/insurance.read",
508 "patient/insurance.write",
509 "patient/insurance_company.read",
510 "patient/insurance_company.write",
511 "patient/insurance_type.read",
513 "patient/medical_problem.read",
514 "patient/medical_problem.write",
515 "patient/medication.read",
516 "patient/medication.write",
517 "patient/message.read",
518 "patient/message.write",
519 "patient/patient.read",
520 "patient/patient.write",
521 "patient/practitioner.read",
522 "patient/practitioner.write",
523 "patient/prescription.read",
524 "patient/procedure.read",
525 "patient/soap_note.read",
526 "patient/soap_note.write",
527 "patient/surgery.read",
528 "patient/surgery.write",
529 "patient/vital.read",
530 "patient/vital.write",
531 "system/allergy.read",
532 "system/allergy.write",
533 "system/appointment.read",
534 "system/appointment.write",
535 "system/dental_issue.read",
536 "system/dental_issue.write",
537 "system/document.read",
538 "system/document.write",
540 "system/encounter.read",
541 "system/encounter.write",
542 "system/facility.read",
543 "system/facility.write",
544 "system/immunization.read",
545 "system/insurance.read",
546 "system/insurance.write",
547 "system/insurance_company.read",
548 "system/insurance_company.write",
549 "system/insurance_type.read",
551 "system/medical_problem.read",
552 "system/medical_problem.write",
553 "system/medication.read",
554 "system/medication.write",
555 "system/message.read",
556 "system/message.write",
557 "system/patient.read",
558 "system/patient.write",
559 "system/practitioner.read",
560 "system/practitioner.write",
561 "system/prescription.read",
562 "system/procedure.read",
563 "system/soap_note.read",
564 "system/soap_note.write",
565 "system/surgery.read",
566 "system/surgery.write",
568 "system/vital.write",
570 "user/allergy.write",
571 "user/appointment.read",
572 "user/appointment.write",
573 "user/dental_issue.read",
574 "user/dental_issue.write",
575 "user/document.read",
576 "user/document.write",
578 "user/encounter.read",
579 "user/encounter.write",
580 "user/facility.read",
581 "user/facility.write",
582 "user/immunization.read",
583 "user/insurance.read",
584 "user/insurance.write",
585 "user/insurance_company.read",
586 "user/insurance_company.write",
587 "user/insurance_type.read",
589 "user/medical_problem.read",
590 "user/medical_problem.write",
591 "user/medication.read",
592 "user/medication.write",
594 "user/message.write",
596 "user/patient.write",
597 "user/practitioner.read",
598 "user/practitioner.write",
599 "user/prescription.read",
600 "user/procedure.read",
601 "user/soap_note.read",
602 "user/soap_note.write",
604 "user/surgery.write",
605 "user/transaction.read",
606 "user/transaction.write",
613 public function getOidcSupportedScopes(): array
615 return $this->oidcScopes();
618 public function getSystemFhirSupportedScopes(): array
620 return $this->systemScopes();
623 public function getStandardApiSupportedScopes(): array
625 return $this->apiScopes();
628 public function getServerScopes(): array
630 $siteScope = $this->getSiteScope();
632 $siteScope->getIdentifier() => $siteScope->getIdentifier()
637 * Method will qualify current scopes based on active FHIR resources.
638 * Allowed permissions are validated from the default scopes and client role.
640 * @param string $role
643 public function getCurrentSmartScopes(): array
645 (new SystemLogger())->debug("ScopeRepository->getCurrentSmartScopes() setting up smart scopes");
646 $gbl = $this->restConfig
;
647 $restHelper = new RestControllerHelper();
648 // Collect all currently enabled FHIR resources.
649 // Then assign all permissions the resource is capable.
651 $restAPIs = $restHelper->getCapabilityRESTObject($this->getFhirRouteMap());
652 foreach ($restAPIs->getResource() as $resource) {
653 $resourceType = $resource->getType()->getValue();
654 $interactions = $resource->getInteraction();
655 foreach ($interactions as $interaction) {
656 $scopeRead = $resourceType . ".read";
657 $scopeWrite = $resourceType . ".write";
658 $interactionCode = $interaction->getCode()->getValue();
659 switch ($interactionCode) {
661 $scopes_api['patient/' . $scopeRead] = 'patient/' . $scopeRead;
662 $scopes_api['user/' . $scopeRead] = 'user/' . $scopeRead;
663 $scopes_api['system/' . $scopeRead] = 'system/' . $scopeRead;
666 $scopes_api['patient/' . $scopeRead] = 'patient/' . $scopeRead;
667 $scopes_api['user/' . $scopeRead] = 'user/' . $scopeRead;
668 $scopes_api['system/' . $scopeRead] = 'system/' . $scopeRead;
672 $scopes_api['patient/' . $scopeWrite] = 'patient/' . $scopeWrite;
673 $scopes_api['user/' . $scopeWrite] = 'user/' . $scopeWrite;
674 $scopes_api['system/' . $scopeWrite] = 'system/' . $scopeWrite;
678 foreach ($resource->getOperation() as $operation) {
679 $operationCall = $resourceType . '.' . $operation->getName();
680 $scopes_api['patient/' . $operationCall] = 'patient/' . $operationCall;
681 $scopes_api['user/' . $operationCall] = 'user/' . $operationCall;
682 $scopes_api['system/' . $operationCall] = 'system/' . $operationCall;
685 // if we needed to define scopes based on operations rather than the predefined Bulk-FHIR operations
686 // we would handle them here. Leaving this commented out just for reference
688 // $operations = $resource->getOperation();
691 $scopesSupported = $this->fhirScopes();
692 $scopes_dict = array_combine($scopesSupported, $scopesSupported);
693 $fhir = array_combine($this->fhirRequiredSmartScopes(), $this->fhirRequiredSmartScopes());
694 $oidc = array_combine($this->oidcScopes(), $this->oidcScopes());
695 // we need to make sure the 'site:' and any other server context vars are permitted
696 $serverScopes = $this->getServerScopes();
697 $scopesSupported = null;
698 // verify scope permissions are allowed for role being used.
699 foreach ($scopes_api as $key => $scope) {
700 if (empty($scopes_dict[$key])) {
703 $scopesSupported[$key] = $scope;
705 asort($scopesSupported);
706 $scopesSupported = array_keys(array_merge($fhir, $oidc, $serverScopes, $scopesSupported));
707 (new SystemLogger())->debug("ScopeRepository->getCurrentSmartScopes() scopes supported ", ["scopes" => $scopesSupported]);
709 $scopesEvent = new RestApiScopeEvent();
710 $scopesEvent->setApiType(RestApiScopeEvent
::API_TYPE_FHIR
);
711 $scopesSupportedList = $scopesSupported;
712 $scopesEvent->setScopes($scopesSupportedList);
714 $scopesEvent = $GLOBALS["kernel"]->getEventDispatcher()->dispatch($scopesEvent, RestApiScopeEvent
::EVENT_TYPE_GET_SUPPORTED_SCOPES
, 10);
716 if ($scopesEvent instanceof RestApiScopeEvent
) {
717 $scopesSupportedList = $scopesEvent->getScopes();
720 return $scopesSupportedList;
723 public function getCurrentStandardScopes(): array
725 (new SystemLogger())->debug("ScopeRepository->getCurrentStandardScopes() setting up standard api scopes");
726 $restHelper = new RestControllerHelper();
727 // Collect all currently enabled resources.
728 // Then assign all permissions the resource is capable.
730 $restAPIs = $restHelper->getCapabilityRESTObject($this->getStandardRouteMap(), "OpenEMR\\Services");
731 foreach ($restAPIs->getResource() as $resource) {
732 $resourceType = $resource->getType()->getValue();
733 $interactions = $resource->getInteraction();
734 foreach ($interactions as $interaction) {
735 $scopeRead = $resourceType . ".read";
736 $scopeWrite = $resourceType . ".write";
737 $interactionCode = $interaction->getCode()->getValue();
738 // these values come from this valuset http://hl7.org/fhir/2021Mar/valueset-type-restful-interaction.html
739 // for SMART on FHIR 2.0 we will have more granular permissions than *.read and *.write
740 switch ($interactionCode) {
742 $scopes_api['user/' . $scopeRead] = 'user/' . $scopeRead;
743 $scopes_api['system/' . $scopeRead] = 'system/' . $scopeRead;
746 $scopes_api['user/' . $scopeRead] = 'user/' . $scopeRead;
747 $scopes_api['system/' . $scopeRead] = 'system/' . $scopeRead;
753 $scopes_api['user/' . $scopeWrite] = 'user/' . $scopeWrite;
754 $scopes_api['system/' . $scopeWrite] = 'system/' . $scopeWrite;
759 $scopes_api_portal = [];
760 // TODO: should we put this into the constructor? makes it hard to unit test this...
761 if (!empty($GLOBALS['rest_portal_api'])) {
762 $restAPIs = $restHelper->getCapabilityRESTObject($this->getPortalRouteMap(), "OpenEMR\\Services");
763 foreach ($restAPIs->getResource() as $resource) {
764 $resourceType = $resource->getType()->getValue();
765 $interactions = $resource->getInteraction();
766 foreach ($interactions as $interaction) {
767 $scopeRead = $resourceType . ".read";
768 $scopeWrite = $resourceType . ".write";
769 $interactionCode = $interaction->getCode()->getValue();
770 // these values come from this valuset http://hl7.org/fhir/2021Mar/valueset-type-restful-interaction.html
771 // for SMART on FHIR 2.0 we will have more granular permissions than *.read and *.write
772 switch ($interactionCode) {
774 $scopes_api_portal['patient/' . $scopeRead] = 'patient/' . $scopeRead;
777 $scopes_api_portal['patient/' . $scopeRead] = 'patient/' . $scopeRead;
783 $scopes_api_portal['patient/' . $scopeWrite] = 'patient/' . $scopeWrite;
789 $oidc = array_combine($this->oidcScopes(), $this->oidcScopes());
790 $scopes_api = array_merge($scopes_api, $scopes_api_portal);
792 $scopesSupported = $this->apiScopes();
793 $scopes_dict = array_combine($scopesSupported, $scopesSupported);
794 $scopesSupported = null; // this is odd, why do we have this?
795 // verify scope permissions are allowed for role being used.
796 foreach ($scopes_api as $key => $scope) {
797 if (empty($scopes_dict[$key])) {
800 $scopesSupported[$key] = $scope;
802 asort($scopesSupported);
803 $serverScopes = $this->getServerScopes();
804 $scopesSupported = array_keys(array_merge($oidc, $serverScopes, $scopesSupported));
806 $scopesEvent = new RestApiScopeEvent();
807 $scopesEvent->setApiType(RestApiScopeEvent
::API_TYPE_STANDARD
);
808 $scopesSupportedList = $scopesSupported;
809 $scopesEvent->setScopes($scopesSupportedList);
811 $scopesEvent = $GLOBALS["kernel"]->getEventDispatcher()->dispatch($scopesEvent, RestApiScopeEvent
::EVENT_TYPE_GET_SUPPORTED_SCOPES
, 10);
813 if ($scopesEvent instanceof RestApiScopeEvent
) {
814 $scopesSupportedList = $scopesEvent->getScopes();
817 return $scopesSupportedList;
821 * Returns true if the session or request has a fhir api scope in it
824 public function hasFhirApiScopes()
826 $requestScopeString = $this->getRequestScopes();
827 $sessionScopeString = $this->getSessionScopes();
829 $isFhir = preg_match('(fhirUser|api:fhir)', $requestScopeString)
830 ||
preg_match('(fhirUser|api:fhir)', $sessionScopeString);
831 return $isFhir !== false;
835 * Returns true if the session or request has a standard or portal api scope in it
838 public function hasStandardApiScopes()
840 $requestScopeString = $this->getRequestScopes();
841 $sessionScopeString = $this->getSessionScopes();
842 $isApi = preg_match('(api:oemr|api:port)', $requestScopeString)
843 ||
preg_match('(api:oemr|api:port)', $sessionScopeString);
845 return $isApi !== false;
848 // made public for now!
849 public function buildScopeValidatorArray(): array
851 $requestScopeString = $this->getRequestScopes();
852 $isFhir = $this->hasFhirApiScopes();
853 $isApi = $this->hasStandardApiScopes();
854 (new SystemLogger())->debug(
855 "ScopeRepository->buildScopeValidatorArray() ",
856 ["requestScopeString" => $requestScopeString, 'isStandardApi' => $isApi, 'isFhirApi' => $isFhir]
859 // TODO: adunsulag check with @bradymiller and @sjpadgett on defaulting api to $isFhir not all SMART apps request
860 // fhirUser and if we want to support the larger ecosystem of apps we need to not require api:fhir or fhirUser
863 if ($isFhir ||
!$isApi) {
864 $scopesFhir = $this->getCurrentSmartScopes();
868 $scopesApi = $this->getCurrentStandardScopes();
870 $mergedScopes = array_merge($scopesFhir, $scopesApi);
873 $scopes['nonce'] = ['description' => 'Nonce value used to detect replay attacks by third parties'];
875 foreach ($mergedScopes as $scope) {
876 // TODO: @adunsulag look at adding the actual scope description here and what the ramifications are.
877 // Looks like this line could be
878 // $scopes[$scope] = ['description' => $this->lookupDescriptionForScope($scope, false)];
879 $scopes[$scope] = ['description' => 'OpenId Connect'];
885 public function lookupDescriptionForScope($scope, bool $isPatient)
888 "openid" => xl("Permission to retrieve information about the current logged-in user"),
889 "fhirUser" => xl("Identity Information - Permission to retrieve information about the current logged-in user"),
890 "online_access" => xl("Request ability to access data while the current logged-in user remains logged in"),
891 "offline_access" => xl("Request ability to access data even when the current logged-in user has logged out"),
892 "launch" => xl("Permission to obtain information from the EHR for the current session context when app is launched from an EHR."),
893 "launch/patient" => xl("When launching outside the EHR, ask for a patient to be selected at launch time."),
894 "api:oemr" => xl("Permission to use the OpenEMR standard api."),
895 "api:fhir" => xl("Permission to use the OpenEMR FHIR api"),
896 "api:port" => xl("Permission to use the OpenEMR apis from inside the patient portal"),
897 'system/Patient.$export' => xl("Permission to export Patient Compartment resources"),
898 'system/Group.$export' => xl("Permission to export Patient Compartment resources connected to a Patient Group"),
899 'system/*.$bulkdata-status' => xl("Permission to check the job status of a bulkdata export"),
900 'system/*.$export' => xl("Permission to export the entire system dataset the is exportable")
903 if (isset($requiredSmart[$scope])) {
904 return $requiredSmart[$scope];
907 $parts = explode("/", $scope);
908 $context = reset($parts);
909 $resourcePerm = $parts[1] ??
"";
910 $resourcePermParts = explode(".", $resourcePerm);
911 $resource = $resourcePermParts[0] ??
"";
912 $permission = $resourcePermParts[1] ??
"";
914 if (!empty($resource)) {
915 $isReadPermission = $permission == "read";
916 if (strpos($permission, "$") !== false) {
917 return $this->lookupDescriptionForResourceOperation($resource, $context, $isPatient, $permission);
919 return $this->lookupDescriptionForResourceScope($resource, $context, $isPatient, $isReadPermission);
926 private function lookupDescriptionForResourceOperation($resource, $context, $isPatient, $permission)
929 if ($resource == "DocumentReference" && $permission == '$docref') {
930 $description = xl("Create a Clinical Summary of Care Document (CCD) or retrieve the most current CCD");
931 if ($context == 'user') {
932 $description .= " " . xl("for a patient that the user has access to");
933 } else if ($context == "system") {
934 $description .= " " . xl("for a patient that exists in the system");
940 private function lookupDescriptionForResourceScope($resource, $context, $isPatient, $isReadPermission)
943 $scopesByResource[$resource] = $scopesByResource[$resource] ??
['permissions' => []];
945 $description = $isReadPermission ?
xl("Read Access: View, search and access") : xl("Write Access: Create or modify");
948 case 'AllergyIntolerance':
949 $description .= xl("allergies/adverse reactions");
952 $description .= xl("appointments");
955 $description .= xl("observations including laboratory,vitals, and social history records");
958 $description .= xl("care plan information including treatment information and notes");
961 $description .= xl("care team information including practitioners, organizations, persons, and related individuals");
964 $description .= xl("conditions including health concerns, problems, and encounter diagnoses");
967 $description .= xl("implantable medical device records");
969 case 'DiagnosticReport':
970 $description .= xl("diagnostic reports including laboratory,cardiology,radiology, and pathology reports");
972 case 'DocumentReference':
973 $description .= xl("clinical and non-clinical documents");
976 $description .= xl("encounter information");
979 $description .= xl("goals");
982 $description .= xl("immunization history");
984 case 'MedicationRequest':
985 $description .= xl("planned and prescribed medication history including self-reported medications");
988 $description .= xl("drug information related to planned and prescribed medication history");
991 $description .= xl("companies, facilities, insurances, and other organizations");
994 $description .= xl("patient basic demographics including names,communication preferences,race,ethnicity,birth sex,previous names and other administrative information");
997 $description .= xl("provider basic demographic information and other administrative information");
999 case 'PractitionerRole':
1000 $description .= xl("practitioner role for a practitioner (including speciality, location, contact information)");
1003 $description .= xl("procedures");
1006 $description .= xl("locations associated with a patient, provider, or organization");
1009 $description .= xl("provenance information (including person(s) responsible for the information, author organizations, and transmitter organizations)");
1012 $description .= xl("value set records");
1015 $description .= xl("medical records for this resource type");
1018 if ($context == "user") {
1019 $description .= ". " . xl("Application is requesting access to all patient data for this resource you have access to");
1020 } else if ($context == "system") {
1021 $description .= ". " . xl("Application is requesting access to all data in entire system for this resource");
1023 return $description;
1027 * Checks if the given scopes array requires any manual approval by an administrator before an oauth2 client can be authorized
1028 * @param bool $is_confidential_client Whether the client is confidential (can keep a secret safe) or a public app
1029 * @param array $scopes The scopes to be checked to see if we need manual approval
1030 * @return bool true if there exist scopes that require manual review by an administrator, false otherwise
1032 public function hasScopesThatRequireManualApproval(bool $is_confidential_client, array $scopes)
1034 // note eventually this method could have a db lookup to check against if admins want to vet this
1035 // possibly we could have an event dispatched here as well if we want someone to provide / extend that kind of functionality
1037 // if a public app requests the launch scope we also do not let them through unless they've been manually
1038 // authorized by an administrator user.
1039 if (!$is_confidential_client) {
1040 if (array_search("launch", $scopes) !== false) {
1044 // as not all jurisdictions have to comply with ONC rules we will still check against the globals flag in case
1045 // a user has turned off auto-enabling of apps and wants to lock down their installation
1046 if (($GLOBALS['oauth_app_manual_approval'] ??
'0') == '1') {
1050 if ($is_confidential_client) {
1051 // ONC requires that a patient be allowed to use an app of their choice and as long as it does not use user/system scopes there can be
1052 // no prohibiting the patient app selection due to Information Blocking Rule, EMRs must authorize the app within 2 business days
1053 // to deal with this we auto-enable confidential apps that ONLY use patient/* scopes even if they request offline_access scope
1054 // we still prohibit any confidential app that is allowing an in-EHR context to be auto-enabled since they are listed inside
1055 // the patient demographics screen (and other locations possibly in the future)
1056 if ($this->hasUserScopes($scopes) ||
$this->hasSystemScopes($scopes)) {
1062 private function hasUserScopes(array $scopes)
1064 return $this->scopeArrayHasString($scopes, 'user/');
1066 private function hasSystemScopes(array $scopes)
1068 return $this->scopeArrayHasString($scopes, 'system/');
1071 private function scopeArrayHasString(array $scopes, $str)
1073 foreach ($scopes as $scope) {
1074 if (strpos($scope, $str) !== false) {