forked from openemr/openemr
-
Notifications
You must be signed in to change notification settings - Fork 0
/
dispatch.php
409 lines (374 loc) · 17.4 KB
/
dispatch.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
<?php
/**
* Rest Dispatch
*
* @package OpenEMR
* @link https://www.open-emr.org
* @author Matthew Vita <[email protected]>
* @author Jerry Padgett <[email protected]>
* @author Brady Miller <[email protected]>
* @copyright Copyright (c) 2018 Matthew Vita <[email protected]>
* @copyright Copyright (c) 2020 Jerry Padgett <[email protected]>
* @copyright Copyright (c) 2019-2020 Brady Miller <[email protected]>
* @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
$restRequest->setRequestSite($gbl::$SITE);
// 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);
$restRequest->setRequestSite($site);
}
}
// 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($restRequest->getRequestSite()) || empty($gbl::$SITE) || preg_match('/[^A-Za-z0-9\\-.]/', $gbl::$SITE)
|| ($restRequest->getRequestSite() !== $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);
$resource = $restRequest->getRequestPath();
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();
}
$logger->debug("dispatch.php request setup for user role", ['authUserID' => $user['id'], 'authUser' => $user['username']]);
if (
$restRequest->requestHasScope(SmartLaunchController::CLIENT_APP_STANDALONE_LAUNCH_SCOPE)
|| $restRequest->requestHasScope(SmartLaunchController::CLIENT_APP_REQUIRED_LAUNCH_SCOPE)
) {
$logger->debug("dispatch.php api is userRole populating token context for request due to smart 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);
$logger->debug("dispatch.php request setup for patient role", ['patient' => $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, RestApiCreateEvent::EVENT_HANDLE, 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();
}
// TODO: @adunsulag we should consider rearranging the order of this code. We would rather return the response interface
// then something that was collected in the buffer... There are things internally that just dump to the screen which
// we really don't want to just spit out to the screen such as prepared statement error failures.
// 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);
}