[ Index ]

PHP Cross Reference of Phabricator

title

Body

[close]

/src/applications/conduit/controller/ -> PhabricatorConduitAPIController.php (source)

   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&params[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  }


Generated: Sun Nov 30 09:20:46 2014 Cross-referenced by PHPXref 0.7.1