1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-11-22 14:52:41 +01:00

Some acutal conduit authentication.

This commit is contained in:
epriestley 2011-02-05 22:36:21 -08:00
parent 1c586db9be
commit 605268f9aa
17 changed files with 379 additions and 22 deletions

View file

@ -22,6 +22,10 @@ return array(
// Example: "http://phabricator.example.com/"
'phabricator.base-uri' => null,
// The Conduit URI for API access to this install. Normally this is just
// the 'base-uri' plus "/api/" (e.g. "http://phabricator.example.com/api/"),
// but make sure you specify 'https' if you have HTTPS configured.
'phabricator.conduit-uri' => null,
'phabricator.csrf-key' => '0b7ec0592e0a2829d8b71df2fa269b2c6172eca3',

View file

@ -197,6 +197,7 @@ phutil_register_library_map(array(
'PhabricatorTypeaheadDatasourceController' => 'applications/typeahead/controller/base',
'PhabricatorUser' => 'applications/people/storage/user',
'PhabricatorUserDAO' => 'applications/people/storage/base',
'PhabricatorUserSettingsController' => 'applications/people/controller/settings',
'PhabricatorXHProfController' => 'applications/xhprof/controller/base',
'PhabricatorXHProfProfileController' => 'applications/xhprof/controller/profile',
'PhabricatorXHProfProfileSymbolView' => 'applications/xhprof/view/symbol',
@ -373,6 +374,7 @@ phutil_register_library_map(array(
'PhabricatorTypeaheadDatasourceController' => 'PhabricatorController',
'PhabricatorUser' => 'PhabricatorUserDAO',
'PhabricatorUserDAO' => 'PhabricatorLiskDAO',
'PhabricatorUserSettingsController' => 'PhabricatorPeopleController',
'PhabricatorXHProfController' => 'PhabricatorController',
'PhabricatorXHProfProfileController' => 'PhabricatorXHProfController',
'PhabricatorXHProfProfileSymbolView' => 'AphrontView',

View file

@ -135,6 +135,10 @@ class AphrontDefaultApplicationConfiguration
),
'/~/' => 'DarkConsoleController',
'/settings/' => array(
'(?:page/(?<page>[^/]+)/)?$' => 'PhabricatorUserSettingsController',
),
);
}

View file

@ -146,4 +146,8 @@ class AphrontRequest {
return id(new PhutilURI($this->getPath()))->setQueryParams($get);
}
final public function isDialogFormPost() {
return $this->isFormPost() && $this->getStr('__dialog__');
}
}

View file

@ -81,14 +81,44 @@ class PhabricatorConduitAPIController
$api_request = new ConduitAPIRequest($params);
try {
$result = $method_handler->executeMethod($api_request);
$error_code = null;
$error_info = null;
} catch (ConduitException $ex) {
$result = null;
$error_code = $ex->getMessage();
$error_info = $method_handler->getErrorDescription($error_code);
if ($method_handler->shouldRequireAuthentication()) {
$session_key = idx($metadata, 'sessionKey');
if (!$session_key) {
$auth_okay = false;
$error_code = 'ERR-NO-CERTIFICATE';
$error_info = "This server requires authentication but your client ".
"is not configured with an authentication certificate.";
} else {
$user = new PhabricatorUser();
$session = queryfx_one(
$user->establishConnection('r'),
'SELECT * FROM %T WHERE sessionKey = %s',
PhabricatorUser::SESSION_TABLE,
$session_key);
if (!$session) {
$auth_okay = false;
$error_code = 'ERR-INVALID-SESSION';
$error_info = 'Session key is invalid.';
} else {
// TODO: Make sessions timeout.
$auth_okay = true;
}
}
// TODO: When we session, read connectionID from the session table.
} else {
$auth_okay = true;
}
if ($auth_okay) {
try {
$result = $method_handler->executeMethod($api_request);
$error_code = null;
$error_info = null;
} catch (ConduitException $ex) {
$result = null;
$error_code = $ex->getMessage();
$error_info = $method_handler->getErrorDescription($error_code);
}
}
} catch (Exception $ex) {
$result = null;

View file

@ -11,6 +11,8 @@ phutil_require_module('phabricator', 'applications/conduit/controller/base');
phutil_require_module('phabricator', 'applications/conduit/method/base');
phutil_require_module('phabricator', 'applications/conduit/protocol/request');
phutil_require_module('phabricator', 'applications/conduit/storage/methodcalllog');
phutil_require_module('phabricator', 'applications/people/storage/user');
phutil_require_module('phabricator', 'storage/queryfx');
phutil_require_module('phabricator', 'view/control/table');
phutil_require_module('phabricator', 'view/layout/panel');

View file

@ -45,6 +45,10 @@ abstract class ConduitAPIMethod {
return 'ConduitAPI_'.$method_fragment.'_Method';
}
public function shouldRequireAuthentication() {
return true;
}
public static function getAPIMethodNameFromClassName($class_name) {
$match = null;
$is_valid = preg_match(

View file

@ -18,6 +18,10 @@
class ConduitAPI_conduit_connect_Method extends ConduitAPIMethod {
public function shouldRequireAuthentication() {
return false;
}
public function getMethodDescription() {
return "Connect a session-based client.";
}
@ -28,6 +32,8 @@ class ConduitAPI_conduit_connect_Method extends ConduitAPIMethod {
'clientVersion' => 'required int',
'clientDescription' => 'optional string',
'user' => 'optional string',
'authToken' => 'optional int',
'authSignature' => 'optional string',
);
}
@ -47,6 +53,17 @@ class ConduitAPI_conduit_connect_Method extends ConduitAPIMethod {
"a Facebook host, or see ".
"<http://www.intern.facebook.com/intern/wiki/index.php/Arcanist> for ".
"laptop instructions.",
"ERR-INVALID-USER" =>
"The username you are attempting to authenticate with is not valid.",
"ERR-INVALID-CERTIFICATE" =>
"Your authentication certificate for this server is invalid.",
"ERR-INVALID-TOKEN" =>
"The challenge token you are authenticating with is outside of the ".
"allowed time range. Either your system clock is out of whack or ".
"you're executing a replay attack.",
"ERR-NO-CERTIFICATE" =>
"This server requires authentication but your client is not ".
"configured with an authentication certificate."
);
}
@ -55,13 +72,14 @@ class ConduitAPI_conduit_connect_Method extends ConduitAPIMethod {
$client = $request->getValue('client');
$client_version = (int)$request->getValue('clientVersion');
$client_description = (string)$request->getValue('clientDescription');
$username = (string)$request->getValue('user');
// Log the connection, regardless of the outcome of checks below.
$connection = new PhabricatorConduitConnectionLog();
$connection->setClient($client);
$connection->setClientVersion($client_version);
$connection->setClientDescription($client_description);
$connection->setUsername((string)$request->getValue('user'));
$connection->setUsername($username);
$connection->save();
switch ($client) {
@ -80,8 +98,58 @@ class ConduitAPI_conduit_connect_Method extends ConduitAPIMethod {
throw new ConduitException('ERR-UNKNOWN-CLIENT');
}
$token = $request->getValue('authToken');
$signature = $request->getValue('authSignature');
$user = id(new PhabricatorUser())->loadOneWhere(
'username = %s',
$username);
if (!$user) {
throw new ConduitException('ERR-INVALID-USER');
}
$session_key = null;
if ($token && $signature) {
if (abs($token - time()) > 60 * 15) {
throw new ConduitException('ERR-INVALID-TOKEN');
}
$valid = sha1($token.$user->getConduitCertificate());
if ($valid != $signature) {
throw new ConduitException('ERR-INVALID-CERTIFICATE');
}
$sessions = queryfx_all(
$user->establishConnection('r'),
'SELECT * FROM %T WHERE userPHID = %s AND type LIKE %>',
PhabricatorUser::SESSION_TABLE,
$user->getPHID(),
'conduit-');
$session_type = null;
$sessions = ipull($sessions, null, 'type');
for ($ii = 1; $ii <= 3; $ii++) {
if (empty($sessions['conduit-'.$ii])) {
$session_type = 'conduit-'.$ii;
break;
}
}
if (!$session_type) {
$sessions = isort($sessions, 'sessionStart');
$oldest = reset($sessions);
$session_type = $oldest['type'];
}
$session_key = $user->establishSession($session_type);
} else {
throw new ConduitException('ERR-NO-CERTIFICATE');
}
return array(
'connectionID' => $connection->getID(),
'connectionID' => $connection->getID(),
'sessionKey' => $session_key,
'userPHID' => $user->getPHID(),
);
}

View file

@ -9,6 +9,10 @@
phutil_require_module('phabricator', 'applications/conduit/method/base');
phutil_require_module('phabricator', 'applications/conduit/protocol/exception');
phutil_require_module('phabricator', 'applications/conduit/storage/connectionlog');
phutil_require_module('phabricator', 'applications/people/storage/user');
phutil_require_module('phabricator', 'storage/queryfx');
phutil_require_module('phutil', 'utils');
phutil_require_source('ConduitAPI_conduit_connect_Method.php');

View file

@ -0,0 +1,168 @@
<?php
/*
* Copyright 2011 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
class PhabricatorUserSettingsController extends PhabricatorPeopleController {
private $page;
public function willProcessRequest(array $data) {
$this->page = idx($data, 'page');
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$pages = array(
// 'personal' => 'Profile',
// 'password' => 'Password',
// 'facebook' => 'Facebook Account',
'arcanist' => 'Arcanist Certificate',
);
if (empty($pages[$this->page])) {
$this->page = key($pages);
}
if ($request->isFormPost()) {
switch ($this->page) {
case 'arcanist':
if (!$request->isDialogFormPost()) {
$dialog = new AphrontDialogView();
$dialog->setUser($user);
$dialog->setTitle('Really regenerate session?');
$dialog->setSubmitURI('/settings/page/arcanist/');
$dialog->addSubmitButton('Regenerate');
$dialog->addCancelbutton('/settings/page/arcanist/');
$dialog->appendChild(
'<p>Really destroy the old certificate? Any established '.
'sessions will be terminated.');
return id(new AphrontDialogResponse())
->setDialog($dialog);
}
$conn = $user->establishConnection('w');
queryfx(
$conn,
'DELETE FROM %T WHERE userPHID = %s AND type LIKE %>',
PhabricatorUser::SESSION_TABLE,
$user->getPHID(),
'conduit');
// This implicitly regenerates the certificate.
$user->setConduitCertificate(null);
$user->save();
return id(new AphrontRedirectResponse())
->setURI('/settings/page/arcanist/?regenerated=true');
}
}
switch ($this->page) {
case 'arcanist':
$content = $this->renderArcanistCertificateForm();
break;
default:
$content = 'derp derp';
break;
}
$sidenav = new AphrontSideNavView();
foreach ($pages as $page => $name) {
$sidenav->addNavItem(
phutil_render_tag(
'a',
array(
'href' => '/settings/page/'.$page.'/',
'class' => ($page == $this->page)
? 'aphront-side-nav-selected'
: null,
),
phutil_escape_html($name)));
}
$sidenav->appendChild($content);
return $this->buildStandardPageResponse(
$sidenav,
array(
'title' => 'Account Settings',
));
}
private function renderArcanistCertificateForm() {
$request = $this->getRequest();
$user = $request->getUser();
if ($request->getStr('regenerated')) {
$notice = new AphrontErrorView();
$notice->setSeverity(AphrontErrorView::SEVERITY_NOTICE);
$notice->setTitle('Certificate Regenerated');
$notice->appendChild(
'<p>Your old certificate has been destroyed and you have been issued '.
'a new certificate. Sessions established under the old certificate '.
'are no longer valid.</p>');
$notice = $notice->render();
} else {
$notice = null;
}
$host = PhabricatorEnv::getEnvConfig('phabricator.conduit-uri');
$cert_form = new AphrontFormView();
$cert_form
->setUser($user)
->appendChild(
'<p class="aphront-form-instructions">Copy and paste this certificate '.
'into your <tt>~/.arcconfig</tt> in the "hosts" section to enable '.
'Arcanist to authenticate against this host.</p>')
->appendChild(
id(new AphrontFormTextAreaControl())
->setLabel('Certificate')
->setHeight(AphrontFormTextAreaControl::HEIGHT_SHORT)
->setValue($user->getConduitCertificate()));
$cert = new AphrontPanelView();
$cert->setHeader('Arcanist Certificate');
$cert->appendChild($cert_form);
$cert->setWidth(AphrontPanelView::WIDTH_FORM);
$regen_form = new AphrontFormView();
$regen_form
->setUser($user)
->setWorkflow(true)
->setAction('/settings/page/arcanist/')
->appendChild(
'<p class="aphront-form-instructions">You can regenerate this '.
'certificate, which will invalidate the old certificate and create '.
'a new one.</p>')
->appendChild(
id(new AphrontFormSubmitControl())
->setValue('Regenerate Certificate'));
$regen = new AphrontPanelView();
$regen->setHeader('Regenerate Certificate');
$regen->appendChild($regen_form);
$regen->setWidth(AphrontPanelView::WIDTH_FORM);
return $notice.$cert->render().$regen->render();
}
}

View file

@ -0,0 +1,27 @@
<?php
/**
* This file is automatically generated. Lint this module to rebuild it.
* @generated
*/
phutil_require_module('phabricator', 'aphront/response/dialog');
phutil_require_module('phabricator', 'aphront/response/redirect');
phutil_require_module('phabricator', 'applications/people/controller/base');
phutil_require_module('phabricator', 'applications/people/storage/user');
phutil_require_module('phabricator', 'infrastructure/env');
phutil_require_module('phabricator', 'storage/queryfx');
phutil_require_module('phabricator', 'view/dialog');
phutil_require_module('phabricator', 'view/form/base');
phutil_require_module('phabricator', 'view/form/control/submit');
phutil_require_module('phabricator', 'view/form/control/textarea');
phutil_require_module('phabricator', 'view/form/error');
phutil_require_module('phabricator', 'view/layout/panel');
phutil_require_module('phabricator', 'view/layout/sidenav');
phutil_require_module('phutil', 'markup');
phutil_require_module('phutil', 'utils');
phutil_require_source('PhabricatorUserSettingsController.php');

View file

@ -20,6 +20,8 @@ class PhabricatorUser extends PhabricatorUserDAO {
const PHID_TYPE = 'USER';
const SESSION_TABLE = 'phabricator_session';
protected $phid;
protected $userName;
protected $realName;
@ -33,6 +35,8 @@ class PhabricatorUser extends PhabricatorUserDAO {
protected $consoleVisible = 0;
protected $consoleTab = '';
protected $conduitCertificate;
public function getProfileImagePHID() {
return nonempty(
$this->profileImagePHID,
@ -56,6 +60,34 @@ class PhabricatorUser extends PhabricatorUserDAO {
return $this;
}
public function save() {
if (!$this->conduitCertificate) {
$this->conduitCertificate = $this->generateConduitCertificate();
}
return parent::save();
}
private function generateConduitCertificate() {
$entropy = $this->generateEntropy($bytes = 256);
$entropy = base64_encode($entropy);
$entropy = substr($entropy, 0, 255);
return $entropy;
}
private function generateEntropy($bytes) {
$urandom = fopen('/dev/urandom', 'r');
if (!$urandom) {
throw new Exception("Failed to open /dev/urandom!");
}
$entropy = fread($urandom, $bytes);
if (strlen($entropy) != $bytes) {
throw new Exception("Failed to read /dev/urandom!");
}
return $entropy;
}
public function comparePassword($password) {
$password = $this->hashPassword($password);
return ($password === $this->getPasswordHash());
@ -105,26 +137,19 @@ class PhabricatorUser extends PhabricatorUserDAO {
public function establishSession($session_type) {
$conn_w = $this->establishConnection('w');
$urandom = fopen('/dev/urandom', 'r');
if (!$urandom) {
throw new Exception("Failed to open /dev/urandom!");
}
$entropy = fread($urandom, 20);
if (strlen($entropy) != 20) {
throw new Exception("Failed to read /dev/urandom!");
}
$entropy = $this->generateEntropy($bytes = 20);
$session_key = sha1($entropy);
queryfx(
$conn_w,
'INSERT INTO phabricator_session '.
'INSERT INTO %T '.
'(userPHID, type, sessionKey, sessionStart)'.
' VALUES '.
'(%s, %s, %s, UNIX_TIMESTAMP()) '.
'ON DUPLICATE KEY UPDATE '.
'sessionKey = VALUES(sessionKey), '.
'sessionStart = VALUES(sessionStart)',
self::SESSION_TABLE,
$this->getPHID(),
$session_type,
$session_key);

View file

@ -113,6 +113,7 @@ class AphrontDialogView extends AphrontView {
),
'<input type="hidden" name="__form__" value="1" />'.
'<input type="hidden" name="__csrf__" value="'.$csrf.'" />'.
'<input type="hidden" name="__dialog__" value="1" />'.
$hidden_inputs.
'<div class="aphront-dialog-head">'.
phutil_escape_html($this->title).

View file

@ -24,6 +24,7 @@ final class AphrontFormView extends AphrontView {
private $data = array();
private $encType;
private $user;
private $workflow;
public function setUser(PhabricatorUser $user) {
$this->user = $user;
@ -50,15 +51,21 @@ final class AphrontFormView extends AphrontView {
return $this;
}
public function setWorkflow($workflow) {
$this->workflow = $workflow;
return $this;
}
public function render() {
require_celerity_resource('aphront-form-view-css');
return phutil_render_tag(
return javelin_render_tag(
'form',
array(
'action' => $this->action,
'method' => $this->method,
'class' => 'aphront-form-view',
'enctype' => $this->encType,
'sigil' => $this->workflow ? 'workflow' : null,
),
$this->renderDataInputs().
$this->renderChildren());

View file

@ -7,6 +7,7 @@
phutil_require_module('phabricator', 'infrastructure/celerity/api');
phutil_require_module('phabricator', 'infrastructure/javelin/markup');
phutil_require_module('phabricator', 'view/base');
phutil_require_module('phutil', 'markup');

View file

@ -20,7 +20,7 @@ final class AphrontErrorView extends AphrontView {
const SEVERITY_ERROR = 'error';
const SEVERITY_WARNING = 'warning';
const SEVERITY_NOTE = 'note';
const SEVERITY_NOTICE = 'notice';
const WIDTH_DEFAULT = 'default';
const WIDTH_WIDE = 'wide';

View file

@ -28,3 +28,9 @@
border: 1px solid #888800;
background: #ffffdd;
}
.aphront-error-severity-notice {
border: 1px solid #000088;
background: #e3e3ff;
}