[ Index ] |
PHP Cross Reference of Phabricator |
[Summary view] [Print] [Text view]
1 <?php 2 3 final class PhabricatorConduitAPIController 4 extends PhabricatorConduitController { 5 6 public function shouldRequireLogin() { 7 return false; 8 } 9 10 private $method; 11 12 public function willProcessRequest(array $data) { 13 $this->method = $data['method']; 14 return $this; 15 } 16 17 public function processRequest() { 18 $time_start = microtime(true); 19 $request = $this->getRequest(); 20 21 $method = $this->method; 22 23 $api_request = null; 24 25 $log = new PhabricatorConduitMethodCallLog(); 26 $log->setMethod($method); 27 $metadata = array(); 28 29 try { 30 31 $params = $this->decodeConduitParams($request, $method); 32 $metadata = idx($params, '__conduit__', array()); 33 unset($params['__conduit__']); 34 35 $call = new ConduitCall( 36 $method, $params, idx($metadata, 'isProxied', false)); 37 38 $result = null; 39 40 // TODO: Straighten out the auth pathway here. We shouldn't be creating 41 // a ConduitAPIRequest at this level, but some of the auth code expects 42 // it. Landing a halfway version of this to unblock T945. 43 44 $api_request = new ConduitAPIRequest($params); 45 46 $allow_unguarded_writes = false; 47 $auth_error = null; 48 $conduit_username = '-'; 49 if ($call->shouldRequireAuthentication()) { 50 $metadata['scope'] = $call->getRequiredScope(); 51 $auth_error = $this->authenticateUser($api_request, $metadata); 52 // If we've explicitly authenticated the user here and either done 53 // CSRF validation or are using a non-web authentication mechanism. 54 $allow_unguarded_writes = true; 55 56 if (isset($metadata['actAsUser'])) { 57 $this->actAsUser($api_request, $metadata['actAsUser']); 58 } 59 60 if ($auth_error === null) { 61 $conduit_user = $api_request->getUser(); 62 if ($conduit_user && $conduit_user->getPHID()) { 63 $conduit_username = $conduit_user->getUsername(); 64 } 65 $call->setUser($api_request->getUser()); 66 } 67 } 68 69 $access_log = PhabricatorAccessLog::getLog(); 70 if ($access_log) { 71 $access_log->setData( 72 array( 73 'u' => $conduit_username, 74 'm' => $method, 75 )); 76 } 77 78 if ($call->shouldAllowUnguardedWrites()) { 79 $allow_unguarded_writes = true; 80 } 81 82 if ($auth_error === null) { 83 if ($allow_unguarded_writes) { 84 $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); 85 } 86 87 try { 88 $result = $call->execute(); 89 $error_code = null; 90 $error_info = null; 91 } catch (ConduitException $ex) { 92 $result = null; 93 $error_code = $ex->getMessage(); 94 if ($ex->getErrorDescription()) { 95 $error_info = $ex->getErrorDescription(); 96 } else { 97 $error_info = $call->getErrorDescription($error_code); 98 } 99 } 100 if ($allow_unguarded_writes) { 101 unset($unguarded); 102 } 103 } else { 104 list($error_code, $error_info) = $auth_error; 105 } 106 } catch (Exception $ex) { 107 if (!($ex instanceof ConduitMethodNotFoundException)) { 108 phlog($ex); 109 } 110 $result = null; 111 $error_code = ($ex instanceof ConduitException 112 ? 'ERR-CONDUIT-CALL' 113 : 'ERR-CONDUIT-CORE'); 114 $error_info = $ex->getMessage(); 115 } 116 117 $time_end = microtime(true); 118 119 $connection_id = null; 120 if (idx($metadata, 'connectionID')) { 121 $connection_id = $metadata['connectionID']; 122 } else if (($method == 'conduit.connect') && $result) { 123 $connection_id = idx($result, 'connectionID'); 124 } 125 126 $log 127 ->setCallerPHID( 128 isset($conduit_user) 129 ? $conduit_user->getPHID() 130 : null) 131 ->setConnectionID($connection_id) 132 ->setError((string)$error_code) 133 ->setDuration(1000000 * ($time_end - $time_start)); 134 135 $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); 136 $log->save(); 137 unset($unguarded); 138 139 $response = id(new ConduitAPIResponse()) 140 ->setResult($result) 141 ->setErrorCode($error_code) 142 ->setErrorInfo($error_info); 143 144 switch ($request->getStr('output')) { 145 case 'human': 146 return $this->buildHumanReadableResponse( 147 $method, 148 $api_request, 149 $response->toDictionary()); 150 case 'json': 151 default: 152 return id(new AphrontJSONResponse()) 153 ->setAddJSONShield(false) 154 ->setContent($response->toDictionary()); 155 } 156 } 157 158 /** 159 * Change the api request user to the user that we want to act as. 160 * Only admins can use actAsUser 161 * 162 * @param ConduitAPIRequest Request being executed. 163 * @param string The username of the user we want to act as 164 */ 165 private function actAsUser( 166 ConduitAPIRequest $api_request, 167 $user_name) { 168 169 $config_key = 'security.allow-conduit-act-as-user'; 170 if (!PhabricatorEnv::getEnvConfig($config_key)) { 171 throw new Exception('security.allow-conduit-act-as-user is disabled'); 172 } 173 174 if (!$api_request->getUser()->getIsAdmin()) { 175 throw new Exception('Only administrators can use actAsUser'); 176 } 177 178 $user = id(new PhabricatorUser())->loadOneWhere( 179 'userName = %s', 180 $user_name); 181 182 if (!$user) { 183 throw new Exception( 184 "The actAsUser username '{$user_name}' is not a valid user." 185 ); 186 } 187 188 $api_request->setUser($user); 189 } 190 191 /** 192 * Authenticate the client making the request to a Phabricator user account. 193 * 194 * @param ConduitAPIRequest Request being executed. 195 * @param dict Request metadata. 196 * @return null|pair Null to indicate successful authentication, or 197 * an error code and error message pair. 198 */ 199 private function authenticateUser( 200 ConduitAPIRequest $api_request, 201 array $metadata) { 202 203 $request = $this->getRequest(); 204 205 if ($request->getUser()->getPHID()) { 206 $request->validateCSRF(); 207 return $this->validateAuthenticatedUser( 208 $api_request, 209 $request->getUser()); 210 } 211 212 $auth_type = idx($metadata, 'auth.type'); 213 if ($auth_type === ConduitClient::AUTH_ASYMMETRIC) { 214 $host = idx($metadata, 'auth.host'); 215 if (!$host) { 216 return array( 217 'ERR-INVALID-AUTH', 218 pht( 219 'Request is missing required "auth.host" parameter.'), 220 ); 221 } 222 223 // TODO: Validate that we are the host! 224 225 $raw_key = idx($metadata, 'auth.key'); 226 $public_key = PhabricatorAuthSSHPublicKey::newFromRawKey($raw_key); 227 $ssl_public_key = $public_key->toPKCS8(); 228 229 // First, verify the signature. 230 try { 231 $protocol_data = $metadata; 232 233 // TODO: We should stop writing this into the protocol data when 234 // processing a request. 235 unset($protocol_data['scope']); 236 237 ConduitClient::verifySignature( 238 $this->method, 239 $api_request->getAllParameters(), 240 $protocol_data, 241 $ssl_public_key); 242 } catch (Exception $ex) { 243 return array( 244 'ERR-INVALID-AUTH', 245 pht( 246 'Signature verification failure. %s', 247 $ex->getMessage()), 248 ); 249 } 250 251 // If the signature is valid, find the user or device which is 252 // associated with this public key. 253 254 $stored_key = id(new PhabricatorAuthSSHKeyQuery()) 255 ->setViewer(PhabricatorUser::getOmnipotentUser()) 256 ->withKeys(array($public_key)) 257 ->executeOne(); 258 if (!$stored_key) { 259 return array( 260 'ERR-INVALID-AUTH', 261 pht( 262 'No user or device is associated with that public key.'), 263 ); 264 } 265 266 $object = $stored_key->getObject(); 267 268 if ($object instanceof PhabricatorUser) { 269 $user = $object; 270 } else { 271 if (!$stored_key->getIsTrusted()) { 272 return array( 273 'ERR-INVALID-AUTH', 274 pht( 275 'The key which signed this request is not trusted. Only '. 276 'trusted keys can be used to sign API calls.'), 277 ); 278 } 279 280 throw new Exception( 281 pht('Not Implemented: Would authenticate Almanac device.')); 282 } 283 284 return $this->validateAuthenticatedUser( 285 $api_request, 286 $user); 287 } else if ($auth_type === null) { 288 // No specified authentication type, continue with other authentication 289 // methods below. 290 } else { 291 return array( 292 'ERR-INVALID-AUTH', 293 pht( 294 'Provided "auth.type" ("%s") is not recognized.', 295 $auth_type), 296 ); 297 } 298 299 // handle oauth 300 $access_token = $request->getStr('access_token'); 301 $method_scope = $metadata['scope']; 302 if ($access_token && 303 $method_scope != PhabricatorOAuthServerScope::SCOPE_NOT_ACCESSIBLE) { 304 $token = id(new PhabricatorOAuthServerAccessToken()) 305 ->loadOneWhere('token = %s', 306 $access_token); 307 if (!$token) { 308 return array( 309 'ERR-INVALID-AUTH', 310 'Access token does not exist.', 311 ); 312 } 313 314 $oauth_server = new PhabricatorOAuthServer(); 315 $valid = $oauth_server->validateAccessToken($token, 316 $method_scope); 317 if (!$valid) { 318 return array( 319 'ERR-INVALID-AUTH', 320 'Access token is invalid.', 321 ); 322 } 323 324 // valid token, so let's log in the user! 325 $user_phid = $token->getUserPHID(); 326 $user = id(new PhabricatorUser()) 327 ->loadOneWhere('phid = %s', 328 $user_phid); 329 if (!$user) { 330 return array( 331 'ERR-INVALID-AUTH', 332 'Access token is for invalid user.', 333 ); 334 } 335 return $this->validateAuthenticatedUser( 336 $api_request, 337 $user); 338 } 339 340 // Handle sessionless auth. TOOD: This is super messy. 341 if (isset($metadata['authUser'])) { 342 $user = id(new PhabricatorUser())->loadOneWhere( 343 'userName = %s', 344 $metadata['authUser']); 345 if (!$user) { 346 return array( 347 'ERR-INVALID-AUTH', 348 'Authentication is invalid.', 349 ); 350 } 351 $token = idx($metadata, 'authToken'); 352 $signature = idx($metadata, 'authSignature'); 353 $certificate = $user->getConduitCertificate(); 354 if (sha1($token.$certificate) !== $signature) { 355 return array( 356 'ERR-INVALID-AUTH', 357 'Authentication is invalid.', 358 ); 359 } 360 return $this->validateAuthenticatedUser( 361 $api_request, 362 $user); 363 } 364 365 $session_key = idx($metadata, 'sessionKey'); 366 if (!$session_key) { 367 return array( 368 'ERR-INVALID-SESSION', 369 'Session key is not present.', 370 ); 371 } 372 373 $user = id(new PhabricatorAuthSessionEngine()) 374 ->loadUserForSession(PhabricatorAuthSession::TYPE_CONDUIT, $session_key); 375 376 if (!$user) { 377 return array( 378 'ERR-INVALID-SESSION', 379 'Session key is invalid.', 380 ); 381 } 382 383 return $this->validateAuthenticatedUser( 384 $api_request, 385 $user); 386 } 387 388 private function validateAuthenticatedUser( 389 ConduitAPIRequest $request, 390 PhabricatorUser $user) { 391 392 if (!$user->isUserActivated()) { 393 return array( 394 'ERR-USER-DISABLED', 395 pht('User account is not activated.'), 396 ); 397 } 398 399 $request->setUser($user); 400 return null; 401 } 402 403 private function buildHumanReadableResponse( 404 $method, 405 ConduitAPIRequest $request = null, 406 $result = null) { 407 408 $param_rows = array(); 409 $param_rows[] = array('Method', $this->renderAPIValue($method)); 410 if ($request) { 411 foreach ($request->getAllParameters() as $key => $value) { 412 $param_rows[] = array( 413 $key, 414 $this->renderAPIValue($value), 415 ); 416 } 417 } 418 419 $param_table = new AphrontTableView($param_rows); 420 $param_table->setDeviceReadyTable(true); 421 $param_table->setColumnClasses( 422 array( 423 'header', 424 'wide', 425 )); 426 427 $result_rows = array(); 428 foreach ($result as $key => $value) { 429 $result_rows[] = array( 430 $key, 431 $this->renderAPIValue($value), 432 ); 433 } 434 435 $result_table = new AphrontTableView($result_rows); 436 $result_table->setDeviceReadyTable(true); 437 $result_table->setColumnClasses( 438 array( 439 'header', 440 'wide', 441 )); 442 443 $param_panel = new AphrontPanelView(); 444 $param_panel->setHeader('Method Parameters'); 445 $param_panel->appendChild($param_table); 446 447 $result_panel = new AphrontPanelView(); 448 $result_panel->setHeader('Method Result'); 449 $result_panel->appendChild($result_table); 450 451 $param_head = id(new PHUIHeaderView()) 452 ->setHeader(pht('Method Parameters')); 453 454 $result_head = id(new PHUIHeaderView()) 455 ->setHeader(pht('Method Result')); 456 457 $method_uri = $this->getApplicationURI('method/'.$method.'/'); 458 459 $crumbs = $this->buildApplicationCrumbs() 460 ->addTextCrumb($method, $method_uri) 461 ->addTextCrumb(pht('Call')); 462 463 return $this->buildApplicationPage( 464 array( 465 $crumbs, 466 $param_head, 467 $param_table, 468 $result_head, 469 $result_table, 470 ), 471 array( 472 'title' => 'Method Call Result', 473 )); 474 } 475 476 private function renderAPIValue($value) { 477 $json = new PhutilJSON(); 478 if (is_array($value)) { 479 $value = $json->encodeFormatted($value); 480 } 481 482 $value = phutil_tag( 483 'pre', 484 array('style' => 'white-space: pre-wrap;'), 485 $value); 486 487 return $value; 488 } 489 490 private function decodeConduitParams( 491 AphrontRequest $request, 492 $method) { 493 494 // Look for parameters from the Conduit API Console, which are encoded 495 // as HTTP POST parameters in an array, e.g.: 496 // 497 // params[name]=value¶ms[name2]=value2 498 // 499 // The fields are individually JSON encoded, since we require users to 500 // enter JSON so that we avoid type ambiguity. 501 502 $params = $request->getArr('params', null); 503 if ($params !== null) { 504 foreach ($params as $key => $value) { 505 if ($value == '') { 506 // Interpret empty string null (e.g., the user didn't type anything 507 // into the box). 508 $value = 'null'; 509 } 510 $decoded_value = json_decode($value, true); 511 if ($decoded_value === null && strtolower($value) != 'null') { 512 // When json_decode() fails, it returns null. This almost certainly 513 // indicates that a user was using the web UI and didn't put quotes 514 // around a string value. We can either do what we think they meant 515 // (treat it as a string) or fail. For now, err on the side of 516 // caution and fail. In the future, if we make the Conduit API 517 // actually do type checking, it might be reasonable to treat it as 518 // a string if the parameter type is string. 519 throw new Exception( 520 "The value for parameter '{$key}' is not valid JSON. All ". 521 "parameters must be encoded as JSON values, including strings ". 522 "(which means you need to surround them in double quotes). ". 523 "Check your syntax. Value was: {$value}"); 524 } 525 $params[$key] = $decoded_value; 526 } 527 528 return $params; 529 } 530 531 // Otherwise, look for a single parameter called 'params' which has the 532 // entire param dictionary JSON encoded. This is the usual case for remote 533 // requests. 534 535 $params_json = $request->getStr('params'); 536 if (!strlen($params_json)) { 537 if ($request->getBool('allowEmptyParams')) { 538 // TODO: This is a bit messy, but otherwise you can't call 539 // "conduit.ping" from the web console. 540 $params = array(); 541 } else { 542 throw new Exception( 543 "Request has no 'params' key. This may mean that an extension like ". 544 "Suhosin has dropped data from the request. Check the PHP ". 545 "configuration on your server. If you are developing a Conduit ". 546 "client, you MUST provide a 'params' parameter when making a ". 547 "Conduit request, even if the value is empty (e.g., provide '{}')."); 548 } 549 } else { 550 $params = json_decode($params_json, true); 551 if (!is_array($params)) { 552 throw new Exception( 553 "Invalid parameter information was passed to method ". 554 "'{$method}', could not decode JSON serialization. Data: ". 555 $params_json); 556 } 557 } 558 559 return $params; 560 } 561 562 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Sun Nov 30 09:20:46 2014 | Cross-referenced by PHPXref 0.7.1 |