Page MenuHomePhabricator

D10985.id26395.diff
No OneTemporary

D10985.id26395.diff

diff --git a/resources/sql/autopatches/20141212.conduittoken.sql b/resources/sql/autopatches/20141212.conduittoken.sql
new file mode 100644
--- /dev/null
+++ b/resources/sql/autopatches/20141212.conduittoken.sql
@@ -0,0 +1,12 @@
+CREATE TABLE {$NAMESPACE}_conduit.conduit_token (
+ id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
+ objectPHID VARBINARY(64) NOT NULL,
+ tokenType VARCHAR(32) COLLATE {$COLLATE_TEXT} NOT NULL,
+ token VARCHAR(32) COLLATE {$COLLATE_TEXT} NOT NULL,
+ expires INT UNSIGNED,
+ dateCreated INT UNSIGNED NOT NULL,
+ dateModified INT UNSIGNED NOT NULL,
+ KEY `key_object` (objectPHID, tokenType),
+ UNIQUE KEY `key_token` (token),
+ KEY `key_expires` (expires)
+) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT};
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
@@ -206,6 +206,7 @@
'ConduitPingConduitAPIMethod' => 'applications/conduit/method/ConduitPingConduitAPIMethod.php',
'ConduitQueryConduitAPIMethod' => 'applications/conduit/method/ConduitQueryConduitAPIMethod.php',
'ConduitSSHWorkflow' => 'applications/conduit/ssh/ConduitSSHWorkflow.php',
+ 'ConduitTokenGarbageCollector' => 'applications/conduit/garbagecollector/ConduitTokenGarbageCollector.php',
'ConpherenceActionMenuEventListener' => 'applications/conpherence/events/ConpherenceActionMenuEventListener.php',
'ConpherenceConduitAPIMethod' => 'applications/conpherence/conduit/ConpherenceConduitAPIMethod.php',
'ConpherenceConfigOptions' => 'applications/conpherence/config/ConpherenceConfigOptions.php',
@@ -1428,7 +1429,12 @@
'PhabricatorConduitMethodCallLog' => 'applications/conduit/storage/PhabricatorConduitMethodCallLog.php',
'PhabricatorConduitMethodQuery' => 'applications/conduit/query/PhabricatorConduitMethodQuery.php',
'PhabricatorConduitSearchEngine' => 'applications/conduit/query/PhabricatorConduitSearchEngine.php',
+ 'PhabricatorConduitSettingsPanel' => 'applications/conduit/settings/PhabricatorConduitSettingsPanel.php',
+ 'PhabricatorConduitToken' => 'applications/conduit/storage/PhabricatorConduitToken.php',
'PhabricatorConduitTokenController' => 'applications/conduit/controller/PhabricatorConduitTokenController.php',
+ 'PhabricatorConduitTokenEditController' => 'applications/conduit/controller/PhabricatorConduitTokenEditController.php',
+ 'PhabricatorConduitTokenQuery' => 'applications/conduit/query/PhabricatorConduitTokenQuery.php',
+ 'PhabricatorConduitTokenTerminateController' => 'applications/conduit/controller/PhabricatorConduitTokenTerminateController.php',
'PhabricatorConfigAllController' => 'applications/config/controller/PhabricatorConfigAllController.php',
'PhabricatorConfigApplication' => 'applications/config/application/PhabricatorConfigApplication.php',
'PhabricatorConfigColumnSchema' => 'applications/config/schema/PhabricatorConfigColumnSchema.php',
@@ -3212,6 +3218,7 @@
'ConduitPingConduitAPIMethod' => 'ConduitAPIMethod',
'ConduitQueryConduitAPIMethod' => 'ConduitAPIMethod',
'ConduitSSHWorkflow' => 'PhabricatorSSHWorkflow',
+ 'ConduitTokenGarbageCollector' => 'PhabricatorGarbageCollector',
'ConpherenceActionMenuEventListener' => 'PhabricatorEventListener',
'ConpherenceConduitAPIMethod' => 'ConduitAPIMethod',
'ConpherenceConfigOptions' => 'PhabricatorApplicationConfigOptions',
@@ -4541,7 +4548,15 @@
),
'PhabricatorConduitMethodQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorConduitSearchEngine' => 'PhabricatorApplicationSearchEngine',
+ 'PhabricatorConduitSettingsPanel' => 'PhabricatorSettingsPanel',
+ 'PhabricatorConduitToken' => array(
+ 'PhabricatorConduitDAO',
+ 'PhabricatorPolicyInterface',
+ ),
'PhabricatorConduitTokenController' => 'PhabricatorConduitController',
+ 'PhabricatorConduitTokenEditController' => 'PhabricatorConduitController',
+ 'PhabricatorConduitTokenQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
+ 'PhabricatorConduitTokenTerminateController' => 'PhabricatorConduitController',
'PhabricatorConfigAllController' => 'PhabricatorConfigController',
'PhabricatorConfigApplication' => 'PhabricatorApplication',
'PhabricatorConfigColumnSchema' => 'PhabricatorConfigStorageSchema',
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
@@ -46,6 +46,10 @@
'log/' => 'PhabricatorConduitLogController',
'log/view/(?P<view>[^/]+)/' => 'PhabricatorConduitLogController',
'token/' => 'PhabricatorConduitTokenController',
+ 'token/edit/(?:(?P<id>\d+)/)?' =>
+ 'PhabricatorConduitTokenEditController',
+ 'token/terminate/(?:(?P<id>\d+)/)?' =>
+ 'PhabricatorConduitTokenTerminateController',
),
'/api/(?P<method>[^/]+)' => 'PhabricatorConduitAPIController',
);
diff --git a/src/applications/conduit/controller/PhabricatorConduitTokenEditController.php b/src/applications/conduit/controller/PhabricatorConduitTokenEditController.php
new file mode 100644
--- /dev/null
+++ b/src/applications/conduit/controller/PhabricatorConduitTokenEditController.php
@@ -0,0 +1,100 @@
+<?php
+
+final class PhabricatorConduitTokenEditController
+ extends PhabricatorConduitController {
+
+ public function handleRequest(AphrontRequest $request) {
+ $viewer = $request->getViewer();
+
+ $id = $request->getURIData('id');
+ if ($id) {
+ $token = id(new PhabricatorConduitTokenQuery())
+ ->setViewer($viewer)
+ ->withIDs(array($id))
+ ->withExpired(false)
+ ->requireCapabilities(
+ array(
+ PhabricatorPolicyCapability::CAN_VIEW,
+ PhabricatorPolicyCapability::CAN_EDIT,
+ ))
+ ->executeOne();
+ if (!$token) {
+ return new Aphront404Response();
+ }
+
+ $object = $token->getObject();
+
+ $is_new = false;
+ $title = pht('View API Token');
+ } else {
+ $object = id(new PhabricatorObjectQuery())
+ ->setViewer($viewer)
+ ->withPHIDs(array($request->getStr('objectPHID')))
+ ->requireCapabilities(
+ array(
+ PhabricatorPolicyCapability::CAN_VIEW,
+ PhabricatorPolicyCapability::CAN_EDIT,
+ ))
+ ->executeOne();
+ if (!$object) {
+ return new Aphront404Response();
+ }
+
+ $token = PhabricatorConduitToken::initializeNewToken(
+ $object->getPHID(),
+ PhabricatorConduitToken::TYPE_STANDARD);
+
+ $is_new = true;
+ $title = pht('Generate API Token');
+ $submit_button = pht('Generate Token');
+ }
+
+ if ($viewer->getPHID() == $object->getPHID()) {
+ $panel_uri = '/settings/panel/apitokens/';
+ } else {
+ $panel_uri = '/settings/'.$object->getID().'/panel/apitokens/';
+ }
+
+ id(new PhabricatorAuthSessionEngine())->requireHighSecuritySession(
+ $viewer,
+ $request,
+ $panel_uri);
+
+ if ($request->isFormPost()) {
+ $token->save();
+
+ if ($is_new) {
+ $token_uri = '/conduit/token/edit/'.$token->getID().'/';
+ } else {
+ $token_uri = $panel_uri;
+ }
+
+ return id(new AphrontRedirectResponse())->setURI($token_uri);
+ }
+
+ $dialog = $this->newDialog()
+ ->setTitle($title)
+ ->addHiddenInput('objectPHID', $object->getPHID());
+
+ if ($is_new) {
+ $dialog
+ ->appendParagraph(pht('Generate a new API token?'))
+ ->addSubmitButton($submit_button)
+ ->addCancelButton($panel_uri);
+ } else {
+ $form = id(new AphrontFormView())
+ ->setUser($viewer)
+ ->appendChild(
+ id(new AphrontFormTextControl())
+ ->setLabel(pht('Token'))
+ ->setValue($token->getToken()));
+
+ $dialog
+ ->appendForm($form)
+ ->addCancelButton($panel_uri, pht('Done'));
+ }
+
+ return $dialog;
+ }
+
+}
diff --git a/src/applications/conduit/controller/PhabricatorConduitTokenTerminateController.php b/src/applications/conduit/controller/PhabricatorConduitTokenTerminateController.php
new file mode 100644
--- /dev/null
+++ b/src/applications/conduit/controller/PhabricatorConduitTokenTerminateController.php
@@ -0,0 +1,96 @@
+<?php
+
+final class PhabricatorConduitTokenTerminateController
+ extends PhabricatorConduitController {
+
+ public function handleRequest(AphrontRequest $request) {
+ $viewer = $request->getViewer();
+
+ $object_phid = $request->getStr('objectPHID');
+ $id = $request->getURIData('id');
+ if ($id) {
+ $token = id(new PhabricatorConduitTokenQuery())
+ ->setViewer($viewer)
+ ->withIDs(array($id))
+ ->withExpired(false)
+ ->requireCapabilities(
+ array(
+ PhabricatorPolicyCapability::CAN_VIEW,
+ PhabricatorPolicyCapability::CAN_EDIT,
+ ))
+ ->executeOne();
+ if (!$token) {
+ return new Aphront404Response();
+ }
+
+ $tokens = array($token);
+ $object_phid = $token->getObjectPHID();
+
+ $title = pht('Terminate API Token');
+ $body = pht(
+ 'Really terminate this token? Any system using this token '.
+ 'will no longer be able to make API requests.');
+ $submit_button = pht('Terminate Token');
+ $panel_uri = '/settings/panel/apitokens/';
+ } else {
+ $tokens = id(new PhabricatorConduitTokenQuery())
+ ->setViewer($viewer)
+ ->withObjectPHIDs(array($object_phid))
+ ->withExpired(false)
+ ->requireCapabilities(
+ array(
+ PhabricatorPolicyCapability::CAN_VIEW,
+ PhabricatorPolicyCapability::CAN_EDIT,
+ ))
+ ->execute();
+
+ $title = pht('Terminate API Tokens');
+ $body = pht(
+ 'Really terminate all active API tokens? Any systems using these '.
+ 'tokens will no longer be able to make API requests.');
+ $submit_button = pht('Terminate Tokens');
+ }
+
+ $panel_uri = '/settings/panel/apitokens/';
+ if ($object_phid != $viewer->getPHID()) {
+ $object = id(new PhabricatorObjectQuery())
+ ->setViewer($viewer)
+ ->withPHIDs(array($object_phid))
+ ->executeOne();
+ if (!$object) {
+ return new Aphront404Response();
+ }
+ $panel_uri = '/settings/'.$object->getID().'/panel/apitokens/';
+ }
+
+ id(new PhabricatorAuthSessionEngine())->requireHighSecuritySession(
+ $viewer,
+ $request,
+ $panel_uri);
+
+ if (!$tokens) {
+ return $this->newDialog()
+ ->setTitle(pht('No Tokens to Terminate'))
+ ->appendParagraph(
+ pht('There are no API tokens to terminate.'))
+ ->addCancelButton($panel_uri);
+ }
+
+ if ($request->isFormPost()) {
+ foreach ($tokens as $token) {
+ $token
+ ->setExpires(PhabricatorTime::getNow() - 60)
+ ->save();
+ }
+ return id(new AphrontRedirectResponse())->setURI($panel_uri);
+ }
+
+ return $this->newDialog()
+ ->setTitle($title)
+ ->addHiddenInput('objectPHID', $object_phid)
+ ->appendParagraph($body)
+ ->addSubmitButton($submit_button)
+ ->addCancelButton($panel_uri);
+ }
+
+}
diff --git a/src/applications/conduit/garbagecollector/ConduitTokenGarbageCollector.php b/src/applications/conduit/garbagecollector/ConduitTokenGarbageCollector.php
new file mode 100644
--- /dev/null
+++ b/src/applications/conduit/garbagecollector/ConduitTokenGarbageCollector.php
@@ -0,0 +1,19 @@
+<?php
+
+final class ConduitTokenGarbageCollector
+ extends PhabricatorGarbageCollector {
+
+ public function collectGarbage() {
+ $table = new PhabricatorConduitToken();
+ $conn_w = $table->establishConnection('w');
+ queryfx(
+ $conn_w,
+ 'DELETE FROM %T WHERE expires <= %d
+ ORDER BY dateCreated ASC LIMIT 100',
+ $table->getTableName(),
+ PhabricatorTime::getNow());
+
+ return ($conn_w->getAffectedRows() == 100);
+ }
+
+}
diff --git a/src/applications/conduit/query/PhabricatorConduitTokenQuery.php b/src/applications/conduit/query/PhabricatorConduitTokenQuery.php
new file mode 100644
--- /dev/null
+++ b/src/applications/conduit/query/PhabricatorConduitTokenQuery.php
@@ -0,0 +1,102 @@
+<?php
+
+final class PhabricatorConduitTokenQuery
+ extends PhabricatorCursorPagedPolicyAwareQuery {
+
+ private $ids;
+ private $objectPHIDs;
+ private $expired;
+
+ public function withExpired($expired) {
+ $this->expired = $expired;
+ return $this;
+ }
+
+ public function withIDs(array $ids) {
+ $this->ids = $ids;
+ return $this;
+ }
+
+ public function withObjectPHIDs(array $phids) {
+ $this->objectPHIDs = $phids;
+ return $this;
+ }
+
+ public function loadPage() {
+ $table = new PhabricatorConduitToken();
+ $conn_r = $table->establishConnection('r');
+
+ $data = queryfx_all(
+ $conn_r,
+ 'SELECT * FROM %T %Q %Q %Q',
+ $table->getTableName(),
+ $this->buildWhereClause($conn_r),
+ $this->buildOrderClause($conn_r),
+ $this->buildLimitClause($conn_r));
+
+ return $table->loadAllFromArray($data);;
+ }
+
+ private function buildWhereClause(AphrontDatabaseConnection $conn_r) {
+ $where = array();
+
+ if ($this->ids !== null) {
+ $where[] = qsprintf(
+ $conn_r,
+ 'id IN (%Ld)',
+ $this->ids);
+ }
+
+ if ($this->objectPHIDs !== null) {
+ $where[] = qsprintf(
+ $conn_r,
+ 'objectPHID IN (%Ls)',
+ $this->objectPHIDs);
+ }
+
+ if ($this->expired !== null) {
+ if ($this->expired) {
+ $where[] = qsprintf(
+ $conn_r,
+ 'expires <= %d',
+ PhabricatorTime::getNow());
+ } else {
+ $where[] = qsprintf(
+ $conn_r,
+ 'expires IS NULL OR expires > %d',
+ PhabricatorTime::getNow());
+ }
+ }
+
+ $where[] = $this->buildPagingClause($conn_r);
+
+ return $this->formatWhereClause($where);
+ }
+
+ protected function willFilterPage(array $tokens) {
+ $object_phids = mpull($tokens, 'getObjectPHID');
+ $objects = id(new PhabricatorObjectQuery())
+ ->setViewer($this->getViewer())
+ ->setParentQuery($this)
+ ->withPHIDs($object_phids)
+ ->execute();
+ $objects = mpull($objects, null, 'getPHID');
+
+ foreach ($tokens as $key => $token) {
+ $object = idx($objects, $token->getObjectPHID(), null);
+ if (!$object) {
+ $this->didRejectResult($token);
+ unset($tokens[$key]);
+ continue;
+ }
+ $token->attachObject($object);
+ }
+
+ return $tokens;
+ }
+
+ public function getQueryApplicationClass() {
+ return 'PhabricatorConduitApplication';
+ }
+
+}
diff --git a/src/applications/conduit/settings/PhabricatorConduitSettingsPanel.php b/src/applications/conduit/settings/PhabricatorConduitSettingsPanel.php
new file mode 100644
--- /dev/null
+++ b/src/applications/conduit/settings/PhabricatorConduitSettingsPanel.php
@@ -0,0 +1,116 @@
+<?php
+
+final class PhabricatorConduitSettingsPanel
+ extends PhabricatorSettingsPanel {
+
+ public function isEditableByAdministrators() {
+ return true;
+ }
+
+ public function getPanelKey() {
+ return 'apitokens';
+ }
+
+ public function getPanelName() {
+ return pht('Conduit API Tokens');
+ }
+
+ public function getPanelGroup() {
+ return pht('Sessions and Logs');
+ }
+
+ public function isEnabled() {
+ return true;
+ }
+
+ public function processRequest(AphrontRequest $request) {
+ $viewer = $this->getViewer();
+ $user = $this->getUser();
+
+ $tokens = id(new PhabricatorConduitTokenQuery())
+ ->setViewer($viewer)
+ ->withObjectPHIDs(array($user->getPHID()))
+ ->withExpired(false)
+ ->requireCapabilities(
+ array(
+ PhabricatorPolicyCapability::CAN_VIEW,
+ PhabricatorPolicyCapability::CAN_EDIT,
+ ))
+ ->execute();
+
+ $rows = array();
+ foreach ($tokens as $token) {
+ $rows[] = array(
+ javelin_tag(
+ 'a',
+ array(
+ 'href' => '/conduit/token/edit/'.$token->getID().'/',
+ 'sigil' => 'workflow',
+ ),
+ substr($token->getToken(), 0, 8).'...'),
+ PhabricatorConduitToken::getTokenTypeName($token->getTokenType()),
+ phabricator_datetime($token->getDateCreated(), $viewer),
+ ($token->getExpires()
+ ? phabricator_datetime($token->getExpires(), $viewer)
+ : pht('Never')),
+ javelin_tag(
+ 'a',
+ array(
+ 'class' => 'button small grey',
+ 'href' => '/conduit/token/terminate/'.$token->getID().'/',
+ 'sigil' => 'workflow',
+ ),
+ pht('Terminate')),
+ );
+ }
+
+ $table = new AphrontTableView($rows);
+ $table->setNoDataString(pht("You don't have any active API tokens."));
+ $table->setHeaders(
+ array(
+ pht('Token'),
+ pht('Type'),
+ pht('Created'),
+ pht('Expires'),
+ null,
+ ));
+ $table->setColumnClasses(
+ array(
+ 'wide pri',
+ '',
+ 'right',
+ 'right',
+ 'action',
+ ));
+
+ $generate_icon = id(new PHUIIconView())
+ ->setIconFont('fa-plus');
+ $generate_button = id(new PHUIButtonView())
+ ->setText(pht('Generate API Token'))
+ ->setHref('/conduit/token/edit/?objectPHID='.$user->getPHID())
+ ->setTag('a')
+ ->setWorkflow(true)
+ ->setIcon($generate_icon);
+
+ $terminate_icon = id(new PHUIIconView())
+ ->setIconFont('fa-exclamation-triangle');
+ $terminate_button = id(new PHUIButtonView())
+ ->setText(pht('Terminate All Tokens'))
+ ->setHref('/conduit/token/terminate/?objectPHID='.$user->getPHID())
+ ->setTag('a')
+ ->setWorkflow(true)
+ ->setIcon($terminate_icon);
+
+ $header = id(new PHUIHeaderView())
+ ->setHeader(pht('Active API Tokens'))
+ ->addActionLink($generate_button)
+ ->addActionLink($terminate_button);
+
+ $panel = id(new PHUIObjectBoxView())
+ ->setHeader($header)
+ ->appendChild($table);
+
+ return $panel;
+ }
+
+}
diff --git a/src/applications/conduit/storage/PhabricatorConduitToken.php b/src/applications/conduit/storage/PhabricatorConduitToken.php
new file mode 100644
--- /dev/null
+++ b/src/applications/conduit/storage/PhabricatorConduitToken.php
@@ -0,0 +1,106 @@
+<?php
+
+final class PhabricatorConduitToken
+ extends PhabricatorConduitDAO
+ implements PhabricatorPolicyInterface {
+
+ protected $objectPHID;
+ protected $tokenType;
+ protected $token;
+ protected $expires;
+
+ private $object = self::ATTACHABLE;
+
+ const TYPE_STANDARD = 'api';
+ const TYPE_TEMPORARY = 'tmp';
+
+ public function getConfiguration() {
+ return array(
+ self::CONFIG_COLUMN_SCHEMA => array(
+ 'tokenType' => 'text32',
+ 'token' => 'text32',
+ 'expires' => 'epoch?',
+ ),
+ self::CONFIG_KEY_SCHEMA => array(
+ 'key_object' => array(
+ 'columns' => array('objectPHID', 'tokenType'),
+ ),
+ 'key_token' => array(
+ 'columns' => array('token'),
+ 'unique' => true,
+ ),
+ 'key_expires' => array(
+ 'columns' => array('expires'),
+ ),
+ ),
+ ) + parent::getConfiguration();
+ }
+
+ public static function initializeNewToken($object_phid, $token_type) {
+ $token = new PhabricatorConduitToken();
+ $token->objectPHID = $object_phid;
+ $token->tokenType = $token_type;
+ $token->expires = $token->getTokenExpires($token_type);
+
+ $secret = $token_type.'-'.Filesystem::readRandomCharacters(32);
+ $secret = substr($secret, 0, 32);
+ $token->token = $secret;
+
+ return $token;
+ }
+
+ public static function getTokenTypeName($type) {
+ $map = array(
+ self::TYPE_STANDARD => pht('Standard API Token'),
+ self::TYPE_TEMPORARY => pht('Temporary API Token'),
+ );
+
+ return idx($map, $type, $type);
+ }
+
+ 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');
+ default:
+ throw new Exception(
+ pht('Unknown Conduit token type "%s"!', $token_type));
+ }
+ }
+
+ public function getObject() {
+ return $this->assertAttached($this->object);
+ }
+
+ public function attachObject(PhabricatorUser $object) {
+ $this->object = $object;
+ return $this;
+ }
+
+
+/* -( PhabricatorPolicyInterface )----------------------------------------- */
+
+
+ public function getCapabilities() {
+ return array(
+ PhabricatorPolicyCapability::CAN_VIEW,
+ PhabricatorPolicyCapability::CAN_EDIT,
+ );
+ }
+
+ public function getPolicy($capability) {
+ return $this->getObject()->getPolicy($capability);
+ }
+
+ public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
+ return $this->getObject()->hasAutomaticCapability($capability, $viewer);
+ }
+
+ public function describeAutomaticCapability($capability) {
+ return pht(
+ 'Conduit tokens inherit the policies of the user they authenticate.');
+ }
+
+}

File Metadata

Mime Type
text/plain
Expires
Thu, May 9, 12:34 AM (2 w, 5 h ago)
Storage Engine
blob
Storage Format
Encrypted (AES-256-CBC)
Storage Handle
6273629
Default Alt Text
D10985.id26395.diff (21 KB)

Event Timeline