diff --git a/src/applications/conduit/call/ConduitCall.php b/src/applications/conduit/call/ConduitCall.php index b9e5a1491a..0b819bd093 100644 --- a/src/applications/conduit/call/ConduitCall.php +++ b/src/applications/conduit/call/ConduitCall.php @@ -1,187 +1,194 @@ 'value')); * $call->setUser($user); * $result = $call->execute(); * */ final class ConduitCall { private $method; private $request; private $user; private $servers; private $forceLocal; public function __construct($method, array $params) { $this->method = $method; $this->handler = $this->buildMethodHandler($method); $this->servers = PhabricatorEnv::getEnvConfig('conduit.servers'); $this->forceLocal = false; $invalid_params = array_diff_key( $params, $this->handler->defineParamTypes()); if ($invalid_params) { throw new ConduitException( "Method '{$method}' doesn't define these parameters: '" . implode("', '", array_keys($invalid_params)) . "'."); } if ($this->servers) { $current_host = AphrontRequest::getHTTPHeader('HOST'); foreach ($this->servers as $server) { if ($current_host === id(new PhutilURI($server))->getDomain()) { $this->forceLocal = true; break; } } } $this->request = new ConduitAPIRequest($params); } public function setUser(PhabricatorUser $user) { $this->user = $user; return $this; } public function getUser() { return $this->user; } public function setForceLocal($force_local) { $this->forceLocal = $force_local; return $this; } public function shouldForceLocal() { return $this->forceLocal; } public function shouldRequireAuthentication() { return $this->handler->shouldRequireAuthentication(); } public function shouldAllowUnguardedWrites() { return $this->handler->shouldAllowUnguardedWrites(); } public function getRequiredScope() { return $this->handler->getRequiredScope(); } public function getErrorDescription($code) { return $this->handler->getErrorDescription($code); } public function execute() { $user = $this->getUser(); if (!$user) { $user = new PhabricatorUser(); } $this->request->setUser($user); - if ($this->shouldRequireAuthentication()) { - // TODO: As per below, this should get centralized and cleaned up. - if (!$user->isLoggedIn() && !$user->isOmnipotent()) { - throw new ConduitException("ERR-INVALID-AUTH"); + if (!$this->shouldRequireAuthentication()) { + // No auth requirement here. + } else { + + $allow_public = $this->handler->shouldAllowPublic() && + PhabricatorEnv::getEnvConfig('policy.allow-public'); + if (!$allow_public) { + if (!$user->isLoggedIn() && !$user->isOmnipotent()) { + // TODO: As per below, this should get centralized and cleaned up. + throw new ConduitException("ERR-INVALID-AUTH"); + } } // TODO: This would be slightly cleaner by just using a Query, but the // Conduit auth workflow requires the Call and User be built separately. // Just do it this way for the moment. $application = $this->handler->getApplication(); if ($application) { $can_view = PhabricatorPolicyFilter::hasCapability( $user, $application, PhabricatorPolicyCapability::CAN_VIEW); if (!$can_view) { throw new ConduitException( pht( "You do not have access to the application which provides this ". "API method.")); } } } if (!$this->shouldForceLocal() && $this->servers) { $server = $this->pickRandomServer($this->servers); $client = new ConduitClient($server); $params = $this->request->getAllParameters(); $params["__conduit__"]["isProxied"] = true; if ($this->handler->shouldRequireAuthentication()) { $client->callMethodSynchronous( 'conduit.connect', array( 'client' => 'PhabricatorConduit', 'clientVersion' => '1.0', 'user' => $this->getUser()->getUserName(), 'certificate' => $this->getUser()->getConduitCertificate(), '__conduit__' => $params["__conduit__"], )); } return $client->callMethodSynchronous( $this->method, $params); } else { return $this->handler->executeMethod($this->request); } } protected function pickRandomServer($servers) { return $servers[array_rand($servers)]; } protected function buildMethodHandler($method) { $method_class = ConduitAPIMethod::getClassNameFromAPIMethodName($method); // Test if the method exists. $ok = false; try { $ok = class_exists($method_class); } catch (Exception $ex) { // Discard, we provide a more specific exception below. } if (!$ok) { throw new ConduitException( "Conduit method '{$method}' does not exist."); } $class_info = new ReflectionClass($method_class); if ($class_info->isAbstract()) { throw new ConduitException( "Method '{$method}' is not valid; the implementation is an abstract ". "base class."); } $method = newv($method_class, array()); if (!($method instanceof ConduitAPIMethod)) { throw new ConduitException( "Method '{$method_class}' is not valid; the implementation must be ". "a subclass of ConduitAPIMethod."); } $application = $method->getApplication(); if ($application && !$application->isInstalled()) { $app_name = $application->getName(); throw new ConduitException( "Method '{$method_class}' belongs to application '{$app_name}', ". "which is not installed."); } return $method; } } diff --git a/src/applications/conduit/method/ConduitAPIMethod.php b/src/applications/conduit/method/ConduitAPIMethod.php index 3a9f389b5f..e549f5755b 100644 --- a/src/applications/conduit/method/ConduitAPIMethod.php +++ b/src/applications/conduit/method/ConduitAPIMethod.php @@ -1,204 +1,208 @@ getAPIMethodName(); } /** * Get the status for this method (e.g., stable, unstable or deprecated). * Should return a METHOD_STATUS_* constant. By default, methods are * "stable". * * @return const METHOD_STATUS_* constant. * @task status */ public function getMethodStatus() { return self::METHOD_STATUS_STABLE; } /** * Optional description to supplement the method status. In particular, if * a method is deprecated, you can return a string here describing the reason * for deprecation and stable alternatives. * * @return string|null Description of the method status, if available. * @task status */ public function getMethodStatusDescription() { return null; } public function getErrorDescription($error_code) { return idx($this->defineErrorTypes(), $error_code, 'Unknown Error'); } public function getRequiredScope() { // by default, conduit methods are not accessible via OAuth return PhabricatorOAuthServerScope::SCOPE_NOT_ACCESSIBLE; } public function executeMethod(ConduitAPIRequest $request) { return $this->execute($request); } public function getAPIMethodName() { return self::getAPIMethodNameFromClassName(get_class($this)); } /** * Return a key which sorts methods by application name, then method status, * then method name. */ public function getSortOrder() { $name = $this->getAPIMethodName(); $map = array( ConduitAPIMethod::METHOD_STATUS_STABLE => 0, ConduitAPIMethod::METHOD_STATUS_UNSTABLE => 1, ConduitAPIMethod::METHOD_STATUS_DEPRECATED => 2, ); $ord = idx($map, $this->getMethodStatus(), 0); list($head, $tail) = explode('.', $name, 2); return "{$head}.{$ord}.{$tail}"; } public function getApplicationName() { return head(explode('.', $this->getAPIMethodName(), 2)); } public static function getClassNameFromAPIMethodName($method_name) { $method_fragment = str_replace('.', '_', $method_name); return 'ConduitAPI_'.$method_fragment.'_Method'; } public function shouldRequireAuthentication() { return true; } + public function shouldAllowPublic() { + return false; + } + public function shouldAllowUnguardedWrites() { return false; } /** * Optionally, return a @{class:PhabricatorApplication} which this call is * part of. The call will be disabled when the application is uninstalled. * * @return PhabricatorApplication|null Related application. */ public function getApplication() { return null; } public static function getAPIMethodNameFromClassName($class_name) { $match = null; $is_valid = preg_match( '/^ConduitAPI_(.*)_Method$/', $class_name, $match); if (!$is_valid) { throw new Exception( "Parameter '{$class_name}' is not a valid Conduit API method class."); } $method_fragment = $match[1]; return str_replace('_', '.', $method_fragment); } protected function validateHost($host) { if (!$host) { // If the client doesn't send a host key, don't complain. We should in // the future, but this change isn't severe enough to bump the protocol // version. // TODO: Remove this once the protocol version gets bumped past 2 (i.e., // require the host key be present and valid). return; } // NOTE: Compare domains only so we aren't sensitive to port specification // or omission. $host = new PhutilURI($host); $host = $host->getDomain(); $self = new PhutilURI(PhabricatorEnv::getURI('/')); $self = $self->getDomain(); if ($self !== $host) { throw new Exception( "Your client is connecting to this install as '{$host}', but it is ". "configured as '{$self}'. The client and server must use the exact ". "same URI to identify the install. Edit your .arcconfig or ". "phabricator/conf so they agree on the URI for the install."); } } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getPHID() { return null; } public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, ); } public function getPolicy($capability) { // Application methods get application visibility; other methods get open // visibility. $application = $this->getApplication(); if ($application) { return $application->getPolicy($capability); } return PhabricatorPolicies::getMostOpenPolicy(); } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { if (!$this->shouldRequireAuthentication()) { // Make unauthenticated methods univerally visible. return true; } return false; } public function describeAutomaticCapability($capability) { return null; } } diff --git a/src/applications/diffusion/conduit/ConduitAPI_diffusion_abstractquery_Method.php b/src/applications/diffusion/conduit/ConduitAPI_diffusion_abstractquery_Method.php index 3b84520d86..c4e48779b7 100644 --- a/src/applications/diffusion/conduit/ConduitAPI_diffusion_abstractquery_Method.php +++ b/src/applications/diffusion/conduit/ConduitAPI_diffusion_abstractquery_Method.php @@ -1,174 +1,179 @@ diffusionRequest = $request; return $this; } protected function getDiffusionRequest() { return $this->diffusionRequest; } /** * A wee bit of magic here. If @{method:shouldCreateDiffusionRequest} * returns false, this function grabs a repository object based on the * callsign directly. Otherwise, the repository was loaded when we created a * @{class:DiffusionRequest}, so this function just pulls it out of the * @{class:DiffusionRequest}. * * @return @{class:PhabricatorRepository} $repository */ protected function getRepository(ConduitAPIRequest $request) { if (!$this->repository) { if ($this->shouldCreateDiffusionRequest()) { $this->repository = $this->getDiffusionRequest()->getRepository(); } else { $callsign = $request->getValue('callsign'); $repository = id(new PhabricatorRepositoryQuery()) ->setViewer($request->getUser()) ->withCallsigns(array($callsign)) ->executeOne(); if (!$repository) { throw new ConduitException('ERR-UNKNOWN-REPOSITORY'); } $this->repository = $repository; } } return $this->repository; } /** * You should probably not mess with this unless your conduit method is * involved with the creation / validation / etc. of * @{class:DiffusionRequest}s. If you are dealing with * @{class:DiffusionRequest}, setting this to false should help avoid * infinite loops. */ protected function setShouldCreateDiffusionRequest($should) { $this->shouldCreateDiffusionRequest = $should; return $this; } private function shouldCreateDiffusionRequest() { return $this->shouldCreateDiffusionRequest; } final public function defineErrorTypes() { return $this->defineCustomErrorTypes() + array( 'ERR-UNKNOWN-REPOSITORY' => pht('There is no repository with that callsign.'), 'ERR-UNKNOWN-VCS-TYPE' => pht('Unknown repository VCS type.'), 'ERR-UNSUPPORTED-VCS' => pht('VCS is not supported for this method.')); } /** * Subclasses should override this to specify custom error types. */ protected function defineCustomErrorTypes() { return array(); } final public function defineParamTypes() { return $this->defineCustomParamTypes() + array( 'callsign' => 'required string', 'branch' => 'optional string', ); } /** * Subclasses should override this to specify custom param types. */ protected function defineCustomParamTypes() { return array(); } /** * Subclasses should override these methods with the proper result for the * pertinent version control system, e.g. getGitResult for Git. * * If the result is not supported for that VCS, do not implement it. e.g. * Subversion (SVN) does not support branches. */ protected function getGitResult(ConduitAPIRequest $request) { throw new ConduitException('ERR-UNSUPPORTED-VCS'); } protected function getSVNResult(ConduitAPIRequest $request) { throw new ConduitException('ERR-UNSUPPORTED-VCS'); } protected function getMercurialResult(ConduitAPIRequest $request) { throw new ConduitException('ERR-UNSUPPORTED-VCS'); } /** * This method is final because most queries will need to construct a * @{class:DiffusionRequest} and use it. Consolidating this codepath and * enforcing @{method:getDiffusionRequest} works when we need it is good. * * @{method:getResult} should be overridden by subclasses as necessary, e.g. * there is a common operation across all version control systems that * should occur after @{method:getResult}, like formatting a timestamp. * * In the rare cases where one does not want to create a * @{class:DiffusionRequest} - suppose to avoid infinite loops in the * creation of a @{class:DiffusionRequest} - make sure to call * * $this->setShouldCreateDiffusionRequest(false); * * in the constructor of the pertinent Conduit method. */ final protected function execute(ConduitAPIRequest $request) { if ($this->shouldCreateDiffusionRequest()) { $drequest = DiffusionRequest::newFromDictionary( array( 'user' => $request->getUser(), 'callsign' => $request->getValue('callsign'), 'branch' => $request->getValue('branch'), 'path' => $request->getValue('path'), 'commit' => $request->getValue('commit'), )); $this->setDiffusionRequest($drequest); } return $this->getResult($request); } protected function getResult(ConduitAPIRequest $request) { $repository = $this->getRepository($request); $result = null; switch ($repository->getVersionControlSystem()) { case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: $result = $this->getGitResult($request); break; case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: $result = $this->getMercurialResult($request); break; case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: $result = $this->getSVNResult($request); break; default: throw new ConduitException('ERR-UNKNOWN-VCS-TYPE'); break; } return $result; } }