diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -199,6 +199,7 @@ 'ConduitConnectConduitAPIMethod' => 'applications/conduit/method/ConduitConnectConduitAPIMethod.php', 'ConduitConnectionGarbageCollector' => 'applications/conduit/garbagecollector/ConduitConnectionGarbageCollector.php', 'ConduitException' => 'applications/conduit/protocol/exception/ConduitException.php', + 'ConduitGetCapabilitiesConduitAPIMethod' => 'applications/conduit/method/ConduitGetCapabilitiesConduitAPIMethod.php', 'ConduitGetCertificateConduitAPIMethod' => 'applications/conduit/method/ConduitGetCertificateConduitAPIMethod.php', 'ConduitLogGarbageCollector' => 'applications/conduit/garbagecollector/ConduitLogGarbageCollector.php', 'ConduitMethodDoesNotExistException' => 'applications/conduit/protocol/exception/ConduitMethodDoesNotExistException.php', @@ -1434,6 +1435,7 @@ 'PhabricatorConduitToken' => 'applications/conduit/storage/PhabricatorConduitToken.php', 'PhabricatorConduitTokenController' => 'applications/conduit/controller/PhabricatorConduitTokenController.php', 'PhabricatorConduitTokenEditController' => 'applications/conduit/controller/PhabricatorConduitTokenEditController.php', + 'PhabricatorConduitTokenHandshakeController' => 'applications/conduit/controller/PhabricatorConduitTokenHandshakeController.php', 'PhabricatorConduitTokenQuery' => 'applications/conduit/query/PhabricatorConduitTokenQuery.php', 'PhabricatorConduitTokenTerminateController' => 'applications/conduit/controller/PhabricatorConduitTokenTerminateController.php', 'PhabricatorConfigAllController' => 'applications/config/controller/PhabricatorConfigAllController.php', @@ -3212,6 +3214,7 @@ 'ConduitConnectConduitAPIMethod' => 'ConduitAPIMethod', 'ConduitConnectionGarbageCollector' => 'PhabricatorGarbageCollector', 'ConduitException' => 'Exception', + 'ConduitGetCapabilitiesConduitAPIMethod' => 'ConduitAPIMethod', 'ConduitGetCertificateConduitAPIMethod' => 'ConduitAPIMethod', 'ConduitLogGarbageCollector' => 'PhabricatorGarbageCollector', 'ConduitMethodDoesNotExistException' => 'ConduitMethodNotFoundException', @@ -4557,6 +4560,7 @@ ), 'PhabricatorConduitTokenController' => 'PhabricatorConduitController', 'PhabricatorConduitTokenEditController' => 'PhabricatorConduitController', + 'PhabricatorConduitTokenHandshakeController' => 'PhabricatorConduitController', 'PhabricatorConduitTokenQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorConduitTokenTerminateController' => 'PhabricatorConduitController', 'PhabricatorConfigAllController' => 'PhabricatorConfigController', diff --git a/src/applications/conduit/application/PhabricatorConduitApplication.php b/src/applications/conduit/application/PhabricatorConduitApplication.php --- a/src/applications/conduit/application/PhabricatorConduitApplication.php +++ b/src/applications/conduit/application/PhabricatorConduitApplication.php @@ -50,6 +50,7 @@ 'PhabricatorConduitTokenEditController', 'token/terminate/(?:(?P\d+)/)?' => 'PhabricatorConduitTokenTerminateController', + 'login/' => 'PhabricatorConduitTokenHandshakeController', ), '/api/(?P[^/]+)' => 'PhabricatorConduitAPIController', ); diff --git a/src/applications/conduit/controller/PhabricatorConduitAPIController.php b/src/applications/conduit/controller/PhabricatorConduitAPIController.php --- a/src/applications/conduit/controller/PhabricatorConduitAPIController.php +++ b/src/applications/conduit/controller/PhabricatorConduitAPIController.php @@ -301,22 +301,22 @@ 'ERR-INVALID-AUTH', pht( 'API token "%s" has the wrong length. API tokens should be '. - '32 characters long.'), + '32 characters long.', + $token_string), ); } $type = head(explode('-', $token_string)); - switch ($type) { - case 'api': - case 'tmp': - break; - default: - return array( - 'ERR-INVALID-AUTH', - pht( - 'API token "%s" has the wrong format. API tokens should begin '. - 'with "api-" or "tmp-" and be 32 characters long.', - $token_string), + $valid_types = PhabricatorConduitToken::getAllTokenTypes(); + $valid_types = array_fuse($valid_types); + if (empty($valid_types[$type])) { + return array( + 'ERR-INVALID-AUTH', + pht( + 'API token "%s" has the wrong format. API tokens should be '. + '32 characters long and begin with one of these prefixes: %s.', + $token_string, + implode(', ', $valid_types)), ); } @@ -348,6 +348,19 @@ } } + // If this is a "cli-" token, it expires shortly after it is generated + // by default. Once it is actually used, we extend its lifetime and make + // it permanent. This allows stray tokens to get cleaned up automatically + // if they aren't being used. + if ($token->getTokenType() == PhabricatorConduitToken::TYPE_COMMANDLINE) { + if ($token->getExpires()) { + $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); + $token->setExpires(null); + $token->save(); + unset($unguarded); + } + } + $user = $token->getObject(); if (!($user instanceof PhabricatorUser)) { return array( diff --git a/src/applications/conduit/controller/PhabricatorConduitTokenHandshakeController.php b/src/applications/conduit/controller/PhabricatorConduitTokenHandshakeController.php new file mode 100644 --- /dev/null +++ b/src/applications/conduit/controller/PhabricatorConduitTokenHandshakeController.php @@ -0,0 +1,46 @@ +getViewer(); + + id(new PhabricatorAuthSessionEngine())->requireHighSecuritySession( + $viewer, + $request, + '/'); + + $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); + $token = PhabricatorConduitToken::initializeNewToken( + $viewer->getPHID(), + PhabricatorConduitToken::TYPE_COMMANDLINE); + $token->save(); + unset($unguarded); + + $form = id(new AphrontFormView()) + ->setUser($viewer) + ->appendRemarkupInstructions( + pht( + 'Copy-paste the API Token below to grant access to your account.')) + ->appendChild( + id(new AphrontFormTextControl()) + ->setLabel(pht('API Token')) + ->setValue($token->getToken())) + ->appendRemarkupInstructions( + pht( + 'This will authorize the requesting script to act on your behalf '. + 'permanently, like giving the script your account password.')) + ->appendRemarkupInstructions( + pht( + 'If you change your mind, you can revoke this token later in '. + '{nav icon=wrench,name=Settings > Conduit API Tokens}.')); + + return $this->newDialog() + ->setTitle(pht('Grant Account Access')) + ->setWidth(AphrontDialogView::WIDTH_FULL) + ->appendForm($form) + ->addCancelButton('/'); + } + +} diff --git a/src/applications/conduit/method/ConduitGetCapabilitiesConduitAPIMethod.php b/src/applications/conduit/method/ConduitGetCapabilitiesConduitAPIMethod.php new file mode 100644 --- /dev/null +++ b/src/applications/conduit/method/ConduitGetCapabilitiesConduitAPIMethod.php @@ -0,0 +1,60 @@ +'; + } + + public function defineErrorTypes() { + return array(); + } + + protected function execute(ConduitAPIRequest $request) { + $authentication = array( + 'token', + 'asymmetric', + 'session', + 'sessionless', + ); + + $oauth_app = 'PhabricatorOAuthServerApplication'; + if (PhabricatorApplication::isClassInstalled($oauth_app)) { + $authentication[] = 'oauth'; + } + + return array( + 'authentication' => $authentication, + 'signatures' => array( + 'consign', + ), + 'input' => array( + 'json', + 'urlencoded', + ), + 'output' => array( + 'json', + 'human', + ), + ); + } + +} diff --git a/src/applications/conduit/storage/PhabricatorConduitToken.php b/src/applications/conduit/storage/PhabricatorConduitToken.php --- a/src/applications/conduit/storage/PhabricatorConduitToken.php +++ b/src/applications/conduit/storage/PhabricatorConduitToken.php @@ -13,6 +13,7 @@ const TYPE_STANDARD = 'api'; const TYPE_TEMPORARY = 'tmp'; + const TYPE_COMMANDLINE = 'cli'; public function getConfiguration() { return array( @@ -53,17 +54,28 @@ $map = array( self::TYPE_STANDARD => pht('Standard API Token'), self::TYPE_TEMPORARY => pht('Temporary API Token'), + self::TYPE_COMMANDLINE => pht('Command Line API Token'), ); return idx($map, $type, $type); } + public static function getAllTokenTypes() { + return array( + self::TYPE_STANDARD, + self::TYPE_TEMPORARY, + self::TYPE_COMMANDLINE, + ); + } + private function getTokenExpires($token_type) { switch ($token_type) { case self::TYPE_STANDARD: return null; case self::TYPE_TEMPORARY: - return PhabricatorTime::getNow() + phutil_units('24h in seconds'); + return PhabricatorTime::getNow() + phutil_units('24 hours in seconds'); + case self::TYPE_COMMANDLINE: + return PhabricatorTime::getNow() + phutil_units('1 hour in seconds'); default: throw new Exception( pht('Unknown Conduit token type "%s"!', $token_type));