You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
397 lines
16 KiB
397 lines
16 KiB
<?php |
|
|
|
/** |
|
* Rest Dispatch |
|
* |
|
* @package OpenEMR |
|
* @link http://www.open-emr.org |
|
* @author Matthew Vita <matthewvita48@gmail.com> |
|
* @author Jerry Padgett <sjpadgett@gmail.com> |
|
* @author Brady Miller <brady.g.miller@gmail.com> |
|
* @copyright Copyright (c) 2018 Matthew Vita <matthewvita48@gmail.com> |
|
* @copyright Copyright (c) 2020 Jerry Padgett <sjpadgett@gmail.com> |
|
* @copyright Copyright (c) 2019-2020 Brady Miller <brady.g.miller@gmail.com> |
|
* @license https://github.com/openemr/openemr/blob/master/LICENSE GNU General Public License 3 |
|
*/ |
|
|
|
// below brings in autoloader |
|
require_once("./../_rest_config.php"); |
|
|
|
use OpenEMR\Common\Auth\UuidUserAccount; |
|
use OpenEMR\Common\Csrf\CsrfUtils; |
|
use OpenEMR\Common\Http\HttpRestRouteHandler; |
|
use OpenEMR\Common\Http\HttpRestRequest; |
|
use OpenEMR\Common\Logging\SystemLogger; |
|
use OpenEMR\Common\Session\SessionUtil; |
|
use OpenEMR\Common\Uuid\UuidRegistry; |
|
use OpenEMR\FHIR\SMART\SmartLaunchController; |
|
use OpenEMR\Events\RestApiExtend\RestApiCreateEvent; |
|
use Psr\Http\Message\ResponseInterface; |
|
|
|
$gbl = RestConfig::GetInstance(); |
|
$restRequest = new HttpRestRequest($gbl, $_SERVER); |
|
$routes = array(); |
|
|
|
// Parse needed information from Redirect or REQUEST_URI |
|
$resource = $gbl::getRequestEndPoint(); |
|
$logger = new SystemLogger(); |
|
$logger->debug("dispatch.php requested", ["resource" => $resource, "method" => $_SERVER['REQUEST_METHOD']]); |
|
|
|
$skipApiAuth = false; |
|
if (!empty($_SERVER['HTTP_APICSRFTOKEN'])) { |
|
// Calling api from within the same session (ie. isLocalApi) since a apicsrftoken header was passed |
|
$isLocalApi = true; |
|
$gbl::setLocalCall(); |
|
$skipApiAuth = false; |
|
$ignoreAuth = false; |
|
} elseif ($gbl::skipApiAuth($resource)) { |
|
// For rest api endpoints that do not require auth, such as the capability statement |
|
// note that the site is validated in the skipApiAuth() function |
|
// refactor resource |
|
$resource = str_replace('/' . $gbl::$SITE, '', $resource); |
|
// set site |
|
$_GET['site'] = $gbl::$SITE; |
|
$isLocalApi = false; |
|
$skipApiAuth = true; |
|
$ignoreAuth = true; |
|
} else { |
|
// Calling api via rest |
|
// ensure token is valid |
|
$tokenRaw = $gbl::verifyAccessToken(); |
|
if ($tokenRaw instanceof ResponseInterface) { |
|
$logger->error("dispatch.php failed token verify for resource", ["resource" => $resource]); |
|
// failed token verify |
|
// not a request object so send the error as response obj |
|
$gbl::emitResponse($tokenRaw); |
|
exit; |
|
} |
|
|
|
// collect token attributes |
|
$attributes = $tokenRaw->getAttributes(); |
|
|
|
// collect site |
|
$site = ''; |
|
$scopes = $attributes['oauth_scopes']; |
|
$logger->debug("Parsed oauth_scopes in AccessToken", ["scopes" => $scopes]); |
|
foreach ($scopes as $attr) { |
|
if (stripos($attr, 'site:') !== false) { |
|
$site = str_replace('site:', '', $attr); |
|
// while here parse site from endpoint |
|
$resource = str_replace('/' . $site, '', $resource); |
|
} |
|
} |
|
// set our scopes and updated resources as needed |
|
$restRequest->setAccessTokenScopes($scopes); |
|
|
|
// ensure 1) sane site 2) site from gbl and access token are the same and 3) ensure the site exists on filesystem |
|
if (empty($site) || empty($gbl::$SITE) || preg_match('/[^A-Za-z0-9\\-.]/', $gbl::$SITE) || ($site !== $gbl::$SITE) || !file_exists(__DIR__ . '/../sites/' . $gbl::$SITE)) { |
|
$logger->error("OpenEMR Error - api site error, so forced exit"); |
|
http_response_code(400); |
|
exit(); |
|
} |
|
// set the site |
|
$_GET['site'] = $site; |
|
|
|
// set the scopes globals for endpoint permission checking |
|
$GLOBALS['oauth_scopes'] = $scopes; |
|
|
|
// collect openemr user uuid |
|
$userId = $attributes['oauth_user_id']; |
|
// collect client id (will be empty for PKCE) |
|
$clientId = $attributes['oauth_client_id'] ?? null; |
|
// collect token id |
|
$tokenId = $attributes['oauth_access_token_id']; |
|
// ensure user uuid and token id are populated |
|
if (empty($userId) || empty($tokenId)) { |
|
$logger->error("OpenEMR Error - userid or tokenid not available, so forced exit", ['attributes' => $attributes]); |
|
http_response_code(400); |
|
exit(); |
|
} |
|
$restRequest->setClientId($clientId); |
|
$restRequest->setAccessTokenId($tokenId); |
|
|
|
// Get a site id from initial login authentication. |
|
$isLocalApi = false; |
|
$skipApiAuth = false; |
|
$ignoreAuth = true; |
|
} |
|
|
|
// set the route as well as the resource information. Note $resource is actually the route and not the resource name. |
|
$restRequest->setRequestPath($resource); |
|
|
|
if (!$isLocalApi) { |
|
// Will start the api OpenEMR session/cookie. |
|
SessionUtil::apiSessionStart($gbl::$web_root); |
|
} |
|
|
|
$GLOBALS['is_local_api'] = $isLocalApi; |
|
$restRequest->setIsLocalApi($isLocalApi); |
|
|
|
// Set $sessionAllowWrite to true here for following reasons: |
|
// 1. !$isLocalApi - not applicable since use the SessionUtil::apiSessionStart session, which was set above |
|
// 2. $isLocalApi - in this case, basically setting this to true downstream after some session sets via session_write_close() call |
|
$sessionAllowWrite = true; |
|
require_once("./../interface/globals.php"); |
|
|
|
// we now can check the database to see if the token is revoked |
|
// Note despite League\OAuth2\Server\AuthorizationValidators\BearerTokenValidator.php:L117 already checking for revoked |
|
// access token we have to do this logic here as we use the access token SCOPE parameter to determine our multi-site setting |
|
// and load up the correct database, our earlier access token logic returns false for revoked as we don't have db access |
|
// for that reason we have this double check on validating the access token. |
|
if (!empty($tokenId)) { |
|
$result = $gbl::validateAccessTokenRevoked($tokenId); |
|
if ($result instanceof ResponseInterface) { |
|
$logger->error("dispatch.php access token was revoked", ["resource" => $resource]); |
|
// failed token verify |
|
// not a request object so send the error as response obj |
|
$gbl::emitResponse($result); |
|
exit; |
|
} |
|
} |
|
|
|
|
|
// recollect this so the DEBUG global can be used if set |
|
$logger = new SystemLogger(); |
|
|
|
$gbl::$apisBaseFullUrl = $GLOBALS['site_addr_oath'] . $GLOBALS['webroot'] . "/apis/" . $gbl::$SITE; |
|
$restRequest->setApiBaseFullUrl($gbl::$apisBaseFullUrl); |
|
|
|
if ($isLocalApi) { |
|
// need to check for csrf match when using api locally |
|
$csrfFail = false; |
|
|
|
if (empty($_SERVER['HTTP_APICSRFTOKEN'])) { |
|
$logger->error("OpenEMR Error: internal api failed because csrf token not received"); |
|
$csrfFail = true; |
|
} |
|
|
|
if ((!$csrfFail) && (!CsrfUtils::verifyCsrfToken($_SERVER['HTTP_APICSRFTOKEN'], 'api'))) { |
|
$logger->error("OpenEMR Error: internal api failed because csrf token did not match"); |
|
$csrfFail = true; |
|
} |
|
|
|
if ($csrfFail) { |
|
$logger->error("dispatch.php CSRF failed", ["resource" => $resource]); |
|
http_response_code(401); |
|
exit(); |
|
} |
|
} elseif ($skipApiAuth) { |
|
$logger->debug("dispatch.php skipping api auth"); |
|
// For endpoints that do not require auth, such as the capability statement |
|
} else { |
|
$logger->debug("dispatch.php authenticating user"); |
|
// verify that user tokens haven't been revoked. |
|
// this is done by verifying the user is trusted with active auth session. |
|
$isTrusted = $gbl::isTrustedUser($attributes["oauth_client_id"], $attributes["oauth_user_id"]); |
|
if ($isTrusted instanceof ResponseInterface) { |
|
$logger->debug("dispatch.php oauth2 inactive user api attempt"); |
|
// user is not logged on to server with an active session. |
|
// too me this is easier than revoking tokens or using phantom tokens. |
|
// give a 400(unsure here, could be a 401) so client can redirect to server. |
|
$gbl::destroySession(); |
|
$gbl::emitResponse($isTrusted); |
|
exit; |
|
} |
|
// $isTrusted can be used for further validations using session_cache |
|
// which is a json. json_decode($isTrusted['session_cache']) |
|
|
|
// authenticate the token |
|
if (!$gbl->authenticateUserToken($tokenId, $clientId, $userId)) { |
|
$logger->error("dispatch.php api call with invalid token"); |
|
$gbl::destroySession(); |
|
http_response_code(401); |
|
exit(); |
|
} |
|
|
|
// collect user information and user role |
|
$uuidToUser = new UuidUserAccount($userId); |
|
$user = $uuidToUser->getUserAccount(); |
|
$userRole = $uuidToUser->getUserRole(); |
|
if (empty($user)) { |
|
// unable to identify the users user role |
|
$logger->error("OpenEMR Error - api user account could not be identified, so forced exit", [ |
|
'userId' => $userId, |
|
'userRole' => $uuidToUser->getUserRole()]); |
|
$gbl::destroySession(); |
|
http_response_code(400); |
|
exit(); |
|
} |
|
if (empty($userRole)) { |
|
// unable to identify the users user role |
|
$logger->error("OpenEMR Error - api user role for user could not be identified, so forced exit"); |
|
$gbl::destroySession(); |
|
http_response_code(400); |
|
exit(); |
|
} |
|
|
|
$restRequest->setAccessTokenId($tokenId); |
|
$restRequest->setRequestUserRole($userRole); |
|
$restRequest->setRequestUser($userId, $user); |
|
|
|
// verify that the scope covers the route |
|
if ( |
|
// fhir routes are the default and can send openid/fhirUser w/ authorization_code, or no scopes at all |
|
// with Client Credentials, so we only reject requests for standard or portal if the correct scope is not |
|
// sent. |
|
($gbl::is_api_request($resource) && !in_array('api:oemr', $GLOBALS['oauth_scopes'])) || |
|
($gbl::is_portal_request($resource) && !in_array('api:port', $GLOBALS['oauth_scopes'])) |
|
) { |
|
$logger->error("dispatch.php api call with token that does not cover the requested route"); |
|
$gbl::destroySession(); |
|
http_response_code(401); |
|
exit(); |
|
} |
|
// ensure user role has access to the resource |
|
// for now assuming: |
|
// users has access to oemr and fhir |
|
// patient has access to port and fhir |
|
if ($userRole == 'users' && ($gbl::is_api_request($resource) || $gbl::is_fhir_request($resource))) { |
|
$logger->debug("dispatch.php valid role and user has access to api/fhir resource", ['resource' => $resource]); |
|
// good to go |
|
} elseif ($userRole == 'patient' && ($gbl::is_portal_request($resource) || $gbl::is_fhir_request($resource))) { |
|
$logger->debug("dispatch.php valid role and patient has access portal resource", ['resource' => $resource]); |
|
// good to go |
|
} elseif ($userRole === 'system' && ($gbl::is_fhir_request($resource))) { |
|
$logger->debug("dispatch.php valid role and system has access to api/fhir resource", ['resource' => $resource]); |
|
} else { |
|
$logger->error("OpenEMR Error: api failed because user role does not have access to the resource", ['resource' => $resource, 'userRole' => $userRole]); |
|
$gbl::destroySession(); |
|
http_response_code(401); |
|
exit(); |
|
} |
|
// set pertinent session variables |
|
if ($userRole == 'users') { |
|
$_SESSION['authUser'] = $user["username"] ?? null; |
|
$_SESSION['authUserID'] = $user["id"] ?? null; |
|
$_SESSION['authProvider'] = sqlQueryNoLog("SELECT `name` FROM `groups` WHERE `user` = ?", [$_SESSION['authUser']])['name'] ?? null; |
|
if (empty($_SESSION['authUser']) || empty($_SESSION['authUserID']) || empty($_SESSION['authProvider'])) { |
|
// this should never happen |
|
$logger->error("OpenEMR Error: api failed because unable to set critical users session variables"); |
|
$gbl::destroySession(); |
|
http_response_code(401); |
|
exit(); |
|
} |
|
if ($restRequest->requestHasScope(SmartLaunchController::CLIENT_APP_STANDALONE_LAUNCH_SCOPE)) { |
|
$restRequest = $gbl->populateTokenContextForRequest($restRequest); |
|
} |
|
} elseif ($userRole == 'patient') { |
|
$_SESSION['pid'] = $user['pid'] ?? null; |
|
$puuidCheck = $user['uuid'] ?? null; |
|
$puuidStringCheck = UuidRegistry::uuidToString($puuidCheck) ?? null; |
|
if (empty($_SESSION['pid']) || empty($puuidCheck) || empty($puuidStringCheck)) { |
|
// this should never happen |
|
$logger->error("OpenEMR Error: api failed because unable to set critical patient session variables"); |
|
$gbl::destroySession(); |
|
http_response_code(401); |
|
exit(); |
|
} |
|
$restRequest->setPatientRequest(true); |
|
$restRequest->setPatientUuidString($puuidStringCheck); |
|
} else if ($userRole === 'system') { |
|
$_SESSION['authUser'] = $user["username"] ?? null; |
|
$_SESSION['authUserID'] = $user["id"] ?? null; |
|
if ( |
|
empty($_SESSION['authUser']) |
|
// this should never happen as the system role depends on the system username... but we safety check it anyways |
|
|| $_SESSION['authUser'] != \OpenEMR\Services\UserService::SYSTEM_USER_USERNAME |
|
|| empty($_SESSION['authUserID']) |
|
) { |
|
$logger->error("OpenEMR Error: api failed because unable to set critical users session variables"); |
|
$gbl::destroySession(); |
|
http_response_code(401); |
|
exit(); |
|
} |
|
} else { |
|
// this user role is not supported |
|
$logger->error("OpenEMR Error - api user role that was provided is not supported, so forced exit"); |
|
$gbl::destroySession(); |
|
http_response_code(400); |
|
exit(); |
|
} |
|
} |
|
|
|
//Extend API using RestApiCreateEvent |
|
$restApiCreateEvent = new RestApiCreateEvent($gbl::$ROUTE_MAP, $gbl::$FHIR_ROUTE_MAP, $gbl::$PORTAL_ROUTE_MAP, $restRequest); |
|
$restApiCreateEvent = $GLOBALS["kernel"]->getEventDispatcher()->dispatch(RestApiCreateEvent::EVENT_HANDLE, $restApiCreateEvent, 10); |
|
$gbl::$ROUTE_MAP = $restApiCreateEvent->getRouteMap(); |
|
$gbl::$FHIR_ROUTE_MAP = $restApiCreateEvent->getFHIRRouteMap(); |
|
$gbl::$PORTAL_ROUTE_MAP = $restApiCreateEvent->getPortalRouteMap(); |
|
$restRequest = $restApiCreateEvent->getRestRequest(); |
|
|
|
// api flag must be four chars |
|
// Pass only routes for current api. |
|
// Also check to ensure route is turned on in globals |
|
if ($gbl::is_fhir_request($resource)) { |
|
if (!$GLOBALS['rest_fhir_api'] && !$isLocalApi) { |
|
// if the external fhir api is turned off and this is not a local api call, then exit |
|
$logger->error("dispatch.php attempted to access resource with FHIR api turned off ", ['resource' => $resource]); |
|
$gbl::destroySession(); |
|
http_response_code(501); |
|
exit(); |
|
} |
|
$_SESSION['api'] = 'fhir'; |
|
$routes = $gbl::$FHIR_ROUTE_MAP; |
|
} elseif ($gbl::is_portal_request($resource)) { |
|
if (!$GLOBALS['rest_portal_api'] && !$isLocalApi) { |
|
$logger->error("dispatch.php attempted to access resource with portal api turned off ", ['resource' => $resource]); |
|
// if the external portal api is turned off and this is not a local api call, then exit |
|
$gbl::destroySession(); |
|
http_response_code(501); |
|
exit(); |
|
} |
|
$_SESSION['api'] = 'port'; |
|
$routes = $gbl::$PORTAL_ROUTE_MAP; |
|
} elseif ($gbl::is_api_request($resource)) { |
|
if (!$GLOBALS['rest_api'] && !$isLocalApi) { |
|
$logger->error( |
|
"dispatch.php attempted to access resource with REST api turned off ", |
|
['resource' => $resource] |
|
); |
|
// if the external api is turned off and this is not a local api call, then exit |
|
$gbl::destroySession(); |
|
http_response_code(501); |
|
exit(); |
|
} |
|
$_SESSION['api'] = 'oemr'; |
|
$routes = $gbl::$ROUTE_MAP; |
|
} else { |
|
$logger->error("dispatch.php invalid access to resource", ['resource' => $resource]); |
|
|
|
// somebody is up to no good |
|
if (!$isLocalApi) { |
|
$gbl::destroySession(); |
|
} |
|
http_response_code(501); |
|
exit(); |
|
} |
|
|
|
$restRequest->setApiType($_SESSION['api']); |
|
|
|
if ($isLocalApi) { |
|
// Ensure that a local process does not hold up other processes |
|
// Note can not do this for !$isLocalApi since need to be able to set |
|
// session variables and it won't help performance anyways. |
|
session_write_close(); |
|
} |
|
|
|
// dispatch $routes called by ref (note storing the output in a variable to allow option |
|
// to destroy the session/cookie before sending the output back) |
|
ob_start(); |
|
$dispatchResult = HttpRestRouteHandler::dispatch($routes, $restRequest); |
|
$apiCallOutput = ob_get_clean(); |
|
// Tear down session for security. |
|
if (!$isLocalApi) { |
|
$gbl::destroySession(); |
|
} |
|
// Send the output if not empty |
|
if (!empty($apiCallOutput)) { |
|
echo $apiCallOutput; |
|
} else if ($dispatchResult instanceof ResponseInterface) { |
|
RestConfig::emitResponse($dispatchResult); |
|
} |
|
|
|
// prevent 200 if route doesn't exist |
|
if ($dispatchResult === false) { |
|
$logger->debug("dispatch.php no route found for resource", ['resource' => $resource]); |
|
http_response_code(404); |
|
}
|
|
|