Page Menu
Home
Phabricator
Search
Configure Global Search
Log In
Files
F15376496
D10985.id26378.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
21 KB
Referenced Files
None
Subscribers
None
D10985.id26378.diff
View Options
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
Details
Attached
Mime Type
text/plain
Expires
Fri, Mar 14, 4:06 AM (1 w, 1 d ago)
Storage Engine
blob
Storage Format
Encrypted (AES-256-CBC)
Storage Handle
7226100
Default Alt Text
D10985.id26378.diff (21 KB)
Attached To
Mode
D10985: Add Conduit Tokens to make authentication in Conduit somewhat more sane
Attached
Detach File
Event Timeline
Log In to Comment