mirror of
https://we.phorge.it/source/phorge.git
synced 2024-12-18 19:40:55 +01:00
Add Conduit Tokens to make authentication in Conduit somewhat more sane
Summary: Ref T5955. Summary of intended changes: **Improve Granularity of Authorization**: Currently, users have one Conduit Certificate. This isn't very flexible, and means that you can't ever generate an API token with limited permissions or IP block controls (see T6706). This moves toward a world where you can generate multiple tokens, revoke them individually, and assign disparate privileges to them. **Standardize Token Management**: This moves Conduit to work the same way that sessions, OAuth authorizations, and temporary tokens already work, instead of being this crazy bizarre mess. **Make Authentication Faster**: Authentication currently requires a handshake (conduit.connect) to establish a session, like the web UI. This is unnecessary from a security point of view and puts an extra round trip in front of all Conduit activity. Essentially no other API anywhere works like this. **Make Authentication Simpler**: The handshake is complex, and involves deriving hashes. The session is also complex, and creates issues like T4377. Handshake and session management require different inputs. **Make Token Management Simpler**: The certificate is this huge long thing right now, which is not necessary from a security perspective. There are separate Arcanist handshake tokens, but they have a different set of issues. We can move forward to a token management world where neither of these problems exist. **Lower Protocol Barrier**: The simplest possible API client is very complex right now. It should be `curl`. Simplifying authentication is a necessary step toward this. **Unblock T2783**: T2783 is blocked on nodes in the cluster making authenticated API calls to other nodes. This provides a simpler way forward than the handshake mess (or enormous-hack-mess) which would currently be required. Test Plan: - Generated tokens. - Generated tokens for a bot account. - Terminated tokens (and for a bot account). - Terminated all tokens (and for a bot account). - Ran GC and saw it reap all the expired tokens. NOTE: These tokens can not actually be used to authenticate yet! {F249658} Reviewers: btrahan Reviewed By: btrahan Subscribers: epriestley Maniphest Tasks: T5955 Differential Revision: https://secure.phabricator.com/D10985
This commit is contained in:
parent
2c7be52fc2
commit
39f2bbaeea
9 changed files with 570 additions and 0 deletions
12
resources/sql/autopatches/20141212.conduittoken.sql
Normal file
12
resources/sql/autopatches/20141212.conduittoken.sql
Normal file
|
@ -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};
|
|
@ -206,6 +206,7 @@ phutil_register_library_map(array(
|
||||||
'ConduitPingConduitAPIMethod' => 'applications/conduit/method/ConduitPingConduitAPIMethod.php',
|
'ConduitPingConduitAPIMethod' => 'applications/conduit/method/ConduitPingConduitAPIMethod.php',
|
||||||
'ConduitQueryConduitAPIMethod' => 'applications/conduit/method/ConduitQueryConduitAPIMethod.php',
|
'ConduitQueryConduitAPIMethod' => 'applications/conduit/method/ConduitQueryConduitAPIMethod.php',
|
||||||
'ConduitSSHWorkflow' => 'applications/conduit/ssh/ConduitSSHWorkflow.php',
|
'ConduitSSHWorkflow' => 'applications/conduit/ssh/ConduitSSHWorkflow.php',
|
||||||
|
'ConduitTokenGarbageCollector' => 'applications/conduit/garbagecollector/ConduitTokenGarbageCollector.php',
|
||||||
'ConpherenceActionMenuEventListener' => 'applications/conpherence/events/ConpherenceActionMenuEventListener.php',
|
'ConpherenceActionMenuEventListener' => 'applications/conpherence/events/ConpherenceActionMenuEventListener.php',
|
||||||
'ConpherenceConduitAPIMethod' => 'applications/conpherence/conduit/ConpherenceConduitAPIMethod.php',
|
'ConpherenceConduitAPIMethod' => 'applications/conpherence/conduit/ConpherenceConduitAPIMethod.php',
|
||||||
'ConpherenceConfigOptions' => 'applications/conpherence/config/ConpherenceConfigOptions.php',
|
'ConpherenceConfigOptions' => 'applications/conpherence/config/ConpherenceConfigOptions.php',
|
||||||
|
@ -1428,7 +1429,12 @@ phutil_register_library_map(array(
|
||||||
'PhabricatorConduitMethodCallLog' => 'applications/conduit/storage/PhabricatorConduitMethodCallLog.php',
|
'PhabricatorConduitMethodCallLog' => 'applications/conduit/storage/PhabricatorConduitMethodCallLog.php',
|
||||||
'PhabricatorConduitMethodQuery' => 'applications/conduit/query/PhabricatorConduitMethodQuery.php',
|
'PhabricatorConduitMethodQuery' => 'applications/conduit/query/PhabricatorConduitMethodQuery.php',
|
||||||
'PhabricatorConduitSearchEngine' => 'applications/conduit/query/PhabricatorConduitSearchEngine.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',
|
'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',
|
'PhabricatorConfigAllController' => 'applications/config/controller/PhabricatorConfigAllController.php',
|
||||||
'PhabricatorConfigApplication' => 'applications/config/application/PhabricatorConfigApplication.php',
|
'PhabricatorConfigApplication' => 'applications/config/application/PhabricatorConfigApplication.php',
|
||||||
'PhabricatorConfigColumnSchema' => 'applications/config/schema/PhabricatorConfigColumnSchema.php',
|
'PhabricatorConfigColumnSchema' => 'applications/config/schema/PhabricatorConfigColumnSchema.php',
|
||||||
|
@ -3212,6 +3218,7 @@ phutil_register_library_map(array(
|
||||||
'ConduitPingConduitAPIMethod' => 'ConduitAPIMethod',
|
'ConduitPingConduitAPIMethod' => 'ConduitAPIMethod',
|
||||||
'ConduitQueryConduitAPIMethod' => 'ConduitAPIMethod',
|
'ConduitQueryConduitAPIMethod' => 'ConduitAPIMethod',
|
||||||
'ConduitSSHWorkflow' => 'PhabricatorSSHWorkflow',
|
'ConduitSSHWorkflow' => 'PhabricatorSSHWorkflow',
|
||||||
|
'ConduitTokenGarbageCollector' => 'PhabricatorGarbageCollector',
|
||||||
'ConpherenceActionMenuEventListener' => 'PhabricatorEventListener',
|
'ConpherenceActionMenuEventListener' => 'PhabricatorEventListener',
|
||||||
'ConpherenceConduitAPIMethod' => 'ConduitAPIMethod',
|
'ConpherenceConduitAPIMethod' => 'ConduitAPIMethod',
|
||||||
'ConpherenceConfigOptions' => 'PhabricatorApplicationConfigOptions',
|
'ConpherenceConfigOptions' => 'PhabricatorApplicationConfigOptions',
|
||||||
|
@ -4541,7 +4548,15 @@ phutil_register_library_map(array(
|
||||||
),
|
),
|
||||||
'PhabricatorConduitMethodQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
|
'PhabricatorConduitMethodQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
|
||||||
'PhabricatorConduitSearchEngine' => 'PhabricatorApplicationSearchEngine',
|
'PhabricatorConduitSearchEngine' => 'PhabricatorApplicationSearchEngine',
|
||||||
|
'PhabricatorConduitSettingsPanel' => 'PhabricatorSettingsPanel',
|
||||||
|
'PhabricatorConduitToken' => array(
|
||||||
|
'PhabricatorConduitDAO',
|
||||||
|
'PhabricatorPolicyInterface',
|
||||||
|
),
|
||||||
'PhabricatorConduitTokenController' => 'PhabricatorConduitController',
|
'PhabricatorConduitTokenController' => 'PhabricatorConduitController',
|
||||||
|
'PhabricatorConduitTokenEditController' => 'PhabricatorConduitController',
|
||||||
|
'PhabricatorConduitTokenQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
|
||||||
|
'PhabricatorConduitTokenTerminateController' => 'PhabricatorConduitController',
|
||||||
'PhabricatorConfigAllController' => 'PhabricatorConfigController',
|
'PhabricatorConfigAllController' => 'PhabricatorConfigController',
|
||||||
'PhabricatorConfigApplication' => 'PhabricatorApplication',
|
'PhabricatorConfigApplication' => 'PhabricatorApplication',
|
||||||
'PhabricatorConfigColumnSchema' => 'PhabricatorConfigStorageSchema',
|
'PhabricatorConfigColumnSchema' => 'PhabricatorConfigStorageSchema',
|
||||||
|
|
|
@ -46,6 +46,10 @@ final class PhabricatorConduitApplication extends PhabricatorApplication {
|
||||||
'log/' => 'PhabricatorConduitLogController',
|
'log/' => 'PhabricatorConduitLogController',
|
||||||
'log/view/(?P<view>[^/]+)/' => 'PhabricatorConduitLogController',
|
'log/view/(?P<view>[^/]+)/' => 'PhabricatorConduitLogController',
|
||||||
'token/' => 'PhabricatorConduitTokenController',
|
'token/' => 'PhabricatorConduitTokenController',
|
||||||
|
'token/edit/(?:(?P<id>\d+)/)?' =>
|
||||||
|
'PhabricatorConduitTokenEditController',
|
||||||
|
'token/terminate/(?:(?P<id>\d+)/)?' =>
|
||||||
|
'PhabricatorConduitTokenTerminateController',
|
||||||
),
|
),
|
||||||
'/api/(?P<method>[^/]+)' => 'PhabricatorConduitAPIController',
|
'/api/(?P<method>[^/]+)' => 'PhabricatorConduitAPIController',
|
||||||
);
|
);
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
102
src/applications/conduit/query/PhabricatorConduitTokenQuery.php
Normal file
102
src/applications/conduit/query/PhabricatorConduitTokenQuery.php
Normal file
|
@ -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';
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
106
src/applications/conduit/storage/PhabricatorConduitToken.php
Normal file
106
src/applications/conduit/storage/PhabricatorConduitToken.php
Normal file
|
@ -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.');
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in a new issue