FHIR Appointment/Patient/Encounter/ValueSet (#7066)
[openemr.git] / src / Common / Auth / OpenIDConnect / Repositories / ScopeRepository.php
blob2b222eaf943e039bc4342db7634f5f292d944f86
1 <?php
3 /**
4 * Authorization Server Member
6 * @package OpenEMR
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
30 /**
31 * @var LoggerInterface
33 private $logger;
35 private $validationScopes;
37 /**
38 * @var
40 private $requestScopes;
42 /**
43 * Session string containing the scopes populated in the current session.
44 * @var string
46 private $sessionScopes;
48 /**
49 * @var \RestConfig
51 private $restConfig;
53 /**
54 * Array(string => callback) Where the string is the route and the callback is the route handler
55 * @var array
57 private $fhirRouteMap = [];
59 /**
60 * Array(string => callback) Where the string is the route and the callback is the route handler
61 * @var array
63 private $routeMap = [];
65 /**
66 * Array(string => callback) Where the string is the route and the callback is the route handler
67 * @var array
69 private $portalRouteMap = [];
71 /**
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 ?? [];
88 /**
89 * @return array
91 public function getFhirRouteMap(): array
93 return $this->fhirRouteMap;
96 /**
97 * @param array $fhirRouteMap
99 public function setFhirRouteMap(array $fhirRouteMap): void
101 $this->fhirRouteMap = $fhirRouteMap;
105 * @return array
107 public function getStandardRouteMap(): array
109 return $this->routeMap;
113 * @param array $routeMap
115 public function setStandardRouteMap(array $routeMap): void
117 $this->routeMap = $routeMap;
121 * @return array
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]);
151 return null;
153 $this->logger->debug("ScopeRepository->getScopeEntityByIdentifier() scope requested exists in system", ["identifier" => $identifier]);
155 $scope = new ScopeEntity();
156 $scope->setIdentifier($identifier);
158 return $scope;
161 public function finalizeScopes(
162 array $scopes,
163 $grantType,
164 ClientEntityInterface $clientEntity,
165 $userIdentifier = null
166 ): array {
167 $finalizedScopes = [];
168 $scopeListNames = [];
169 $finalizedScopeNames = [];
170 $clientScopes = [];
171 // we only let scopes that the client initially registered with through instead of whatever they request in
172 // their grant.
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();
182 } else {
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'];
214 } else {
215 $siteScope = "site:default";
217 $scope = new ScopeEntity();
218 $scope->setIdentifier($siteScope);
219 return $scope;
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
246 return array(
247 "profile",
248 "email",
249 "email_verified",
250 "phone",
251 "phone_verified",
252 "family_name",
253 "given_name",
254 "fhirUser",
255 "locale",
256 "api:oemr",
257 "api:fhir",
258 "api:port",
259 "aud", //client_id
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
269 return [
270 "openid",
271 "profile",
272 "name",
273 "address",
274 "given_name",
275 "family_name",
276 "nickname",
277 "phone",
278 "phone_verified",
279 "email",
280 "email_verified",
281 "offline_access",
282 "api:oemr",
283 "api:fhir",
284 "api:port"
288 public function fhirRequiredSmartScopes(): array
290 $requiredSmart = [
291 "openid",
292 "fhirUser",
293 "online_access",
294 "offline_access",
295 "launch",
296 "launch/patient",
297 "api:oemr",
298 "api:fhir",
299 "api:port",
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;
315 * @Method fhirScopes
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).
321 * @return array
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
327 // for problems.
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.
331 $permitted = [
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",
352 "patient/Goal.read",
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",
379 "user/Account.read",
380 "user/AllergyIntolerance.read",
381 "user/AllergyIntolerance.write",
382 "user/Appointment.read",
383 "user/Appointment.write",
384 "user/Binary.read",
385 "user/CarePlan.read",
386 "user/CareTeam.read",
387 "user/Condition.read",
388 "user/Condition.write",
389 "user/Consent.read",
390 "user/Coverage.read",
391 "user/Coverage.write",
392 "user/Device.read",
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",
399 "user/Goal.read",
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",
411 "user/Patient.read",
412 "user/Patient.write",
413 "user/Person.read",
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());
432 return $permitted;
435 public function systemScopes(): array
437 return [
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",
458 "system/Goal.read",
459 "system/Group.read",
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
492 return [
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",
501 "patient/drug.read",
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",
512 "patient/list.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",
539 "system/drug.read",
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",
550 "system/list.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",
567 "system/vital.read",
568 "system/vital.write",
569 "user/allergy.read",
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",
577 "user/drug.read",
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",
588 "user/list.read",
589 "user/medical_problem.read",
590 "user/medical_problem.write",
591 "user/medication.read",
592 "user/medication.write",
593 "user/message.read",
594 "user/message.write",
595 "user/patient.read",
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",
603 "user/surgery.read",
604 "user/surgery.write",
605 "user/transaction.read",
606 "user/transaction.write",
607 "user/user.read",
608 "user/vital.read",
609 "user/vital.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();
631 return [
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
641 * @return array
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.
650 $scopes_api = [];
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) {
660 case 'read':
661 $scopes_api['patient/' . $scopeRead] = 'patient/' . $scopeRead;
662 $scopes_api['user/' . $scopeRead] = 'user/' . $scopeRead;
663 $scopes_api['system/' . $scopeRead] = 'system/' . $scopeRead;
664 break;
665 case 'search-type':
666 $scopes_api['patient/' . $scopeRead] = 'patient/' . $scopeRead;
667 $scopes_api['user/' . $scopeRead] = 'user/' . $scopeRead;
668 $scopes_api['system/' . $scopeRead] = 'system/' . $scopeRead;
669 break;
670 case 'insert':
671 case 'update':
672 $scopes_api['patient/' . $scopeWrite] = 'patient/' . $scopeWrite;
673 $scopes_api['user/' . $scopeWrite] = 'user/' . $scopeWrite;
674 $scopes_api['system/' . $scopeWrite] = 'system/' . $scopeWrite;
675 break;
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
687 // @var array
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])) {
701 continue;
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.
729 $scopes_api = [];
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) {
741 case 'read':
742 $scopes_api['user/' . $scopeRead] = 'user/' . $scopeRead;
743 $scopes_api['system/' . $scopeRead] = 'system/' . $scopeRead;
744 break;
745 case 'search-type':
746 $scopes_api['user/' . $scopeRead] = 'user/' . $scopeRead;
747 $scopes_api['system/' . $scopeRead] = 'system/' . $scopeRead;
748 break;
749 case 'put':
750 case 'create':
751 case 'update':
752 case 'delete':
753 $scopes_api['user/' . $scopeWrite] = 'user/' . $scopeWrite;
754 $scopes_api['system/' . $scopeWrite] = 'system/' . $scopeWrite;
755 break;
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) {
773 case 'read':
774 $scopes_api_portal['patient/' . $scopeRead] = 'patient/' . $scopeRead;
775 break;
776 case 'search-type':
777 $scopes_api_portal['patient/' . $scopeRead] = 'patient/' . $scopeRead;
778 break;
779 case 'put':
780 case 'create':
781 case 'update':
782 case 'delete':
783 $scopes_api_portal['patient/' . $scopeWrite] = 'patient/' . $scopeWrite;
784 break;
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])) {
798 continue;
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
822 * @return bool
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
836 * @return bool
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
862 $scopesFhir = [];
863 if ($isFhir || !$isApi) {
864 $scopesFhir = $this->getCurrentSmartScopes();
866 $scopesApi = [];
867 if ($isApi) {
868 $scopesApi = $this->getCurrentStandardScopes();
870 $mergedScopes = array_merge($scopesFhir, $scopesApi);
871 $scopes = [];
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'];
882 return $scopes;
885 public function lookupDescriptionForScope($scope, bool $isPatient)
887 $requiredSmart = [
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);
918 } else {
919 return $this->lookupDescriptionForResourceScope($resource, $context, $isPatient, $isReadPermission);
921 } else {
922 return null;
926 private function lookupDescriptionForResourceOperation($resource, $context, $isPatient, $permission)
928 $description = null;
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");
937 return $description;
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");
946 $description .= " ";
947 switch ($resource) {
948 case 'AllergyIntolerance':
949 $description .= xl("allergies/adverse reactions");
950 break;
951 case 'Appointment':
952 $description .= xl("appointments");
953 break;
954 case 'Observation':
955 $description .= xl("observations including laboratory,vitals, and social history records");
956 break;
957 case 'CarePlan':
958 $description .= xl("care plan information including treatment information and notes");
959 break;
960 case 'CareTeam':
961 $description .= xl("care team information including practitioners, organizations, persons, and related individuals");
962 break;
963 case 'Condition':
964 $description .= xl("conditions including health concerns, problems, and encounter diagnoses");
965 break;
966 case 'Device':
967 $description .= xl("implantable medical device records");
968 break;
969 case 'DiagnosticReport':
970 $description .= xl("diagnostic reports including laboratory,cardiology,radiology, and pathology reports");
971 break;
972 case 'DocumentReference':
973 $description .= xl("clinical and non-clinical documents");
974 break;
975 case 'Encounter':
976 $description .= xl("encounter information");
977 break;
978 case 'Goal':
979 $description .= xl("goals");
980 break;
981 case 'Immunization':
982 $description .= xl("immunization history");
983 break;
984 case 'MedicationRequest':
985 $description .= xl("planned and prescribed medication history including self-reported medications");
986 break;
987 case 'Medication':
988 $description .= xl("drug information related to planned and prescribed medication history");
989 break;
990 case 'Organization':
991 $description .= xl("companies, facilities, insurances, and other organizations");
992 break;
993 case 'Patient':
994 $description .= xl("patient basic demographics including names,communication preferences,race,ethnicity,birth sex,previous names and other administrative information");
995 break;
996 case 'Practitioner':
997 $description .= xl("provider basic demographic information and other administrative information");
998 break;
999 case 'PractitionerRole':
1000 $description .= xl("practitioner role for a practitioner (including speciality, location, contact information)");
1001 break;
1002 case 'Procedure':
1003 $description .= xl("procedures");
1004 break;
1005 case 'Location':
1006 $description .= xl("locations associated with a patient, provider, or organization");
1007 break;
1008 case 'Provenance':
1009 $description .= xl("provenance information (including person(s) responsible for the information, author organizations, and transmitter organizations)");
1010 break;
1011 case 'ValueSet':
1012 $description .= xl("value set records");
1013 break;
1014 default:
1015 $description .= xl("medical records for this resource type");
1016 break;
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) {
1041 return true;
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') {
1047 return true;
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)) {
1057 return true;
1060 return false;
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) {
1075 return true;
1078 return false;